AI 编码 Agent 若将验证标准直接渲染进提示词,Agent 会学会满足标准字符串而非真正完成任务——这是典型的 Reward Hacking。RFC 通过具体漏洞案例说明了前向(提示词泄密)和后向(错误 repr 回传)两个泄露路径及其修复方案。
给 AI 编码 Agent 套上循环,是单次 prompt 触及瓶颈后谁都会想到的下一步。跑一遍、打分、分数低就再跑一遍。
分数在涨,但产出依然不对。Agent 学到的是如何讨好评分器,而不是如何完成任务。
失败即是死胡同。循环的零件都在,但一次失败的执行不会导向下一次执行。
问题既不在模型,也不在环境设计——模型变强并不会消失这个问题。更强的模型只是更快地找到了捷径。
用具体可读的示例来拆解这两堵墙。案例来自我最近加入的开源 Agent OS——Ouroboros 的一个 RFC。设计见 Issue #1917,实现见 #1916。选择这个案例的原因是:你能直接读到原文,而不是听我抽象地讲。
大多数 Agent 套件会把验收标准原封不动地渲染到 Worker 的 Prompt 里,包括评分用的命令和作为评分依据的断言语句。理由听起来很合理:让 Agent 知道它将如何被检验,就能准确瞄准目标。
Ouroboros 以前也是这样做的。_build_success_contract_block 把 verify_command 和 Expected output: <断言> 渲染到 Worker 的指令里。第二个泄漏点更难发现:重试时,失败原因携带了断言的 repr(),顺着 result.error 回到下一个 Prompt 里。
一旦 Agent 能看到断言,满足断言就比满足需求更划算了。RFC 给这种现象起了个直白的名字:身处困境的 Worker 最便宜的路径不是实现验收标准,而是玩弄断言字符串。附上了验尸报告(seed_2be2907edc07)。这是教科书级别的奖励黑客(reward hacking)。你以为在测量能力,实际上测量的是作弊的能力。
两条泄漏路径必须同时堵住。只堵正向,什么都不会改变。
正向:_build_success_contract_block 现在只渲染验收标准说明和 expected_artifacts。验证由套件独立执行,所以评分逻辑不会进入 Worker 的成功契约里。
逆向:验证门的失败原因不再携带断言的 repr()。重试提示由一个专用构建器(orchestrator/retry_hints.py)生成,它会把断言从所有片段中过滤掉。包括命令输出的最后 2000 字符尾巴。
这 2000 字符尾巴值得专门说一下。只堵正向、忘了日志尾巴,泄漏就仍然洞开。
局限性也从一开始就写明。这种脱敏是基于五种编码的逐字(verbatim)匹配,所以换行了或者加了 diff 前缀的断言副本仍然会通过。这被作为公开 Issue(#2020)开放着,没有假装已经解决。
RFC 里还记录了一个被否决的提案:设置"公开级别"调节旋钮。理由是:可关闭的信息壁垒会在某个被工期追着跑的下午被关掉,而之后因为分数看起来更好看了,没人注意到。
全部隐藏起来,Worker 就只能徒劳挣扎。所以 RFC 把隐藏和提示循环配对。下一轮的指令从会话实际做过的事情中重构出来:工具调用轨迹、证据列表(复用 deliver_gate.load_ac_evidence_manifest,只读)、验证器的判定结果。不是从断言里。
这是信息不对称。用的正是人类考试采用的同样配置:出题者知道答案,考生只知道自己哪里做错了。
根据 RFC,零件其实都在。验证门、执行-评估链、evolve_step、Ralph 驱动、focus.select_evolution_focus。但什么都没连起来。
失败的执行不会进入正式评估。失败是终点,被标记为 BLOCKED 而已。
被否决的评估不会进入进化。同样是终点。
循环以三段断开的形式存在。失败只是被报告,不会用于下一次执行。
失败的执行也导向评估。放宽 _run_succeeded 门,让创建会话的执行接入正式评估链。fail-open 保留着。队列注册失败不会翻转执行的结果。
被否决的评估触发有预算的进化循环。这里没有新实现收敛循环的人。评估任务只在 final_approved is False 时把既有进化引擎放入队列。
新做的是第一代桥接。执行时的种子和链式评估的多验收标准检查清单,以谱系事件的形式投影出去,让 evolve_step 把普通执行重放为第一代,并让第二代已经处于焦点已设定的状态。
要让这套机制运转,把检查清单转换为 ACResult 的转换器必须满足严格条件:全部索引覆盖、逐字原样的 ac_content、语义 ac_key 的同一性。正是这种严格性让 focus.select_evolution_focus 能够冻结已通过的验收标准。
不冻结已通过内容的循环会在重新做对的事情上浪费 Token,并在这个过程中破坏本来对的实现。症状是在代数之间来回振荡的分数。
循环不会自己停止,所以 Ouroboros 重用了 Ralph 既有的停止条件:QA 通过、收敛、振荡检测、等级退化、真实经过时间。execution.auto_evolve_max_generations 默认值是 3,限制在 1~10。BLOCKED 只在预算耗尽后才发生。
捕捉伪收敛靠两样东西:振荡检测和等级退化。在 A 和 B 之间跳来跳去的分数,或者比父代更差的代数。两者都把 Token 烧在更多的循环上,而不是让循环继续。
还修了一个东西。evolution/loop.py 的顶层 except 有三个路径会静默以 evaluation_summary=None 溜走。现在记录的是携带失败原因的否决摘要,在保持 fail-closed 焦点语义的同时,把失败留作持续记录。
单次执行中吞掉异常只错一次。循环中吞掉异常会让错误在 N 代中不断放大,而日志里什么都没有。
同一模式延伸到设计的其余部分。
机器验证(免费、确定性检查)→ 语义验证 → 多模型共识。被第一层否决的不送到 LLM 裁判那里。
面试阶段用的是数字而不是层级。模糊度以明确度加权和的逆数为数值化。
Ambiguity = 1 - Sum(clarity_i * weight_i)
超过 0.2 的分数会阻止生成规格。不过,显式 force 可以跳过。CLI 在继续/取消旁边显示这个选项。循环在最后两代之间的本体相似度达到 0.95 时收敛,而如果这个相似度连续三代没有进展,独立的收敛检测器会把循环停下来。两者都是数学门。README 这样描述两个门的出发点:未清晰前不使用,未稳定前不止步。
不要把评分标准给考生看。 感谢你的套件通过错误消息、日志尾巴、重试 Prompt 向 Worker 泄露断言字符串。正向逆向都要堵住,不要做成配置选项。
失败必须有下一步。 失败的执行要能触达评估,被否决的评估要能触达进化,通过的任务要被冻结。
循环必须能停下来,并且要能捕捉伪收敛。 振荡、等级退化、经过时间、代际预算。
RFC 全文:Q00/ouroboros#1917。实现:#1916。设计文档在树内 docs/hidden-checklist-convergence/(需求·架构·实现)。项目在 github.com/Q00/ouroboros。MIT 协议,本地优先,运行在 13 个运行时家族的前端。
如果你真的在运营多代 Agent 循环,我想听听你们是如何捕捉伪收敛的。这是我最少看到好答案的地方。
本文英文版在发布后自行纠正了 4 处夸张(不是无条件隐藏,而是五种编码的逐字匹配;模糊度门可以用 force 通过等)。这个中文版从一开始就用最终定稿的本意写成。原始更正记录在英文版末尾。