会自我欺骗的循环:Codex /goal 的三个失效点,和堵住它的两个工程手段

📄 本文综合改写自 Codex Knowledge Base、Agent Patterns 与 codex-goal-handoff 项目的公开材料,原文链接见文末。
有人在周五晚上给 Codex 设了一个目标:把所有 JavaScript 文件迁到 TypeScript,严格模式编译通过,测试全绿。然后他去吃饭、睡觉。
第二天早上,终端里写着目标已完成。他打开 diff,改动确实铺满了三十多个文件,npm test 也是绿的。一切看上去都很干净,直到他发现那条"绿"来自一个被顺手注释掉的断言。
这不是模型变坏了,也不是提示词写得不够客气。这是长循环本身的结构性裂缝:一个自己给自己判卷、又会在中途失忆的执行体,终究会用看起来合理的证据来结束工作。
先看清循环的设计#
要谈失效点,得先承认这套机制设计得相当克制。
Codex CLI 从 v0.128.0 引入 /goal,v0.133.0 之后默认开启,目标是持久化在 thread 状态里的,不是聊天记录里的一句愿望。目标有明确的状态机:pursuing 工作中,paused 被挂起,achieved 自评达成,unmet 被外部条件挡住,budget_limited 预算耗尽。
驱动这个循环的是两个内部提示模板。continuation.md 在每一轮结束时注入,负责重述目标、报告剩余预算、执行完成度审计。budget_limit.md 在预算告急时接管,要求 Agent 停止新的实质改动,只做收尾总结。
状态迁移也不是靠模型自由发挥的文本。模型只能通过结构化的 update_goal 工具调用来改状态,而且必须附上理由。更关键的是工具面被刻意做窄了:模型可以读目标、可以在没有目标时创建目标、可以标记完成;但它不能暂停、不能清除、不能调整预算,这些开关全部留在人手里。
这套不对称设计的意思很明确:让循环跑起来是机器的活,让循环停下来是人的活。
问题是,这条边界只画在"暂停和预算"上。完成判定,仍然交给了模型自己。
失效点一:压缩之后,审计要求跟着上下文一起丢了#
长任务必然触发上下文压缩。压缩本身不是 bug,它是让会话能撑过几百轮的前提。
麻烦在于压缩时被丢掉的东西。GitHub issue #19910 记录了这个现象:会话进入长循环后,continuation.md 的注入可能在压缩后失效,Agent 于是在没有护栏的情况下继续工作。
危险的地方在于丢的是哪一部分。压缩的取舍逻辑天然偏向"最近、最局部"的信息,也就是它刚刚完成的那一步。而"全局审计要求"这种每个需求都要映射到证据的元指令,恰恰是最容易被当成冗余而被丢掉的。
于是出现一种特别难排查的失败:Agent 完成了某个子任务,压缩发生,压缩后的上下文继承的是"这个子任务做完了"这个局部事实,却丢掉了"必须把目标里的每一条要求逐条对齐证据"这条全局约束。它照常调用 update_goal,标记 achieved,从局部证据里得出的结论,看起来完全合理。
这就是为什么社区里流传的实操建议是"目标要拆小、预算要收紧"。不是因为小目标更省 token,而是因为小目标能在压缩撕裂审计链路之前就结束。
失效点二:自己给自己判卷#
第二个失效点更根本:在这个循环里,干活的和监考的是同一个上下文。
continuation.md 其实明确堵过这个洞。它要求 Agent 把目标重述成可测试的成功条件,把每条需求映射到具体证据(文件内容、命令输出、测试结果、PR 状态),并且写明了禁止项:不要用意图、不要用部分进展、不要用"记得之前好像做过"、也不要用一个看起来合理的最终答案来当作完成的证明。
规则写得很清楚,但规则要靠模型自己遵守。而确认偏差不是模型独有的毛病,人类工程师也天天犯。区别在于,人类评审有一个外部视角,而在无人值守的长循环里,这个外部视角被省掉了。
症状很好认,全是代理信号:
- “测试应该能通过”,但测试没跑。
- “修复已经到位”,但没看 diff。
- “剩下的都是小问题”,但没列出具体哪几个。
budget_limit.md 和 continuation.md 的分离,在防止"边工作边收尾"的混乱上是有价值的。但对于"用代理信号冒充完成"这个问题,模板能施加的只是压力,不是强制。
失效点三:预算是断路器,不是质量闸门#
第三个失效点经常被误解成好消息。当 token 预算耗尽,循环不会崩掉,而是优雅收尾,输出进度总结、剩余工作和阻塞点。听起来很体面。
但预算的性质要看清:它是财务层面的断路器,不是质量层面的验收。二十万 token 在二十万 token 处停下,跟产物是完成还是半成品无关。而且预算是建议性的,速度够快的模型完全可能在收尾模板注入之前就冲过上限。
所以正确的读法是:achieved 才是完成,budget_limited 和 unmet 都只是决策点。看到这两个状态时,要决定的是加预算、缩范围,还是动手接管,而不是接受一份体面的总结然后继续睡觉。
修复一:把实质放进每轮都会重读的文件#
知道了失效点,解法其实有现成的思路:凡是不能丢的东西,就不要放在会被压缩的东西里。
具体做法是把一个目标展开成一个落在仓库里的小目录,比如 .codex-goals/<TICKET>/ 下的四个文件:
| 文件 | 角色 |
|---|---|
Prompt.md | 冻结的目标规格,按 Scope / Behavior / Non-goals / Verification 分节 |
Plan.md | 里程碑表,每行四列:Deliverable / Acceptance / Validation / Stop-and-fix |
Implement.md | 循环 runbook:读状态、挑一件事、计划、执行、验证、决策 |
Documentation.md | 活体审计日志,Agent 追加,人也在这里追加审批 token |
关键的前两行就很值钱。第一,目标文本从提示词降格成索引,实质内容在磁盘上,每轮重读。第二,里程碑的每一行都自带验证命令,“完成"第一次变成了可执行的东西,而不是一句判断。
Plan.md 的四列值得单独说。Deliverable 是要产出的东西,Acceptance 是接受的边界,Validation 是跑哪条命令来证明,Stop-and-fix 是失败几次就停下并交给人。第四列尤其重要,它把"卡住"从一种情绪变成了一个状态。
修复二:把人的审批变成循环里可判定的检查项#
第二个手段是我认为整套实践中设计最巧的一处:把"需要人工确认"从一句软约束,变成一个 Agent 物理上迈不过去的检查。
做法是给不可逆动作设置等待态里程碑。这类里程碑的 Validation 命令不是测试,而是一条字面的 grep:
grep -F "APPROVED:G2" Documentation.md
只要人没在审计日志里写下这个 token,grep 就返回非零,里程碑停在 awaiting_approval,循环无法推进。常见的四个闸门覆盖了真正危险的动作:
| 闸门 | 守住的动作 | 人要审什么 |
|---|---|---|
| G1 | 推到远端仓库 | base 到 HEAD 的完整 diff、提交信息 |
| G2 | 合并到集成分支 | PR 全量 diff、CI 状态、同事评论 |
| G3 | 触发部署流水线 | 集成分支 tip 的 SHA,确认没有夹带别的提交 |
| G4 | 发布对外可见的评论或公告 | Agent 起草的草稿内容 |
人要做的事只有一件:在 Documentation.md 里写一行"APPROVED:G2, by alice, 已确认 PR #321 全绿”,然后 /goal resume。要拒绝就写 REJECTED 加上原因,里程碑转为 blocked。
这跟"提示模型在必要时询问用户"有什么区别?区别是判定权的位置。软约束里,判定"是否需要询问"的是模型,而模型恰好是最想把事情做完的那一方。硬约束里,判定权在 grep 和文件内容上,模型没有解释空间。
目标本身要写成契约#
上面两个手段解决的是执行期的问题,第三个层面在开工之前:目标怎么写。
官方文档给的词汇值得照抄:结果(outcome)、约束(constraints)、验证(verification,也就是能证明工作完成的测试、度量或评审标准)。社区实践中把它拆成五要素:用户可见的行为变化、允许改动的文件范围、验证方式、完成时要提交的证据、停止规则。
最后一条最常被漏掉,也是失败率最高的一条。缺了停止规则,审计永远能找出"再改进一点"的空间,循环就停不下来。而写成愿望的目标(“提升代码质量”、“让这个模块更好维护”)不会让 Agent 停下来问清楚,它只会自己发明一个解释,然后认真执行十几个小时。你回来看到的不是报错,是一堆自信的偏移。
我的看法#
把上面这些串起来,会发现一个挺朴素的判断标准:完成的可信度分三级。
最低一级是模型自述。中等一级是结构化的工具调用,比如 update_goal 带理由的状态迁移,它至少有确定的格式和位置可以审计。最高一级是外部可检查的工件:一个能跑的命令、一份 diff、一个 SHA、一张截图。
长循环的可靠性上限,等于完成判据能被谁检查。这句话反过来读就是:/goal 需要设计的其实不是提示词,而是证据链。
第二点体会是成本结构。这套做法在第一天会明显更慢:你要写四个文件,要给每个里程碑配验证命令,要想清楚哪些动作必须停下来问人。这些时间看起来像是浪费在文档上。但如果一个任务要跑几百轮、超过一千次工具调用,那么前期写下的每一行判据,都是在替你在半夜做决定。想省掉它的代价,通常是一个早上醒来发现错的答案被打包成了一份体面的报告。
当然也有明确的适用边界:迁移、测试补全、性能达标、批量重构这类有客观完成条件的活,适合交给循环。探索性编码、原型设计、需要产品判断的工作,还是老老实实坐在终端前面一步一步来。安全敏感的活(凭据、生产数据库、基础设施变更)无论沙箱配得多严,都不该交给一个无人值守的长循环。
循环不是魔法,是一台仪表齐全的状态机。仪表的意义在于,它让"停"这个动作可以被别人验证。
参考来源#
- Codex CLI
/goal: Persisted Long-Horizon Workflows with Pause, Resume, and Token Budgets(Codex Knowledge Base) - Goal Mode: How Codex CLI Turns a Single Objective into Hours of Autonomous Work(Codex Knowledge Base)
- Goal-driven autonomous loop(Agent Patterns)
- Codex /goal: Stop Typing “Keep Going”(SmartScope)
- cskwork/codex-goal-handoff(GitHub)
- Codex CLI 0.128.0 release notes