前言

codex-rs 是 OpenAI 开源的 Codex CLI 的 Rust 核心实现。Codex 是一个编程 Agent:用户输入自然语言任务,模型通过多轮对话、调用工具(主要是 shell 命令)来完成任务。

这篇文章从 codex-rs 源码出发,系统梳理 Codex 的上下文管理机制——也就是它是怎么组织、维护和压缩发送给大模型的 messages 列表的。

整体架构:两层状态 + 一个 ContextManager

Codex 的状态管理分成两层:

  • SessionState(会话级状态):整个对话生命周期内持续存在,持有 ContextManager、上一轮设置快照(previous_turn_settings)、已授权权限等。
  • TurnState / ActiveTurn(轮次级状态):每一轮对话的临时状态,包含待审批的工具调用、挂起的用户输入等。任务类型通过 TaskKind 区分:Regular(正常对话)、Review(代码审查)、Compact(上下文压缩)。

ContextManager 是整个上下文管理的核心结构体:

1
2
3
4
5
struct ContextManager {
    items: Vec<ResponseItem>,                        // 核心:append-only 的消息历史
    token_info: Option<TokenUsageInfo>,               // 上一次 API 返回的 token 用量
    reference_context_item: Option<TurnContextItem>,  // 上下文快照基线(用于 diff)
}

items:一个 append-only 的列表

items 是一个 Vec<ResponseItem>,所有消息按时间顺序追加,不会在中间插入。这一点非常重要:它意味着 Codex 的历史管理逻辑只有"往后加"和"从前面/后面删"两种操作,没有"在中间改"。

ResponseItem 是一个枚举,包含所有可能出现在上下文里的消息类型:

类型 含义
Message 普通消息(user / assistant / developer 角色)
Reasoning 模型的推理过程(encrypted 或 summary)
FunctionCall / FunctionCallOutput 工具调用及其结果
LocalShellCall 本地 shell 命令调用
Compaction 压缩标记(“从这里开始是压缩后的历史”)
GhostSnapshot 隐形快照(模型看不到,用于 undo)
Other 其他类型

reference_context_item:设置快照基线

这个字段存储的是"上一次注入完整上下文时的设置快照"。每次新一轮对话开始时,Codex 会拿当前设置和这个快照做对比(diff),只把变化的部分注入上下文。如果快照为空(首轮),就注入完整的初始上下文。

指令体系:三层分离

Codex 使用 OpenAI 的 Responses API(而不是 Chat Completions API),这个 API 的消息结构和我们熟悉的 Chat API 有一个重要区别:system 指令不在 messages 数组里,而是放在一个独立的 instructions 字段中。

Codex 的指令体系因此分成三层:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
API 请求结构:
{
  "model": "o3",
  "instructions": "...",      ← 第一层:Base Instructions
  "input": [                  ← 第二层 + 第三层:在 messages 数组中
    { "role": "developer", ... },  ← 第二层:Developer Instructions
    { "role": "user", ... },       ← 第三层:Contextual User Messages
    { "role": "user", ... },       ← 真实用户消息
    ...
  ]
}

第一层:Base Instructions(基础指令 / Meta Prompt)

通过 API 的 instructions 字段发送,不出现在 items 数组中。这是模型的"身份定义",告诉模型"你是谁、你能干什么"。

内容来自模板文件(如 prompts/base_instructions/default.md),或者针对特定模型定制的版本(如 gpt_5_2_prompt.md)。整个会话期间基本不变。

在代码层面,is_api_message() 函数会过滤掉 role=system 的消息——因为 Responses API 中 system 指令走的是 instructions 字段,不需要也不应该出现在 items 数组里。

第二层:Developer Instructions(开发者指令)

role=developer 的消息形式存在于 items 数组中。这一层是本文重点讨论的内容,下文详细展开。

第三层:Contextual User Messages(上下文用户消息)

role=user 的消息形式存在于 items 数组中,但并不是用户真正说的话,而是 Codex 自动注入的环境信息。包括:

  • EnvironmentContext:用 <environment_context> XML 标签包裹,包含当前工作目录(cwd)、shell 类型、日期时间、时区、网络权限、子 agent 信息等。
  • UserInstructions:用 <user_instructions> 标签包裹,内容来自用户工作目录下的 AGENTS.md 文件。

这些消息通过 XML 标签和真正的用户消息区分开来,在回滚(rollback)和压缩(compact)时可以被特殊处理。

Developer Instructions 全景

Developer prompt 不是一段"固定的提示词",而是由多个模板片段按需拼装而成的。核心拼装逻辑在 build_initial_context() 函数中:

1
2
3
4
5
6
7
8
9
pub(crate) async fn build_initial_context(
    &self,
    turn_context: &TurnContext,
) -> Vec<ResponseItem> {
    let mut developer_sections = Vec::<String>::with_capacity(8);
    let mut contextual_user_sections = Vec::<String>::with_capacity(2);
    // ... 按顺序往 developer_sections 里塞各种片段 ...
    // ... 最后合并成一条 developer message ...
}

它准备一个空数组 developer_sections,按顺序往里塞各种片段,最后通过 build_developer_update_item() 合并成一条 developer message 注入到上下文中。

XML 标签包装机制

每个片段都用特定的 XML 标签包裹。这些标签不是装饰,而是有实际工程作用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<permissions instructions>
You are running in a sandbox with workspace write access...
</permissions instructions>

<collaboration_mode>
Execute tasks directly when the user's intent is clear...
</collaboration_mode>

<personality_spec>
You should be friendly, concise, and pragmatic...
</personality_spec>

标签的三个作用:

  1. 帮助模型理解结构——模型可以通过标签名知道"这段是权限规则"还是"这段是人格设定",不会混淆不同主题的指令。

  2. 支持 Diff 机制的识别——代码中定义了一个常量数组:

1
2
3
4
5
6
7
CONTEXTUAL_DEVELOPER_PREFIXES = [
    "<permissions instructions>",
    "<model_switch>",
    COLLABORATION_MODE_OPEN_TAG,       // "<collaboration_mode>"
    REALTIME_CONVERSATION_OPEN_TAG,    // "<realtime_conversation>"
    "<personality_spec>",
]

is_contextual_dev_message_content() 函数通过检查消息内容是否以这些前缀开头,来判断一条 developer message 是 Codex 自动注入的上下文消息,还是用户自定义的开发者指令。这在回滚、裁剪和 diff 时至关重要。

  1. 支持 Compact 时的安全清理和重注入——压缩上下文时,Codex 知道这些标签包裹的消息是"可以重新生成的",压缩后可以重新注入最新版本。

片段清单(按注入顺序)

以下是 build_initial_context() 按顺序注入的所有 developer 片段:

① 模型切换提示(Model Switch)

  • 触发条件:用户在对话中途切换了模型(如从 GPT-4o 切到 o3)
  • 标签<model_switch> ... </model_switch>
  • 内容:告诉新模型"用户之前在用另一个模型,上面的对话历史是那个模型产生的,请自然地接续对话"
  • 生成方式DeveloperInstructions::model_switch_message()

② 权限指令(Permissions)

  • 触发条件:始终注入
  • 标签<permissions instructions> ... </permissions instructions>
  • 生成方式DeveloperInstructions::from_policy()
  • 内容:由两部分模板拼接而成——

沙箱模式(选其一):

模式 模板文件 含义
DangerFullAccess sandbox_mode/danger_full_access.md 完全访问,无限制
WorkspaceWrite sandbox_mode/workspace_write.md 只能写工作区
ReadOnly sandbox_mode/read_only.md 只读模式

审批策略(选其一):

策略 模板文件 含义
Never approval_policy/never.md 全自动,永远不需要审批
UnlessTrusted approval_policy/unless_trusted.md 不在信任列表里的命令需审批
OnFailure approval_policy/on_failure.md 执行失败时才需审批
OnRequest approval_policy/on_request.md 模型主动请求时才审批

如果开启了 RequestPermissionsTool 特性,还会额外加载 on_request_rule_request_permission.md,允许模型通过工具请求权限提升。

③ 自定义 Developer 指令

  • 触发条件:用户通过配置文件设置了 developer_instructions
  • 无标签包裹(原样插入)
  • 内容:用户自己写的指令,相当于"用户级别的开发者提示"

④ 记忆工具指令(Memory Tool)

  • 触发条件:启用了 MemoryTool feature 且配置了 use_memories
  • 内容:告诉模型如何使用记忆工具来存取用户的长期偏好
  • 生成方式build_memory_tool_developer_instructions()

⑤ 协作模式指令(Collaboration Mode)

  • 触发条件:始终注入(只要有对应模板)
  • 标签<collaboration_mode> ... </collaboration_mode>
  • 生成方式DeveloperInstructions::from_collaboration_mode()
模式 模板文件 含义
default collaboration_mode/default.md 默认模式
execute collaboration_mode/execute.md 执行模式——少废话多干活
plan collaboration_mode/plan.md 规划模式——只制定计划不执行
pair_programming collaboration_mode/pair_programming.md 结对编程模式

这个指令定义了模型的行为风格:是该主动执行命令,还是先列计划等确认。

⑥ 实时对话指令(Realtime)

  • 触发条件:进入或退出语音实时对话模式
  • 标签<realtime_conversation> ... </realtime_conversation>
  • 模板文件:开始用 prompts/realtime/realtime_start.md,结束用 prompts/realtime/realtime_end.md
  • 生成方式DeveloperInstructions::realtime_start_message() / realtime_end_message()

⑦ 人格指令(Personality)

  • 触发条件:启用了 Personality feature,且当前模型没有内置人格
  • 标签<personality_spec> ... </personality_spec>
  • 生成方式DeveloperInstructions::personality_spec_message()

已知的人格模板:

  • gpt-5.2-codex_friendly.md(友好风格)
  • gpt-5.2-codex_pragmatic.md(务实风格)

如果模型在 Base Instructions 层已经"烘焙"了人格(supports_personality() 返回 true),则跳过此片段。

⑧ Apps / MCP 连接器

  • 触发条件:启用了 apps 且有可用的 MCP 连接器
  • 内容:列出模型可以通过哪些 apps/连接器访问外部服务

⑨ 技能指令(Skills)

  • 触发条件:有允许隐式调用的技能
  • 内容:补充说明模型可以使用哪些技能

⑩ 插件指令(Plugins)

  • 触发条件:有加载的插件
  • 内容:插件的能力摘要

⑪ Git Commit 署名指令

  • 触发条件:启用了 CodexGitCommit feature
  • 内容:告诉模型在生成 git commit message 时加上特定的 trailer 署名

注入位置

这些 developer 片段不是"加在 messages 列表最开头"的,而是紧贴在当轮用户消息之前

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
items[] = [
    ... (之前轮次的所有消息) ...

    ── 当轮注入 ──
    [developer message]        所有 developer_sections 合并成的一条消息
    [contextual user message]  environment_context + AGENTS.md
    ── 注入结束 ──

    [user's actual message]   ← 用户真正说的话
    [model response]
    [tool calls / outputs]
    ...
]

首轮时,由于前面没有历史消息,所以看起来像是"在开头"。但后续轮次如果有设置变化,diff items 是追加在当前 history 尾部、新用户消息之前的。

首轮 vs 后续轮次

首轮 后续轮次
触发条件 reference_context_item 为 None reference_context_item 有值
调用函数 build_initial_context() build_settings_update_items()
注入内容 完整的所有片段 只注入设置变化的 diff
注入后 设置当前设置为新的 reference_context_item 更新 reference_context_item

这个 diff 机制是节省 token 的关键设计:如果用户没有切换模型、没有改变协作模式、没有改变权限设置,后续轮次就不会重复注入这些指令。

历史管理与 Normalize

每轮对话结束后,所有新产生的消息(模型回复、工具调用、工具输出等)通过 record_items() 追加到 items 数组。

但在下一次发送给模型之前,需要先做一次"清洗"(normalize),确保历史消息的结构是合法的。这个过程由 normalize_history() 协调,包含三个子步骤:

ensure_call_outputs_present

确保每个工具调用(FunctionCall)都有对应的输出(FunctionCallOutput)。如果某个调用没有输出(比如被中断了),自动补一个 "aborted" 输出。

这是因为 API 要求调用和输出必须成对出现,否则会报错。

remove_orphan_outputs

如果一个工具输出没有对应的调用(孤儿输出),把它删掉。这种情况可能出现在回滚(rollback)或压缩(compact)后。

strip_images_when_unsupported

如果当前模型不支持图片输入,把历史消息中的图片内容剥离掉。这是因为切换模型后,之前上传的图片可能变得不可用。

上下文压缩(Compact)

当上下文的 token 数量接近模型的窗口限制时,Codex 需要压缩历史。这有两种实现:本地压缩和远程压缩。

本地压缩(Local Compact)

核心逻辑在 compact.rsrun_compact_task() 中。

思路是:让模型自己读一遍对话历史,然后生成一段摘要。摘要替换掉原始历史,大幅减少 token 数量。

关键设计:

  1. 摘要提示词:用 SUMMARIZATION_PROMPT 告诉模型"请总结上面的对话"。模型生成的摘要以 SUMMARY_PREFIX(“Here is a summary of the conversation so far:")开头。

  2. 用户消息截断:单条用户消息最多保留 COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000 个 token,超出部分会被截断。这防止一条超长消息独占压缩预算。

  3. 初始上下文重注入:压缩后,需要把 developer instructions 重新注入到新的压缩历史中。InitialContextInjection 枚举控制注入策略:

    • BeforeLastUserMessage:注入到最后一条真实用户消息之前(默认)
    • DoNotInject:不注入(某些场景下使用)
  4. 压缩后的历史结构

1
2
3
4
5
6
7
8
压缩后的 items[] = [
    [Compaction marker]            标记"从这里开始是压缩后的内容"
    [Summary message]              模型生成的摘要
    [developer instructions]       重新注入的完整 developer 上下文
    [contextual user message]      重新注入的环境上下文
    [last user message]            保留最后一条用户消息
    [last model response + tools]  保留最后一轮的模型回复
]

远程压缩(Remote Compact)

核心逻辑在 compact_remote.rs 中,调用 OpenAI 提供的服务端压缩能力。

与本地压缩不同,远程压缩由服务端完成摘要工作。客户端主要做:

  1. 处理压缩后的历史process_compacted_history() 解析服务端返回的压缩结果。
  2. 过滤保留项should_keep_compacted_history_item() 决定哪些消息应该保留在压缩后的历史中。
  3. 裁剪工具调用历史trim_function_call_history_to_fit_context_window() 确保压缩后的历史不会超过上下文窗口。

Token 估算

Codex 用一个简单但实用的经验公式来估算 token 数量:

1
token 数 ≈ 字节数 / 4

即大约每 4 个字节对应 1 个 token。这个公式对英文文本来说相当准确。

对于特殊内容有单独处理:

  • 图片:按固定 token 数估算(不同分辨率有不同的值)
  • 加密推理(encrypted reasoning):有专门的 token 计数字段

回滚机制(Rollback)

drop_last_n_user_turns() 方法用于回滚最后 N 轮用户对话。

回滚的粒度是"用户轮次”——从最后一条用户消息开始,往前找到上一条用户消息为止,中间的所有消息(包括模型回复、工具调用等)全部删除。

一个重要的细节:回滚时会同时清理上下文消息(contextual messages)。trim_pre_turn_context_updates() 方法会识别用户消息前面紧贴的 contextual developer messages 和 contextual user messages(通过 XML 标签前缀识别),一并删除。否则这些"无主"的上下文消息会留在历史里造成混淆。

轮次边界的判断由 is_user_turn_boundary() 函数完成:一条消息是用户轮次边界,当且仅当它是 role=user 的消息,且不是 Codex 自动注入的上下文消息。

GhostSnapshot:隐形快照

GhostSnapshot 是一种特殊的 ResponseItem,它存在于 items 数组中,但对模型完全不可见

它的作用是支持 undo(撤销)操作:在某些关键时刻(比如工具执行前),Codex 会插入一个 GhostSnapshot,记录当时的状态。如果后续需要撤销,可以回退到这个快照点。

normalize_history()for_prompt()(生成发送给模型的消息列表)时,GhostSnapshot 会被自动跳过,不会出现在 API 请求中。

Contextual User Messages 详解

除了 developer instructions,Codex 还会注入一些 role=user 的上下文消息。这些消息在 contextual_user_message.rs 中定义,每个都是一个 ContextualUserFragmentDefinition,用 XML 标签包裹:

片段 含义
ENVIRONMENT_CONTEXT_FRAGMENT 环境上下文(cwd, shell, date, timezone 等)
USER_SHELL_COMMAND_FRAGMENT 用户执行的 shell 命令
TURN_ABORTED_FRAGMENT 当轮被中断的通知
SUBAGENT_NOTIFICATION_FRAGMENT 子 agent 通知
AGENTS_MD_FRAGMENT AGENTS.md 文件内容
SKILL_FRAGMENT 技能信息

EnvironmentContext 的结构

EnvironmentContext 是最重要的 contextual user message,它被序列化成 XML 格式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<environment_context>
  <cwd>/Users/alice/project</cwd>
  <shell>zsh</shell>
  <date>2026-03-31</date>
  <timezone>Asia/Shanghai</timezone>
  <network_access>true</network_access>
  <subagents>
    ...
  </subagents>
</environment_context>

这些信息让模型知道"自己运行在什么环境里",从而生成正确的命令(比如知道用户用的是 zsh 而不是 bash,或者知道当前目录是什么)。

实时对话上下文(Realtime Context)

当用户进入语音实时对话模式时,Codex 会构建一个特殊的 startup context,包含多个有严格 token 预算的部分:

部分 Token 预算 内容
Current Thread 1200 当前对话线程的摘要
Recent Work 2200 最近的工作内容
Workspace Map 1600 工作区文件结构
Notes 300 备注信息

这些预算确保实时对话模式下的上下文不会过大,保证语音交互的低延迟。

关键源文件索引

文件 核心内容
core/src/context_manager/history.rs ContextManager 结构体、历史管理、token 估算
core/src/context_manager/updates.rs 设置 diff/更新逻辑、developer message 构建
core/src/context_manager/normalize.rs 历史清洗(调用-输出配对、孤儿清理)
core/src/codex.rs build_initial_context() 核心拼装逻辑
core/src/compact.rs 本地压缩逻辑
core/src/compact_remote.rs 远程压缩逻辑
core/src/event_mapping.rs CONTEXTUAL_DEVELOPER_PREFIXES 定义、消息解析
core/src/contextual_user_message.rs 上下文用户消息片段定义
core/src/environment_context.rs EnvironmentContext 结构及序列化
core/src/realtime_context.rs 实时对话 startup context
core/src/state/session.rs SessionState(会话级状态)
core/src/state/turn.rs TurnState / ActiveTurn(轮次级状态)
core/src/thread_rollout_truncation.rs Rollout 裁剪
protocol/src/models.rs DeveloperInstructions 结构体及所有工厂方法
instructions/src/fragment.rs ContextualUserFragmentDefinition
instructions/src/user_instructions.rs UserInstructions / SkillInstructions 序列化

总结:一张图看全貌

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌──────────────────────────────────────────────────────────┐
│  API 请求                                                 │
│                                                          │
│  instructions: Base Instructions (Meta Prompt)           │
│  ┌────────────────────────────────────────────────┐      │
│  │  "你是 Codex,一个编程 agent..."                  │      │
│  │  来自 prompts/base_instructions/default.md      │      │
│  │  或模型专属版本 (gpt_5_2_prompt.md 等)           │      │
│  │  整个会话基本不变                                 │      │
│  └────────────────────────────────────────────────┘      │
│                                                          │
│  input: [                                                │
│    ┌─────── 首轮完整注入 / 后续轮次只注入 diff ────────┐  │
│    │ [developer message]  ← 多个片段合并成一条         │  │
│    │   ├ <model_switch>           模型切换时           │  │
│    │   ├ <permissions>            始终(沙箱+审批)     │  │
│    │   ├ 自定义 developer 指令     用户配置时           │  │
│    │   ├ Memory Tool 指令          启用记忆时           │  │
│    │   ├ <collaboration_mode>     始终(4种模式选1)    │  │
│    │   ├ <realtime_conversation>  语音模式时           │  │
│    │   ├ <personality_spec>       启用人格时           │  │
│    │   ├ Apps / MCP               有连接器时           │  │
│    │   ├ Skills                   有技能时             │  │
│    │   ├ Plugins                  有插件时             │  │
│    │   └ Git Commit Trailer       启用 git commit 时  │  │
│    │                                                   │  │
│    │ [contextual user message]  ← 环境信息             │  │
│    │   ├ <user_instructions>  AGENTS.md               │  │
│    │   └ <environment_context>  cwd/shell/date/...    │  │
│    └───────────────────────────────────────────────────┘  │
│                                                          │
│    [user message]    ← 用户真正说的话                     │
│    [assistant msg]   ← 模型回复                          │
│    [function_call]   ← 工具调用                          │
│    [function_output] ← 工具结果                          │
│    ...                                                   │
│  ]                                                       │
│                                                          │
│  ┌─────── 上下文过大时触发压缩 ───────────────────────┐  │
│  │  Local Compact: 模型自己生成摘要替换历史             │  │
│  │  Remote Compact: 服务端压缩                         │  │
│  │  压缩后重新注入 developer instructions              │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

核心设计理念可以概括为:

  1. Append-only 历史——所有消息只追加不插入,结构简单可靠。
  2. 模板化 + 按需拼装——developer prompt 不是一段固定文本,而是根据当前配置从模板库里挑选片段动态组装。
  3. Diff 驱动的增量更新——首轮注入完整上下文,后续只注入变化部分,节省 token。
  4. XML 标签做结构化标记——既帮助模型理解指令边界,又为代码层面的识别和清理提供锚点。
  5. 压缩时重注入——上下文压缩后,developer instructions 会被重新生成并注入,确保模型始终能看到最新的规则。

References

[1] OpenAI. codex-rs GitHub repository.

[2] OpenAI. Responses API Reference.