Agent Mentor Learn
Agent Harness 基础:循环与控制 · 第 2 / 6 节

第 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 块,你的宿主代码读出 nameinput、真去执行、把输出打包成 tool_result 发回去,模型这才说出最终答案。三段 JSON,一趟就完事。

可真实任务很少这么客气。换个场景:你在写一个值班机器人,用户说「帮我把 api 服务重启一下,重启完看看日志里还有没有报错,有的话贴给我」。这一句话里其实压着两件事,而且第二件依赖第一件的结果——不重启完,看日志没意义。模型没法在第一轮就把两件事都办了,它只能:

  1. 第一轮返回 tool_use,调用 restart_service。你执行,把「重启成功」传回去。
  2. 第二轮又返回 tool_use,这次调用 read_logs。你执行,把日志内容传回去。
  3. 第三轮才返回 stop_reason: "end_turn",附上一句「重启完成,日志里有两条 timeout 报错,贴在下面」。

同一句用户请求,模型来回跑了三趟。它每一步能看到什么、下一步做什么,取决于上一步的 tool_result 里回来了什么——这正是 Anthropic 对 Agent 的定义:一个在循环里、靠环境反馈用工具的 LLM1。上一课那种一趟结束的往返,只是这个循环恰好只转了一圈的特例。这一课要做的,就是把「一次往返」接成「持续往返」,看清中间那个循环长什么样、由什么驱动、又该在哪里踩刹车。

循环的四步

把单次往返接成循环,其实不用发明任何新东西,只是把你已经会的那套动作重复做。Claude API 文档把这个多轮过程写成了一套固定步骤2

  1. 你带着 messagestools 清单发一次请求(tools 每一轮都要重新带上)。
  2. 模型回一个响应。如果它还需要用工具,stop_reason 就是 "tool_use"content 里带一个或多个 tool_use 块。
  3. 你执行每一个 tool_use 块,把各自的输出做成 tool_result 块。这一步的关键是「每一个」——一轮响应里有几个 tool_use,下一条 user 消息里就得有几个对应的 tool_result,靠 tool_use_id 一一认领,全部打包进紧随其后的同一条 user 消息3
  4. 你把模型那一轮的完整响应(assistant 角色)和你拼好的 tool_resultuser 角色)都追加进 messages,再发一次请求。

然后是最关键的一句:只要新响应的 stop_reason 还是 "tool_use",就从第二步再来一遍2。这个「只要……就重复」,就是把单次往返撑成循环的那根轴。上一课的例子只走到第一次 end_turn 就停了,是因为那个任务一趟就够;值班机器人那个例子会把第二步到第四步跑三遍,直到第三轮拿到 end_turn

值得记住的是 tool_use 块和 tool_result 块各自的字段没变——tool_useid / name / inputtool_resulttool_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 出现的那一刻,动作还没发生;动作发生在你读出 nameinput、去执行的那几行代码里。把「收到 tool_use」当成「工具跑完了」,是新手在从单次往返走向循环时最容易栽的一跤——它会让你误判循环现在到底转到了哪一步。

写成代码,就是这么几行

把这四步和 stop_reason 这个 while 条件落成 JavaScript,骨架短得出乎意料:

对着代码把四步再走一遍:while 那行就是「只要还是 tool_use 就重复」;循环体里先把 assistant 响应和 usertool_result 双双 pushmessages,再重新给 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_reasontool_use 块 → 执行工具并打包 tool_result → 追加历史后再发一次;只要 stop_reason 还是 tool_use 就重复2
  • stop_reason 就是这个循环的 while 条件:tool_use 表示模型还要用工具、循环继续,end_turn 表示模型收尾、循环自然结束——「模型→工具→模型」这根轴由它驱动4
  • tool_use 是「模型想用工具」的信号,不是「工具已跑完」的回执;模型自己从不执行,动作发生在宿主读出 nameinput 去执行的那一步2
  • 循环每转一圈,历史都在只增不减地变长,Agent 会不断产出更多可能相关的数据5;而注意力预算有限、上下文越长召回越差,token 得当成有限且边际递减的资源5
  • 光有循环不够:自主性带来更高成本和误差累积,模型可能连续跑很多轮1,所以除了模型自己的 end_turn,通常还要加显式停止条件(如最大轮次)把控制权攥在自己手里1——具体怎么设,是第 3 课的正题

>> 第 3 课:停止条件:Agent 什么时候该收手

Footnotes

  1. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3 4 5 6 7

  2. How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works 2 3 4 5

  3. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  4. The 2026 Agent Engineering Roadmap — GitHub (codejunkie99/agent-roadmap-2026) — https://github.com/codejunkie99/agent-roadmap-2026 2 3

  5. Effective context engineering for AI agents — Anthropic Engineering — https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents 2 3 4 5 6 7

练习

01

一个日程助理 Agent 配了 search_calendar(查日历)和 create_event(建日程)两个工具。用户说「看看我周四下午有没有空,有的话约个 30 分钟的评审会」。宿主用本课的骨架循环驱动它,实际发生的 stop_reason 序列如下:

Level 1:数一数这个循环转了几圈
  • 第 1 次 callModel 返回 → stop_reason: "tool_use"(一个 tool_use 块,调 search_calendar
  • 第 2 次 callModel 返回 → stop_reason: "tool_use"(一个 tool_use 块,调 create_event
  • 第 3 次 callModel 返回 → stop_reason: "end_turn"(文字:「已约周四 14:00 的评审会」)

请回答:(1) callModel 一共被调用了几次?(2) executeTool 一共被调用了几次?(3) while 循环体一共执行了几遍?(4) 循环是在读到哪个 stop_reason 时退出的?

完成标准 · 本地勾选
02

同事想把单次往返改成多轮循环,写了下面这段代码。它在只需要一次工具调用的任务上「看起来能用」,可一旦任务需要模型连续调用两次工具,进程就卡死、资源被耗尽,日志里同一个工具被反复调用。找出根本原因,并改对。

Level 2:这个循环为什么停不下来
完成标准 · 本地勾选