05 · Verifier:什么证据足以让 Agent 停下
把“完成”拆成可审计的硬信号、预算信号和止损上限,避免让模型自评替代验证。
本章任务
要回答的问题
模型说“完成了”时,什么外部证据才足以让 Loop 真正停下?
读完你能
- 区分硬验证、软启发式和资源上限
- 按误判成本为任务选择验证信号
- 把完成声明、验证失败与继续原因写进事件流
- 适合现在读
- 正在定义完成条件、评测门禁、预算止损或人工复核的工程师
- 先修知识
- 读过 Agent Loop,理解停止条件与工具结果
- 实践产物
- 一张按风险分级的 Verifier 与完成门禁矩阵
- 证据边界
- 验证器只能覆盖已定义的 Oracle;任何信号都可能留下盲区
承接 Agent Loop:那一章给了三层 verifier 的概览。本章继续看每一层的源码、失败模式与选型逻辑。
什么证据足以让 Agent 停下
Section titled “什么证据足以让 Agent 停下”场景:Agent 完成一次数据库重构,单元测试全绿,也生成了非空 Diff,于是它宣布完成;但生产迁移脚本没有更新,真实部署仍会失败。测试退出码和“确实改过文件”都是证据,却都没有覆盖业务完成条件。
通过标准:完成门禁明确列出硬证据、软启发式和人工判断;每个信号都有适用范围与误判代价;任一关键 Oracle 缺失时状态是 needs_review,而不是把多个弱信号相加成“已完成”。
先区分证明、预算和止损
Section titled “先区分证明、预算和止损”三层在 4 个系统里覆盖度差别很大:
| 层级 | Codex | Claude Code | OpenClaw | Hermes |
|---|---|---|---|---|
| 外部可复核信号 | 宿主可接 tests 退出码;`apply_patch` 验证语法;`execpolicy` 约束命令;`goals.rs` 记录状态与预算 | query.ts 没有通用 verifier 插件口 | `before、after_tool_call` 等 hooks 可接宿主检查 | 主循环没有结构化外部完成门禁 |
| 软 verifier | Goal token_budget 加 retry backoff 加 Turn 上限 | `TOKEN_BUDGET` 90% 阈值加连 3 次 <500 token 判 diminishing returns | `tool-loop-detection.ts` 4 种 detector(generic_repeat、poll_no_progress、ping_pong、global_circuit_breaker) | IterationBudget 90/50 加 grace call |
| 放任型 verifier | 模型 `output_type: completed` | 流里没 `tool_use` block | lifecycle:end 事件 | 模型不发 tool_call 即结束 turn |
| 硬上限 | Turn 计数加服务端 ratelimit 加 `GOAL_BUDGET_LIMITED_METRIC` 上报 | maxTurns(默认很大) | runtime timeout;global circuit breaker 可选且源码默认关闭 | iteration_budget 耗尽走 grace call 强制总结 |
只比较会改变停止语义的实现
Section titled “只比较会改变停止语义的实现”Codex · 把不同层的完成信号分开记录
Section titled “Codex · 把不同层的完成信号分开记录”Codex 在 verifier 设计上的出发点是:模型说「我完成了」基本不能信。很多时候模型实际没有完成任务但它觉得自己完成了(比如改了一个文件就以为整个 bug 都修好了,但还有 3 个相关文件没改。或者跑了一次测试看到通过就以为修好了,但其实测试覆盖不全)。Coding 场景的好处是有大量客观可验证的信号:exit code 是 0 还是非 0、patch 语法对不对、lint 是否通过、tests 是否全 pass,这些信号都是机器可读不需要模型判断。
Codex 的源码把这些信号放在不同层处理;它们能减少只听模型自评的风险,但是否组成完成门禁要由宿主工作流决定。
goals.rs 的 GoalRuntimeEvent 把 turn、工具、预算与中止等运行事件送入同一状态机。它让状态和资源变化可记录;turn_completed 等字段仍来自上游流程,不能独立证明业务目标完成:
Codex codex/codex-rs/core/src/goals.rs:98-130 GoalRuntimeEvent: turn 完成 / 工具完成 / 中止统一收敛
pub(crate) enum GoalRuntimeEvent<'a> { TurnStarted { turn_context: &'a TurnContext, token_usage: TokenUsage, }, ToolCompleted { turn_context: &'a TurnContext, tool_name: &'a str, }, ToolCompletedGoal { turn_context: &'a TurnContext, }, TurnFinished { turn_context: &'a TurnContext, turn_completed: bool, }, MaybeContinueIfIdle, TaskAborted { /* ... */ },}更狠的是 execpolicy:每条 shell 命令都走 Allow、Prompt、Forbidden 三态决策器,把 boolean 通过或不通过的常见做法扩到三档:
Codex codex/codex-rs/execpolicy/src/decision.rs:7-28 execpolicy 三态决策
pub enum Decision { /// Command may run without further approval. Allow, /// Request explicit user approval; rejected outright /// when running with `approval_policy="never"`. Prompt, /// Command is blocked without further consideration. Forbidden,}Codex 里的四类控制各自对应一个可复核信号,但不应被读成“全 pass 就等于业务完成”:
1. apply_patch 校验(详见 06 章 V4A):模型生成的 patch 必须通过 V4A diff 算法的语法校验,不合法的 patch 直接拒绝执行,loop 强制重试。这层 verifier 防止「模型生成了一坨乱码假装是 patch」的情况。模型有时候会编造 patch(特别是上下文压缩之后忘了文件原始内容),不校验就直接 apply 会破坏文件。
2. run tests 退出码:如果宿主工作流配置了相关测试,非 0 可以作为失败信号并反馈给 loop。源码样本不能证明每个任务都会自动跑测试,也不能把测试通过解释成业务完成。
3. execpolicy::Decision:每条 shell 命令执行前过三态决策器:Allow、Prompt 或 Forbidden。它是命令级策略,不是结果 verifier;真正的隔离和风险边界还取决于 sandbox、权限与部署配置。
4. goals.rs 收敛检测:Goal 的 token_budget 耗尽时走 GOAL_BUDGET_LIMITED_METRIC 指标上报加 steering(注入 system message 引导模型「先把当前进度叙述清楚再退出」),强制 loop 不能在没说清楚的情况下硬退。这层 verifier 防止「token 烧完模型 silent fail」的情况。没有 steering 模型会直接断在某个工具调用中间,用户看到一半的执行不知道发生了什么。
这些信号组合后能提高可审计性,但“任一失败都继续、全部通过就完成”并不是这四个源码片段共同保证的协议。它们主要适用于 coding;写 PRD 或做调研时,要另行定义结果评审和终止条件。
Claude Code · 60 行算法判 token 收敛速率
Section titled “Claude Code · 60 行算法判 token 收敛速率”Claude Code 在 verifier 设计上的出发点是:作为 IDE 集成的 coding agent,硬 verifier(exit code、tests)的接入成本太高(用户的项目可能没测试、测试可能很慢、IDE 不该擅自跑测试),但完全靠模型自评又会出现「模型啰嗦没完没了」(特别是用户问个简单问题模型却写了一大篇分析)。
所以 Claude Code 选择把软 verifier 做得很深:用 token 收敛速率做主要判退信号,便宜(不用跑外部命令)、快速(每轮就能算)。它适合能观察 token 增量的任务,但阈值仍要按模型和工作流校准。
实际实现在 query/tokenBudget.ts 几十行,把按 token 收敛速率判退做成一个标准算法:
Claude Code claude-code/src/query/tokenBudget.ts:1-82 checkTokenBudget: 90% 阈值 + 连 3 轮 <500 token 判 diminishing
const COMPLETION_THRESHOLD = 0.9const DIMINISHING_THRESHOLD = 500
export function checkTokenBudget( tracker: BudgetTracker, agentId: string | undefined, budget: number | null, globalTurnTokens: number,): TokenBudgetDecision { if (agentId || budget === null || budget <= 0) { return { action: 'stop', completionEvent: null } }
const turnTokens = globalTurnTokens const pct = Math.round((turnTokens / budget) * 100) const deltaSinceLastCheck = globalTurnTokens - tracker.lastGlobalTurnTokens
const isDiminishing = tracker.continuationCount >= 3 && deltaSinceLastCheck < DIMINISHING_THRESHOLD && tracker.lastDeltaTokens < DIMINISHING_THRESHOLD
if (!isDiminishing && turnTokens < budget * COMPLETION_THRESHOLD) { tracker.continuationCount++ /* ... nudge model to continue ... */ return { action: 'continue', /* ... */ } }
/* over budget OR diminishing returns → stop */ return { action: 'stop', /* ... */ }}三个数字决定 loop 形态:0.9 是软边界,500 是 token 增量阈值,3 是连续轮数。三者组合后,源码把低增量状态标成 diminishing returns。它展示了如何把启发式写进运行时,但没有证明这组常量适合所有模型或任务。
算法看的是 token 增量趋势,而不是绝对 token 数。相对趋势更容易迁移,但 500 和连续轮数仍然是阈值,换模型或任务后需要重测。
但 Claude Code 这套做得再好也只是软 verifier,没有硬 verifier 接口。query.ts 1729 行单文件没开放外部 hook,想让 loop 强制等 pnpm test pass 再退出,只能 fork 整个文件改。stopHooks 系统只允许反向门控(阻止模型自停,让 loop 继续跑),不接「必须先通过外部检查」这种正向要求(强制必须 pass 才能退)。
这份快照的边界很清楚:Claude Code 没有内置的正向门禁,不能配置成“外部测试通过后才能退出”。需要「测试通过才能 PR」的工作流,要由宿主另接门禁或维护 fork;是否适合取决于外围集成。
OpenClaw · verifier 中间件化加 4 种 loop detector
Section titled “OpenClaw · verifier 中间件化加 4 种 loop detector”OpenClaw 在 verifier 设计上的出发点是:作为通用 agent 控制面(不只服务 coding),verifier 不能写死在 loop 里。不同企业、不同业务场景对「什么算完成」的定义完全不同(销售 agent 完成等于 CRM 字段全填、客服 agent 完成等于工单关闭、coding agent 完成等于 tests pass),如果 verifier 是写死的就没法适配多样性。
一种可行做法是把 verifier 做成中间件接口,让使用者注册具体的场景检查,而不是把完成定义写死。
实际实现是 tool-policy-pipeline 把 verifier 抽象成可注册的中间件,十几个 hook 点(before_tool_call「调用前」、after_tool_call「调用后」、tool_result_persist「结果持久化前」等)让外部任意挂 lint check、typecheck、human approval、业务规则验证。
比如想给某个企业 agent 加「PR 必须有 reviewer 才能 merge」的 verifier?写个 plugin 注册到 after_tool_call 即可,不用改 OpenClaw 源码。
但这种「verifier 中间件化」只解决了外部能注入检查的问题,还有一个 OpenClaw 内部要解的问题:loop 死循环检测。模型有时候会陷入「不停调用同一个工具」或「在两个工具之间来回切」的死循环,单纯靠用户写中间件不一定及时发现,所以 OpenClaw 内置了一个专门的子系统 tool-loop-detection.ts,4 种 detector 分管不同的死循环模式:
OpenClaw openclaw/src/agents/tool-loop-detection.ts:9-42 4 种死循环检测器 + 阈值常量
export type LoopDetectorKind = | "generic_repeat" // 同一调用重复 | "known_poll_no_progress" // command_status / process poll 无进展 | "global_circuit_breaker" // 总数超线 | "ping_pong"; // A→B→A→B 摆动
export const TOOL_CALL_HISTORY_SIZE = 30;export const WARNING_THRESHOLD = 10;export const CRITICAL_THRESHOLD = 20;export const GLOBAL_CIRCUIT_BREAKER_THRESHOLD = 30;四种 detector 各自针对一种典型死循环模式,设计取舍如下:
generic_repeat:同一调用重复达阈值就警告。实现是哈希 toolName 加参数稳定序列化(用 sha256(stableStringify(params)) 而不是直接 JSON.stringify,避免 key 顺序不同造成漏检:同一参数 {a:1,b:2} 和 {b:2,a:1} JSON.stringify 出来不同但语义相同),最近 30 次调用里同一哈希出现达 10 次警告(WARNING_THRESHOLD),20 次熔断(CRITICAL_THRESHOLD)。这层主要拦截「模型卡在某个工具里反复调」的情况。
known_poll_no_progress:专门识别 command_status 和 process: poll/log 两种长轮询调用。这两个工具的语义是「轮询某个状态」,重复调用本身合法(每秒 poll 一次正常),但调用结果一直没变化就异常(说明被 poll 的进程已经卡死或者根本没启动)。实现是 hash 包含调用结果文本,结果无变化才算「无进展」(区别于 generic_repeat 只看输入)。这层拦截「模型一直 poll 一个不会变的状态」浪费 token 的情况。
ping_pong:识别 A→B→A→B 摆动。两个工具互相依赖时模型可能陷入「调 A 看到结果不满意调 B 修一下又回来调 A 看结果」的循环。这层拦截「两个工具互相 cancel 对方的工作」的情况。
global_circuit_breaker:启用 loop detection 后,30 次总阈值可作为模式无关的兜底。它只限制调用次数,不识别任务是否仍在推进;默认关闭时也不会生效。
默认 enabled: false。OpenClaw 不强制每个 session 开 loop detection,因为有合法的高重复 workflow(比如 watch + recompile 这种持续监控类的工作就是天然要重复调用同一个工具几十次),开 loop detection 反而误伤。这是 OpenClaw 给用户的开关:需要的时候打开,不需要就关掉。
Hermes · 单 loop 无硬 verifier,verifier 跨会话累积
Section titled “Hermes · 单 loop 无硬 verifier,verifier 跨会话累积”Hermes 把长跑助理的反馈拆到 loop 之后:每次运行做事后分析并写回 memory,下次类似任务可以 prefetch 这些记录。源码支持这条反馈路径,但不能据此断言后续运行一定更聪明;需要测量成功率、回归和错误记忆。
在本文固定的源码快照里,Hermes 单 loop 的父预算是 90 步、subagent 是 50 步,耗尽后进入 grace call 与总结。agent/insights.py 不是运行时 verifier,而是事后的会话分析引擎,看 token、成本、工具分布:
Hermes hermes-agent/agent/insights.py:1-17 insights.py 是事后分析引擎,不是 loop 内 verifier
"""Session Insights Engine for Hermes Agent.
Analyzes historical session data from the SQLite state database to producecomprehensive usage insights: token consumption, cost estimates, tool usagepatterns, activity trends, model/platform breakdowns, and session metrics.
Inspired by Claude Code's /insights command, adapted for Hermes Agent'smulti-platform architecture with additional cost estimation and platformbreakdown capabilities."""Hermes 的 verifier 设计可以分三个时间维度:
运行时(loop 内):只有 IterationBudget(90/50) 软上限加 grace call 兜底。父 90 步、subagent 50 步耗尽后给模型一次 grace call 说最后一句话,再不够就剥工具强制总结。这层只保证 loop 不会无限跑,不保证任务做对。
跨会话(loop 之间):memory_manager.prefetch_all() 在 loop 开始前把相关历史注入 context。这提供跨会话上下文,但是否带来更高成功率、以及是否引入污染,需要用任务结果和 memory 审计验证。
训练侧(不参与正常运行):environments/*.py 里有 evaluate()、score() 方法(如 yc_bench_env.py:475),但那是 RL 训练数据收集,不参与正常 agent 运行。这部分是 Hermes 团队在做「让 agent 通过 RL 自我改进」的实验,跟用户实际跑 agent 无关。
Hermes 提供了跨会话反馈路径,但没有在这个 loop 里提供业务结果门禁。它可以用于开放式助理;CI、自动 merge 或关键流程仍需宿主接入外部检查。反馈能否提高后续成功率、是否放大错误记忆,需要评测。
先写停止信号,再谈自治
Section titled “先写停止信号,再谈自治”从四个样本可以提炼出五个检查问题;它们不是现成的生产认证清单:
第一,为可能重复执行的路径设置止损:可用 Turn 计数、token budget、wall-clock timeout 或 circuit breaker。具体组合取决于调用成本与副作用;至少要通过故障测试确认存在可达的停止路径。
第二,完成门禁要读取任务外部信号:exit code、lint、类型检查或人工审批可以比模型自评更可靠。diff 合法性只证明 patch 可解析,policy 决策只约束动作;不要把它们直接等同于任务完成。
第三,软 verifier 用预算和启发式控制资源:token 增量、重试次数和调用模式哈希不需要外部命令,但误判率取决于任务分布。它们用于停止或提醒,不证明结果正确。
第四,放任型 verifier 只能兜底:模型不再发 tool_use 只能说明它选择停止,不能单独证明任务完成。
第五,记录 verifier 与停止原因:transition.reason、GOAL_*_METRIC 和 LoopDetectorKind 是不同形态的运行信号。把它们与任务结果关联,才能区分正常完成、预算截断和策略拒绝。
把证据强度和自治范围放在一起
Section titled “把证据强度和自治范围放在一起”四家代表 verifier 设计的四种典型取舍:
让 loop 进入 CI、自动 merge 或关键业务流程:先定义真正能阻止发布的外部判据,例如测试、lint、类型检查、部署预检或人工审批。Codex 的 patch、tests、policy 和 Goal 信号可以提供素材,但它们不是默认串联的“四件套”,也不能替代仓库自己的保护规则。
省 token、避免无意义续写:参考 Claude Code 的 TOKEN_BUDGET 算法(90% 阈值加连 3 次小于 500 token)。可以从这组常量开始,但换模型或任务后要重新校准;它不是通用的停止答案。
给二开方挂自定义检查:参考 OpenClaw 的中间件 pipeline 和 loop detector。它便于接 lint、typecheck、人工审批或业务规则,代价是调试链路更长;是否适合多租户,要看隔离、事件契约和运维能力。
长期保存反馈,不强求单 loop 严格:参考 Hermes 的 memory prefetch 和事后 insights。跨会话记忆能让后续运行看到旧反馈,但是否改善结果需要按任务评测;单次运行仍要有预算上限和明确的失败状态。
停止条件要匹配任务后果
Section titled “停止条件要匹配任务后果”| 完成证据 | 借鉴路线 | 代价或边界 |
|---|---|---|
| 有 tests、退出码或 patch grammar | Codex 的硬 verifier | 强证据只适用于对应领域 |
| 进度可测量,但没有单一的通过/失败值 | Claude Code 的预算与 transition 信号 | 启发式可能过早停下 |
| 需要用策略和工具事件阻止或解释继续 | OpenClaw 的 middleware hooks | 必须先稳定事件契约 |
| 质量要在多次会话中积累 | Hermes 的 trajectory 与 memory 反馈 | 跨会话学习不能证明本次完成 |
从一个可审计的领域验证器开始
Section titled “从一个可审计的领域验证器开始”自己写 Verifier 时,先写清楚完成证据和资源上限,再加入启发式停止、观测和人工复核。
复刻方案
最小可行
- 按成本和副作用配置 max_iterations、token_budget 或 wall_clock_timeout,并用故障测试确认停止路径;不是每个短任务都需要同时启用三项
- 给高风险任务接一个真正对应结果的外部信号。tests 退出码和 output schema 只证明各自覆盖的层面,不能单独代表业务正确
- 记录结构化 stop reason,至少区分正常结束、资源截断、策略拒绝和错误;标签集合应服务于实际重试与告警逻辑
进阶
- 参考 Claude Code 的 `0.9、500、3` 三常量加入 token budget 软 verifier。用 token 增量趋势做判退信号,但在自己的模型和任务集上重新校准
- 参考 OpenClaw 的 4 种 detector(generic_repeat、poll_no_progress、ping_pong、global_circuit_breaker)。先加 generic_repeat 与 global_circuit_breaker,再用运行日志补齐其他模式
- 高风险 shell 调用可以参考 execpolicy 的 Allow、Prompt、Forbidden 三态;具体决策还要结合 sandbox、挂载、凭据和审批策略
- verifier 失败必须可恢复:写 transition 标签加落盘可 replay,别直接 panic。硬 verifier 拒绝(如 tests fail)应该让 loop 继续(feed error 回模型),不是停掉整个 agent。把 verifier 失败写进事件流方便后续分析
一开始别做
- 别把「模型说停了」直接写成业务完成。宿主可按任务把它与外部证据、资源状态或人工复核组合,而不是固定要求前两层全部通过
- 别把 verifier 串成 boolean & boolean & boolean。会出现「4 个都过但任务没完成」的情况(每个 verifier 都通过但组合起来没保证任务真完成)。用投票机制或分类层级更合理
- 别把 verifier 写死在 loop 主体。抽成 hook、middleware 才方便后续替换。不同场景需要不同 verifier 组合(CI 跑、本地 dev),写死意味着要改 loop 主体才能切换
- 别一上来就上 RL 评分。先定义硬上限和一个可复核的完成信号,再判断软 verifier 是否能降低成本。RL 评分还需要训练数据与偏差评估,应该由任务风险决定是否引入
失败怎样回到循环
Section titled “失败怎样回到循环”把 verifier 拆成几类信号后,监控更容易区分「loop 还在干活」、「loop 在原地打转」和「loop 应该停了」。如果把这些状态混成一个 boolean,后续诊断会丢失信息。
核对停止信号的实现
Section titled “核对停止信号的实现”本章带走什么与下一步实验
Section titled “本章带走什么与下一步实验”Verifier 的核心不是多跑几个检查,而是先定义“错误放行”和“错误阻塞”各自要付什么代价。硬信号能证明的范围必须写清,软评分不能伪装成确定性 Oracle,人工门禁也要有输入与超时。
下一步实验:准备 20 个已知结局的任务样本,其中包含测试通过但业务未完成、输出正确但成本超限、来源缺失和需要人工裁决的案例。统计 false completion、unnecessary continuation、人工升级率与平均额外成本,再决定 Verifier 组合,而不是凭感觉调阈值。
附录:练习与复盘
Section titled “附录:练习与复盘”按需展开练习和十道复盘题
- 自查(简单):你现在做的 agent 有几层 verifier?把它们列出来,分类成硬、软、放任型三层。哪一层是空的?补上的成本是什么?
- 移植一段(中等):把 Claude Code 的
checkTokenBudget函数移植到你自己的 agent。0.9、500、3三个常量按你的场景调整一遍,记录调整理由。 - 移植一段(中等):实现 OpenClaw 的
generic_repeatdetector:哈希toolName加JSON.stringify(sorted params),最近 30 次调用里同一哈希出现 10 次以上警告,20 次以上熔断。 - 设计(高难):给一个非 coding 场景的 agent 设计硬 verifier(比如「写周报」或「做调研」)。任务没 tests 也没 exit code,怎么造一个机器可判断的「完成」信号?
Q1 · 概念:「硬 verifier」「软 verifier」「放任型 verifier」三者怎么界定?
按照「判定依据来自哪里」来分:
硬 verifier 读取宿主可复核的信号,例如测试、lint、类型检查或人工批准。patch valid、schema valid 和 HTTP 200 只验证各自那一层,不能自动升级成“任务正确”。Codex 的 patch、policy、tests 和 Goal 状态也分属不同职责。
软 verifier 看内部预算与启发式:token 增量、重试次数、tool 调用模式哈希、cost 阈值、调用频率。判定结果通常是「diminishing returns」或「circuit breaker」一类的预防性熔断。Claude Code 的 tokenBudget.ts 是软 verifier 的工程化代表。
放任型 verifier(lazy verifier)信模型自己说停:assistant message 不再带 tool_use block、output_type: completed、stop_reason: end_turn。本质是把决策权交回模型。
三类信号并不互斥。一个工作流可以组合外部完成检查、资源启发式和模型停止信号,但顺序与成员应跟随任务后果,不必固定成三层级联。
为什么不能只靠放任型 verifier? 因为模型的「自信」和「任务完成」是脱钩的。模型说「我已经做完了」时,仍可能存在 lint 未通过、测试未运行或文件漏改;本章没有生产样本,不给这个失败模式编一个占比。
源码定位:Codex 的 goals.rs 记录 goal 状态与预算,execpolicy 约束命令,apply_patch 验证编辑格式;它们不是一个默认串联的硬 verifier。claude-code/src/query/tokenBudget.ts 展示预算启发式。
追问:「Verifier 和 sandbox 是一回事吗?」不是。Sandbox(chapter 13)防 agent 干坏事(限制可执行的命令),verifier 判 agent 干完了没。两者维度正交。
Q2 · 架构:Codex 的 GoalRuntimeEvent 状态机为什么 1500+ 行那么大?
因为它把 4 件事并到一个状态机:
- Token budget 收敛检测:每 turn 算
current_tokens / budget,命中阈值触发GOAL_BUDGET_LIMITED_METRIC。 - Goal 完成判定:一个任务可能分解成多个 sub-goal,需要追踪每个 sub-goal 的完成状态。
- 外部 goal 变更:用户在 turn 之间可能改 goal(比如说「除了那个 bug 之外,也帮我加 tests」),状态机要支持运行时 goal 修改。
- Tool completion 与 goal 关联:一个 tool 调完不代表 goal 完成。某些 tool(
apply_patch、run_tests)会推动 goal 状态前进,需要明确建模。
状态机的好处是所有判定都在一个地方。Claude Code 把 verifier 逻辑散在 query.ts 1729 行单文件里,调试时要在十几个分支跳。Codex 一个文件,状态明确(TurnStarted、ToolCompleted、TurnFinished 等),可以画状态图核对。
代价是状态和持久化路径较长。阅读成本取决于 Rust 熟悉度、调用链与测试覆盖,不能从行数换算成一天。新增字段或事件是否只改一个 enum,也要沿调用方和存储 schema 验证。
实操建议:当状态转移开始在多个分支重复、需要持久化或难以覆盖测试时,再抽状态机。信号数量本身不是可靠阈值。
源码定位:codex/codex-rs/core/src/goals.rs 全文,重点看 GoalRuntimeEvent 和 GoalRuntime::handle_event。
追问:「Rust 状态机这么大不会有性能问题吗?」Rust enum match 是 O(1) 分发,状态机大小不是性能瓶颈。瓶颈一般在 LLM 调用本身。
Q3 · 工程:Claude Code 用 0.9 / 500 / 3 三个常量定义「diminishing returns」。这三个数字为什么是这三个?
源码能确认三件事:0.9 决定何时进入预算末段,500 定义低增量,3 要求连续出现。三项组合才触发 diminishing returns。
源码没有附带阈值评测,也没有说明它们针对哪些 Claude 型号调过。因此不能从常量反推出“300 会误杀、800 会放过重复”之类的结论。
移植时,把 0.9 / 500 / 3 当成待验证的默认值。记录每次 stop/continue、任务结果和人工复核,再调整其中一个变量;不要同时改三项,否则无法解释效果。
源码定位:claude-code/src/query/tokenBudget.ts:1-82(完整算法)。
追问:「为什么不用一个动态算法(如指数加权平均)?」固定常量更容易审阅,但源码只展示了 0.9 / 500 / 3,没有随附评测样本。换模型或任务后,应重新记录 stop/continue 结果,再决定是否调整。
Q4 · 工程:OpenClaw 的 tool-loop-detection 有 4 种 detector,能不能只用一种就够?
不能。4 种 detector 对应 4 种死循环模式,覆盖范围互补:
generic_repeat:同一 tool 名加同一 args 反复调用。最常见,但只能 catch 完全重复。如果模型每次调 args 微调(比如 file path 改个 case),generic_repeat 漏掉。
known_poll_no_progress:专门识别 command_status、process: poll 这类长轮询。这类 tool 本身就是设计来「重复调用看进度」的,generic_repeat 会把它们误杀。所以专门做一个 detector,hash 包含调用 result,result 不变才算无进展。
ping_pong:A → B → A → B 摆动模式。比如模型先用 Read 看文件,发现不对调 Edit 改,再 Read 看是不是改对了,再 Edit 改,两个工具互相 cancel。generic_repeat 因为 tool 名在变,看不到这种模式。
global_circuit_breaker:30 次工具调用就触发,跟模式无关。这是最后的兜底,前 3 个 detector 都漏了,至少不会无限调下去。
只用一种的问题:
- 只用 generic_repeat:漏掉 ping_pong 和 long-poll false positive。
- 只用 global_circuit_breaker:太迟,30 次工具调用等于用户已经等了很久。
- 只用 ping_pong:完全 catch 不到 single tool 死循环。
实操建议:起步至少 generic_repeat 加 global_circuit_breaker(2/4)。watch、poll 类型的工具用得多再加 known_poll_no_progress。ping_pong 是最后加的,它最难调(false positive 高)。
源码定位:openclaw/src/agents/tool-loop-detection.ts:9-42(detector kind 枚举),整个文件 600 多行讲实现细节。
追问:「不能让模型自己识别 loop 吗?」可以让它输出 self-reflection(chapter 19),但识别能力远不如 detector。模型对自己的行为有偏见。
Q5 · 概念:「transition reason」是用来做什么的?为什么 Claude Code 把所有退出原因都标签化?
transition reason 是每次 loop 退出时附带的标签,告诉调用方「这次为什么停了」。Claude Code 的标签集大概十几种:
end_turn:模型自然停止,没有 tool_use。max_tokens:触达 model output token 上限。token_budget_exhausted:触发 0.9 阈值且 diminishing。max_turns:触达 maxTurns 硬上限。stop_hook_block:stopHooks 系统拒绝了停止请求。user_interrupt:用户按了 Ctrl+C。error:发生不可恢复错误。permission_denied:canUseTool 拒绝且无 fallback。
为什么要标签化?三个原因:
- 监控可读性:看 dashboard 时区分「这次正常完成」和「这次被预算砍掉」一目了然,不用看完整 trajectory。
- 聚合分析:按周观察
token_budget_exhausted占比与任务成功率的关系。先建立基线,再决定告警阈值;单个比例不能独立解释是预算太紧还是 prompt 有问题。 - 重试策略:
max_tokens可以自动 retry(增大 budget),error不可以,permission_denied提示用户而非 retry。标签让自动化流程有依据。
反例:如果你的 agent loop 只返回 success: bool,监控完全分不出「完成了」和「被 budget 砍了,任务没做完」。这两个 case 的处理策略完全不同。
实操建议:从能驱动处理逻辑的标签开始,例如 completed、truncated、error。字段名不是行业标准;关键是调用方能区分是否可重试、是否需要人工处理。
源码定位:claude-code/src/query.ts 全文 grep transition 看所有 reason 类型。
追问:「OpenClaw、Codex、Hermes 也有 transition reason 吗?」有但不一样。Codex 走 metric: GOAL_BUDGET_LIMITED_METRIC 这类指标,OpenClaw 用 LoopDetectorKind,Hermes 直接在 grace call 里写理由。
Q6 · 实操:给一个非 coding 的 agent(比如「写周报」)设计硬 verifier,没 tests 没 exit code,怎么办?
非 coding 场景没有天然的外部判官,需要自己造一个。常用 4 种思路:
方案 1:Schema 校验。要求输出是结构化的(JSON schema、TypeScript interface)。「周报有本周完成、下周计划、blocker、metrics、链接」。Schema 只能验证字段、类型和约束,不能判断事实是否正确或内容是否有用。
interface WeeklyReport { completed: string[]; // 至少 3 项 planned: string[]; // 至少 3 项 blockers: string[]; // 可为空 metrics: { name: string; value: number }[]; // 至少 1 个 links: string[]; // 至少 2 个外部链接}宿主可以把 schema 失败接成 reject 并让 loop 继续;这是工作流配置,不是 schema 自带的控制流。
方案 2:LLM-as-judge。让一个独立的小模型读输出,按 rubric 打分。「评分小于 7 分则 loop 继续」。注意是独立模型(不是同一个 loop 的模型),避免自己评自己。Hermes 的 evaluate() 走这条路。
方案 3:人工 checkpoint。合规报告或合同审核可由宿主加入人工审批状态,并在批准前阻止发布或结束。Codex 的 approval_mode: on-request 面向工具调用审批,不等同于内容完成 checkpoint;宿主需要单独实现后者。
方案 4:参考样本对比。维护一组经人工确认的样本,生成后用 embedding similarity 做辅助信号。样本数量和 cosine 阈值必须按任务校准;相似不等于正确,不能单独决定继续或停止。
实际选型:
- 周报:方案 1(schema 校验,最简单)。
- 调研:方案 2(LLM judge,rubric 有 3-5 维度:覆盖度、引用数、深度)。
- 合规报告:按组织流程接人工 checkpoint。
- 客服回复:方案 4(embedding 对比)。
混合也很常见:方案 1 加方案 2 组合。先 schema 卡住格式,再 LLM judge 卡住质量。
源码定位:hermes-agent/environments/benchmarks/yc_bench/yc_bench_env.py:475(evaluate() 实现),schema 验证可参考任何 JSON schema 库。
追问:「LLM judge 不会有偏见吗?」会。先准备一组人工标注样本,按任务类型比较 judge 与人工的一致率,并由团队预先定义可接受门槛;低于门槛时再调整 prompt 或模型。
Q7 · 架构:Hermes 没有单 loop 硬门禁,跨会话反馈补上了什么,又没补上什么?
Hermes 用跨 session memory 保存历史反馈,但这是一条可用机制,不是长期收敛的证据。源码里可以看到三件事:
事件回放:每个 session 的 trajectory 都写进 SQLite(agent/insights.py 分析),memory_manager.prefetch_all() 在每次 loop 开始前注入「上次类似任务哪里出错」「哪里做对」的经验。所以单 loop 没硬 verifier,但模型有上下文知道哪里容易出错。
Skill 自评:Skill(chapter 17)系统让任务的成功标准内化到 skill 文档里。用户写 weekly-report.md,里面定义完成条件,模型按 skill 自检。这是把 verifier 责任从 harness 推给 skill 作者。
Grace call 兜底:Iteration budget 耗尽时强制让模型做最后一次「总结当前状态」的调用,输出会保留下来给下次会话作 memory。这样即使 loop 强行结束,下次启动时模型能从总结里看到「上次卡哪儿了」。
它可能解决什么? 后续会话能读到先前轨迹、skill 约束和收尾摘要,减少上下文完全丢失。收益要用重复任务成功率、人工纠错量和 memory 污染率验证。
它没解决什么? 一次性 CI 或自动 merge 仍需要当前运行的外部 oracle。Codex 更容易接 coding 信号;这不等于对所有 CI 工作负载有已测优势。
实操建议:
- 短期可信任务:参考 Codex。
- 长期累积任务:参考 Hermes 的 memory 加 skill。
- 通用场景:两者都参考,硬 verifier 兜底,memory 加速。
源码定位:hermes-agent/agent/memory_manager.py 是 memory 实现,hermes-agent/agent/insights.py:1-100 是事后分析。
追问:「Hermes 的 memory 怎么避免污染?」memory 有 TTL 加 relevance score,老 memory 自动淡出。具体 chapter 16 讲。
Q8 · 工程:你的 agent 已经有 max_iterations 和 wall_clock_timeout,还需要 verifier 吗?
需要,因为两者解决的问题不一样。
max_iterations、wall_clock_timeout 是硬上限(hard cap),用来限制 loop 的运行时间和花费。它们像「电源保险丝」,只能约束一类失控路径,不能证明任务正确或消除所有运维风险。
Verifier 是决策层,告诉系统这次该停了(在硬上限之前)。它的角色是「方向盘」:让 loop 知道何时是合适的退出点。
只有硬上限的问题:
- 过早停止:loop 还在推进时触达
max_iterations,未完成任务被截断。 - 过晚停止:loop 已经在原地打转 10 轮了,max_iterations 等于 30 还没到,浪费 20 轮 token。
- 看不出原因:日志只看到
iteration_exceeded,分不出「任务太大没做完」和「loop 卡住了」。
有 verifier 后:
- 软 verifier 判 diminishing returns,loop 卡住第 5 轮就主动停(不用等到 30)。
- 硬 verifier 判任务完成,第 8 轮就停(不用走完 max_iterations)。
- transition.reason 区分「task_complete」「diminishing」「loop_stuck」「iteration_max」「timeout」5 种,监控可读。
硬上限和 verifier 各自触发多少次,取决于任务集、预算和模型。把 transition.reason 记入日志,才能知道哪个机制在你的系统里是主力,哪个只是兜底。
实操建议:起步阶段先把硬上限做对(max_turns、token_budget、wall_clock),同时立刻加 transition.reason 标签。verifier 哪怕只做最简单的 diminishing returns 也能让监控立刻可读。
源码定位:Claude Code 的 tokenBudget.ts 是 verifier 与硬上限结合的范例。Codex 的 goals.rs 把两者都装进状态机。
追问:「Wall clock timeout 设多少合适?」不要把别人的任务分布当默认值。先记录自己的 wall-clock 分布,再按 p95/p99 和可接受的等待时间设 timeout;长任务才考虑转到后台(chapter 18 cron、background tasks)。
Q9 · 概念:什么是 verifier middleware?跟 tool middleware(chapter 04)有什么区别?
Verifier middleware 是把「判断 loop 该不该停」这件事变成可插拔中间件链。OpenClaw 的 tool-policy-pipeline 严格说同时承担了 tool middleware 和 verifier middleware 两个角色:它的 hook 既能改 tool 调用也能注入 verifier 逻辑。
两者区别:
Tool middleware(chapter 04 §Q5):拦截 tool 调用本身。before_tool_call 改 args、after_tool_call 改 result。关注的是「这次 tool 调用合法吗、能优化吗」。
Verifier middleware:在 turn 边界(不是 tool 边界)执行。每次 turn 结束时跑一遍所有注册的 verifier,问「现在该停吗」。关注的是「整体 loop 该不该继续」。
但工程上它们常合到一起,因为:
- 共享同一份 history:tool 调用历史和 loop 状态都在同一份数据结构里。
- 生命周期相似:两者都是「注册 → loop 期间触发 → 卸载」。
- OpenClaw 干脆同一个 pipeline:
after_tool_call钩子里既能改 tool result,也能触发「检测连续 5 次相同调用 → 停 loop」的 verifier 逻辑。
但 Codex 故意分开:
- Tool 调用走
execpolicy静态规则。 - Verifier 走
GoalRuntimeEvent状态机。 - 两者用不同的判定数据(execpolicy 看 command 加 args,GoalRuntimeEvent 看 token、iter、goal)。
实操建议:
- 起步阶段合一个(OpenClaw 风格),简单。
- 当 tool 调用的判定逻辑明显跟 loop 退出判定不相关时,拆开(Codex 风格)。判断标准:是否有 verifier 完全不关心 tool 调用?如果有(比如「整体 token 大于阈值」),拆开。
源码定位:openclaw/src/agents/tool-policy-pipeline.ts(合并)对比 codex/codex-rs/core/src/goals.rs 加 codex/codex-rs/execpolicy/src/policy.rs(拆分)。
追问:「stop_hooks 算 verifier middleware 吗?」算,但是单点钩子(Claude Code 风格),只能反向 deny stop,没办法主动触发 stop。
Q10 · 开放:如果让你设计一个跨场景(coding + 非 coding)的 verifier 框架,会怎么组合?
我的设计目标:让 verifier 同时支持外部判官(coding)和内部判官(非 coding),且对 user 开放配置。
三层架构:
Layer 1 · 资源上限(按运行风险配置):
{ max_iterations: 30, token_budget: 100_000, wall_clock_seconds: 600 }这三个数只是接口示例,不是推荐默认值。短任务可只设关键上限;长任务需要结合单次成本、并发和副作用校准。
Layer 2 · 软 verifier(可选实验配置):
{ token_budget_check: { threshold: 0.9, diminishing_min: 500, diminishing_rounds: 3 }, loop_detection: ['generic_repeat', 'global_circuit_breaker'], // 高级: 'ping_pong', 'poll_no_progress'}这里复用了 Claude Code 当前快照的常量和 OpenClaw detector 名称,只适合作为待校准起点。
Layer 3 · 硬 verifier(user 注册):
// Coding agent{ verifiers: [tests_pass, lint_pass, typecheck_pass] }
// Weekly report agent{ verifiers: [schema_validate(WeeklyReportSchema), llm_judge(rubric)] }
// Customer support agent{ verifiers: [human_approve, response_length_min(100)] }每个 verifier 可以返回 (state) => { passed: bool; reason: string; can_retry: bool }。哪些检查采用 all-pass、投票或人工覆盖,应由宿主根据误判代价配置;下面代码只是本站的接口草案。
Transition reason 标签:
type Reason = | 'task_complete' // 所有硬 verifier 都过 | 'hard_cap' // 触达 iter、token、clock 上限 | 'diminishing' // 软 verifier 判 | 'loop_detected' // 检测器触发 | 'verifier_failed_unrecoverable' // 硬 verifier 永久失败 | 'user_interrupt' | 'error';每个 loop 结束必须返回一个 reason。监控按 reason 聚合。
API 设计:
const loop = createAgentLoop({ hardCap: { max_iterations: 30, token_budget: 100_000 }, softVerifiers: { tokenBudget: defaultConfig, loopDetect: ['generic_repeat'] }, hardVerifiers: [testsPass(), schemaValidate(MySchema)],});
const result = await loop.run(initialMessage);console.log(result.transition.reason);为什么不全部沿用 Codex? Codex 的硬 verifier 写死在 GoalRuntimeEvent 里,非 coding 场景没法用。我的设计把硬 verifier 抽成 user-supplied 函数,coding 场景塞 tests,非 coding 场景塞 schema 或 judge。
为什么不全部沿用 OpenClaw? OpenClaw 让宿主自行装配检查。这里给出 token budget 加两个 detector 的示例 preset,实际默认值必须通过目标任务的误报、漏报和成本数据决定。
工程量要从 verifier 数量、宿主接入点、回放存储和评测样本倒推;在这些范围明确前,固定周数没有参考价值。
源码定位:综合参考 codex/codex-rs/core/src/goals.rs、claude-code/src/query/tokenBudget.ts、openclaw/src/agents/tool-loop-detection.ts。
追问:「这套框架能开源吗?」能。Verifier 抽象不依赖具体 model provider,跟 LangChain 这种偏 protocol 的框架正交。可作为独立 lib(@agent/verifier-kit)发到 npm。