本文分析的是 /home/xm1994/Projects/codex/ 源码里的 LLM 上下文管理,不是普通的会话状态或 UI 状态。结论先说:Codex 确实有一套很明确的上下文治理机制,而且和 Claude Code 的思路高度接近,但实现路径更偏向于 线程历史 baseline + 增量上下文更新 + 自动/手动 compaction + token 预算控制。它不是一个单纯“把聊天记录原样累积”的系统。
Codex 的历史管理不是简单 list append。核心结构在 core/src/context_manager/history.rs:
items保存线程历史token_info保存 token 使用reference_context_item保存上下文 baseline
注释已经把语义写得很直白:reference_context_item 是“下一轮 regular turn 的 baseline”,如果它是 None,下一个 turn 会走 full reinjection;否则走 settings diff。
这意味着 Codex 在上下文管理上用的是“基线 + 差分”模型,而不是每轮完全重建一份完整 prompt。
真正构造上下文的代码在 core/src/codex.rs:
- 如果
reference_context_item为空,就调用build_initial_context() - 如果 baseline 已存在,就调用
build_settings_update_items()
对应逻辑如下:
let should_inject_full_context = reference_context_item.is_none();
let context_items = if should_inject_full_context {
self.build_initial_context(turn_context).await
} else {
self.build_settings_update_items(reference_context_item.as_ref(), turn_context)
.await
};这里的注释也明确说了 steady-state 路径是 “append only context diffs to minimize token overhead”。这就是典型的上下文节流手法:稳定部分不重复灌入,变化部分才更新。
同一个函数还会把 TurnContextItem 持久化,并且在内存里更新 reference_context_item。这说明 Codex 的上下文不是纯运行时临时拼装,而是有跨 turn 的持久 baseline。
可以把 Codex 的上下文管理流程理解成下面这样:
flowchart TD
A[Start turn] --> B[Read reference_context_item]
B --> C{Baseline exists?}
C -- No --> D[build_initial_context]
C -- Yes --> E[build_settings_update_items]
D --> F[Record context items]
E --> F
F --> G[Persist TurnContextItem]
G --> H[Update reference_context_item]
H --> I[Clone history for prompt]
I --> J[Send sampling request]
J --> K[Update token usage]
K --> L{Token limit reached?}
L -- No --> M[Continue turn / finish]
L -- Yes --> N[run_auto_compact]
N --> O[Replace compacted history]
O --> P[Recompute token usage]
P --> Q[Next turn uses new baseline]
这个流程对应到源码,大致是:
turn开始后,先看有没有reference_context_item- 有基线就走 diff,没有基线就走完整初始上下文
- 采样结束后更新 token 使用
- 如果 token 超阈值,就触发 compaction
- compaction 会替换历史,并为后续 turn 建立新的 baseline
所以 turn 可以理解成“一次执行回合”,而 baseline diff 是“每次回合只补变化,不重复整份上下文”的机制。
在 core/src/codex.rs 的 run_turn() 里,Codex 会先做 pre-sampling compaction,然后在采样之后检查 token 使用:
let auto_compact_limit = model_info.auto_compact_token_limit().unwrap_or(i64::MAX);
...
let total_usage_tokens = sess.get_total_token_usage().await;
let token_limit_reached = total_usage_tokens >= auto_compact_limit;
...
if token_limit_reached && needs_follow_up {
run_auto_compact(...).await?;
continue;
}这和 Claude Code 的上下文治理思路非常接近:
- 先统计当前上下文是否接近阈值
- 再触发压缩
- 再继续后续对话
Codex 不是等到彻底爆掉才处理,而是把 compaction 当成运行时控制流的一部分。
run_pre_sampling_compact() 会在真正采样前先判断是否需要压缩。见 core/src/codex.rs。
其中一个关键逻辑是 core/src/codex.rs 的 maybe_run_previous_model_inline_compact():
- 取出上一轮 regular turn 的模型
- 比较旧模型和新模型的
context_window - 如果旧窗口更大、新窗口更小,并且当前 token 使用已经超过新模型的 compaction 阈值,就先对旧模型执行 compaction
这说明 Codex 不只是“当前模型 token 紧张才压缩”,还考虑了 模型切换带来的上下文窗缩窄。这类行为已经很接近成熟 agent 的上下文调度器,而不是简单聊天前端。
Codex 的 app-server 明确暴露了 thread/compact/start。文档在 app-server/README.md 写得很清楚:
- 这是手动触发历史 compaction 的接口
- 请求立即返回
{} - 进度通过标准
turn/*和item/*事件流出 - 只会看到一个
contextCompactionitem
文档还说这个 item 也可能自动发生,见 app-server/README.md。
这点很重要,因为它说明 Codex 把上下文压缩当成了一等能力,而不是某个偶发补救逻辑。
Codex 的 compaction 不是只生成一段摘要文本,它会把原历史替换成新的 compacted history。
在 core/src/compact.rs 里,run_inline_auto_compact_task() 会把 compact_prompt 当成合成输入,跑一个 contextCompaction item,然后在结束后构造新的历史:
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
...
sess.replace_compacted_history(new_history, reference_context_item, compacted_item).await;
sess.recompute_token_usage(turn_context).await;这里不是“保存一个摘要,继续沿用旧历史”,而是替换历史本身。
OpenAI provider 走的是远程路径。在 core/src/compact_remote.rs 里:
- 先裁剪掉一部分函数调用历史,避免超窗
- 再调用
compact_conversation_history() - 再
process_compacted_history() - 最后
replace_compacted_history()并recompute_token_usage()
这说明 Codex 的上下文压缩不是一种“UI 视图压缩”,而是对线程历史的结构性重写。
在远程 compaction 之前,Codex 会先把历史里最容易爆上下文的部分裁掉。见 core/src/compact_remote.rs:
let deleted_items = trim_function_call_history_to_fit_context_window(
&mut history,
turn_context.as_ref(),
&base_instructions,
);这个函数会在历史超窗时,从末尾回退删除 Codex 生成的项,直到能塞进 context window。对应逻辑在同文件后半段,说明 Codex 处理上下文的方式不仅有“摘要”,还有更细粒度的 历史裁剪。
这和 Claude Code 里常见的“工具输出裁剪 / 大块上下文外置”属于同一类技术路线。
Codex 会在每轮更新 token 使用,并把它发给 UI。见 core/src/codex.rs:
state.update_token_info_from_usage(token_usage, turn_context.model_context_window());
self.send_token_count_event(turn_context).await;还有一个回退路径是 recompute_token_usage(),它会直接根据当前历史重新估算总 token,并更新 TokenUsageInfo。见 core/src/codex.rs。
这说明 Codex 的上下文管理不是只在“快满了”的时候才看一眼,而是持续跟踪:
- 当前总 token
- 模型 context window
- auto compact limit
- 历史项增长速度
这里很容易混淆:Codex 里确实有 memories,但那不是当前 thread 的短期上下文本身。
例如 core/src/memories/prompts.rs 里,rollout 内容会被按模型窗口截断;memory_summary.md 也会被单独裁剪后注入到 developer instructions。见 core/src/memories/prompts.rs。
这说明 Codex 的设计是:
- 当前会话上下文:由 history / baseline / compaction 管
- 长期工作记忆:由 memories 管
二者是分层的,不是混在一个 message buffer 里。
如果只看上下文管理,Codex 和 Claude Code 的相似点很明显:
- 都有明确的 context window 意识
- 都会在阈值触发时做 compaction
- 都会保留结构化的长期上下文,而不是把所有东西永久塞进 prompt
- 都会把工具输出、历史项、模型切换视为上下文治理对象
但 Codex 的实现又有自己的特点:
- 更强的线程/turn/compaction 一体化协议
thread/compact/start直接暴露为正式 APIreference_context_item让它更像“baseline diff 系统”memories是单独子系统,而不是把长期记忆揉进聊天历史
所以如果你要一句话概括:
Codex 不是“没有上下文管理”,而是有一套相当完整的上下文预算与压缩系统;它和 Claude Code 的思路很接近,但更强调 thread 级结构化 compaction 和 baseline diff。
Codex 代码里能明确看到这些上下文管理技术:
reference_context_item作为上下文 baseline- regular turn 的增量上下文注入
- token 使用的实时 tracking
- 自动 compaction
- 手动 compaction API
- 远程 compaction endpoint
- compaction 后替换历史并重算 token
- 长期 memories 与短期上下文分离
如果你的问题是“Codex 有没有类似 Claude Code 的上下文管理技术”,答案是:有,而且不是浅层相似,是同一类 agent 上下文治理思想的实现。区别主要在于 Codex 的线程协议和 compaction 机制更显式、更结构化。