第6章 光说不练假把式
第5章我们给Agent装了个脑子——LLM。但光有脑子不够。你见过哪个助理只会动嘴不动手的?这一章,我们给Agent装上"手脚",让它能真正帮你干活——查数据库、调API、发邮件、改代码……Agent从"会说"变成"会做",关键就在这一章。
6.1 Function Calling是怎么工作的?
6.1.1 一个场景:你说"帮我查天气",它怎么查?
假如你有一个Agent,你对它说:
"帮我查一下北京今天的天气。"
Agent的脑子(LLM)能理解"你想查天气",但它自己调不了天气API。LLM只是一个文字生成器,它没有办法发起网络请求。
这时候就需要 Function Calling(函数调用)——让LLM"告诉Agent该调哪个函数,Agent去执行,结果拿回来给LLM,LLM再生成回答"。
关键理解:不是LLM在执行工具,是Agent在LLM的"指挥"下执行工具。LLM像将军下命令,Agent是士兵去执行。
6.1.2 代码长什么样
用LangChain实现上面的流程:
python
from langchain_openai import ChatOpenAI
from langchain.tools import tool
# 第一步:定义一个工具(就是一个带装饰器的普通函数)
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气"""
# 实际项目中这里调API,现在用模拟数据
weather_data = {
"北京": "晴,25度",
"上海": "多云,28度",
"深圳": "阵雨,30度",
}
return weather_data.get(city, "未找到该城市")
# 第二步:把工具绑定到LLM
llm = ChatOpenAI(model="gpt-4")
llm_with_tools = llm.bind_tools([get_weather])
# 第三步:用户提问
from langchain_core.messages import HumanMessage
response = llm_with_tools.invoke(
[HumanMessage(content="北京今天天气怎么样?")]
)
# LLM返回的不是最终答案,而是"需要调哪个工具"的指令
print(response.tool_calls)
# [{'name': 'get_weather', 'args': {'city': '北京'}}]6.1.3 LLM怎么知道该调哪个函数?
你不需要写if-else判断"如果用户说天气就调get_weather"——LLM会自动判断。
秘密在于函数定义里的文档字符串(docstring)和参数类型注解:
python
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气""" # ← LLM通过这行判断这个函数是干什么的当你调用 llm.bind_tools([get_weather, search_news, ...]) 时,LangChain把每个函数的名称、描述、参数类型打包发给LLM。LLM根据用户的意图,自己决定该调哪个、传什么参数。
不需要你写任何规则。 这就是Function Calling最颠覆的地方。
6.2 怎么设计好用的工具?
6.2.1 工具就是函数,但有好坏之分
工具本质上就是一个Python函数。但"能被LLM调用的函数"和"普通函数"的设计思路完全不同。
差工具:
python
@tool
def do_stuff(x):
"""处理东西"""
return x * 2LLM看到这个函数定义:名字看不懂、描述没信息、没有类型注解。它根本不知道该什么时候调用、传什么参数。
好工具:
python
@tool
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
"""将金额从一种货币转换为另一种货币。
amount: 要转换的金额
from_currency: 源货币代码(如 USD, CNY)
to_currency: 目标货币代码(如 JPY, EUR)
"""
# 转换逻辑
rate = get_exchange_rate(from_currency, to_currency)
return f"{amount} {from_currency} = {amount * rate:.2f} {to_currency}"LLM一看就知道:这是货币转换工具,需要金额和两种货币代码。
6.2.2 工具设计的三个原则
原则一:函数名就是工具的"名字",要让LLM一眼看懂。
使用 get_weather 而不是 gw,使用 search_documents 而不是 sd。LLM是根据函数名来匹配用户意图的。
原则二:docstring 是工具的"说明书",越详细越好。
"""查询指定城市的天气。返回温度、天气状况和湿度。"""比
"""查天气"""好十倍。LLM会读你的docstring来决定什么时候用这个工具。
原则三:参数类型要明确,最好有默认值。
python
def search_news(topic: str, max_results: int = 5) -> list:max_results: int = 5 告诉LLM:这个参数是可选的,默认5条。如果用户没说"给我10条新闻",LLM就会传5。
6.2.3 工具不是越多越好
刚学Agent开发的人容易犯一个错误:一口气注册几十个工具。
问题:每个工具定义都会发给LLM,占用上下文窗口。工具越多,LLM越容易"选错"。而且有些工具的功能重叠,LLM会懵。
建议:从3-5个核心工具开始,跑通了再加。宁愿少而精,不要多而乱。
6.3 不能让Agent为所欲为
6.3.1 Agent调工具,你能控制吗?
想象一个场景:Agent接入了你的数据库,你可以让它"帮我查上个月的销售数据"。
但如果用户问的是"帮我把所有用户的数据删掉",而Agent又刚好有 delete_from_database 这个工具……它真的可能执行。
工具调用赋予了Agent真实的能力,但也带来了真实的风险。
6.3.2 三个必须做的安全措施
措施一:权限分级。 不是所有工具都该在任何场景下可用。
python
# 分级示例
safe_tools = [get_weather, search_news, calculate] # 只读,随便调
dangerous_tools = [delete_user, modify_database] # 写操作,需确认
# 普通用户只给 safe_tools
# 管理员才给 full_tools措施二:人工确认。 涉及钱、数据删除、外部发送的操作,让用户确认后再执行。
python
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""发送邮件。此操作需要用户确认。"""
confirmation = input(f"确认发送邮件到 {to}?(y/n): ")
if confirmation.lower() != 'y':
return "已取消发送"
# 实际发送逻辑
return f"邮件已发送到 {to}"措施三:参数校验。 别信任LLM传过来的参数。
python
@tool
def transfer_money(from_account: str, to_account: str, amount: float) -> str:
"""转账"""
# 校验:金额必须是正数
if amount <= 0:
return "错误:转账金额必须大于0"
# 校验:不能转给自己
if from_account == to_account:
return "错误:不能转账给自己"
# 校验:金额不能超过上限
if amount > 50000:
return "错误:单笔转账不能超过50000元,需要人工审核"
# 实际转账逻辑6.4 让工具调用变得像"插座插插头"一样简单
6.4.1 工具调用的碎片化问题
你写了3个Agent:一个用OpenAI的Function Calling格式,一个用Anthropic的Tool Use格式,一个用开源模型的JSON模式。三个Agent,三种工具定义方式。
这就是工具调用的"碎片化"——每个LLM提供商有自己的工具调用规范,你的工具代码要适配不同的格式。
6.4.2 MCP的思路
**MCP(Model Context Protocol)**想要解决的就是这个问题——让工具定义变成"标准插座"。
你只需要按MCP的标准定义一次工具,然后任何支持MCP的LLM都能直接调用。不用再为每个模型写不同的适配代码。
本质就是:工具定义和LLM解耦。就像USB-C出现之前,每部手机有自己的充电口;有了USB-C之后,一根线充所有手机。
这本书第17章会详细讲MCP的原理和实战。现在你只需要知道:MCP是让工具调用走向标准化的方向,值得关注。
6.5 模型调用与流式输出
6.1 节我们用了 ChatOpenAI,但 LangChain 能做的事情远不止绑定 OpenAI。这一节,我们深入模型层:LLM 和 ChatModel 到底有什么区别?怎么让输出像 ChatGPT 那样一个字一个字蹦出来?怎么在 OpenAI、Claude、Gemini 之间随意切换?
6.5.1 LLM vs ChatModel:不只是"名字不同"
你可能同时见过这两种写法:
python
# LLM 调用 —— 纯文本输入
from langchain_openai import OpenAI
llm = OpenAI(model="gpt-3.5-turbo-instruct")
response = llm.invoke("翻译成法语:Hello")
print(response) # "Bonjour"
# ChatModel 调用 —— 消息列表输入
from langchain_openai import ChatOpenAI
chat = ChatOpenAI(model="gpt-4o-mini")
response = chat.invoke([
SystemMessage(content="你是一个翻译专家"),
HumanMessage(content="翻译成法语:Hello")
])
print(response.content) # "Bonjour"| 特性 | LLM | ChatModel |
|---|---|---|
| 输入 | 纯文本字符串 | 消息列表(System/Human/AI) |
| 输出 | 纯文本字符串 | AIMessage 对象(含 content 等属性) |
| 代表模型 | text-davinci-003 | gpt-4o, Claude, Gemini |
| 适用场景 | 文本补全 | 对话、Agent、多轮交互 |
在 Agent 开发中,推荐使用 ChatModel。因为 Agent 本质上就是多轮对话——用户说、AI 想、工具返回、AI 再回答。ChatModel 的消息列表天生适合这种多角色交互。
6.5.2 流式输出:让用户不用干等
Agent 执行一个复杂任务可能需要几秒甚至几十秒。如果用户盯着一个空白屏幕等结果,体验极差。
流式输出(Streaming)就是:LLM 每生成一个 token,就立刻显示出来。就像 ChatGPT 一个字一个字打出来的效果。
用 LangChain 实现只需一行改动:
python
from langchain_openai import ChatOpenAI
chat = ChatOpenAI(model="gpt-4o-mini", streaming=True)
# stream() 返回一个生成器,每拿到一个 chunk 就 yield
for chunk in chat.stream([HumanMessage(content="解释一下什么是AI Agent")]):
print(chunk.content, end="", flush=True) # 立刻打印,不缓存stream() 和 invoke() 的区别:invoke() 一次性返回完整结果,stream() 像一个水龙头——开一点,流一点。对聊天机器人、实时写作辅助、代码补全这类"需要实时反馈"的场景,流式输出是标配。
6.5.3 结构化输出:from_dict 到 with_structured_output
6.1 节我们用 bind_tools 实现 Function Calling。但 LLM 返回的 JSON 需要手动解析:
python
# 旧写法:拿到 raw JSON,自己 json.loads
tool_calls = response.additional_kwargs.get("tool_calls", [])
result = json.loads(tool_calls[0]["function"]["arguments"])LangChain 提供了更简洁的写法 with_structured_output——直接把 Pydantic 模型和 LLM 绑定,返回的就是 Python 对象:
python
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import List
# 定义你想要的输出结构
class PersonInfo(BaseModel):
name: str = Field(description="人名")
age: int = Field(description="年龄")
occupation: str = Field(description="职业")
skills: List[str] = Field(description="技能列表")
chat = ChatOpenAI(model="gpt-4o", temperature=0)
# 一行绑定:LLM 输出自动解析成 PersonInfo 对象
structured_chat = chat.with_structured_output(PersonInfo)
# 调用
result = structured_chat.invoke(
[HumanMessage(content="张三,28岁,是一名Python程序员,擅长Web开发和数据分析")]
)
# result 是 PersonInfo 对象,直接 . 访问属性
print(result.name) # 张三
print(result.age) # 28
print(result.occupation) # Python程序员
print(result.skills) # ['Web开发', '数据分析']with_structured_output 的妙处在于:你不关心 LLM 用什么协议(Function Calling、JSON Mode 还是别的),你只定义"我要什么结构",框架帮你处理剩下的。这比手写 JSON 解析强一百倍——类型安全、IDE 有自动补全、不会因为 LLM 的格式波动而崩溃。
6.5.4 模型切换不换代码
你的 Agent 可能今天用 OpenAI,明天客户要求用 Claude,后天老板说"把成本降下来用本地模型"。
如果直接调 OpenAI SDK,每换一次模型你都要重写调用代码。LangChain 的价值在这里体现得淋漓尽致——接口统一,切换模型不需要动业务逻辑:
python
# 用 OpenAI
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini")
# 换成 Anthropic Claude——只需改导入和模型名
# pip install langchain-anthropic
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-sonnet-4-20250514")
# 换成 Google Gemini——同样,只改这两行
# pip install langchain-google-vertexai
from langchain_google_vertexai import ChatGemini
model = ChatGemini(model="gemini-1.5-pro")
# 甚至本地模型也行
# pip install langchain-huggingface
from langchain_huggingface import HuggingFacePipeline
model = HuggingFacePipeline.from_model_id(
model_id="microsoft/Phi-3-mini-128k-instruct",
task="text-generation",
pipeline_kwargs={"max_new_tokens": 256},
)
# ⬆️ 上面改了模型,下面的业务代码一行都不用变
response = model.invoke([HumanMessage(content="你好")])
print(response.content)这就是 LangChain 的"模型无关"设计哲学——它抽象了模型调用的接口,让你可以在 OpenAI、Anthropic、Google、开源模型之间随意切换,业务逻辑始终不变。
安全提醒:API Key 永远不要硬编码在代码里。用环境变量
export OPENAI_API_KEY="sk-..."或.env文件管理。
一句话总结这一节:LangChain 的模型层让你——用 ChatModel 管理多轮对话、用 stream() 实现实时输出、用 with_structured_output 获得类型安全的结构化数据、用一个接口切换所有模型提供商。
6.6 本章小结
这一章我们给Agent装上了"手脚":
- Function Calling原理:LLM不是自己调用工具,而是"告诉Agent调哪个"。像将军下令,士兵执行。
- 工具设计三原则:名字见名知意、docstring写清楚、参数有类型。LLM靠这些信息决定什么时候调什么工具。
- 安全不可忽视:权限分级、人工确认、参数校验——三道防线,缺一不可。
- MCP的方向:让工具定义变成"标准插座",一套代码适配所有LLM。
- 模型调用层:LLM(纯文本)和ChatModel(消息对象)是两种接口;
stream()实现流式输出提升体验;with_structured_output()用 Pydantic 约束输出格式;模型切换只需改一行代码——LangChain 屏蔽了底层差异。
✅ 知识点检查
学完这一章,试试回答这几个问题:
- [ ] Function Calling中,LLM的角色是什么?真正执行工具的是谁?
- [ ] 设计一个LLM可调用的工具,需要注意哪三个原则?
- [ ] Agent调用工具时,有哪三道安全防线?
- [ ] MCP解决了什么问题?为什么把它比作"USB-C"?
- [ ] LLM 和 ChatModel 的核心区别是什么?流式输出和结构化输出分别怎么实现?
📚 延伸阅读
- OpenAI Function Calling文档:https://platform.openai.com/docs/guides/function-calling
- LangChain Tools文档:https://python.langchain.com/docs/how_to/custom_tools/
- MCP协议官方文档:https://modelcontextprotocol.io
- 本书配套源码:关注公众号「图解AI系列」免费领取
🎯 下一章预告
第7章,Agent光会干活不够,还得有记性——
"没记性的助理谁敢用?让Agent学会记住该记住的事。"

