数据截至 (上游 commit 1eb2bb1ec895)
工具循环与 MCP:把模型的话落到真实工具
30 秒导读: 模型只会"说"要调哪个工具,真去调、把结果再喂回去让它接着想的是运行时。本章讲 Yao 的这套"手脚":一个最多 5 轮的工具循环(把 tool 结果回灌 LLM 直到它不再要工具),循环失败时的兜底委派,以及底层怎么把 MCP server 的工具翻成模型能调的 schema、怎么并行/顺序地执行它们。
本章只讲运行时执行,不讲 MCP 传输层协议(那在仓库 mcp/ 包,不属本 area)。上游怎么把 DSL 装载成 Assistant 见 01-loading;一次请求的完整生命周期见 02-pipeline;V8 里的 Hooks 见 03-hooks-jsapi。
1. 这是什么(零基础也能懂)
一句话定义: 当模型回一句"请帮我调 weather__get 查北京天气"时,这段代码负责真的去调、把返回值再交回模型、让它基于结果继续答或继续调,直到模型给出最终文字。
它解决什么问题: 一次问答里,模型常常要多步才能答完——先查天气、再查航班、最后综合。每一步模型都只输出"我要调 X",工具的真实结果得由运行时补上、回灌,模型才有下一步。这个"调→喂回→再想"的往复,就是工具循环(tool loop)。
一个直观场景:
用户:北京今天适合穿什么?
模型:(要调工具) weather__get(city=北京) ← 第 1 轮
运行时:真去调 → {"temp":5,"wind":"大"} ← 落到真实工具
模型:(拿到结果,再要工具) clothing__advise(temp=5) ← 第 2 轮
运行时:真去调 → {"advice":"厚外套"}
模型:今天北京 5℃ 风大,建议厚外套。 ← 无工具 = 收敛,结束
一句话直觉: 把工具循环想成模型和现实之间的传话筒 + 计次器——它替模型跑腿,但只肯跑固定几趟(默认 5 趟),跑不完就换个"擦屁股"的备用 agent 收场。
本节不出现代码细节。记住一件事:模型负责决策,运行时负责落地和收敛。
2. 先分清两条"重试",别混为一谈(最关键的一节)
Yao 里有两 条名字都像"重试"、但机制完全不同的循环。读代码最容易在这里迷路,先钉死区别。
| 维度 | 工具参数重试(retry) | 工具循环(tool loop) |
|---|---|---|
| 解决什么 | 模型这一轮参数填错了(缺字段、类型不符),让它改参数重来 | 模型答一步不够,要基于结果继续下一步 |
| 触发 | 工具执行报可修复错误 | 工具执行成功,但还要把结果喂回模型 |
| 次数上限 | maxToolRetries = 3(硬编码) | max_turn,默认 5(可配) |
| 每轮塞给模型 | 结果 + 一段重试 system 提示 | 结果,不加重试提示 |
| 代码位置 | agent/assistant/agent.go:366 起 | agent/assistant/loop.go:32 executeToolLoop |
| 消息拼装 | buildToolRetryMessages(agent.go) | buildToolLoopMessages(loop.go) |
它俩前后接力,不是二选一:
一次请求里,先跑参数重试块,再进工具循环
────────────────────────────────────────────────
① 模型首答带 tool_calls
│
▼
② 参数重试块(agent.go:369 for attempt<3)
├─ 执行工具 → 全成功? ── 是 ─┐
├─ 有可修复错? 改参数重来(≤3) │ 这里只跑"第 1 轮"的工具,
└─ 有不可修复错? 直接失败退出 │ 拿到干净结果
▼
③ 工具循环(agent.go:549 → loop.go) 把结果喂回模型,
└─ 循环 ≤ max_turn 轮,直到模型不再要工具
一句话:参数重试管"这一步调对没有",工具循环管"要不要再来一步"。 本章主讲 ③,①②(参数重试)的细节属于 02-pipeline 的主管道叙事,这里只借它交代边界。
依据:
agent.go:365硬编码maxToolRetries := 3;loop.go:178getMaxToolLoopTurns默认5。两处常量、两个函数,互不共享。