代码语言

知识点思维导图

41 个知识节点

Tool 与 Function Calling(03) - 完整 Tool Calling 循环:执行、回传与停止

读完后,你应能完成以下任务:

  • 给定一条模型工具请求,能实现“请求模型、执行工具、回传结果、再次请求模型”的循环,并用消息 Trace 证明每个调用和结果通过调用 ID 对应。
  • 给定工具超时、重复调用和连续空转三个故障,能设置最大步数、超时、幂等键和停止原因,并用运行日志证明循环会停止而不是无限消耗 Token。
  • 给定包含两个工具调用的模型响应,能选择串行或并行策略,输出决策表,并说明存在数据依赖或写副作用时为什么不能盲目并行。
  • 在文章沙盒运行订单查询闭环,输出三个场景的停止日志,验证正常任务以 final_answer 结束,未知工具和超步数分别以明确状态终止。

一、一次 tool call 不是一个完整应用

第一篇已经说明模型只生成调用请求。

但真实应用还要把工具结果送回模型。

因此 Tool Calling 不是一条函数调用语句,而是一个受控循环:

用户消息
  -> 模型响应
  -> 0 个或多个 tool_call
  -> 代码校验并执行
  -> tool_result 消息
  -> 再次请求模型
  -> 最终回答或下一轮 tool_call

循环成立必须有三个出口:

  • 模型返回最终回答。
  • 应用发现不可恢复错误。
  • 应用达到步骤、时间或成本上限。

没有出口的 Agent 循环只是一个没有停止条件的 while True

1.1 为什么工具结果必须回到消息历史

模型不会自动知道你的 Python 函数返回了什么。

应用要追加两类信息:

  • 模型原始的工具调用请求。
  • 与调用 ID 对应的工具结果。

如果只追加结果文本、不保留调用请求,后续请求可能违反供应商协议,也无法审计模型为什么调用该工具。

如果只保留调用请求、不追加结果,模型只能再次请求同一个工具或凭空猜答案。

二、把循环拆成五个稳定阶段

stateDiagram-v2
  [*] --> RequestModel
  RequestModel --> FinalAnswer: 普通回答
  RequestModel --> ValidateCalls: tool_calls
  ValidateCalls --> Failed: 协议或权限失败
  ValidateCalls --> ExecuteTools: 校验通过
  ExecuteTools --> AppendResults: 成功或受控错误
  AppendResults --> CheckBudget
  CheckBudget --> RequestModel: 预算未耗尽
  CheckBudget --> Stopped: 达到上限
  FinalAnswer --> [*]
  Failed --> [*]
  Stopped --> [*]

五个阶段分别是:

  1. 请求模型并保存原始响应。
  2. 识别普通回答或工具调用。
  3. 校验和执行每个工具。
  4. 追加结构化工具结果。
  5. 检查预算后进入下一轮。

这些阶段应该在 Trace 中拥有独立 Span。

否则工具慢、模型慢和循环空转都会表现成同一个“接口超时”。

三、消息契约必须保留调用 ID

一轮消息可以抽象成:

[
  {"role": "user", "content": "订单 A100 到哪了?"},
  {
    "role": "assistant",
    "tool_calls": [
      {"id": "call-1", "name": "query_order", "arguments": {"order_id": "A100"}}
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call-1",
    "name": "query_order",
    "content": {"status": "shipped"}
  },
  {"role": "assistant", "content": "订单 A100 已发货。"}
]

不同 SDK 的对象名称不同,但关联关系相同。

3.1 工具错误也要回传吗

可恢复错误可以作为工具结果回传,例如:

  • 查询没有结果。
  • 参数需要用户补充。
  • 下游暂时限流。

模型可以据此追问或换方案。

不可恢复或安全错误不应只交给模型自行处理,例如:

  • 工具不在白名单。
  • 当前身份没有权限。
  • 写操作缺少人工确认。
  • 达到全局成本上限。

这些情况应由应用停止循环并返回确定性状态。

四、停止条件要在运行前定义

至少设置以下预算:

预算 防止什么问题 典型停止状态
最大模型轮数 模型反复调用同一工具 max_steps_exceeded
单工具超时 一个依赖占满请求时间 tool_timeout
总运行时间 多个步骤累计超时 deadline_exceeded
最大工具调用数 并行或递归调用爆炸 tool_budget_exceeded
最大 Token / 成本 任务价值低于调用成本 cost_budget_exceeded

预算不是报错后的补丁。

它们是循环协议的一部分,应该进入运行配置和 Trace。

4.1 怎样识别空转

即使没达到最大步数,也可以提前识别:

  • 连续两次生成相同工具名和相同参数。
  • 工具已经返回确定性错误,模型仍原样重试。
  • 消息没有新增有效信息。
  • 同一幂等键的写操作被重复请求。

空转检测可以减少延迟和成本,但不能替代硬上限。

五、多个工具调用怎样执行

模型可能在一次响应中给出多个调用。

适合并行:

  • 查询两个互不依赖的数据源。
  • 多个只读工具没有共享限流瓶颈。
  • 结果顺序可通过调用 ID 恢复。

必须串行:

  • 第二个调用依赖第一个结果。
  • 多个调用修改同一资源。
  • 写操作需要逐项确认。
  • 工具共享事务或严格速率限制。

并行是执行器决策,不应仅因为模型一次返回多个调用就自动开启。

5.1 写工具为什么需要幂等键

网络超时可能发生在工具已经写入、但应用还没收到结果之后。

如果直接重试,就可能重复创建工单或重复退款。

幂等键可以来自:

  • 稳定的业务请求 ID。
  • 当前运行 ID 加工具调用 ID。
  • 业务对象和动作构成的唯一键。

工具执行器应保存幂等键对应的最终结果,并在重试时直接回放。

六、可运行源码:一个会停止的工具循环

示例使用固定模型脚本模拟三种响应,不需要 API Key。

重点是消息追加、调用 ID 和停止状态。

main.py

运行结果必须出现三个不同终止状态:

mode=normal status=final_answer
mode=unknown_tool status=tool_not_allowed
mode=loop_forever status=max_steps_exceeded

如果第三个场景持续运行,说明最大步数没有真正控制循环。

七、接入真实 SDK 时保留哪些接口

建议把实现拆成以下边界:

模块 输入 输出 不负责什么
Model Client 消息、工具定义、模型配置 普通回答或工具提议 不执行业务工具
Tool Registry 工具名 定义和处理器 不做模型请求
Policy Engine 身份、工具、参数、风险 允许、拒绝或待确认 不相信 Prompt 承诺
Tool Executor 已授权调用 工具结果或分类错误 不决定下一个步骤
Loop Controller 消息和预算 最终状态 不绕过前述边界

模块化后可以分别测试:

  • 模型适配是否正确解析协议。
  • 策略是否阻断越权和副作用。
  • 工具是否支持超时和幂等。
  • 循环是否在所有出口稳定停止。

八、Trace 至少记录什么

一次运行至少记录:

  • run_id 和会话 ID。
  • 模型、Prompt 和工具定义版本。
  • 每轮模型耗时与 Token。
  • 工具调用 ID、名称和脱敏参数摘要。
  • 授权决定和确认记录。
  • 工具耗时、重试和结果状态。
  • 最终停止原因。

不要只记录最终回答。

最终回答错误时,需要沿消息链找到第一个错误工具选择、参数或结果。

九、常见故障与排查

现象 第一个检查点 修复方式
模型反复调用相同工具 每轮工具名和参数摘要 加重复调用检测和最大步数
工具成功但模型继续说没结果 工具结果消息和调用 ID 按供应商协议回传完整结果
超时后重复创建资源 幂等键和工具最终状态 先查询原调用状态,再决定重试
多工具结果对应错位 调用 ID 与结果映射 不用数组位置或完成顺序猜关系
一个工具失败导致所有结果丢失 执行策略和错误分类 明确部分成功、全失败和补偿策略
成本突然升高 轮数、工具数和 Token 分段 设置预算并对空转提前停止

十、总结

  • Tool Calling 是受控循环,不是一次函数调用。
  • 模型工具提议和工具结果都要进入消息历史,并通过调用 ID 对应。
  • 最终回答、不可恢复错误和预算耗尽是三个必要出口。
  • 多工具是否并行由依赖和副作用决定,不由模型返回形式决定。
  • 最大步数、超时、幂等、重复检测和 Trace 共同保证循环可停止、可恢复、可审计。

10.1 参考资料

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“完整 Tool Calling 循环:执行、回传与停止”中,需要同时满足“一次 tool call 不是一个完整应用”与“为什么工具结果必须回到消息历史”。给定正文约束“因此 Tool Calling 不是一条函数调用语句,而是一个受控循环。”,哪些判断保持了原有处理机制?多选
2“完整 Tool Calling 循环:执行、回传与停止”出现偏差:“在“完整 Tool Calling 循环:执行、回传与停止 / 把循环拆成五个稳定阶段”中,即使不满足“1. 请求模型并保存原始响应”,结果与副作用仍会保持不变。”已成为实际行为。围绕“把循环拆成五个稳定阶段”与“消息契约必须保留调用 ID”,哪些判断能定位被改变的职责或边界?多选
3评审“完整 Tool Calling 循环:执行、回传与停止”方案时,验收条件包含“不可恢复或安全错误不应只交给模型自行处理,例如。”。关于“工具错误也要回传吗”与“停止条件要在运行前定义”的哪些决策符合正文机制?多选