跳到主要内容
版本:Next

工具调用与 ReAct

Agent 方向的核心机制篇。最容易被追问的三个点:模型到底执不执行工具、工具报错该怎么处理、Observation 是谁产生的。

@tool:docstring 就是给模型看的说明书​

from langchain_core.tools import tool

@tool
def search_order(order_id: str) -> str:
"""按订单号查询物流状态。

适用:用户提供完整订单号时。
不适用:用户只给姓名或手机号(应先调用 search_user 定位订单)。
"""
return query_db(order_id)

@tool 把函数签名和 docstring 解析成模型可见的工具描述。模型就靠这段文字决定"要不要用、什么时候用、参数怎么传",所以 docstring 必须写清:

  • 适用与不适用场景(不适用场景能显著减少误调用);
  • 参数格式与含义(类型注解 + 说明);
  • 返回内容模型可读。

模型不执行工具:只输出 tool_calls​

这是本篇最重要的结论:模型从头到尾没有执行任何代码。 它读你的工具描述,判断该调工具时,输出一段结构化的 tool_calls(工具名 + 参数 JSON)——仅此而已。真正的执行者是你的代码:

1. 用户消息发给模型
2. 模型返回 tool_calls("我想调 search_order(order_id='A001')")
3. 你的代码执行函数,拿到结果
4. 把结果包成 ToolMessage(必须带 tool_call_id 与请求配对)append 进 messages
5. 再次调模型 → 模型基于工具结果生成最终回答
messages.append(resp.choices[0].message) # 带 tool_calls 的 assistant 消息
for call in resp.choices[0].message.tool_calls:
result = dispatch(call.function.name, call.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": call.id, # ← 不带配对会报 400
"content": str(result),
})

tool_call_id 配对是硬要求:一次可能并发多个工具调用,服务端靠 id 知道哪条结果对应哪次调用,漏了直接 400。

工具报错:返回可读文本,不抛异常​

工具执行失败(订单不存在、外部 API 超时)时,把错误包装成可读文本作为 ToolMessage 返回,让模型知道"这个工具失败了、原因是什么",它才能向用户解释或换一条路(比如让用户补订单号)。

如果直接抛异常:Agent 循环当场崩溃,模型也没有机会自我修复。错误是信息,不是故障——这是 Agent 工程和普通函数式思维最大的差异。

手写循环:三步 + 四道闸​

for step in range(MAX_STEPS): # 闸 1:最大步数
resp = call_model(messages)
if not resp.tool_calls: # 没有工具调用 → 最终回答,循环结束
return resp.content
for call in resp.tool_calls:
counter.check(call.function.name) # 闸 2:单个工具最大调用次数
if seen(call): # 闸 3:重复调用检测(同参数反复调)
result = "重复调用已拦截"
else:
result = run_with_timeout(call, 30) # 闸 4:单次超时
messages.append(tool_msg(call.id, result))

四道闸缺一不可:最大步数防整体失控、调用次数防单工具刷屏、重复检测防模型陷入"失败→重试→失败"死循环、超时防单个工具卡死整个会话。

ReAct:谁产生 Thought、谁产生 Observation​

ReAct = Reason + Act 循环:

要素谁产生内容
Thought(思考)模型"用户要查订单,我应该先调 search_order"
Action(行动)模型选择工具 + 参数
Observation(观察)代码工具的真实执行结果

Thought/Action 由模型产生,Observation 由代码写回。 如果循环里不设 stop(停止条件),模型看不到真实工具结果时会自己编一个观察继续往下走——一本正经地"查询成功,订单已发货",全是幻觉。stop 参数在生成 "Observation:" 前截断,把控制权交还给你的代码,这就是它必需的原因。

ReAct 与 CoT 的区别:CoT(思维链)只是让模型"多想几步"再作答,全程不接触外部信息,纯推理;ReAct 在推理中穿插真实的工具执行,用外部观察修正推理。一句话:CoT 是想,ReAct 是边想边做。

三种实现怎么选​

实现特点适合
手写循环三步 + 四道闸全部自己写学原理、深度定制控制流
create_agent / AgentExecutor框架封装好循环与闸快速交付标准 Agent
LangGraph显式画状态图,节点/边/条件分支要拿回控制权:审批、暂停、多 Agent 编排

演进逻辑是"手写学原理 → create_agent 交付 → LangGraph 拿回控制权"。危险操作(删除数据、发邮件、支付)无论哪种实现,都要给工具加 confirmed 参数:第一次调用返回"请向用户确认",用户确认后带 confirmed=True 再执行。

高频面试题​

问题答题要点
模型会自己调工具吗?不会。它只输出 tool_calls(工具名+参数),执行永远在你的代码里
tool_calls 与 ToolMessage 的关系?一次响应可含多个 tool_calls;每条结果用 ToolMessage 回填,必须带 tool_call_id 配对,否则 400
工具报错该抛异常还是返回文本?返回可读文本。抛异常 Agent 崩溃且模型失去自我修复机会;错误是信息
ReAct 与 CoT 的区别?Observation 谁产生?CoT 纯推理不接外部,ReAct 推理中穿插真实工具结果;Observation 由代码写回,不是模型编的
stop 为什么必需?不截断的话模型会自编 Observation 继续幻觉;stop 把控制权交还代码
三种实现怎么选?手写学原理 → create_agent 交付 → LangGraph 画图拿回控制权;危险操作加 confirmed 参数