复盘 · 对照《从 LLM 到 Coding Agent》
把《从 LLM 到 Coding Agent》的二十一个概念逐个对上 Codex 的生产实现与本专栏的篇目,列出 Codex 刻意没照教学建议做的几处,再提炼贯穿全书的几条规律:界面与内核之间隔着一层协议,历史只追加,有副作用的动作都走同一个编排器,沙箱与审批各管一件事。最后是教学实现里没有对应物的那一半。
复盘 · 对照《从 LLM 到 Coding Agent》
前 54 篇把 codex-rs 从入口一路拆到了发布。收尾的第一篇回到起点:站里的《从 LLM 到 Coding Agent》用二十篇正文加一篇实战番外,把一块只会输出文本的“裸芯片”焊成了一台能自主干活的机器,每一步都配了一个用 TypeScript 写的最小实现。这一篇把两本书并排摆开:左边是教学最小实现,右边是 Codex 的生产实现,最后一列指向本专栏里讲细节的篇目。
教学循环落在 Codex 的哪一层
那本书的内核是一个 while 循环:问模型,有 tool_use 就执行,把 tool_result 喂回去,再问。在 Codex 里,这个循环几乎原样活在 run_turn 里,只是上下都多出了好几层:
上面多出的一层让同一个内核服务多个前端,下面多出的一层让命令在操作系统强制的边界里执行。这两处是教学实现与生产实现距离最远的地方,本文最后一节再回来看。
逐概念对照
按那本书的阶段分组。“Codex 的生产实现”一列只写结构上的差别,细节见最后一列。
起点与“让模型能做事”
| 概念 | 教学最小实现 | Codex 的生产实现 | 本专栏 |
|---|---|---|---|
| 裸 LLM API | 一次 messages.create;无状态,每轮重发全部历史;一个 API key | 只说 Responses API;请求带 store: false,走 HTTP 时每次带完整历史,走 WebSocket 时只发增量;认证单独成一个子系统,八种凭据统一成 CodexAuth | 模型客户端、登录与认证 |
| 工具调用 | 用 JSON Schema 声明工具;模型回 tool_use,宿主执行后按 id 回填 tool_result | 工具以 ToolSpec 发出(function、namespace、custom 等五种);ToolRouter::build_tool_call 把输出项变成 ToolCall;缺配对的调用在生成输入时补一条 aborted 输出 | 工具系统总览、上下文与历史 |
| Agent Loop | while (true),没有 tool_use 就退出 | RegularTask 反复调用 run_turn;“还要不要再问”就是 needs_follow_up(有工具调用,或期间来了新输入);开轮准备、中途压缩与 Stop hook 都围着它 | 一条消息的生命周期、run_turn 主循环 |
| 工具抽象 | 一个富接口:schema、校验、权限、isConcurrencySafe 等元数据与两套渲染,默认值往安全方向兜 | ToolExecutor 把 spec 与处理器绑在同一个对象上,supports_parallel_tool_calls 默认 false;CoreToolRuntime 补上 hook 与遥测;工具表每次采样前按开关、模型目录与执行环境重建,暴露方式有六种 | 工具系统总览 |
流式与并发
| 概念 | 教学最小实现 | Codex 的生产实现 | 本专栏 |
|---|---|---|---|
| 流式处理 | SSE 事件序列;文本来一片打一片,工具参数是攒到块结束才解析的部分 JSON | codex-api 把 SSE 与 WebSocket 事件统一翻译成 ResponseEvent,函数参数的增量事件直接忽略,调用以完整的输出项为准;app-server 再把消息、命令、补丁、MCP 调用统一成 item,按开始、增量、完成推给前端 | 模型客户端、v2 协议、工程实践 |
| 边流边执行 | 工具块一结束就派发;并发安全的一起跑,其余串行 | 输出项一完成就 tokio::spawn,future 进 FuturesOrdered,流结束后 drain_in_flight 按调用顺序写回;并发靠一把读写锁,可并行的拿读锁、其余独占写锁,不必事先看到整批调用 | 工具系统总览、run_turn 主循环 |
安全与控制
| 概念 | 教学最小实现 | Codex 的生产实现 | 本专栏 |
|---|---|---|---|
| 权限系统 | 工具自分类、规则、从 plan 到 bypass 的权限模式;canUseTool 返回 allow、deny 或 ask;拒绝要喂回模型 | 拆成两个正交的量:PermissionProfile 管技术上能碰什么,由操作系统强制;AskForApproval 管越界前问不问。规则是 Starlark 写的执行策略,审批依次经过 hook、自动审查与用户,Denied 的理由作为工具结果交还 | 权限模型、审批流程、执行策略、自动审查 |
| 中断与转向 | AbortSignal 贯穿到底;给悬空的工具合成“已取消”结果;转向是“打断加追加”,新话接到下一轮 | Op::Interrupt 触发取消令牌并以子令牌一路下传,任务有 100 毫秒自行收尾;历史里写一条 <turn_aborted> 标记,发出 TurnAborted 而不是错误事件;转向走 turn/steer,插进正在跑的这一轮 | 任务类型与 Plan 模式、run_turn 主循环、输入框 |
| Hooks | PreToolUse、PostToolUse、Stop、UserPromptSubmit;外部命令从 stdin 读事件、用 stdout 表态 | 引擎叫 ClaudeHooksEngine,12 个事件,处理器是命令或 MCP 工具;按内容哈希记账,新增或改动的钩子要在 /hooks 里审阅信任才运行;goal 扩展相当于编译进来的 Stop 钩子 | Hooks、扩展 API |
上下文工程
| 概念 | 教学最小实现 | Codex 的生产实现 | 本专栏 |
|---|---|---|---|
| Token 预算 | 字符数除以 4 粗估,以 API 回报的 input_tokens 为锚;分级水位线,给抢救操作留余量 | 以上一次响应的 total_tokens 为准,加上之后新条目的按字节粗估;自动压缩阈值是窗口的 90%,另有 95% 的硬上限;开发中的 token_budget 开关还给模型一个 get_context_remaining 工具 | 上下文压缩、其它内置工具 |
| 上下文注入 | 系统提示、环境、项目记忆、历史、动态附件分层拼装;静态放高处,动态放低处 | 系统提示只放基础指令,线程创建时定下;开发者指令、权限、协作模式、AGENTS.md、环境都是历史开头的消息,之后的变化以差异追加;每段注入都是实现 ContextualUserFragment 的具名结构体 | 指令从哪来、上下文与历史、Skills |
| Prompt 缓存 | 显式缓存断点加滚动断点;守住字节稳定,要改先拷贝 | 请求里没有断点,只有默认取根线程 ID 的 prompt_cache_key;字节稳定靠只追加的历史、世界状态差分与确定性的合成条目 ID | 模型客户端、上下文与历史 |
| 自动压缩 | 保留近期、摘要远古;proactive 为主、reactive 兜底;摘要提示词决定成败 | 四个时机:开轮前、一轮中途、轮末、手动 /compact;OpenAI、Azure、Amazon Bedrock 走远程压缩,由服务端返回一个不透明的压缩条目,其余提供方在本地写交接摘要;新历史基本只剩最近的用户消息加上摘要 | 上下文压缩 |
| 精细化上下文管理 | 工具结果超限就外置存盘,按 id 定点掏空,大块内容只留句柄 | 工具输出写进历史时按模型的截断策略截断,rollout 保留全文;命令输出先进 1 MiB 头尾缓冲,再按 token 预算保留头尾、截掉中间;压缩请求超窗时把末尾的工具输出换成占位文字 | 上下文与历史、执行命令、上下文压缩 |
健壮性
| 概念 | 教学最小实现 | Codex 的生产实现 | 本专栏 |
|---|---|---|---|
| 错误恢复 | 分清可恢复与致命;指数退避加抖动、听 Retry-After;过载换模型;清理孤儿消息;每条恢复路径都要硬闸 | CodexErr::retry_delay 决定重不重试;流级重试默认 5 次,从 200 毫秒倍增、带 10% 抖动,优先用服务端建议的等待;WebSocket 重试用完退到 HTTPS;只把完整的输出项写进历史,重试接着已有进度做 | run_turn 主循环、模型客户端 |
| 成本追踪 | 四类 token 分价目表累加;覆盖所有出口;美元预算做硬闸 | 客户端不计价,ModelInfo 没有价格字段;token 用量随 thread/tokenUsage/updated 通知到达,额度来自响应头里的限额快照;估算花费由服务端给出,另有 codex.turn.cost_microusd 指标 | 模型目录与提供方、其它界面、遥测与分析 |
| 扩展思考 | 思考块带签名,必须原样保管;签名与生成它的模型绑定 | 请求的 include 固定为 reasoning.encrypted_content,推理以密文回到客户端、随历史再发回去;统计用量时先看服务端是否声明已计入推理;TUI 把推理摘要渲染成单独的单元 | 模型客户端、上下文压缩、历史记录单元 |
扩展、规模与实战
| 概念 | 教学最小实现 | Codex 的生产实现 | 本专栏 |
|---|---|---|---|
| 子 Agent | 一个工具,内部递归跑 runAgent,只带回结论;子 Agent 的工具集里不放 spawn_agent | 子代理是同一个 ThreadManager 里的另一条线程;V1 按深度限制(agents.max_depth 默认 1),V2 限驻留数,结论作为 FINAL_ANSWER 投进父线程的信箱;/review、自动审查与记忆整合也都是受限的子代理 | 多 agent、自动审查、Codex Cloud、代码审查与 worktree |
| MCP 与懒加载 | 外部工具适配成统一的 Tool;工具太多时只发简介,靠 search_tools 按需加载 | McpRuntime、McpConnectionSet 与每步冻结的 McpBinding 三层;工具改名进 mcp__服务器 命名空间,支持工具搜索的模型上默认延迟加载,由 tool_search(BM25,默认返回 8 个)取回;传输有 stdio 与可流式 HTTP,带 OAuth 与 elicitation | MCP 工具调用、MCP 客户端、其它内置工具 |
| 会话历史 | append-only JSONL;抄本即真相,压缩只是运行时视图;恢复就是读回来接着循环 | 每个线程一个只追加的 rollout JSONL,压缩结果以 compacted 条目追加;SQLite 里的线程列表与分页历史是可以重建的投影;新建、恢复、分叉都汇入 spawn_thread | 会话持久化、ThreadManager 与 CodexThread |
| 仓库总结 | 六阶段流水线:git 裁剪、确定性骨架、符号地图、重要性排序、分治归并、验证与失效 | 没有专门的流水线:/init 把一段固定的提示词当作用户消息发出,让 agent 用平常的工具自己探索,写一份 AGENTS.md | 指令从哪来 |
最后一行值得多看一眼。/init 的实现只有三行:
SlashCommand::Init => {
const INIT_PROMPT: &str = include_str!("../../assets/prompt_for_init_command.md");
self.submit_user_message(INIT_PROMPT.to_string().into());
}(codex-rs/tui/src/chatwidget/slash_dispatch.rs:290)
提示词 codex-rs/tui/assets/prompt_for_init_command.md 要求生成一份标题为 “Repository Guidelines” 的贡献者指南,200 到 400 词为宜,动笔前先看当前目录是否已有 AGENTS.md,有就不改。那本书用一整篇讲“注定读不完时怎么画地图”;Codex 把画地图交给模型现场探索,画好的地图再按指令从哪来的规则注入之后的每个会话,正好是那篇结尾说的分工:一份薄的全局摘要常驻上下文,具体问题靠工具现场去查。
Codex 没照教学建议做的地方
对照表里有几行,Codex 的做法与那本书的建议不同,而且是有意为之。它们都出自各篇末尾的对照小节:
| 那本书的建议 | Codex 的做法 | 篇目 |
|---|---|---|
| 主模型过载时自动降级到备用模型 | 过载错误不在重试之列,本轮结束并提示换一个模型,换不换交给用户 | 模型目录与提供方 |
| 在客户端按价目表把 token 换算成钱 | 没有价格字段,也没有计价代码,花费以服务端给的数为准 | 模型目录与提供方、其它界面 |
| 在稳定前缀末尾打缓存断点 | 不打断点,靠稳定的缓存键与处处维护的字节稳定 | 模型客户端 |
| plan 是最严的权限模式,写操作一律拒绝 | Plan 是协作模式,“不改文件”只写在提示词里,代码只拦下 update_plan;要硬性只读得靠沙箱与审批 | 任务类型与 Plan 模式 |
| Stop 钩子要配最大重试次数 | 不设硬上限,把 stop_hook_active 传给钩子,由钩子自己刹车 | Hooks |
| 超长的工具结果存盘,只给模型预览和路径 | 不存盘,保留头尾、截掉中间 | 执行命令 |
| 转向时结束当前回合,新消息接到下一轮 | 插话并入正在跑的这一轮,下一次采样就交给模型;想排到下一轮按 Tab | run_turn 主循环、输入框 |
| 子 Agent 是一次同步的函数调用 | spawn_agent 立即返回,结论经通知或信箱回来 | 多 agent |
这些差别大致出于两种考虑。一是 Codex 只说 Responses API 这一种协议,服务端能做好的事(计价、远程压缩、提示缓存与粘性路由)就交给服务端;二是能交给模型判断的事(什么时候申请提权、用哪个技能、Plan 模式下别动手),它更愿意写进提示词与工具说明,而不是写死在循环里。
贯穿全书的几条规律
- 界面与内核之间隔着一层协议。 TUI 的依赖里没有
codex-core,所有前端都经 app-server 的 v2 JSON-RPC 访问同一个服务端:turn/start一启动就返回,进度全靠通知,审批是服务端反过来发给客户端的请求。连进程内嵌的那一份,也刻意保留请求、通知、事件这套模型(workspace 全景、app-server)。 - 内核是一对队列。 一个线程在内核里就是一个
CodexThread:一端是提交Op的队列,一端是吐出EventMsg的队列,常驻的submission_loop逐个处理提交;一个会话同一时刻只跑一个任务(ThreadManager 与 CodexThread、任务类型与 Plan 模式)。 - 历史只追加,改写只有两个出口。 根目录
AGENTS.md把“不改写历史”写成了规矩;会变的上下文建模成世界状态的分区,第一轮全量、之后只追加差异;改写只剩压缩与回滚两处。落盘也一样:rollout 只追加,SQLite 只是它的投影(上下文与历史、会话持久化)。 - 每次请求冻结一份快照。 设置在开轮时提交成线程默认值,再冻结成只读的
TurnContext;每次采样前还要捕获一个StepContext,发给模型的工具清单与随后执行调用的注册表是同一份(Session 与 TurnContext、工具系统总览)。 - 有副作用的动作都走同一个编排器。 命令与补丁都经
ToolOrchestrator:审批、选沙箱、执行、被沙箱拒绝时视策略请求提权重试;审批请求先交给 hook,开了自动审查就交给审查模型,最后才轮到你(审批流程)。 - 沙箱与审批各管一件事,三个平台各自落地。 一个决定命令技术上能碰什么,一个决定越界前问不问,两者自由组合;macOS 现拼 Seatbelt 策略,Linux 用 bubblewrap 加 seccomp,Windows 用写受限令牌、elevated 模式另建专门的沙箱用户,联网再交给本地代理按名单放行(权限模型、网络代理)。
- 可覆盖的配置与不可越过的约束分成两摞。 config 层逐键合并出生效值,requirements 层合成管理员的约束,再把取值关进
Constrained校验器,会话中途切换也越不过去(配置系统)。 - 拿不准就拒绝。 跨进程的审批答复解析失败或客户端报错,一律按拒绝处理;Linux 沙箱遇到 glob 展开过多、残留特权、代理端点解析不出这类情况直接报错,不降级成无沙箱;执行策略多条命中时取最严格的那条(app-server、Linux 沙箱、执行策略)。
Codex 独有的那一半
对照表只覆盖两本书都讲到的交集。Codex 作为一个多端产品,还有一大块在教学实现里找不到对应物:
| 方面 | 做什么 | 篇目 |
|---|---|---|
| 服务化 | app-server 守护进程被多个客户端共享,可经 stdio、Unix socket、WebSocket 或远程控制接入;exec-server 把起进程、读写文件、发 HTTP 请求搬到本机或另一台机器 | 守护进程与传输、exec-server |
| 操作系统级安全 | 三套沙箱、联网代理、Starlark 执行策略,以及让另一个模型替你审批的自动审查 | macOS 沙箱、Linux 沙箱、Windows 沙箱、自动审查 |
| 组织与分发 | 托管的 requirements、插件与 marketplace、从 Claude Code 与 Cursor 导入配置 | 配置系统、插件、从别的 agent 迁移 |
| 跨会话的积累 | 后台把旧会话提炼成记忆;goal 扩展在线程空闲时自动开下一轮,直到目标完成或预算耗尽 | 记忆、扩展 API |
| 编辑与执行的格式 | 自有的 apply_patch 补丁格式、可跨轮续写的命令会话、让模型写 JavaScript 编排工具的 Code mode | apply_patch、执行命令、Code mode |
| 产品表面 | 占全仓约四分之一非测试代码的终端界面,以及登录认证、实时语音、遥测、Codex Cloud 与代码审查 | TUI 架构、登录与认证、实时语音、遥测与分析 |
教学实现回答“为什么要这样做、最少要做什么”;Codex 回答的是“同一个内核要服务多个前端、三个操作系统和一个组织时,还得长出哪些层”。想深挖哪个概念,查上面的表,先读本专栏对应的篇目,再点回那本书看最小版本。
下一篇换个角度,把 Codex 和站里另外两本读得最细的开源实现 Grok Build、OpenCode 放在一起比较。
上一篇:工程实践 · 测试、构建与发布 · 下一篇:横向对比 · Codex 与 Grok Build、OpenCode