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。
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}) # 回填
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。
一句收束:理解这点,就知道为什么「给模型工具」本质上还是在塑造它的生成行为,而不是给它接了根函数指针。
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 直接塞回去,会用无关细节把模型带偏——它读的是给人看的报错,不是给模型看的提示。
# 让模型困惑:原始 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'。"}
3.5并行 vs 串行编排
并行暴露的是工具之间隐藏的耦合;有顺序前提或共享可变状态,就强制串行。
模型可在一个回合请求多个工具(02 章的并行调用)。独立的工具可以并发执行;但有依赖、有状态、有顺序前提的工具如果并行,会出不崩溃的隐蔽 bug——比如工具 A 读了本该由工具 B 先写入的共享状态,拿到旧值,agent 在被悄悄搞坏的状态上继续往下走。
一句机制:判断标准是两个调用之间有没有「谁先谁后」或「共享可变状态」——有就强制串行。
§本章 self-check
先合上教程,把答案写在纸上或编辑器里。写完再展开对照——直接点开等于把这一节当成再读一遍。
- agent loop 的循环和判定条件,运行在模型内部还是 harness 里?模型每一轮实际只做了什么?
- 一个工具持续失败,你的 agent 却停不下来。最缺的是哪一类机制?举出两种具体 guard。
- (设计层)两个工具调用,一个查库存、一个按库存下单。能并行吗?判断依据是什么?
答案(先做完再展开)
- 循环和判定条件都在 harness 里,不在模型内部。模型每一轮只做一件事:读当前对话、生成这一轮的输出(要么一个
tool_use请求并停机,要么最终答案),然后停机交还控制。执行、回填、判断是否再转一圈,全是 harness。 - 缺的是 harness 侧的终止 guard。两种具体 guard:
max_iterations硬上限(转过 N 轮无条件中止);重复调用检测(同样的工具、同样的参数连续调用 N 次即中止)。也可用最大执行时间或无进展检测。 - 不能并行。判断依据:两个调用之间存在「谁先谁后」与「共享可变状态」——下单依赖查库存的结果(顺序前提),且两者读写同一份库存状态。并行会让下单读到旧库存,必须强制串行。
给两步任务写循环 + 终止 guard
任务:「查订单状态;若已超时则发起退款」。写出 agent loop 的骨架,并加入至少两个终止 guard,使它在工具反复失败时也能安全退出。
提示(卡住再展开)
至少要有 max_iterations 硬上限;再加一个「同样的工具 + 同样的参数连续调用 N 次就中止」的重复检测。