新闻详情

新闻详情

首页 / 资讯中心 / 详情

Codex Harness 上下文工程设计原理

发布时间:2026/6/16 2:37:21
Codex Harness 上下文工程设计原理
当我们与一个大语言模型进行多轮对话时每一次交互都在不断积累信息——用户的指令、模型的回复、工具调用的结果、系统提示的变更……这些信息构成了模型理解当前任务的上下文。然而模型的上下文窗口是有限的。当对话不断拉长历史信息不断膨胀一个核心问题就浮现出来如何在有限的窗口内让模型始终看到最关键的信息而不丢失对任务的理解这个问题就是上下文工程—它不是简单的裁剪或丢弃而是一套精密的管理体系决定了什么信息需要保留、什么时候需要压缩、压缩后如何让模型无缝衔接。OpenAI 的Codexcodex-rust作为当前最先进的 AI 编程助手之一其上下文工程化设计堪称精妙。本文将带我们深入 codex 的源码理解它如何系统性地解决这个核心难题。Codex 的核心思路是信息的性质差异化管理它并不是把所有内容一视同仁地塞进去而是让重要的信息始终可见、不变的信息不要重复、膨胀的信息及时压缩瘦身。上下文信息的分类不场景不同信息Codex 需要在有限的窗口内同时承载五种信息它们的性质完全不同1策略信息——你必须这样做比如只允许写入 /tmp/project 目录、所有网络请求必须经过审批、你现在是 autonomous模式不需要每步都问用户。这些是系统级的强制指令模型必须遵守不能违背。特点指令性强、不可违背、一个 session 内几乎不变。2能力信息——你可以做什么比如你有一个 imagegen Skill 可以生成图片、你有 web_search 工具可以搜索网页、你可以通过 MCP 调用 GitHubAPI。这些是告诉模型有哪些工具和能力可用模型可以选择调用或不调用。Codex 对能力信息做了渐进式披露——Skill 的名字和简短描述始终在上下文中占 ~2% 空间但完整的 Skill 使用手册只有用户显式提及Codex Skill工程设计器原理时才加载脚本和参考资料更是执行时才读取。特点描述性数量可能很多3环境信息——你现在在哪里比如当前工作目录是 /tmp/project、Shell 是 bash、文件系统只允许读写项目目录、今天是 2024-06-15 UTC8。这些是运行时的状态描述模型需要知道但不必遵守。特点描述性、可能随时间变化用户切换了目录、日期推进了、占空间不大但不可或缺。4意图信息——用户要我做什么比如帮我重构这个函数、用 $imagegen 生成一张 Logo 图片、检查 CI 是否通过。这是真实的用户输入模型需要理解并响应——这是整个上下文中最重要的内容。特点每次不同、是模型工作的核心目标、绝对不能被压缩丢弃。5执行信息——之前做了什么、结果是什么比如模型回答我建议把函数拆成三个模块、工具调用执行了 npm run lint、工具输出lint 发现 42 个 warning、推理过程让我分析一下这个文件的结构...这是模型产出和工具执行的过程与结果。一个 lint 输出可能几千字一次文件搜索返回几千字十轮下来执行信息就可能占掉 80% 的窗口空间。特点过程性、占用空间最大、增长最快、压缩时最应该优先瘦身。五种信息性质不同但如果分别用不同的格式存储、不同的方式注入、不同的逻辑压缩整个系统会变得极度复杂Codex 设计了统一载体设计ResponseItem。上下文信息的组织从碎片到秩序想象一个场景你在和一位同事协作完成一个项目桌上堆满了各种文档——邮件、笔记、代码片段、会议纪要。如果没有一个得力的档案管理员这些信息很快就会变成一团混乱。在 codex 中ContextManager 就是这个档案管理员。ContextManager是 codex 上下文管理的核心结构它承载了整个对话的完整历史记录// codex-rs/core/src/context_manager/history.rs pub(crate) struct ContextManager { items: VecResponseItem, // 按时间顺序最旧→最新存储所有对话消息基于ResponseItem的统一消息格式未来通过record_items() 时逐条处理后追加 history_version: u64, // 版本号,在发生压缩或者回滚时递增记录 token_info: OptionTokenUsageInfo, // token计数优先使用服务端真实数据本地估计仅用于服务端数据到达前的过渡期和历史变更后的即时校验。 reference_context_item: OptionTurnContextItem, // Diff 更新的基线快照 }items 对话历史是对话历史的有序队列最老的条目在队首、最新的在队尾。但它并非只存用户说了什么、模型回了什么——它承载的是整个对话过程中出现的所有结构化事件用户消息、模型推理、工具调用、工具输出、压缩摘要等全部以ResponseItem枚举的形式统一表达。history_version 版本号是一个单调递增的版本号每当历史被重写比如压缩替换、回滚裁剪就自增。它的存在不是为了并发控制而是为了让下游的缓存、diff增量更新追踪等机制能够感知历史已经发生了结构性变更之前的快照不再有效。这是一个轻量但关键的变更信号。token_info token 用量承载的是服务端返回的真实 token 用量信息而非本地估算包含 total_token_usage、last_token_usage 和 model_context_window。Codex 的 token预算判断之所以能做到较为精准正是因为它优先使用服务端报告的真实用量只在服务端数据尚未返回时才用本地估算补位。reference_context_item 配置快照是整个上下文管理中最精妙的设计之一。它保存的是上一轮对话结束时的配置快照—包含当时的模型、权限、协作模式、环境上下文等。它的核心作用是支撑增量 diff注入如果当前轮的配置与上一轮相同就不需要重新注入完整的系统指令和环境信息只需发送变更的部分如果它被置为None比如压缩后、回滚后则意味着基线已丢失必须重新全量注入。这个字段是 Codex 在长对话中节省 token 的关键杠杆。 ResponseItem 通用消息格式ResponseItem 是 OpenAI Responses API 的通用消息格式每条消息有一个 role 字段标记语义层级后续无论什么消息类型都通过record_items() 时逐条处理后追加类似于Langchain中的Message append设计模型看到的只是一个有序的消息列表没有特殊字段——策略信息、能力信息、环境信息、用户意图、执行结果全部混在一起靠 role字段区分这是规则还是这是背景还是这是请求还是这是回答。这个设计带来了一个重要的工程收益所有注入逻辑统一都是往列表里追加一条消息所有归一化逻辑统一都是对列表做一次格式修整所有压缩逻辑统一都是对列表做一次整体替换。 不需要为不同类型的信息维护不同的容器和不同的管理逻辑。但这也带来了一个挑战如何让模型正确区分五种信息的优先级如果所有内容都是同样的格式模型可能把一条过时的 developer指令当作当前规则也可能把用户的核心意图当作背景信息忽略掉。Codex 的解决方案是语义分层——通过 role字段和注入顺序共同表达优先级这正是后面上下文注入步骤要解决的核心问题。 上下文信息的三层分类Codex 不是简单地把所有信息堆在一起发给模型。它将上下文信息分成了三个明确的插槽Slot对应不同的角色和注入方式Developer Slot开发者层系统级指令以 roledeveloper消息注入。包括权限策略、开发者指令AGENTS.md、协作模式说明、人格配置等。这是幕后指挥官告诉模型应该如何行为。Contextual User Slot上下文用户层面向用户的上下文信息以 roleuser消息注入。包括用户自定义指令、环境信息工作目录、Shell类型、时区等。这是前台信息台给模型提供当前工作环境的概况。Separate Developer Slot隔离开发者层独立的开发者消息比如 guardian 安全策略。这是安全守卫确保关键的安全约束不会被其他信息干扰。 上下文的配置基线TurnContextItemTurnContextItem 是 reference_context_item 的类型它序列化保存了某个时间点的完整配置状态模型信息、权限配置、协作模式、环境上下文、实时模式开关等。当一个新的 turn 开始时Codex 都会把当前配置与 reference_context_item中保存的上一轮配置逐字段比对例如模型是否换了→ 发送 ModelSwitchInstructions权限策略是否变了→ 发送 PermissionsInstructions diff协作模式是否变了→ 发送 CollaborationModeInstructions diff环境信息是否变了→ 发送 EnvironmentContext diff实时模式是否开关→ 发送 RealtimeStart/EndInstructionspersonality 是否变了→ 发送 PersonalitySpecInstructions diff所有有变更的字段合并为一个 developer 角色的消息注入历史没有变更的字段完全不发送。这意味着在绝大多数 turn 中系统指令策略信息、环境信息的 token开销几乎为零只有第一次 turn 或压缩后的第一次 turn 需要全量注入。这是一个典型的基线 diff策略用版本化的快照作为参照点只传输 delta。上下文处理的时机与原理run_turn 的生命周期理解了上下文装了什么之后下一个关键问题是什么时候处理。Codex 的上下文处理不是随机的而是在对话轮次turn的不同阶段精确执行的。让我们追踪 run_turn函数的完整执行流程看清楚每个阶段做了什么。1采样前压缩检查——防患于未然Pre-Turn 压缩这是整个管线的第一道关卡在模型开始推理之前就检查上下文是否已经超载。它的逻辑非常清晰async fn run_pre_sampling_compact(...) - CodexResult() { // 先检查是否切换了更小的模型需要适配 maybe_run_previous_model_inline_compact(...).await?; // 再检查token是否已经超出限制 let token_status auto_compact_token_status(...).await; if token_status.token_limit_reached { // 立即触发压缩使用DoNotInject策略 run_auto_compact(..., InitialContextInjection::DoNotInject, CompactionReason::ContextLimit, CompactionPhase::PreTurn).await?; } Ok(()) }这里有两个重要检查模式模型降级检查由于不同的模型上下文窗口是不一样的假设上一轮用的是 200K 上下文窗口的模型这一轮切换到了 128K 的模型那么之前的历史可能已经超出了新模型的窗口。这种情况下codex会先用旧模型的配置做一次压缩确保历史能适配新模型的窗口。Token限制检查如果当前上下文的token数已经达到了配置的压缩阈值就在模型推理之前先做压缩。注意这里用的是 InitialContextInjection::DoNotInject策略——意味着压缩后会清空基准快照下一轮会重新注入完整初始上下文。打个比方这就像出发前检查油箱——如果油量不够到达目的地先去加油站加油而不是等开到半路油箱报警了再处理。2上下文更新与基准设定—Diff增量注入的艺术这个阶段是 codex 上下文工程中最精妙的环节之一。这是什么意思举个例子首次对话模型什么都不知道需要注入完整的环境信息策略信息环境信息—工作目录、Shell类型、权限策略、协作模式、可用技能等。这是一大块信息。第二轮对话工作目录没变、权限没变、Shell没变……几乎所有配置都和上一轮一样。这时候如果再注入一大块完全相同的信息就浪费了大量token。所以 codex只注入变化的差异—比如如果用户切换了协作模式就只注入一条协作模式已从suggest切换到auto的更新消息。这个设计通过TurnContextItem基准快照实现pub struct TurnContextItem { turn_id: OptionString, cwd: PathBuf, // 工作目录 timezone: String, // 时区 approval_policy: ..., // 审批策略 sandbox_policy: ..., // 沙箱策略 permission_profile: ..., // 权限配置 model: String, // 模型名称 personality: ..., // 人格配置 collaboration_mode: ..., // 协作模式 realtime_active: bool, // 实时模式状态 // ... }每轮对话开始时codex 将当前配置序列化为 TurnContextItem然后与上一轮的基准做逐字段比较。codex会针对每个维度环境、权限、协作模式、实时模式、人格分别检查是否有变化只在有变化时才生成对应的更新消息。这种增量注入策略带来的token节省是显著的。假设初始上下文有 2000 tokens10轮对话后如果每轮都完整注入就要消耗 20000tokens而增量注入可能只需要几轮各几十 tokens 的更新消息。reference_context_item 的作用增量diff的锚点如果为NONE则全量注入否则增量注入通常在上下文压缩后就会清理reference_context_item下一轮就会重新注入上下文系统信息这是因为压缩后历史已经被替换为 [保留的用户消息 摘要]原来的初始上下文开发者指令、权限说明、环境信息等都被丢弃了。模型此时看不到任何系统配置信息。如果保留旧的 reference_context_item作为基准下一轮做diff时会认为这些配置没变所以不需要注入——但问题是历史里已经没有这些信息了模型看不到diff也不注入结果就是模型丢失了所有系统约束。所以必须清空基准强制下一轮全量注入重新把完整的系统指令塞进历史。3采样循环——核心推理与动态压缩Mid-Turn 压缩Pre-Turn 压缩是指在turn开始初期做压缩检查时的压缩过程此时LLM还没调用在起跑线上检查和压缩。Mid-Turn 压缩是指LLM已经调用了至少一次在两次LLM调用的间隙里检查和压缩即ReACT中的压缩。这个实现的核心在于它是在任务执行过程中来包保证上下文不超。关键点在于Mid-Turn 压缩。这是在模型推理过程中发现上下文爆满时的紧急处理if token_limit_reached needs_follow_up { run_auto_compact(..., InitialContextInjection::BeforeLastUserMessage, CompactionReason::ContextLimit, CompactionPhase::MidTurn).await?; can_drain_pending_input !model_needs_follow_up; continue; }压缩算法从冗长到精炼的三条路径Codex 提供了三种不同的压缩路径实现Local Inline、Remote V1、Remote V2主要是对不同场景和模型能力的适配Local Inline模型自身生成摘要。适用于不支持远程压缩API的模型。Remote V1调用服务端的 /responses/compact API。利用服务端更强大的压缩能力。Remote V2使用 Responses API 的 CompactionTrigger 标记项让服务端在推理过程中直接产出压缩结果。是最新的、最集成的方案。其实逻辑很简单if provider.supports_remote_compaction() { if features.enabled(Feature::RemoteCompactionV2) { → 使用 Remote V2 } else { → 使用 Remote V1 } } else { → 使用 Local Inline }Local Inline 压缩摘要 近期保留策略这是最基础但也最直观的压缩方式。核心思路是用一段特殊的提示词让模型回顾整个对话历史生成一份交接摘要然后用这份摘要替换冗长的原始历史。如果清楚常规的压缩策略的可以看Langchain的记忆策略。需要注意的是摘要生成后codex 不是简单地用摘要替换所有历史而是执行一个精密的筛选拼接算法这个算法的精髓在于从最新到最旧的贪心选择策略从最新的用户消息开始向前遍历优先保留最新的信息在 20,000 token 的预算内尽可能多地保留完整的用户消息预算耗尽时对当前消息做截断保留开头部分最后附上压缩摘要上下文保障机制确保历史的完整性在把历史发给模型之前codex 会执行三项规范化检查确保历史结构完整codex做了3个内容的检查来确保上下文信息的完整性1每个工具调用必须有对应的输出想象一下模型发出了一条shell命令但执行结果还没返回历史里就只有调用没有结果。如果直接把这样的历史发给模型模型会困惑——我调用了命令但看不到结果。规范化会为缺失的输出插入一条合成的 aborted 结果。// 为缺失输出的FunctionCall插入合成结果 ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output: FunctionCallOutputPayload::from_text(aborted.to_string()), }2每个输出必须有对应的调用这是反向检查如果历史里有工具输出但没有对应的调用可能是压缩时只保留了输出但丢弃了调用这些孤儿输出会被移除。没有调用的输出对模型来说是毫无意义的噪音。3不支持图片的模型不看到图片如果当前模型不支持图片输入历史中的所有图片内容会被替换为文本说明 image content omitted because you do not support imageinput。这避免了模型收到无法处理的信息。写在最后Codex 的上下文工程不是单一的压缩算法而是从数据结构统一化、到七阶段处理管线、到增量diff注入、到三层压缩路径、到规范化三道锁的完整系统工程——核心目标是在有限窗口内让模型始终看到最关键的信息压缩不是丢弃而是重构增量不是省略而是精准规范化不是多余而是兜底。
网站建设 高端定制 企业官网