Codex 上下文管理机制技术分析

基于 codex-rs 源码的完整分析,所有引用均标注精确文件路径和行号。


架构总览

scss 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                            用户输入 / 工具输出                               │
└───────────────────────────────────┬─────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          ContextManager (history.rs)                        │
│                                                                             │
│   items: Vec<ResponseItem>          ← 全量对话历史(时序,最老在前)          │
│   token_info: Option<TokenUsageInfo> ← 上次 API 返回的权威 token 数          │
│   reference_context_item            ← diff 基线,用于增量注入初始上下文       │
│                                                                             │
│   ┌──────────────────────────────────────────────────────────────────────┐ │
│   │                   record_items() 写入流程                            │ │
│   │                                                                      │ │
│   │  新 item 到来                                                        │ │
│   │      │                                                               │ │
│   │      ▼                                                               │ │
│   │  ┌─────────────────────────────────────────────┐                    │ │
│   │  │         机制 ①  Output Truncation           │                    │ │
│   │  │                                             │                    │ │
│   │  │  仅对 FunctionCallOutput /                  │                    │ │
│   │  │       CustomToolCallOutput 生效             │                    │ │
│   │  │                                             │                    │ │
│   │  │  单条输出 > 10,000 tokens?                  │                    │ │
│   │  │      ├── 否 → 原样写入                       │                    │ │
│   │  │      └── 是 → Middle Truncation             │                    │ │
│   │  │              保留前50% + 后50%               │                    │ │
│   │  │              中间替换为截断标记               │                    │ │
│   │  └─────────────────────────────────────────────┘                    │ │
│   │      │                                                               │ │
│   │      ▼                                                               │ │
│   │  写入 items[]                                                        │ │
│   └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                          每轮 API 调用前
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Token 估算(机制 ②,度量工具)                       │
│                                                                             │
│  total_tokens = last_api_total_tokens                                       │
│               + Σ estimate(items added since last API response)             │
│                                                                             │
│  estimate 规则:                                                             │
│    普通 item  → JSON 序列化字节数                                            │
│    推理/压缩  → base64 解码公式 (encoded × 3/4 - 650)                       │
│    图像       → 7,373 bytes(original detail 按 patch 数计算)               │
│    GhostSnapshot → 0(模型不可见)                                           │
│    ÷ 4 → tokens(ceil 除法)                                                │
└──────────────────────────────┬──────────────────────────────────────────────┘
                               │
               total_tokens >= auto_compact_limit?
               (context_window × 90%,如 128k 模型 = 115,200)
                               │
              ┌────────────────┴─────────────────┐
              │ 否                               │ 是
              ▼                                 ▼
    正常构建 Prompt              ┌───────────────────────────────────────┐
    发送 API 请求                │       机制 ③  Auto-Compact            │
                                │                                       │
                                │  1. 追加 compact prompt(prompt.md)  │
                                │  2. 发送"压缩轮"给 LLM               │
                                │  3. 提取 LLM 摘要                     │
                                │  4. 构建新历史:                      │
                                │     最近用户消息(≤20k tokens)        │
                                │     + summary_prefix + 摘要           │
                                │  5. 替换整个历史                      │
                                └───────────────────────────────────────┘
                                                 │
                                                 ▼
                                       继续正常 API 请求

机制一:Output Truncation(输出截断)

单条工具输出防溢出 。针对 shell 命令、代码执行等工具返回的文本,超过 10,000 tokens 时触发,与历史总量无关。

采用 Middle Truncation:保留前后各 50%,丢弃中间,插入截断标记。之所以不从尾部裁,是因为开头通常含最重要的摘要信息,结尾是最新状态和错误信息,中间往往是价值最低的重复细节。

css 复制代码
原始输出(30,000 tokens):
[开头内容 ················ 中间内容 ················ 结尾内容]

截断后(10,000 tokens):
[开头 5,000 tokens] ...20,000 tokens truncated... [结尾 5,000 tokens]

只截断工具输出的文本体,用户消息、助手消息、推理内容均原样保留。图像永远不截断。


机制二:Token 估算(Token Estimation)

度量工具,不做任何修改。为 Truncation 提供 budget 参数,为 Auto-Compact 提供触发判据。

估算逻辑

不调用任何 tokenizer API,全部本地计算,分两层叠加:

scss 复制代码
total_tokens = last_api_total_tokens          // 上次 API 响应返回的精确值(基线)
             + Σ estimate(新增 items)          // 基线之后新增消息的本地估算(增量)

基线在每次 API 响应后自动更新为精确值,两次 API 调用之间的新消息用估算补足。这样避免了为每条新消息单独调 API 计 token。

单条 item 的估算规则:

item 类型 估算方式
普通消息、工具调用/输出 JSON 序列化字节数 ÷ 4(向上取整)
推理内容、压缩历史(加密 base64) encoded_len × 3/4 - 650,还原为原始字节数后 ÷ 4
图像 固定 7,373 bytes ÷ 4 ≈ 1,844 tokens;detail:original 按 32×32 patch 数计算
GhostSnapshot 0(客户端内部状态,模型不可见)

bytes ÷ 4 的比例来自 OpenAI tokenizer 对英文文本的经验均值。用天花板除法保守估计,宁多不少,确保不会低估历史长度而漏触发 Auto-Compact。

Demo:这段文字估算是多少 token?

css 复制代码
Hello, how are you doing today? I'm working on a Rust project.

计算步骤:

  1. 字节数:63 bytes(纯 ASCII,1 字符 = 1 字节)
  2. tokens = ceil(63 / 4) = 16 tokens

实际用 tiktoken 精确计数是 15 tokens,误差 1 个,在可接受范围内。

再看中文:

复制代码
你好,今天工作进展怎么样?我在做一个 Rust 项目。

计算步骤:

  1. 字节数:66 bytes(中文每字 3 字节,标点 3 字节,ASCII 字符 1 字节)
  2. tokens = ceil(66 / 4) = 17 tokens

实际 tiktoken 结果约 30 tokens------中文的 bytes/token 比例远高于 4,此处估算明显偏低。这是 bytes/4 启发式的固有局限:对中文、日文等多字节语言会低估,但 Codex 主要处理代码和英文,整体偏差可接受。


机制三:Auto-Compact(自动压缩)

定位

全局兜底。当对话历史积累到模型 context window 的 90% 时触发,用 LLM 生成摘要替换整个历史。

触发阈值

rust 复制代码
// codex-rs/protocol/src/openai_models.rs
pub fn auto_compact_token_limit(&self) -> Option<i64> {
    self.context_window.map(|w| (w * 9) / 10)  // context_window × 90%
}

以 128k 模型为例:

ini 复制代码
context_window      = 128,000 tokens
auto_compact_limit  = 115,200 tokens(90%)
tool_output_limit   =  10,000 tokens(7.8%,单条上限)

三种触发场景

场景 触发时机 初始上下文处理
Pre-turn 用户发消息,采样前检查 不注入(下次正式轮次自动重注入)
Mid-turn 模型输出中途超限 注入到最后一条用户消息之前
Manual 用户执行 /compact 不注入

三种场景的区别在于初始上下文(system prompt 等)的注入位置:Mid-turn 时模型训练期望在最后一条用户消息前看到初始上下文,其余场景留给下次正式轮次处理。

Compact Prompt(完整原文)

作为"最后一条用户消息"追加到历史末尾,告诉 LLM 它的任务是生成交接摘要:

vbnet 复制代码
// codex-rs/core/templates/compact/prompt.md

You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary
for another LLM that will resume the task.

Include:
- Current progress and key decisions made
- Important context, constraints, or user preferences
- What remains to be done (clear next steps)
- Any critical data, examples, or references needed to continue

Be concise, structured, and focused on helping the next LLM seamlessly
continue the work.

Summary Prefix(完整原文)

压缩完成后,这段前缀 + LLM 生成的摘要,拼接为新历史的最后一条 user 消息:

kotlin 复制代码
// codex-rs/core/templates/compact/summary_prefix.md

Another language model started to solve this problem and produced a summary
of its thinking process. You also have access to the state of the tools that
were used by that language model. Use this to build on the work that has
already been done and avoid duplicating work. Here is the summary produced
by the other language model, use the information in this summary to assist
with your own analysis:

设计意图:明确告知下一轮 LLM "这是另一个模型的思考过程",防止模型把自己的历史混淆进来。

完整压缩流程

sql 复制代码
触发 Auto-Compact
      │
      ▼
将 compact prompt 作为最后一条 user 消息追加到当前历史
      │
      ▼
发送"压缩轮"给 LLM(流式)
      │
      ├─── 成功 ──────────────────────────────────────────┐
      │                                                   │
      ├─── ContextWindowExceeded                          │
      │    (连压缩本身也超限)                             │
      │         │                                         │
      │         └── 从历史最前面移除一条,重试 ────────────┤
      │             (保留后缀以复用 KV cache)             │
      │                                                   │
      └─── 网络错误                                        │
               │                                          │
               └── 指数退避,重试 ───────────────────────┘
                                                          │
                                                          ▼
                                          提取 LLM 返回的最后一条 assistant 消息作为摘要
                                                          │
                                                          ▼
                                          构建新历史:
                                            从旧历史中收集所有真实用户消息
                                            从新到旧贪心选取,上限 20,000 tokens
                                            末尾追加 summary_prefix + 摘要
                                                          │
                                                          ▼
                                          替换整个历史,重新计算 token 用量

新历史的结构

从最新到最旧贪心选取用户消息(上限 20,000 tokens),助手消息和工具调用历史全部丢弃,用摘要代替:

less 复制代码
压缩前(100k+ tokens):
  [user]      帮我分析这段代码
  [assistant] 好的,我来看看...
  [tool_call] shell: cat main.rs
  [tool_output] <5000 行>
  [assistant] 发现了几个问题...
  ... × 数十轮

压缩后(< 25k tokens):
  [user]  帮我分析这段代码          ← 最近用户消息(≤ 20k tokens)
  [user]  还有这个函数也看一下
  [user]  SUMMARY_PREFIX\n          ← LLM 摘要,role 故意设为 "user"
          当前进度:已分析 main.rs,
          发现 line 42 内存泄漏...
          下一步:检查 utils.rs

摘要用 role: "user" 写入而非新角色,是因为模型训练时就期望这种格式。


三种机制的协作关系

vbnet 复制代码
                   是"眼睛"
Token 估算 ─────────────────────────────→ Auto-Compact
    │                                       (总量超 90%?)
    │         提供 TruncationPolicy 参数
    └────────────────────────────────────→ Output Truncation
                                           (单条超 10k tokens?)

Output Truncation  ─── 治标 ───→ 控制单条体积,减缓历史增长速度
Auto-Compact       ─── 治本 ───→ 总量超限时,重置历史到可控规模

典型的长对话生命周期

ini 复制代码
第 1 轮:执行 cat 大文件,输出 50k tokens
  → Truncation 截断到 12k tokens,写入历史 ✓

第 3 轮:再次执行工具,输出 30k tokens
  → Truncation 截断,历史持续累积

第 15 轮:Token 估算发现 total = 116k ≥ 115.2k(90% 阈值)
  → Auto-Compact 触发
  → LLM 生成摘要,历史替换为 ~5k tokens
  → 重新从低点开始积累

第 30 轮:再次触发 Auto-Compact...
相关推荐
胡哈3 小时前
OpenMAIC 课程生成的三阶段流水线架构
llm·agent
Flying pigs~~4 小时前
Dify平台入门指南:开源LLM应用开发平台深度解析
人工智能·开源·大模型·agent·dify·rag
donglianyou4 小时前
Agent技术详解与实战
python·langchain·agent·langgraph
葡萄城技术团队4 小时前
实战指南:下一代AI开发必备范式Agent Skills
agent
creator_Li4 小时前
Agent笔记
agent
怪兽同学5 小时前
统一管理Agent Skills
前端·agent
前端双越老师6 小时前
为什么我现在不安装 Hermes Agent
程序员·agent
花千树-0106 小时前
MCP HTTP 传输详解:比 SSE 简单,但有一个意外的坑
java·agent·sse·function call·ai agent·mcp·harness
花千树-0106 小时前
三个 Agent 并行调研:用 concurrent 节点构建并发-汇聚式旅游规划助手
java·langchain·agent·function call·multi agent·mcp·harness