Skip to content

第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 * 2

LLM看到这个函数定义:名字看不懂、描述没信息、没有类型注解。它根本不知道该什么时候调用、传什么参数。

好工具

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"
特性LLMChatModel
输入纯文本字符串消息列表(System/Human/AI)
输出纯文本字符串AIMessage 对象(含 content 等属性)
代表模型text-davinci-003gpt-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装上了"手脚":

  1. Function Calling原理:LLM不是自己调用工具,而是"告诉Agent调哪个"。像将军下令,士兵执行。
  2. 工具设计三原则:名字见名知意、docstring写清楚、参数有类型。LLM靠这些信息决定什么时候调什么工具。
  3. 安全不可忽视:权限分级、人工确认、参数校验——三道防线,缺一不可。
  4. MCP的方向:让工具定义变成"标准插座",一套代码适配所有LLM。
  5. 模型调用层:LLM(纯文本)和ChatModel(消息对象)是两种接口;stream() 实现流式输出提升体验;with_structured_output() 用 Pydantic 约束输出格式;模型切换只需改一行代码——LangChain 屏蔽了底层差异。

✅ 知识点检查

学完这一章,试试回答这几个问题:

  • [ ] Function Calling中,LLM的角色是什么?真正执行工具的是谁?
  • [ ] 设计一个LLM可调用的工具,需要注意哪三个原则?
  • [ ] Agent调用工具时,有哪三道安全防线?
  • [ ] MCP解决了什么问题?为什么把它比作"USB-C"?
  • [ ] LLM 和 ChatModel 的核心区别是什么?流式输出和结构化输出分别怎么实现?

📚 延伸阅读


🎯 下一章预告

第7章,Agent光会干活不够,还得有记性——

"没记性的助理谁敢用?让Agent学会记住该记住的事。"