3136 字
约 10 分钟
1
往模型上下文里塞东西,先给它造一个类型
无标签

OpenAI Codex · 代码模式 往模型上下文里塞东西,先给它造一个类型 批准前缀、工作区、AGENTS.md、被中断的 turn,全是告诉模型一件它自己看不到的事实。Codex 先给每一种注入造一个类型,类型自己知道 role、marker 和 body。 一句话速览 每一种注入都是一个类型,自己知道 role、marker 和 body;组装按字段分拣,事后靠同一对 marker 认回

课程目标 读完能说清三件事。第一,往模型上下文里塞的每一段文字,为什么要先收成一个类型。第二,有标和无标差在哪,压缩和恢复靠什么认回。第三,一条发给模型的消息按什么字段分拣,顺序从哪来。

先玩一遍 · 开哪些开关,信封里就有哪些块

同一轮输入:五个 fragment 开关,看消息怎么被拼出来

播放 单步 重置

打开

环境信息 AGENTS.md 窗口身份 批准前缀 中断通知

开关一变,左边信封立刻重排。点播放,看每一块怎么被问 role、问 marker、再分进三条消息。

发给模型的信封 0 条消息 · 0 块

产地标签 先问类型

逻辑轨迹 · 动画每一步对应源码里的哪一段

取 role,决定进 user 还是 developer fragment.rs L15 问要不要单独成条 fragment.rs L18 实例要 marker,拿去 render fragment.rs L22 空 marker 只输出 body,且永不匹配 fragment.rs L41 窗口身份在循环前推进单独组 session/mod.rs L3661 其余 fragment 按 role 和 marker 分拣 session/mod.rs L3677 user 侧按注册表 .any() 认回 contextual_user_message.rs L18 developer 侧按前缀表认回 event_mapping.rs L40

打开或关掉开关,看最终注入模型的内容由哪几块构成。点播放,看它们怎么被分拣。

构成从哪来 可合并的 developer 段先收成一条消息,必须单独成条的各成一条,user 段再收成一条。顺序写在类型字段上,字符串里有没有某个词帮不上忙。 事后能不能认 有标的靠首尾 marker 认回。无标的默认认不回,developer 侧用前缀表补了一部分。 为什么要先有类型 组装入口收的是已实现 trait 的 fragment。随手 format 出来的标签进不去,matcher 列表也认不出没登记的标签。

教学示意:开关组合与正文措辞为课程化设定,分拣顺序对齐 build_initial_context_with_world_state 。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。

思路一 · 注入先变成类型

它解决什么问题 你给 agent 加过这类功能:用户刚批准了 npm * ,下一轮模型还在问要不要跑 npm test。或者压缩刚结束,模型突然忘了工作区在哪、沙箱是只读还是可写。再或者 fork 一条会话,旧的 AGENTS.md 还在,新的目录提示叠上去,模型同时看见两套互相打架的规则。 这三件事的共同来源是同一类操作:运行时往模型上下文里塞了一段文字。批准前缀、工作区、权限档案、被中断的 turn、当前 UTC 时间,全是告诉模型一件它自己看不到的事实。一段 format! 就能写出来。 问题出在事后。这段文字进了历史之后,压缩要不要留它、会话恢复要不要认它、UI 要不要把它藏起来、world state 要不要拿它当模型已经知道了的证据,全都依赖一件事:你还能从纯文本里把它认回来。各处再写一遍 startsWith ,过几周四处会漂。未知标签还会漏进用户消息解析。

思路是什么 Codex 把每一种注入收成一个实现 ContextualUserFragment 的类型。trait 不在 codex-core 里,而在独立 crate codex-context-fragments 。每个实现必须同时回答:这段以 user 还是 developer 进 Responses API,头尾 marker 是什么,中间 body 怎么写,要不要单独成一条消息。 render() 把头尾 marker 和 body 直接首尾相接,中间不加分隔符。空白和换行算 body 的。空 marker 只输出 body。 into() 把渲染结果收成一条 ResponseItem::Message ,content 只有一个 InputText 。注入的终点是协议对象。

codex-rs/context-fragments/src/fragment.rs 第 38 至 46 行

 fn render(&self) -> String {
 let (start_marker, end_marker) = self.markers();
 let body = self.body();
 if start_marker.is_empty() && end_marker.is_empty() {
 return body;
 }

 format!("{start_marker}{body}{end_marker}")
 }

源码快照说明:依据本地仓库 openai/codex ,核对文件 codex-rs/context-fragments/src/fragment.rs ,commit 4f39251a01 ,核对日期 2026-08-22。代码块保留源码原文,这段就是类型把自己收成一段可注入文本的全部规则。 markers() 带 self ,渲染走实例。 type_markers() 不带 self ,识别走类型。手里只有历史文本、没有原对象时,仍然能问这段像不像某某 fragment。 dyn ContextualUserFragment 调不了 matches_text ,反向识别必须点到具体类型。 加一种注入要新建文件、实现 trait、挂进 core/src/context/mod.rs 。目录里 39 个模块。这个摩擦力本身就是治理:临时 format! 走不通,因为组装函数收的是 Box 。 出处: codex-rs/core/src/context/mod.rs 第 3 至 41 行; codex-rs/core/src/session/mod.rs 第 3677 至 3707 行

运行时事实 工作区、指令、中断

fragment 类型 role · markers · body

render 首尾相接

Message

事后只剩一段 InputText 用 type_markers 问具体类型,matches_text 看首尾,没有实例也能认回

教学化结构图:渲染走实例,识别走类型,两套调用读同一对 marker。

为什么长期成立 渲染和识别共用一份定义。换个语言重写,最小形态仍是一份接口、一份注册表、同一对 marker。组装函数的参数写成 fragment 类型,业务侧就失去了随手拼 XML 的调用点。识别函数只从注册表做匹配,不许再写一套 text.startsWith 。

思路二 · 有标和无标要写清楚

它解决什么问题 所有注入都带标,一次性通知也会在压缩后被重认、再喂一遍。所有注入都不带标,压缩后的历史里分不清用户原话和运行时塞进去的说明。fork 会把过期的 <environment_context> 当成用户输入重新提交。

思路是什么 窗口身份、环境、中断、图片缩放、管理侧开发者指令,事后要认、要 diff、要在压缩后决定是否重注,所以带 begin 和 end。一次性通知,比如批准前缀、网络规则入册、剩余 token 一句提醒, type_markers() 返回两个空串。默认 matches_text 恒为 false。 匹配规则只看整段文本的首尾。先 trim 开头比前缀,再 trim 结尾比后缀,ASCII 大小写不敏感,两个都中才算命中。中间夹什么不管。无标 fragment 主动放弃可逆性。 出处: codex-rs/context-fragments/src/fragment.rs 第 89 至 103 行 developer 侧另有一张前缀表,在 event_mapping.rs ,用来补一部分无标识别。覆盖面比 user 侧 matcher 列表窄。前缀表里至今留着 <token_budget> 旧标签,注释写明是为了认旧版本持久化下来的包装。marker 一旦写进 rollout,就变成恢复合同的一部分。改标签等于改协议。 UserInstructions 有一处容易看走眼。它的 begin marker 是 Markdown 标题 # AGENTS.md instructions ,end marker 是 。协议里另有 <user_instructions> 那对标签,这个 struct 没用。按协议常量去写 matches_text ,会认漏仓库里真实渲染出来的文本。 出处: codex-rs/core/src/context/user_instructions.rs 第 18 至 19 行; codex-rs/protocol/src/protocol.rs 第 112 至 113 行

有标 · 事后要认

环境 / 窗口 / 中断

begin + body + end

matches_text 能认回 无标 · 只当时说一声

批准前缀 / 网络规则

只输出 body

默认识别放弃 developer 靠前缀表补

教学化对照:需要压缩后重认的做成协议,一次性通知主动放弃可逆。

为什么长期成立 需要事后认的做成协议,一次性的主动放弃,并且在类型上写清楚。压缩后要重认的强制 begin 和 end,一次性通知可以无标,但要在注释里写放弃可逆。

思路三 · 组装按类型字段分拣

它解决什么问题 各处随手 push 字符串,顺序靠约定,单独成条靠注释。 TokenBudgetContext 会和权限说明挤在同一条 developer 消息里。压缩滤网没法按条处理。混装之后回滚也拆不开。 event_mapping.rs 的注释承认: build_initial_context 可能把 contextual fragment 和持久 developer 文本捆在一起。 出处: codex-rs/core/src/event_mapping.rs 第 69 至 71 行

思路是什么 第一次组装发生在 Session::build_initial_context_with_world_state 。它把 fragment 按 role() 、 markers() 的开头、 requires_separate_message() 分进几组:可合并的 developer 段、必须单独成条的 developer 段、user 段,以及要置顶或置底的特殊段。 窗口身份这一条有点特别。 Feature::TokenBudget 打开且模型有 context window 时,它在 world_state 循环之前就被推进单独组。输出顺序是:先一条合并好的 developer 消息,再一条条单独的 developer 消息,再一条 contextual user 消息。分拣依据是类型字段。 出处: codex-rs/core/src/session/mod.rs 第 3630 至 3728 行 requires_separate_message() 把能不能和别人挤在同一条 developer 消息里也收进类型。 TokenBudgetContext 、 ImageResizeNotice 、 ManagedDeveloperInstructions 选择单独成条。单独成条的代价是多占一条消息、多一次 role 切换。好处是压缩滤网可以按条处理。

已打开的类型 批准前缀 窗口身份 环境信息 AGENTS.md 中断通知

分拣 role() markers().0 requires_separate 看字段,不扫正文

developer · 可合并成一条

developer · 各成一条

user · 收成一条 contextual

教学化分拣图:输出顺序是合并 developer、单独 developer、contextual user。

点进模型输入的任意一段,都能回到某个 fragment 类型。

为什么长期成立 组装入口只收已登记类型。这是把没登记的注入挡在门外的通用形状。类型挡得住没登记,挡不住登记了但每轮塞 40KB。那一层是评审规则,下一章展开。

横向对比 · 同一道题的另一种答法

闸开在哪:DeepSeek Harness 选择发请求时对账 DSH 写在仓库根 AGENTS.md 第 107 行,中文原则收成「模型可见即已记录」。抵达模型请求的一切都必须能从会话日志重建,新增一项模型可见输入就需要新增一个会话事件。 执行面是 invariant.ts 。它在 llm/stream 上挂监听,从 session 日志 deriveMessages() 得到期望值,再和即将发出的 options.messages 做 JSON.stringify 比较,对不上就 fail 。闸开在崩溃点,能抓住组装之后又改了 messages 这类只有运行时才出现的漂移。关掉 invariants 服务,这道闸就没了。 两侧均已核对源码 · 2026-08-22 · DSH · Model-visible ⟺ logged

真源不同:事件日志,还是封闭的类型集合 DSH 可以没有 fragment trait,因为它的真源是事件日志,messages 是投影。Codex 把能出现在上下文里的东西先收成封闭的类型集合,再用 marker 做事后识别。 代价也不同。Codex 的类型挡得住没实现 trait 就塞进 render_full ,挡不住 impl 里面 format! 出来的动态字符串。 matches_text 认的是文本形状。两边都为可追溯付钱,一个付在每次请求的断言,一个付在每次加注入的类型摩擦。 两侧均已核对源码 · 2026-08-22

课堂练习

01

认漏的会是哪一段 UserInstructions 的 begin marker 是 # AGENTS.md instructions ,协议常量却是 <user_instructions> 。如果有人按协议常量去写 matches_text ,会认漏现在仓库里真实渲染出来的哪一种文本? 再推一步:把上面演示里的批准前缀打开,压缩之后默认 matches_text 还能不能把它认回来。developer 侧要靠哪张表补这一刀,改错常量时编译器会不会响。

Takeaway: 往模型上下文里塞的每一段,先收成一个类型,类型自己知道 role、marker 和 body。需要事后认的带 begin 和 end,一次性通知主动放弃可逆。组装按类型字段分拣,没登记的字符串进不了信封。

往模型上下文里塞东西,先给它造一个类型
http://www.clxhxhhr.top/posts/4049/
作者
clxstart
发布于
2026-09-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。