Zero 架构 · 第四课
第一课的循环有个 maxTurns = 12 的天花板 —— 那是最后的硬止损。但一个只靠天花板
兜底的 agent 会很糟:模型可能连着 12 回合空转、或对同一个坏调用反复撞墙、或一直只调工具
从不产出结论。护栏(guardState)就是循环用来提前识别这些失控
模式的一组有界计数器。
对齐 mission:一个真实的 coding agent,一半复杂度不在「让它能干活」,而在「让它别乱干」。 这一课解释 Zero 怎么把「模型可能不听话」这个前提,编码成具体的停止与提醒规则。
护栏状态挂在每次运行开头(newGuardState),在第一课循环的两个点被喂数据:
每个回合后 observeTurn、每个工具结果后 observeToolResult。
guardrails.go:353
它把失控分成两类处理:
一个回合既无可见文本、又无工具调用就是「空回合」。连续
maxEmptyTurns = 3 次就停止运行,以免在撞到 maxTurns 之前白烧回合。
任何一次有产出的回合都会把计数器清零。
guardrails.go:19、
guardrails.go:414
这是最精妙的一道。observeToolResult 按工具名 + 错误签名记录
连续失败次数(errorSignature 把错误输出归一成一个签名,
guardrails.go:252)。它是一个升级阶梯:
guardrails.go:381
toolFailureHintAt):注入一次性纠正提示 —— 把该工具的
schema + 精确错误喂回去,让模型自己改。toolFailureStopAt):halt。任何模型(弱或强)都不该
在一个坏调用上循环。isRetriableToolError 的门槛 —— 权限拒绝、沙箱阻断这类策略性失败不驱动它,因为
「照 schema 重写」根本修不了它们。
| 触发 | 推什么 | 阈值 |
|---|---|---|
| 只调工具、连着不产文本 | 提醒它「先把已知的综合成结论」 | 6 回合 |
多步任务却没调 update_plan | 提醒它建计划(not-called) | 到第 3 回合 |
| 调了计划但很久没更新 | 提醒它更新计划(stale) | 10 次调用未更新 |
| 停下但工作未完(headless 门) | continue-nudge:继续或标记完成 | 最多 3 次 |
关键:每条提醒都是一次性的(...ReminderSent 标志),或每个区间
一次(计划更新会开启新的 stale 区间并重置标志)。这避免了「每回合都唠叨同一句」的噪音。
guardrails.go:475、
guardrails.go:486
emptyTurns;工具成功清空失败连击;计划更新
开启新区间。计数器度量的是「连续的坏」,不是「累计的坏」。maxTurns 是「不管发生什么都停」的粗止损;护栏是「识别出具体失控模式就早停或早提醒」
的细止损。前者保证有限,后者保证高效地有限 —— 不浪费到天花板才发现早就在空转。
连续多少个「既无文本又无工具调用」的空回合会 halt?
同一工具用同一错误连续失败,循环怎么处理?
为什么只有「相同错误签名」的失败才累加计数?
less +14 ../zero/internal/agent/guardrails.go # 所有阈值常量一目了然
less +381 ../zero/internal/agent/guardrails.go # observeToolResult 升级阶梯
less +414 ../zero/internal/agent/guardrails.go # observeTurn 空回合/tool-only 计数
读的时候先只看顶部那组 const(:14)—— 每个数字就是一条策略。然后看两个
observe* 方法怎么在「进展时清零、连续坏时累加」。护栏的全部智慧就在这两件事里。
errorSignature 到底怎么把错误归一成签名的?」、
「headless 的 completion gate(RequireCompletionSignal)和这些护栏怎么协作?」,
或者「endsWithContinuationCue 怎么判断模型『话说到一半停了』?」