第 2 课:核心循环:从一次往返到持续运转
学习目标:
- 复述由 stop_reason 驱动的多轮循环四步,把一次工具调用往返接成一个能持续运转的 while 循环
- 用 stop_reason 的取值(tool_use / end_turn)判断循环该继续还是该停,说清它为什么就是循环的 while 条件
- 指出这个骨架循环缺了哪些边界,说明为什么每转一圈历史都在变长、不能只靠模型说「我说完了」来收尾
前置要求:读过第 1 课,知道 harness 是模型之外那层控制代码;会读一次工具调用往返的 tool_use / tool_result | 上一课 第 1 课 << | 下一课 第 3 课 >>
一次往返,不够用了
上一课你已经拆开过一次完整的工具调用往返:模型返回 stop_reason: "tool_use" 和一个 tool_use 块,你的宿主代码读出 name 和 input、真去执行、把输出打包成 tool_result 发回去,模型这才说出最终答案。三段 JSON,一趟就完事。
可真实任务很少这么客气。换个场景:你在写一个值班机器人,用户说「帮我把 api 服务重启一下,重启完看看日志里还有没有报错,有的话贴给我」。这一句话里其实压着两件事,而且第二件依赖第一件的结果——不重启完,看日志没意义。模型没法在第一轮就把两件事都办了,它只能:
- 第一轮返回
tool_use,调用 restart_service。你执行,把「重启成功」传回去。
- 第二轮又返回
tool_use,这次调用 read_logs。你执行,把日志内容传回去。
- 第三轮才返回
stop_reason: "end_turn",附上一句「重启完成,日志里有两条 timeout 报错,贴在下面」。
同一句用户请求,模型来回跑了三趟。它每一步能看到什么、下一步做什么,取决于上一步的 tool_result 里回来了什么——这正是 Anthropic 对 Agent 的定义:一个在循环里、靠环境反馈用工具的 LLM1。上一课那种一趟结束的往返,只是这个循环恰好只转了一圈的特例。这一课要做的,就是把「一次往返」接成「持续往返」,看清中间那个循环长什么样、由什么驱动、又该在哪里踩刹车。
循环的四步
把单次往返接成循环,其实不用发明任何新东西,只是把你已经会的那套动作重复做。Claude API 文档把这个多轮过程写成了一套固定步骤2:
- 你带着
messages 和 tools 清单发一次请求(tools 每一轮都要重新带上)。
- 模型回一个响应。如果它还需要用工具,
stop_reason 就是 "tool_use",content 里带一个或多个 tool_use 块。
- 你执行每一个
tool_use 块,把各自的输出做成 tool_result 块。这一步的关键是「每一个」——一轮响应里有几个 tool_use,下一条 user 消息里就得有几个对应的 tool_result,靠 tool_use_id 一一认领,全部打包进紧随其后的同一条 user 消息3。
- 你把模型那一轮的完整响应(
assistant 角色)和你拼好的 tool_result(user 角色)都追加进 messages,再发一次请求。
然后是最关键的一句:只要新响应的 stop_reason 还是 "tool_use",就从第二步再来一遍2。这个「只要……就重复」,就是把单次往返撑成循环的那根轴。上一课的例子只走到第一次 end_turn 就停了,是因为那个任务一趟就够;值班机器人那个例子会把第二步到第四步跑三遍,直到第三轮拿到 end_turn。
值得记住的是 tool_use 块和 tool_result 块各自的字段没变——tool_use 带 id / name / input,tool_result 带 tool_use_id / content、失败时还可以带 is_error3。循环没有改写这些字段的含义,它只是让这套字段被反复填写、反复回传。
stop_reason 就是这个循环的 while 条件
上面那句「只要 stop_reason 还是 tool_use 就重复」,翻译成代码就是一个 while 循环的判断条件。而这一课你最该带走的一句话是:**判断循环继续还是停下,就看 stop_reason 这一个字段。**它有很多取值,但对循环控制来说,先分清两个就够了:
"tool_use":模型还想用工具,它把请求交给你,等你执行完回传后再继续。循环转到下一圈。
"end_turn":模型不再要工具了,它认为话说完了。循环自然结束,你把最终文字交给用户。
一份讲 harness 工程的开源路线图把这层控制说得很直白:驱动 harness 的核心,就是那个「模型→工具→模型」的 while 循环4。而这个 while 的条件表达式,装的正是 stop_reason。同样一个模型、同样一套工具,它转几圈、什么时候停,全由宿主这边怎么读这个字段、怎么写这个条件决定——这也是为什么第 1 课说「同样的模型、不同的 harness,结果可能天差地别」4。
有一点要提前说清楚,免得把这个信号读反:stop_reason: "tool_use" 表示的是模型想要用工具,不是工具已经被用过了。模型自己从不执行任何东西,它只发出一段结构化的请求,真正跑工具的是你的宿主代码(或 Anthropic 的服务器),结果之后才回流进对话2。所以循环里 tool_use 出现的那一刻,动作还没发生;动作发生在你读出 name、input、去执行的那几行代码里。把「收到 tool_use」当成「工具跑完了」,是新手在从单次往返走向循环时最容易栽的一跤——它会让你误判循环现在到底转到了哪一步。
写成代码,就是这么几行
把这四步和 stop_reason 这个 while 条件落成 JavaScript,骨架短得出乎意料:
对着代码把四步再走一遍:while 那行就是「只要还是 tool_use 就重复」;循环体里先把 assistant 响应和 user 的 tool_result 双双 push 进 messages,再重新给 response 赋值。最后这次赋值是循环能停下来的前提——漏了它,response.stop_reason 永远是老值,while 就再也出不来了(这类死循环是第 4 课的主角)。
这段代码能跑,但它只是骨架,够简单到能看清循环本身,还远不能放心丢给生产。它默认模型总会规规矩矩地在某一轮回 end_turn,默认每个工具都能顺利执行,默认历史怎么长都无所谓——这三个「默认」,恰好是后面几课要逐个拆掉的。
每多转一圈,历史就长一截
回头盯着那行 messages.push:循环每转一圈,都会往 messages 里塞两条消息——模型的 assistant 响应、你回传的 tool_result。而下一次 callModel 又得把整个 messages 原样发出去。也就是说,这个循环转得越久,每一轮请求携带的历史就越长,而且只增不减。
这不是实现上的疏忽,是循环这种结构的固有性质:一个在循环里运转的 Agent,会不断产出更多可能与下一轮推理相关的数据5。值班机器人转三圈,攒下的还只是重启结果加一段日志;可要是任务需要几十轮,历史就会滚成一大坨。
这里藏着一个要留到「记忆与状态」那门课才展开、但现在必须先埋下的隐患:模型有一份「注意力预算」,每塞进一个新 token 都会消耗掉一点5。历史越长,token 越多,模型准确回忆起上下文里某条信息的能力反而越差5——注意这是一条随长度平缓下滑的性能曲线,不是过了某个长度就突然崩掉的悬崖5,别把它理解成「超了就废」。但方向是明确的:上下文得当成一种有限、且边际收益递减的资源来对待5。骨架循环那句无脑的 messages.push 完全没管这件事,它假设历史可以无限长——这个假设,「记忆与状态」那门课会来还账。
光有循环还不够,边界得另外加
现在你手里有一个能转起来的循环了。但「能转」和「转得住」是两码事。骨架循环把停不停的决定权完全交给了模型:它哪轮回 end_turn,循环哪轮停。可 Agent 是自己动态决定接下来做什么、用什么工具的系统1——这份自主正是它有用的地方,也正是风险所在:自主意味着更高的成本,以及误差沿着一圈圈循环累积放大的可能1。模型有可能连续跑很多轮,你得对它的决策有一定程度的信任才敢让它跑1。
问题在于,「信任」不等于「放任」。如果模型因为某个环节卡住、或者被工具返回的内容带偏,一直不肯回 end_turn,这个只认 stop_reason 的循环就会一直陪着它空转下去。所以在模型自己的收尾信号之外,通常还要另外加上显式的停止条件——比如给循环设一个最大轮次上限,来把控制权攥在自己手里1。骨架里那个光秃秃的 while (response.stop_reason === "tool_use") 没有这道保险:它信任模型,却没给自己留后路。
到这里,这一课的两个伏笔就都埋下了:这个循环需要边界(不能只靠模型说 end_turn,得有显式的停止条件——第 3 课),这个循环产出的历史需要治理(token 是有限资源,不能无脑往里塞——留给记忆与状态那门课)。而循环真正失控时会长成什么样、又该怎么兜住,是第 4 课的正题。这一课你只要先把「循环本身怎么转起来、由 stop_reason 驱动」这根轴立住就够了。
小结
- 把一次工具调用往返接成循环,不用新机制,只是重复四步:发请求 → 读
stop_reason 与 tool_use 块 → 执行工具并打包 tool_result → 追加历史后再发一次;只要 stop_reason 还是 tool_use 就重复2
stop_reason 就是这个循环的 while 条件:tool_use 表示模型还要用工具、循环继续,end_turn 表示模型收尾、循环自然结束——「模型→工具→模型」这根轴由它驱动4
tool_use 是「模型想用工具」的信号,不是「工具已跑完」的回执;模型自己从不执行,动作发生在宿主读出 name、input 去执行的那一步2
- 循环每转一圈,历史都在只增不减地变长,Agent 会不断产出更多可能相关的数据5;而注意力预算有限、上下文越长召回越差,token 得当成有限且边际递减的资源5
- 光有循环不够:自主性带来更高成本和误差累积,模型可能连续跑很多轮1,所以除了模型自己的
end_turn,通常还要加显式停止条件(如最大轮次)把控制权攥在自己手里1——具体怎么设,是第 3 课的正题
>> 第 3 课:停止条件:Agent 什么时候该收手