Codex Agent Loop 深度解析:AI 编程代理如何持续交付

0阅读11分钟

一次让我重新理解 Agent Loop 的翻车

我在团队里第一次上线 Codex 做自动化代码修改时,给它丢了一个「修复登录页 token 过期后重复请求」的任务。Codex 读到了报错日志,定位到了 auth.ts,改了拦截器逻辑,然后告诉我「已完成」。我跑了一遍测试,挂了 14 个。它看到失败,又改了一轮,这次挂了 21 个。第三轮它把整个鉴权模块重构了,挂了 38 个。

问题不在模型——它每一步的推理都是合理的。问题在于我给它的工作方式缺少两个关键机制:失败分类上下文压缩。它在第三轮时已经丢失了第一轮读到的关键约束信息,把环境问题、测试夹具问题和真实逻辑问题混在一起修,每一轮都在用不完整的上下文做猜测。

这件事让我花了一周时间拆解 Codex 的 Agent Loop 到底是怎么运转的,以及为什么「能让模型写代码」和「能让模型持续交付」之间的距离,比大多数人想的要远得多。

Agent Loop 的本质:一个带工具的状态机

Agent Loop 不是一个新功能,它是 ReAct 框架(Reasoning and Acting)在工程上的落地实现。Yao et al.(2022)在 ReAct 论文中提出的核心观点是:让语言模型交替进行推理(Reasoning)和行动(Acting),比单纯推理或单纯行动都更有效。Codex 把这个学术想法变成了一个可运行的生产系统。

OpenAI 工程师 Michael Bolin 在「Unrolling the Codex Agent Loop」一文中给出了最精确的定义:

Codex 是一个循环:用户输入 → 模型推理 → 工具调用 → 观察结果 → 重复,直到模型产出 assistant 消息而非更多工具调用。

这个定义看起来简单,工程实现却涉及五个相互耦合的子系统。arXiv 论文「Building AI Coding Agents for the Terminal」(2026)把它们组织成了分层架构:

层级职责核心挑战
Agent Loop编排推理和工具调用的循环终止条件、循环检测、状态管理
Planner任务分解和步骤规划计划漂移、多步依赖
Executor执行文件编辑、命令运行沙箱隔离、审批流程
State Store上下文持久化和恢复Token 预算、压缩策略
Environment终端、文件系统、外部工具安全边界、超时控制

LangChain 团队在 2026 年初用一组实验说明了这套架构的现实意义:他们在 Terminal Bench 2.0 上,只改 harness(脚手架)不改模型,就把得分从 52.8% 提升到 66.5%,排名从 Top 30 进入 Top 5。模型固定为 gpt-5.2-codex,变量只有系统提示、工具定义和中间件。这说明 Agent Loop 的工程细节,比模型本身的选择更决定最终效果。

六阶段循环:Codex 每一轮到底在做什么

Codex 的 Agent Loop 不是简单的 while not done。根据 OpenAI 官方工程博客和 arXiv 论文的拆解,每一轮循环包含六个阶段:

流程图画布 · 115%
Mermaid 流程图加载中...

阶段 0:预检查与压缩。 排空消息队列中的注入消息,检查 token 预算。如果接近上限,触发自动压缩。这是最容易被忽略但最关键的阶段——不压缩就意味着后面所有阶段都在一个快要溢出的上下文里工作。

阶段 1:思考。 可选的思维链推理,让模型在动手之前先理清思路。Codex 通过不同的 reasoning effort 参数控制这个阶段的深度。

阶段 2:自我批评。 可选的自省阶段,模型评估自己的推理是否有漏洞。LangChain 的实验发现,这个阶段对减少「写完就认为自己对了」的过早终止特别有效。

阶段 3:行动。 核心 LLM 调用。模型看到完整的工具 schema 和上下文,产出工具调用或最终文本响应。这里的关键设计是消息排列顺序——Codex 把静态内容(系统指令、工具定义)放在最前面,动态内容(对话历史、工具结果)放在后面,目的是最大化 prompt cache 命中率。

阶段 4:工具执行。 把模型的工具调用请求分发到具体执行器。文件编辑走 9 级模糊匹配链,Shell 命令走 6 阶段管道(验证 → 危险模式检测 → 审批 → 执行 → 输出截断 → 过期读取检测)。每一步都有对应的安全检查。

阶段 5:后处理。 持久化会话、触发 Stop 钩子、判断是继续循环还是返回最终结果。

三个真实翻车案例

案例一:Doom Loop——同一个文件改了 12 次

场景。 让 Codex 给一个 REST API 添加参数校验。任务本身不复杂,就是在现有 middleware 里加几行检查。

翻车。 Codex 改完代码后跑测试,发现有个 fixture 路径不对导致测试环境初始化失败。它把这个问题当成了「我的代码写错了」,于是修改实现代码。再跑,还是失败,继续改实现。12 轮之后,它把一个原本 20 行的改动变成了一个 200 行的重写,测试依然全红。

根因。 没有失败分类机制。环境失败(fixture 路径不对)和业务逻辑失败(实现代码有 bug)在 Codex 看来都是「测试挂了」,它无法区分。

修复。 引入 LangChain 团队提出的 LoopDetectionMiddleware 思路:跟踪每个文件的编辑次数,超过阈值后注入干预提示,要求模型重新审视策略而不是继续局部修改。

// ❌ 错误做法:对所有失败一视同仁地重试
while (!testsPassed) {
  const result = await agent.run()       // 模型看到失败就改代码
  await runTests()                        // 环境问题和代码问题混在一起
  // 模型永远不知道 fixture 路径才是根因
}
 
// ✅ 正确做法:分类失败,识别循环
const editCounts = new Map<string, number>()
 
function onToolComplete(toolCall: ToolCall) {
  if (toolCall.name === 'edit_file') {
    const count = (editCounts.get(toolCall.args.path) ?? 0) + 1
    editCounts.set(toolCall.args.path, count)
    // 同一文件编辑超过 3 次,强制模型停下来重新分析
    if (count >= 3) {
      injectSystemMessage(
        `你已经修改了 ${toolCall.args.path} ${count} 次,` +
        `请先运行诊断命令确认失败根因,再继续修改。`
      )
    }
  }
}

差异在于:前者把循环当成「模型还不够努力」,后者把循环当成系统级信号——同一文件被反复修改意味着根因可能不在代码里。

案例二:上下文压缩失败导致「失忆」

场景。 一个跨 5 个文件的鉴权模块重构任务,Codex 在前 3 轮里读完了所有相关文件并制定了计划。任务进行到第 7 轮时,token 用量逼近上下文窗口极限。

翻车。 Codex 触发了自动压缩,压缩把 26 条消息合并成了 4 条。用户原始消息被保留了,但中间几轮关键的架构决策讨论——比如「新 token 刷新逻辑必须兼容 OAuth2 和 SAML 两种流程」——在压缩摘要中被省略了。第 8 轮 Codex 写出的代码只处理了 OAuth2,完全忘记了 SAML 的约束。

根因。 Codex CLI 的本地压缩路径(compact.rs)在提取最近用户消息时有约 20,000 token 的硬上限,超出部分会被压缩摘要替代。压缩摘要的质量依赖 LLM 的判断,它无法保证每条架构约束都被保留。

修复。 参考 Claude Code 的三层渐进压缩策略:先裁剪工具结果(零成本),再利用 prompt cache 做尾部修剪(零成本),最后才调用 LLM 做结构化摘要。关键改进是在压缩后自动重读最近编辑过的文件,把丢失的代码上下文补回来。

// ❌ 错误做法:只有一种压缩策略——调 LLM 做全量摘要
async function compact(messages: Message[]) {
  const summary = await llm.summarize(messages)  // 所有细节都可能丢失
  return [summary]                                 // 不可逆删除
}
 
// ✅ 正确做法:分层渐进压缩
async function compact(messages: Message[], budget: number) {
  let current = [...messages]
 
  // 第1层:裁剪旧工具结果,零 LLM 成本
  current = current.map((msg, i) => {
    if (msg.role === 'tool' && i < current.length - 6) {
      return { ...msg, content: '[旧工具结果已裁剪]' }
    }
    return msg
  })
 
  if (estimateTokens(current) <= budget) return current
 
  // 第2层:保留最近消息,对旧消息做结构化摘要
  const recent = current.slice(-6)  // 保护最近的对话
  const old = current.slice(0, -6)
  const summary = await llm.structuredSummarize(old, {
    sections: ['用户意图', '关键决策', '待办事项', '当前进度']
  })
 
  // 第3层:压缩后自动重读关键文件,补回代码上下文
  const filesToReread = extractReferencedFiles(summary)
  const fileContext = await Promise.all(
    filesToReread.slice(0, 5).map(f => readFile(f))
  )
 
  return [summary, ...fileContext.map(f => asToolResult(f)), ...recent]
}

两者的核心区别:前者把压缩当成「把对话变短」,后者把压缩当成「在有限预算内保留最高价值的信息」。

案例三:Prompt Cache 失效导致成本爆炸

场景。 团队把 Codex 接入了内部开发平台,发现一个任务的 API 费用是正常的 8 倍。排查后发现 prompt cache 命中率不到 10%。

翻车。 每次循环时,系统在发送给模型的 prompt 中间插入了一段动态的「当前时间戳」和「任务进度百分比」。这些动态内容打断了 prompt 的前缀匹配,导致服务端无法复用之前缓存的计算。每一轮循环都在重新处理全部内容。

根因。 Codex 的 prompt cache 依赖精确前缀匹配——从第一个 token 开始必须完全一致才能命中缓存。在动态内容插入到 prompt 中间时,插入点之后的所有内容都无法命中。

修复。 严格遵循 Codex 的消息排列原则:静态内容(系统指令、工具定义、项目约束)放最前面,动态内容放最后。中途的配置变更不修改已有消息,而是追加为新消息。

// ❌ 错误做法:动态内容插在 prompt 中间
const messages = [
  { role: 'system', content: systemPrompt },
  { role: 'system', content: `当前时间: ${Date.now()}` },    // 每次都变
  { role: 'system', content: `进度: ${progress}%` },          // 每次都变
  { role: 'user', content: taskDescription },
  ...conversationHistory                                       // 被前面的动态内容破坏缓存
]
 
// ✅ 正确做法:静态在前,动态追加
const messages = [
  { role: 'system', content: systemPrompt },         // 不变 → 缓存命中
  { role: 'system', content: toolDefinitions },      // 不变 → 缓存命中
  { role: 'system', content: projectConstraints },   // 不变 → 缓存命中
  { role: 'user', content: taskDescription },        // 不变 → 缓存命中
  ...conversationHistory,                             // 追加式增长 → 前缀缓存命中
  // 动态信息放最后,不影响前面的缓存前缀
  { role: 'user', content: `[系统提示] 时间: ${Date.now()}, 进度: ${progress}%` }
]

Michael Bolin 在博客中明确提到,Codex 团队曾经因为外部工具的不可预测排序导致 cache 失效,排查了很久才定位到问题。prompt cache 的命中率直接决定了长任务的运行成本和延迟。

上下文压缩策略对比

三个主流 AI 编程代理的上下文压缩方案代表了三种不同的工程取舍:

维度Codex CLIClaude CodeOpenCode
压缩层级1 层(LLM 摘要)3 层(裁剪 / cache / 摘要)2 层(隐藏 / 摘要)
是否必须调 LLM否(仅第 3 层才调)否(仅第 2 步才调)
用户消息处理原文保留被摘要被摘要 + 最后消息重放
工具结果处理物理删除占位符替换时间戳隐藏
压缩后可逆性不可逆不可逆可逆(数据保留在数据库)
压缩后恢复策略被动等待主动重读文件(50K 预算)自动重放最后指令
失败保护回退到头部截断3 次连续失败后暂停最少释放 20K token 才执行

Codex 的方案最简单直接,适合对话轮次不多的任务。Claude Code 的三层策略在长任务中成本最优,因为它尽量不调 LLM 做压缩。OpenCode 的非破坏性隐藏最安全——数据永远不会真正丢失,最差的情况也只是摘要质量不理想。

失败分类决定修复方向

在 Agent Loop 中,不是所有「失败」都应该用同一种方式处理。我在实际使用中总结出的分类方式:

失败类型观察信号正确处理错误处理
类型错误tsc 指向接口不匹配回到类型定义和调用点继续改运行时逻辑
断言失败测试期望与实际输出不同确认是需求偏差还是实现偏差直接改测试让它通过
环境失败端口/依赖/密钥缺失修复环境,不改业务代码改代码去适配错误的环境
快照失败UI 或文案差异确认是否属于预期变化无条件更新快照
超时失败命令执行时间超限检查是否有死循环或性能退化增加超时时间
权限失败沙箱拒绝或文件锁检查审批配置和文件归属绕过安全检查

没有失败分类的 Agent Loop,每一轮修复都在引入新的猜测。Codex 的循环能力只有在失败被正确分类时才有价值。

推理算力的分配策略

LangChain 团队在实验中发现了一个反直觉的现象:全程使用最高推理算力(xhigh)的效果反而不如混合使用。他们把它叫做「推理三明治」:

阶段推理级别目的Token 消耗
规划xhigh充分理解问题和约束
实现high平衡速度和准确性
验证xhigh提交前最后一道检查

全程 xhigh 得分 53.9%(大量超时),全程 high 得分 63.6%,三明治策略得分 66.5%。原因是高推理级别的 token 消耗是 2 倍以上,省下来的 token 预算意味着更多轮循环和更充分的验证。

这个发现对应到 Agent Loop 的阶段设计:阶段 1(思考)和阶段 2(自我批评)应该是推理密度最高的地方,阶段 3 的行动阶段反而可以适当降低推理级别以节省预算。

循环的终止条件

Agent Loop 什么时候该停下来,远比什么时候该开始复杂。根据 arXiv 论文和 Codex 的实际行为,合法的终止条件至少有五种:

终止条件触发方式风险
正常完成模型产出纯文本响应,无工具调用可能过早终止(没验证就认为自己对了)
安全上限达到预设的最大迭代次数任务未完成,需要人工接管
循环检测系统检测到重复模式可能误判正常迭代为循环
用户中断用户主动取消中间状态需要清理
资源耗尽token 预算或时间预算用完需要保存进度供后续恢复

LangChain 的 PreCompletionChecklistMiddleware 解决的是第一种风险——模型认为自己对了但实际上没跑过测试。这个中间件在模型准备退出时拦截,强制执行一轮验证,对比输出和任务规格说明,而不是让模型「读一遍自己的代码就觉得没问题」。

落地检查清单

根据上面的分析,团队在接入 Codex Agent Loop 时,可以按阶段对照以下清单:

接入前

  • 确认任务有明确的验证标准(typecheck / lint / test 能通过什么命令确认)
  • 准备好项目约束文件(AGENTS.md / README / 目录结构说明)
  • 设定 sandbox 安全边界:可写目录、可执行命令白名单、审批级别
  • 确认模型 fallback 链:主模型不可用时是否有备用模型
  • 设定最大迭代次数和 token 预算上限

执行中

  • 每轮循环要求模型输出验证证据(测试输出、类型检查结果),不接受「我认为已完成」
  • 监控同文件编辑次数,超过 3 次触发干预
  • 关注 prompt cache 命中率,动态内容不要插在 prompt 前缀中间
  • 失败后先分类再修复,不要把环境问题和代码问题混在一起处理
  • 上下文接近上限时确认压缩策略已生效,关注压缩摘要是否遗漏关键约束

交付后

  • 检查变更是否只涉及任务相关文件,没有误改其他模块
  • 验证命令覆盖了主要风险路径(不只是 happy path)
  • 确认失败修复有日志或测试输出支撑,不是模型自述「已修复」
  • 剩余风险明确写给了 reviewer
  • 如果任务跨多轮,检查压缩后是否有上下文丢失的迹象(如遗漏了某个约束条件)

参考资料

  1. Yao, S., et al. "ReAct: Synergizing Reasoning and Acting in Language Models." ICLR 2023. https://arxiv.org/abs/2210.03629
  2. Bolin, M. "Unrolling the Codex Agent Loop." OpenAI Engineering Blog, 2026. https://openai.com/index/harness-engineering/
  3. "Building AI Coding Agents for the Terminal: Scaffolding, Harness, and the Path to Production." arXiv, March 2026. https://arxiv.org/html/2603.05344v1
  4. LangChain. "Improving Deep Agents with Harness Engineering." LangChain Blog, 2026. https://www.langchain.com/blog/improving-deep-agents-with-harness-engineering
  5. "Context Compaction in Codex, Claude Code, and OpenCode." justin3go.com, April 2026. https://justin3go.com/en/posts/2026/04/09-context-compaction-in-codex-claude-code-and-opencode
  6. OpenAI. "Run Long Horizon Tasks with Codex." OpenAI Developers Blog, February 2026. https://developers.openai.com/blog/run-long-horizon-tasks-with-codex
  7. "从 Claude Code 到 Codex:拆解 Agent Runtime 五层架构的新技术演进." CSDN, April 2026. https://blog.csdn.net/fenglingguitar/article/details/159737161
  8. OpenAI. "Build an Agent Improvement Loop with Traces, Evals, and Codex." OpenAI Cookbook, May 2026. https://developers.openai.com/cookbook/examples/agents_sdk/agent_improvement_loop

评论 0

0 / 1000