Chapter 03

Agent 编排循环

上一章拆清了单次往返的报文和 stop_reason。这一章把单次往返放进一个 while 循环——这才是「agent」区别于「带工具的一次问答」的地方。

本章你将建立的 schema

  • ① agent loop 就是 while(stop_reason == "tool_use") { 执行 → 回填 → 再请求 };
  • ② 终止由 harness 决定,模型不会自己说「够了」;
  • ③ ReAct(2022) 是 prompt 出来的循环,原生工具调用把它训练进了模型;
  • ④ 错误恢复与并行 / 串行编排的取舍。

3.1循环本身:while stop_reason == tool_use

agent 不是一次调用,而是一个「请求→停机→执行→回填→再请求」的循环,直到模型不再请求工具。

02 章里的单次往返,到模型停机、harness 回填一次 tool_result 就结束了。把这一步包进 while 循环,让 harness 在回填后再发起下一轮请求——单步问答就变成了多步自主行为。循环的退出条件只有一个:模型这一轮的 stop_reason 不再是 tool_use。

agent-loop.py Python
messages = [{"role": "user", "content": user_input}]
while True:
    resp = client.messages.create(model=..., tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": resp.content})
    if resp.stop_reason != "tool_use":
        break                      # 模型不再请求工具 → 任务这一轮结束
    results = []
    for block in resp.content:
        if block.type == "tool_use":
            out = run_tool(block.name, block.input)   # 你的代码真正执行
            results.append({"type": "tool_result",
                            "tool_use_id": block.id, "content": out})
    messages.append({"role": "user", "content": results})  # 回填
while 循环在 harness 侧,不在模型里 · stop_reason 唯一的继续 / 停止信号 · run_tool 模型看不到的真实执行。
用户输入 调用模型 (带 tools) stop_reason == tool_use ? 执行工具 + 回填 tool_result 输出最终答案 是 回填后再请求 否
图 3.1agent loop 的真身:一个带回边的判定循环。注意:判定框和那条朱红回边都在 harness 里——模型每轮只走「调用模型」这一格,是否再转一圈由 stop_reason 和你的循环条件共同决定。

底层机制深一层:模型每一步都停机;循环、执行、回填全在 harness。每多一轮,都要把不断变长的对话历史整个重新发一遍(re-prefill),所以多步 agent 的 token 成本和延迟随轮数累积——一个 20 步的 agent,第 20 步要把前 19 步的所有内容再喂一次。

3.2ReAct → 原生工具调用

同一个控制循环,ReAct 是 prompt 出来的,原生工具调用是训练 + 结构化出来的。

ReAct(Yao et al. 2022)让一个冻结的大模型靠 few-shot prompt 交替输出 Thought → Action → Observation,用外部 API(如 Wikipedia)给 Action 落地,以对抗纯推理链的幻觉与错误累积。这里的 Action 和 Observation 都是被解析的散文。

原生 function calling 把 ReAct 的这个循环用后训练内化了:Thought / Action / Observation 的模式变成模型训练进去的行为,接口从脆弱的「解析散文」换成 typed JSON。

ReAct (2022)·prompt 出来的 Thought(散文) Action(散文→解析) Observation(散文) 原生 (今天)·训练进模型 (隐式) 推理 tool_use(typed JSON) tool_result(结构化) 同一个循环,实现层不同
图 3.2ReAct 和原生工具调用是同一个 Thought→Action→Observation 循环。注意:差别只在实现层——ReAct 靠 few-shot prompt 和散文解析,原生靠后训练和 typed JSON。控制流没变。

一句收束:理解这点,就知道为什么「给模型工具」本质上还是在塑造它的生成行为,而不是给它接了根函数指针。

3.3谁决定「做完了」

模型停机只表示「这一步要不要调工具」;整个任务何时结束,由 harness 的循环条件决定。

模型自己不能可靠判断「完成」。它能判断的只是「下一步要不要再调一个工具」,而不是「整件任务到此为止」。任务级的终止,必须由 harness 用确定性的 guard 兜底。常见 guard(都在 harness 侧):

  • max_iterations 硬上限——循环转过 N 轮无条件中止。
  • 最大执行时间——挂钟超时即中止。
  • 重复调用检测——同样的工具、同样的参数反复调,判定为卡死。
  • 无进展检测——若干轮内状态没有有效推进即中止。
陷阱

没有 max_iterations 上限时,模型若一直请求工具,循环就一直转——真实事故里有 agent 因此烧掉上千美元。模型不会自己喊停,硬上限是必需的。

想一想

「prompt 里写『一直重试直到成功』,再没有 max_iterations,会出什么事?」

展开答案(先停 10 秒)

工具持续失败时,模型会无限重试,循环永不退出。终止条件不能交给模型的善意,必须由 harness 用确定性的 guard 兜底。

3.4错误恢复

工具失败时,把可读的错误回填给模型,它才能据此自我纠正。

工具执行失败时,把错误作为 tool_result 且 is_error: true 回填,并给一句模型可读的信息(缺了什么字段、合法取值是什么),模型据此能自我纠正。

反面是:把原始异常 / stack trace 直接塞回去,会用无关细节把模型带偏——它读的是给人看的报错,不是给模型看的提示。

error-result.py Python
# 让模型困惑:原始 stack trace
{"type": "tool_result", "tool_use_id": id, "is_error": True,
 "content": "Traceback ... KeyError: 'city'"}

# 让模型自纠:可读的错误 + 怎么改
{"type": "tool_result", "tool_use_id": id, "is_error": True,
 "content": "缺少参数 city。需要一个城市名,例如 'Shanghai'。"}
is_error 标记这一轮工具失败 · content 给模型可读的纠错线索,而非 stack trace。

3.5并行 vs 串行编排

并行暴露的是工具之间隐藏的耦合;有顺序前提或共享可变状态,就强制串行。

模型可在一个回合请求多个工具(02 章的并行调用)。独立的工具可以并发执行;但有依赖、有状态、有顺序前提的工具如果并行,会出不崩溃的隐蔽 bug——比如工具 A 读了本该由工具 B 先写入的共享状态,拿到旧值,agent 在被悄悄搞坏的状态上继续往下走。

一句机制:判断标准是两个调用之间有没有「谁先谁后」或「共享可变状态」——有就强制串行。

§本章 self-check

先合上教程,把答案写在纸上或编辑器里。写完再展开对照——直接点开等于把这一节当成再读一遍。

  1. agent loop 的循环和判定条件,运行在模型内部还是 harness 里?模型每一轮实际只做了什么?
  2. 一个工具持续失败,你的 agent 却停不下来。最缺的是哪一类机制?举出两种具体 guard。
  3. (设计层)两个工具调用,一个查库存、一个按库存下单。能并行吗?判断依据是什么?
答案(先做完再展开)
  1. 循环和判定条件都在 harness 里,不在模型内部。模型每一轮只做一件事:读当前对话、生成这一轮的输出(要么一个 tool_use 请求并停机,要么最终答案),然后停机交还控制。执行、回填、判断是否再转一圈,全是 harness。
  2. 缺的是 harness 侧的终止 guard。两种具体 guard:max_iterations 硬上限(转过 N 轮无条件中止);重复调用检测(同样的工具、同样的参数连续调用 N 次即中止)。也可用最大执行时间或无进展检测。
  3. 不能并行。判断依据:两个调用之间存在「谁先谁后」与「共享可变状态」——下单依赖查库存的结果(顺序前提),且两者读写同一份库存状态。并行会让下单读到旧库存,必须强制串行。
进阶挑战 · 刚好够不着

给两步任务写循环 + 终止 guard

任务:「查订单状态;若已超时则发起退款」。写出 agent loop 的骨架,并加入至少两个终止 guard,使它在工具反复失败时也能安全退出。

提示(卡住再展开)

至少要有 max_iterations 硬上限;再加一个「同样的工具 + 同样的参数连续调用 N 次就中止」的重复检测。