跳到主要内容

codex — 本课题摘录

读了哪几篇: 01-agent-loop(回合循环)。 其余两篇(模糊补丁格式、安全模型)本轮没读——属工具层与权限边界课题。

这是 OpenAI 官方的终端编码 agent,厂商原生工具调用的参照实现。

它对本课题回答了什么

决定四:先分清"一个回合"和"一次模型调用"

关键直觉:一个回合不等于一次模型调用。一个回合内部可能向模型采样很多次。

┌─────────── 一个回合 ───────────┐
│ 采样 #1 → 模型要跑命令 → 执行 → 回灌 │
│ 采样 #2 → 模型要打补丁 → 执行 → 回灌 │
│ 采样 #3 → 模型只回一句「改好了」→ 结束│
└──────────────────────────────────┘

(依据:Agent 库 · OpenAI Codex CLI · 「采样→工具→回灌」的心跳 —— 一个回合(turn)不等于一次模型调用,回合内部可能多次采样(sampling request);每次采样模型回的要么是一批函数调用、要么是一句助手消息)

每次采样,模型回的要么是一批函数调用、要么是一句助手消息——只有这两种。

收工判据极简单:这轮模型没要任何工具(也没有积压的用户输入)→ 回合结束。 (依据:Agent 库 · OpenAI Codex CLI · 「采样→工具→回灌」的心跳 —— 收工判据是 needs_follow_up 这个布尔——任一工具调用或积压输入会置 true 则继续采样,否则 break)

"回合 / 采样"两级命名值得抄。 前面在 langchain4j 那边我们已经发现"最多 20 轮"这句话口径不一; codex 用两个词把它说清了:上限数的是"采样次数",不是"回合数"。

决定一:跑着的时候还能插话

外层套了一个小循环:一个回合跑完,如果有积压的用户输入就再跑一个回合。 (依据:Agent 库 · OpenAI Codex CLI · 「采样→工具→回灌」的心跳 —— RegularTask::run 外层套一个循环——run_turn 跑完若还有积压输入就再跑一次,于是用户可以在模型还在跑时继续打字,这些 pending input 会在合适时机被排进历史)

于是你可以在模型还在跑时继续打字,这些输入会在合适的时机被排进历史。

跟 cherry-studio 的"插话是排队+让步+续接"、kun 的"四类打断"是同一件事的三种做法。 codex 这一份最轻:不打断当前回合,而是排到下一回合开头。

决定三:边收边跑,而且用有序队列保住回灌顺序

这是这一篇技术含量最高的一段。

模型的回复是流式的。它不等整段收完,而是边收边处理:

  • 文本增量实时转发给界面显示;
  • 一旦某个工具调用项收完,立刻把它的执行推进一个有序的并发队列,让工具开始跑,同时继续收模型后面的输出。

(依据:Agent 库 · OpenAI Codex CLI · 「采样→工具→回灌」的心跳 —— 一次采样请求边流式收模型输出边触发工具——某个工具调用项收完就立刻把执行 future 推进 FuturesOrdered 有序并发队列,保证回灌顺序与模型给出的顺序一致)

"有序并发队列"这个数据结构选得很准:并发地跑,但按模型给出的顺序回灌。

这解决了一个我们迟早会遇到的矛盾: 并发能省时间,但工具结果写回历史的顺序如果乱了,模型下一轮看到的因果关系就错了。 答案不是"要么并发要么有序",是"并发执行、有序回灌"。

决定四补充:历史撑爆不报错,就地压缩后继续

它的做法不是报错退出,而是就地压缩:把旧历史换成一段模型生成的摘要,腾出空间继续。

两个触发点:

触发点位置
进循环前拼下一次请求前先看要不要瘦身
循环中途还要继续、且 token 超限时,压缩完直接继续下一轮

(依据:Agent 库 · OpenAI Codex CLI · 「采样→工具→回灌」的心跳 —— 压缩有两个触发点——run_pre_sampling_compact 在进循环前、run_auto_compact 在循环中途 token 超限时压缩后 continue;压缩用一段专门的摘要提示词把历史浓缩,新历史以摘要前缀开头)

它给的直觉很好:压缩 = 给 agent 做"记忆整理"。 把"我们之前读了哪些文件、跑了哪些命令、得到什么结论"压成几段话,丢掉逐字细节。

它也诚实承认代价:压缩有损。摘要会丢逐字细节,极长会话里偶发"模型忘了早先约定"是这一机制的固有代价。

对比 deepagents / nanobot / deepseek-harness 的"不改历史、只做投影": codex 是真改历史(旧历史被换成摘要)。前者保留原文可回放,后者更简单但不可逆。 这是本课题一个明确的分歧点。

决定三补充:工具执行走一条统一的编排

模型给的工具调用项
│ 路由(决定调哪个处理器)

对应处理器(跑命令 / 打补丁 / 外部工具…)
│ 统一编排

审批 → 选沙箱 → 执行 →(被拒可升级)→ 输出


包成一条结果,回灌进历史 → 下一次采样

"被拒可升级"是它的特色: 沙箱拒绝时按策略再试一次更宽的沙箱,而不是直接失败。

它没回答什么

  • 不用原生工具调用怎么办——它假设模型支持。
  • 工具清单怎么每轮变——每一步会"冻结这一步看到的工具视图",但没讲这个视图怎么算出来。
  • 循环状态怎么整个存下来——没有这一层。

坑与代价

  • 主循环极重。 它自己承认:压缩、钩子、技能注入、计划模式、多 agent 抢占全塞进同一个循环,两千多行,分支极多。 (依据:Agent 库 · OpenAI Codex CLI · 「采样→工具→回灌」的心跳 —— run_turn 把压缩、hooks、技能/插件注入、计划模式、多 agent 邮箱抢占全塞进同一个循环,2000+ 行、分支极多)

    这是一条重要的反面教材: 它跟 kimi-code 的"循环自己不碰会话/传输/权限/压缩"、webwright 的"循环体只有一行"正好相反。 同样是能跑的产品,一个把什么都塞进循环,一个把什么都挪出去。我们该学后者。

  • "一轮一项"是经验而非保证。 注释明说模型可能一次返回多个项,只是实践中通常一项——代码用有序并发队列正是为了应对多项。

    这条要记:不能假设"模型一次只调一个工具"。 即使实测都是一个,也要按多个写。

  • 压缩有损(见上)。