前言
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 是整个上下文管理的核心结构体:
|
|
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 的指令体系因此分成三层:
|
|
第一层: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() 函数中:
|
|
它准备一个空数组 developer_sections,按顺序往里塞各种片段,最后通过 build_developer_update_item() 合并成一条 developer message 注入到上下文中。
XML 标签包装机制
每个片段都用特定的 XML 标签包裹。这些标签不是装饰,而是有实际工程作用的:
|
|
标签的三个作用:
-
帮助模型理解结构——模型可以通过标签名知道"这段是权限规则"还是"这段是人格设定",不会混淆不同主题的指令。
-
支持 Diff 机制的识别——代码中定义了一个常量数组:
|
|
is_contextual_dev_message_content() 函数通过检查消息内容是否以这些前缀开头,来判断一条 developer message 是 Codex 自动注入的上下文消息,还是用户自定义的开发者指令。这在回滚、裁剪和 diff 时至关重要。
- 支持 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)
- 触发条件:启用了
MemoryToolfeature 且配置了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)
- 触发条件:启用了
Personalityfeature,且当前模型没有内置人格 - 标签:
<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 署名指令
- 触发条件:启用了
CodexGitCommitfeature - 内容:告诉模型在生成 git commit message 时加上特定的 trailer 署名
注入位置
这些 developer 片段不是"加在 messages 列表最开头"的,而是紧贴在当轮用户消息之前:
|
|
首轮时,由于前面没有历史消息,所以看起来像是"在开头"。但后续轮次如果有设置变化,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.rs 的 run_compact_task() 中。
思路是:让模型自己读一遍对话历史,然后生成一段摘要。摘要替换掉原始历史,大幅减少 token 数量。
关键设计:
-
摘要提示词:用
SUMMARIZATION_PROMPT告诉模型"请总结上面的对话"。模型生成的摘要以SUMMARY_PREFIX(“Here is a summary of the conversation so far:")开头。 -
用户消息截断:单条用户消息最多保留
COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000个 token,超出部分会被截断。这防止一条超长消息独占压缩预算。 -
初始上下文重注入:压缩后,需要把 developer instructions 重新注入到新的压缩历史中。
InitialContextInjection枚举控制注入策略:BeforeLastUserMessage:注入到最后一条真实用户消息之前(默认)DoNotInject:不注入(某些场景下使用)
-
压缩后的历史结构:
|
|
远程压缩(Remote Compact)
核心逻辑在 compact_remote.rs 中,调用 OpenAI 提供的服务端压缩能力。
与本地压缩不同,远程压缩由服务端完成摘要工作。客户端主要做:
- 处理压缩后的历史:
process_compacted_history()解析服务端返回的压缩结果。 - 过滤保留项:
should_keep_compacted_history_item()决定哪些消息应该保留在压缩后的历史中。 - 裁剪工具调用历史:
trim_function_call_history_to_fit_context_window()确保压缩后的历史不会超过上下文窗口。
Token 估算
Codex 用一个简单但实用的经验公式来估算 token 数量:
|
|
即大约每 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 格式:
|
|
这些信息让模型知道"自己运行在什么环境里",从而生成正确的命令(比如知道用户用的是 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 序列化 |
总结:一张图看全貌
|
|
核心设计理念可以概括为:
- Append-only 历史——所有消息只追加不插入,结构简单可靠。
- 模板化 + 按需拼装——developer prompt 不是一段固定文本,而是根据当前配置从模板库里挑选片段动态组装。
- Diff 驱动的增量更新——首轮注入完整上下文,后续只注入变化部分,节省 token。
- XML 标签做结构化标记——既帮助模型理解指令边界,又为代码层面的识别和清理提供锚点。
- 压缩时重注入——上下文压缩后,developer instructions 会被重新生成并注入,确保模型始终能看到最新的规则。
References
[1] OpenAI. codex-rs GitHub repository.
[2] OpenAI. Responses API Reference.