Agent Harness 基础:循环与控制 · Lesson 6 of 6

第 6 课:实战:手写一个带控制的 Agent Harness

学习目标:

  • @anthropic-ai/sdk 把前两课的 stop_reason 循环写成一个能跑通的 while 循环,自己判断该继续调工具还是返回文字收尾
  • 按规范构造 tool_usetool_result 内容块,把一轮里的多个结果拼进同一条 user 消息回传
  • 给这个循环装上四道控制阀(最大轮次、预算上限、无进展检测、高影响操作审批),并说清每道阀该插在循环的哪一步

前置要求:读过第 2 到第 5 课,理解 stop_reason 驱动的循环、停止条件、失控兜底与人在环干预 | 上一课 第 5 课 <<

先看它跑起来的样子

前五课全是拆零件:循环怎么转、什么时候停、失控长什么样、人怎么干预。这一课把它们焊成一个能跑的最小 harness。先别看代码,先看它在终端里跑起来是什么样——一个配了两个玩具工具(get_time 报时、read_file 读项目内文件)的 Agent,接到一句「读一下 README.md 的第一行,再告诉我现在几点」:

text
$ node agent.js "读一下 README.md 的第一行,再告诉我现在几点"
[turn 1] 模型请求工具: read_file({"path":"README.md"})[turn 1] 工具返回: "# Agent Harness 基础\n..."[turn 2] 模型请求工具: get_time({})[turn 2] 工具返回: "2026-08-26T10:42:07+08:00"[turn 3] 模型收尾 (end_turn)
README.md 第一行是「# Agent Harness 基础」,现在是 2026 年 8 月 26 日 10:42。用了 2 轮工具调用,共 3 次模型请求。

看清楚这里发生了什么:用户只说了一句话,几次调工具、先调哪个、什么时候停,全是模型在循环里自己定的——这正是 Agent 区别于工作流的地方,工作流的路径是代码写死的,Agent 是模型在循环里动态主导流程、自己决定用什么工具1。宿主代码(就是我们这一课要写的 harness)没有规定「先读文件再报时」,它只是老老实实地转循环、执行模型点名的工具、把结果喂回去。这一次两个工具都无害,所以一路畅通没打断;但这套 harness 还焊着一道审批阀——真碰上删文件、发请求这类高影响操作,它会在动手前停下来等人点头(本课后面就写它)。这一课剩下的篇幅,就是把这段终端输出背后的代码,一行行搭出来。

核心循环:把骨架搬进来,换成真 SDK

第 2 课那个 callModel 是伪代码,现在换成真的 @anthropic-ai/sdk。循环的骨架一模一样:带着 messages 发请求,看 response.stop_reason——是 "tool_use" 就执行工具、拼回结果再发一次,不是(比如 end_turn)就返回文字、跳出循环2

先看不带任何控制阀的最小版,好把循环本身看清楚:

和第 2 课的骨架对着看,结构没变:while 那行还是「只要 stop_reason 还是 tool_use 就重复」,循环体还是「push assistant → 执行工具 → push tool_result → 重新赋值 response」那四步。唯一的实质变化是 callModel 变成了 client.messages.create(...),以及循环体末尾那次重新赋值——它是循环能停下来的前提,漏了它 stop_reason 永远是老值,就成了第 4 课讲的死循环。

tool_use / tool_result 的字段,按规范一个不漏

runToolUses 是把「模型点名的工具」真正跑起来的地方。这里最容易出错的是内容块的字段,照规范来:tool_use 块带 id / name / inputtool_result 块带 tool_use_id(认领是哪一次调用的结果)/ content,工具执行失败时再加一个 is_error: true3。还有一条硬规则:一轮响应里有几个 tool_use 块,就得回几个 tool_result,并且全部塞进紧随其后的同一条 user 消息里3——上面循环体那句 messages.push({ role: "user", content: toolResults }) 就是在守这条规则。

注意 try/catch:工具跑挂了不该让整个 harness 崩,而是把错误包成 is_error: truetool_result 传回去,模型看到后有机会换个参数重试或换条路走。这比直接抛异常、让进程死掉要稳得多。

装上四道控制阀

到这里循环能转了,但它是第 2 课那个「信任模型、不给自己留后路」的裸循环:模型哪轮回 end_turn 它哪轮停,中间不设任何边界。而 Agent 的自主性意味着更高的成本,以及误差沿着一圈圈循环累积放大的可能,模型有可能连续跑很多轮1——裸循环把停不停完全押在模型身上,太险。现在把前几课的四道阀逐个焊上去。

四道阀各自守一件事,位置都不是随便放的:

  • 阀1 最大轮次(第 3 课):turns >= MAX_TURNS 放在循环体最前、turns++ 之前。含义是「进这一圈之前先检查还允不允许再转」。这道显式停止条件是为了在模型自己的 end_turn 之外,把控制权攥在自己手里1
  • 阀2 预算上限(第 4 课):每次拿到响应就用 response.usage 累加 token,到顶即停。轮次少但每轮上下文巨大时,光靠轮数拦不住烧钱,得靠 token 这道独立的闸。
  • 阀3 无进展检测(第 4 课):把这一轮的工具调用「拍平成一个签名」,和上一轮比,一样就判空转。这拦的是那种「轮次没超、预算没爆,但模型在原地打转、反复调同一个工具同样参数」的死水局面。
  • 阀4 审批阀(第 5 课):在 runToolUses 里、真正执行工具之前,对高影响操作先要一个人工确认。对高影响动作引入人在环审批,正是抑制过度授权风险的推荐手段4

阀3 的签名函数很朴素——把这一轮所有 tool_use 块的名字和参数拼成一个字符串,能区分「调了什么、参数是什么」就够:

审批阀:卡在「执行之前」那一刻

四道阀里,审批阀的位置最讲究,也最容易写错。它必须卡在「模型点了名、但工具还没真的跑」的那一刻——先打印将要执行的动作,等人确认,确认了才执行。放晚一步,文件就已经被写了、请求就已经发出去了,再问「确认吗」毫无意义。所以它得写进 runToolUses 里、impl(...) 那一行之前:

approve 是外面传进来的一个函数,在终端里就是「打印动作、读一行输入」:

一个关键细节:即便用户拒绝,也要回一个 is_error: truetool_result,而不是什么都不返回。因为规范要求每个 tool_use 都得有对应的 tool_result 回传3;漏掉它,下一次请求就会因为「有个工具调用没有结果」而报错。拒绝不等于无视,拒绝也是一种要如实告诉模型的结果——模型收到「被拒绝」后,往往会改走一条不需要高影响操作的路。

两个玩具工具:把循环真正跑通

控制阀都装好了,还差能被调用的工具。这一课只用两个绝对安全的玩具,把危险操作挡在门外:get_time 报当前时间,read_file 读文件——但用 path.resolve 把它死死限制在项目目录内,防止模型(或被工具输出带偏后)去读 /etc/passwd 这类越界路径:

这两个工具都不在 HIGH_IMPACT 集合里,所以不触发审批——它们本就无害。要演示审批阀,把一个 write_file 加进 toolImplsHIGH_IMPACT 就行,本课刻意不引入真的写操作,免得跑示例时改坏你的文件。

拼起来:一个能 node agent.js 的入口

最后把 runAgentrunToolUses、工具定义、审批函数凑成一个能直接跑的入口,就是本课开头那段终端输出背后的东西:

把前面几段代码(importclientMODELrunAgentrunToolUsessignatureOfapproveInTerminaltoolImplstoolsmain)放进一个 agent.js,设好 ANTHROPIC_API_KEYnpm i @anthropic-ai/sdk,就能 node agent.js "你的任务" 跑起来。

回头看这一百来行代码,会发现它没有一处是新概念:while 循环和 stop_reason 是第 2 课的,MAX_TURNS 是第 3 课的,预算和空转检测是第 4 课的,审批阀是第 5 课的。**harness 不是某个高深的框架,就是这层「你亲手写、亲手控制」的循环加几道阀。**同样的模型、同样两个工具,装了这四道阀的 harness 和第 2 课那个裸循环,跑同一个任务的稳当程度可以天差地别——因为决定 Agent 靠不靠谱的,很大程度是外面这层控制代码,而不只是里面那个模型5

也别忘了给复杂度留个分寸:这四道阀不是每个 Agent 都必须全上,值得记住的一条是,只在复杂度确实能改善结果时才考虑增加它1。一个只在受控环境里跑三五轮的小工具,也许 MAX_TURNS 一道阀就够了;四道阀是给那些「会连续跑很多轮、还可能碰高影响操作」的场景准备的。

小结

  • 一个能跑的 harness 核心还是第 2 课那个循环:带 messages 请求 → 看 stop_reason,是 tool_use 就执行工具、拼 tool_result 回传再发一次,不是就返回文字收尾2;换成真 SDK 只是把 callModel 变成 client.messages.create(...)
  • 内容块字段按规范一个不漏:tool_useid / name / inputtool_resulttool_use_id / content、失败加 is_error;一轮里几个 tool_use 就回几个 tool_result,全塞进紧随其后的同一条 user 消息3
  • 四道控制阀各守一处、位置不能乱:最大轮次(第 3 课)和预算上限(第 4 课)是「循环一定会停」的硬边界,无进展检测(第 4 课)拦原地打转,审批阀(第 5 课)必须卡在工具执行之前——因为 Agent 的自主性带来更高成本和误差累积、模型可能连续跑很多轮1,光靠模型自己的 end_turn 收不住
  • 审批阀对高影响操作要人工确认,是抑制过度授权风险的推荐手段4;即便被拒也要回一个 is_errortool_result,别让调用悬空3
  • harness 不是高深框架,就是这层你亲手写、亲手控制的循环加几道阀——同样的模型换套控制代码,可靠性可以天差地别5;但也别过度堆阀,只在复杂度确实能改善结果时才考虑加1

你已经走完这门课。从「什么是 harness」到亲手写出一个带四道控制阀的循环,你现在手里有的不只是概念,而是一段能跑、能改、能往上加控制的真代码——去把它接上你自己的工具,让它替你干活吧。

Footnotes

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

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

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

  4. LLM06:2025 Excessive Agency — OWASP Gen AI Security Project — https://genai.owasp.org/llmrisk/llm062025-excessive-agency/ 2

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

Exercises

01

现在的审批阀只有「高影响就问、其余放行」两档。产品同学提了个更细的需求:希望按工具名分三档管控——allow(直接放行,如 get_time)、ask(执行前必须人工确认,如 write_file)、deny(一律拒绝、根本不许调,如某个已下线的 send_email)。请你给这个 harness 加上这道「策略阀」:设计它的数据结构,说清它该插在循环的哪一步、和现有审批阀是什么关系,并写出 deny 命中时该怎么回给模型。

Level 1:给 harness 再加一道「按工具名分级」的控制阀
Done criteria · checked locally
02

下面这段 harness 循环,同事说「能跑」,但只要模型不主动回 end_turn,或者陷进原地打转,它就会出事。请指出:(1) 它缺了哪些控制、各会导致什么失控现象;(2) 给出最小修复——至少补上一道能保证循环「一定会停」的硬边界,并说清补在哪一步。

Level 2:这个循环少了哪几道闸,会怎么失控
Done criteria · checked locally

My note

Jot down thoughts, sticking points, things you didn't get. Written to this course's appendix only — the lesson file is never touched.