第四章 上下文与指令管理:模型的"记忆"和"世界观"如何维护

第三章我们看到,harness 坚持无状态请求:每次采样都把完整历史重新发给模型,服务端不保存任何会话。 这把“记忆”的责任全部压回了 harness 一侧:历史要自己攒、窗口要自己盯、超长要自己压缩、环境信息要自己喂。 本章回答四个问题:发给模型的那份“上下文”到底由什么构成?环境事实以什么形式注入?指令分几层、从哪来?历史滚到装不下时怎么办?


4.1 为什么“记忆”是 harness 最难的工程问题

一个新人常犯的想象是:上下文不就是“聊天记录”吗?把用户和模型说过的话按顺序存起来,每次请求带上就行。

真实场景里,这个“聊天记录”面对的是一团互相拉扯的约束:

  • 模型是无状态的,窗口是有限的。 每次请求都要自足可解释(第 3 章的 store=false),但上下文窗口有硬上限——从几万到一两百万 token 不等。而历史只会单调增长:用户的话、模型的话、思考记录、工具调用、工具吐出的成千上万行日志……
  • 模型需要知道的远不止对话。 今天几号、当前在哪个目录、用什么 shell、哪些路径能读写、网络通不通、项目里有哪些约定(AGENTS.md)、当前是什么协作模式、上一次压缩发生在什么时候……这些“世界观”信息都不在聊天记录里,但模型缺了它们就会犯错。
  • 缓存要求前缀稳定。 第 3 章讲过,服务端 prompt cache 命中的前提是请求前缀逐字节不变。上下文怎么追加、什么时候动、动哪里,直接决定账单大小。
  • 信息会过期。 目录切换了、权限变了、AGENTS.md 被用户改了、模型切换了——旧的事实不能赖在上下文里,模型读到过期信息比读不到信息更危险。
  • 注入的东西必须有界。 一条命令输出可能有几十兆,一份 AGENTS.md 可能被写成论文。任何注入上下文的片段都必须有大小上限,否则一个工具结果就能把窗口撑爆。

Codex 对这团乱麻的解法,可以概括为一条核心原则:

把“说过的话”(对话历史)和“当前的事实”(世界状态)分开管理。历史只追加、不重写;事实按状态快照做差分注入;装不下时,压缩历史但重建事实。

下面逐层拆开。


4.2 一次采样请求的解剖:上下文里到底有什么

先看最终发给模型的请求体。它由三大部分组成(第 2、3 章已经露过面):

graph TD
    REQ["一次采样请求"] --> INS["顶层指令 instructions<br/>(系统级基础指令)"]
    REQ --> TOOLS["工具清单 tools<br/>(本步骤可见工具的 JSON Schema)"]
    REQ --> INPUT["输入历史 input<br/>(一串有序的 item)"]
    INPUT --> I1["用户消息:真人的话 + 部分注入片段"]
    INPUT --> I2["助手消息:模型的最终回复"]
    INPUT --> I3["推理记录:加密思维链 + 摘要"]
    INPUT --> I4["工具调用 & 工具结果(成对出现)"]
    INPUT --> I5["开发者消息:harness 注入的状态/指令片段"]
    INPUT --> I6["压缩标记:摘要与窗口边界"]

注意历史里的消息有三种角色,语义截然不同:

角色 谁在说话 典型内容
user(用户) 真人,或 harness 伪装成用户注入的片段 真人输入;AGENTS.md 指令;harness 的环境片段(部分刻意用用户角色,见 4.5)
assistant(助手) 模型 最终回复文本;工具调用
developer(开发者) harness 自己 环境状态、权限说明、时间提醒、预算提醒、模式切换通知

顶层 instructions 是第四种声音——系统级基础指令(工具怎么用、行为准则),它不属于历史,每次请求单独携带。第 3 章讲过它跟着模型档案走:每个模型有自己的基础指令模板,用户也可以用配置整体覆盖(覆盖时会记录来源:来自模型档案还是用户自定义)。某些精简传输格式(Responses Lite)下,它会被改写成历史开头的一条开发者消息,语义不变。

一个容易忽略的事实:harness 往上下文里塞的东西,条目数量和种类远超对话本身。 环境说明、权限清单、时间、预算、AGENTS.md、多 agent 模式说明、插件用法提示……这些注入片段统称上下文片段(context fragment)。Codex 给它们立了一套统一的规矩。


4.3 片段协议:所有注入都必须是“带标签的结构体”

harness 内部有几十种需要注入上下文的内容:AGENTS.md、环境信息、权限说明、当前时间、token 预算提醒、中断标记、子 agent 来信、模型切换通知、hook 注入的额外上下文……如果每种都各自往历史里 push("一段字符串"),系统很快就会失控:无法识别哪些是自己注入的、无法在过期时替换、无法统计大小、压缩时无法判断哪些该重建。

Codex 的约束是:任何注入上下文的片段,都必须实现一个统一的片段接口,定义成一个结构体。这个接口要求每种片段回答四个问题:

  1. 我以什么角色出现? 用户还是开发者?
  2. 我的开始/结束标签是什么? 比如 <current_time_reminder> ... </current_time_reminder>,包裹在正文外层;
  3. 我的正文是什么? 渲染成最终给模型看的文本;
  4. 我是否必须独占一条消息? 大多数片段可以和同类片段合并成一条消息,少数(如审查策略指令)必须独立成条,便于审计。

这套设计最精妙的地方是标签的双重身份

  • 模型,标签是结构化信号——“这是 harness 提供的环境事实,不是用户说的话”,XML 式标签也是模型最容易学会遵守的格式;
  • harness 自己,标签是日后认出自己的依据。历史是只追加的,harness 需要随时扫描历史、回答“我上次注入的 AGENTS.md 还在不在?”“当前权限说明是哪条?”——靠的就是逐条匹配标签。识别不出来的片段,按普通对话内容对待。
flowchart LR
    F["片段结构体<br/>(角色 + 标签 + 正文)"] -->|"渲染"| H["写入历史<br/>带标签的消息"]
    H -->|"下次扫描"| M{"标签匹配?"}
    M -->|"匹配"| OWN["识别为 harness 注入<br/>可替换 / 可判定是否仍在"]
    M -->|"不匹配"| USER["普通对话内容<br/>原样保留"]

片段还有“可合并”与“独占”之分:同角色、可合并的片段会被拼进同一条消息(减少消息条目数,也利于缓存前缀稳定);独占片段各自成条。

一个旁证能说明这套规矩的严格:连 hook(外部扩展)注入的上下文也走同一个片段接口,扩展拿到的是结构化的贡献点,返回的也是带角色、带标签的片段对象——harness 不接受扩展直接往历史里塞裸字符串。

还有一个容易被忽略的细节:这些 harness 注入的“伪用户消息”,对第三方消费者是隐形的。当扩展(记忆巩固、技能系统等)读取“对话历史快照”时,harness 会把所有带标签的注入片段过滤掉——扩展看到的是真人与模型的真实对话,harness 的私房标注不会污染外部视角。同一份历史,给模型看的是“全集”,给扩展看的是“净集”。


4.4 世界状态:把“当前的事实”从“流水账”里分离出来

片段协议解决了“一条注入长什么样”,但没解决一个更根本的问题:环境信息是会变的,而历史是不能改的。

设想最朴素的做法:每次采样前把“当前目录、日期、权限、AGENTS.md……“拼一段塞进历史。后果是灾难性的——同一份环境说明会在历史里重复几十次,窗口迅速被废话撑满,而且模型会读到一堆互相矛盾的旧快照(“工作目录是 /a”……“工作目录是 /b”……),它不知道该信哪条。

Codex 的解法是引入**世界状态(World State)**这个一等概念,把上下文内容劈成两半:

  • 对话历史:流水账。谁在什么时候说了什么、调了什么工具、结果如何——只追加,不修改(第 1 章的核心原则);
  • 世界状态:此刻的事实。当前目录、日期、权限画像、AGENTS.md 内容、协作模式、模型信息、多 agent 模式……状态只关心“现在是什么”,不关心“怎么变成这样的”。

世界状态被切成若干区段(section),每个区段有一个稳定 ID 和一份可序列化的快照:环境、AGENTS.md、权限、模型指令、个性、协作模式、工具延迟加载信息、插件/应用用法提示、实时语音状态、上下文窗口信息……扩展也可以注册自己的区段。

4.4.1 差分注入:只告诉模型“变化了什么”

每个步骤边界(第 1 章讲过,快照都在步骤边界冻结),harness 做这样一件事:

flowchart TD
    A["步骤边界:构建当前世界状态"] --> B["逐区段生成快照"]
    B --> C{"与上次模型可见的<br/>基线快照对比"}
    C -->|"首次 / 基线丢失"| D["全量渲染该区段"]
    C -->|"内容相同"| E["不产生任何片段"]
    C -->|"内容变化"| F["只渲染变化区段的更新片段"]
    D --> G["片段写入历史"]
    F --> G
    G --> H["基线更新为当前快照"]

也就是说,稳定的环境事实在整个会话里原则上只说一次;目录切换、权限调整、AGENTS.md 被改,才会产出一条“更新通知”。通知的措辞经过专门设计,带有明确的替换语义。比如 AGENTS.md 发生变化时,新片段的正文开头是:

“以下 AGENTS.md 指令替换此前提供的全部 AGENTS.md 指令。”

如果 AGENTS.md 被删除,则注入:“此前提供的 AGENTS.md 指令不再适用。”

模型不需要翻阅历史去推断“哪条指令还有效”——每条更新都显式声明它与旧状态的关系。这比“让模型自己看到矛盾后悟出来”可靠得多。

4.4.2 状态快照也要持久化:全量一次,之后打补丁

世界状态的基线不能只活在内存里——进程重启后 resume(第 1 章的跨进程恢复)时,harness 必须知道“模型最后被告知的事实是什么”,否则只能把所有区段全量重发一遍。

做法是把快照写进持久化的事件流(rollout,第 10 章详述),而且同样遵循差分原则:

  • 第一次,写入一份全量快照
  • 之后每次变化,写入一份 JSON merge patch(RFC 7386 标准的“补丁”格式:变化的字段给新值,删除的字段标 null),恢复时逐个补丁 apply 回全量。

于是持久化流里,状态和对话共用一条只追加的日志:对话是 item,状态是快照与补丁,顺序交错,天然保持“模型被告知事实”的完整时序。

4.4.3 压缩后如何不丢“世界观”

第 10 章会讲压缩会物理替换掉大段历史,这意味着旧的状态片段可能从历史里消失——但补丁基线还在。这里有一个三方校验:

  1. 有精确基线快照?→ 按差分渲染;
  2. 基线在,但某个区段的片段必须留在历史里模型才看得到(比如权限说明)?→ 扫描现存历史,确认它的标签还在;被压缩掉了就当作“从未注入”,全量重发;
  3. 连基线都没有(更老版本的持久化文件、分叉出来的会话)?→ 用标签在历史里模糊匹配旧格式片段;匹配到了就把当前状态当“未知”处理——未知的安全策略是:当作没注入过,重发

宁可重复注入,也不让模型在缺失事实的情况下工作。这套“基线 + 历史标签”双保险,让世界状态在压缩、回滚、跨进程恢复之后总能自愈。

类比:对话历史像一个组织的会议纪要——逐字记录,谁也不能涂改;世界状态像办公室墙上的白板——写着当前的项目状态、约定、负责人。换人接手时(压缩),会议纪要可以摘要归档,但白板必须照着最新状态重新画一遍,而且只重画被擦掉的部分。


4.5 指令体系:模型到底在听谁的

上下文里的“指令性内容”比“事实性内容”更需要分层管理——因为它们来自不同的权威,冲突时必须有明确的优先级。Codex 的指令分四层:

graph TD
    L0["第 0 层:基础指令(顶层 instructions)<br/>模型档案自带模板;用户配置可整体覆盖<br/>——回答『你是谁、工具怎么用、行为准则』"]
    L1["第 1 层:全局用户指令<br/>~/.codex/AGENTS.md / 前端宿主下发<br/>——回答『我这个用户的通用偏好』"]
    L2["第 2 层:项目指令 AGENTS.md<br/>从项目根到当前目录逐层发现、拼接<br/>——回答『这个仓库的约定』"]
    L3["第 3 层:会话内动态指令<br/>世界状态片段、时间/预算提醒、hook 注入、停止 hook 续跑指令<br/>——回答『此刻你需要知道的事』"]
    L0 --> REQ["每次采样请求"]
    L1 --> REQ
    L2 --> REQ
    L3 --> REQ

第 0 层:基础指令。 第 3 章已经讲过:它不是全局常量,而是模型档案的一部分——不同模型有不同的调教模板(含个性变量、审批话术、多 agent 话术等),harness 只内置一份通用模板兜底。用户配置可以整体覆盖,系统会记录来源(“来自模型档案”还是“用户自定义”),切换模型时这个来源信息还会触发一条模型切换通知。

第 1 层:全局用户指令。 来自用户主目录下的全局 AGENTS.md(或前端宿主——比如 IDE 插件——通过接口直接下发的指令文本)。内容是跨项目的个人偏好:“我习惯用 pnpm 不用 npm”“回复尽量简洁”。它在拼接顺序上排在项目指令之前。

第 2 层:项目指令(AGENTS.md)。 这是信息量最大、也最有工程讲究的一层。发现过程如下:

  1. 找项目根:从当前工作目录向上走,直到遇到项目根标记(默认是 .git 目录,标记列表可配置);找不到就只认当前目录;
  2. 从上到下收集:从项目根到当前目录(含),每层目录找一个指令文件,按从根到叶的顺序拼接——外层目录的约定更通用,内层目录的约定更具体,越靠后的内容越贴近当前工作;
  3. 文件名优先级:每层优先看本地覆盖文件(AGENTS.override.md),其次才是标准的 AGENTS.md;还可以配置额外的候选文件名(用于兼容其他工具生态的约定文件);
  4. 多环境标注:一个会话可以连着多个执行环境(本地 + 远程,第 8 章),不同环境各自发现自己的 AGENTS.md,拼接时按环境分组标注(“以下指令针对 环境X,根目录是 …”);
  5. 硬性大小上限:所有项目指令加起来有字节预算(默认 32KB),超了从后往前截断——指令注入是有界的,绝不能让文档把窗口吃掉。

发现结果会按“环境选择”缓存:环境没变就不重新扫盘;环境切换或会话恢复时才重新加载。而加载后的内容不是直接写死在历史里——它被包装成世界状态的一个区段(4.4),所以用户在会话中途编辑 AGENTS.md,下一个步骤就能以“替换全部旧指令”的方式生效,不需要重启会话。

一个反直觉但刻意的设计:AGENTS.md 是以“用户”角色注入的(外层包着 # AGENTS.md instructions ... <INSTRUCTIONS>...</INSTRUCTIONS> 标签)。为什么不用开发者角色?因为模型对“用户说的话”注意力权重天然更高——项目约定是希望模型严格遵守的内容,用用户角色表达遵从度更好;而 harness 自己的运维信息(权限、预算、时间)用开发者角色,明确“这是系统旁白,不是任务要求”。角色本身也是一种注意力编程。

第 3 层:会话内动态指令。 就是 4.3/4.4 的片段体系:世界状态差分、时间提醒(按可配置的间隔,且只在“用户发言或工具结果之后”的边界投放,避免在模型连续工作时用同一条提醒刷屏)、token 预算提醒、rollout 预算提醒(整棵 agent 树共享的加权 token 预算)、中断标记(第 1 章)、hook 注入的额外上下文、停止 hook 要求续跑时注入的“你还有事没做完”指令。此外前端宿主还可以下发独占的开发者指令(比如自动审查子 agent 拿到的审查策略,必须独立成条、便于审计)。


4.6 历史的累积、清洗与计量

再回到对话历史这一侧。它的数据结构简单得出奇:一个按时间顺序排列的 item 向量,包在引用计数指针里——克隆历史是廉价的(共享底层数据,只有修改时才复制),轮次循环、压缩逻辑、扩展读快照都可以各拿一份“自己的历史”而不付深拷贝代价。历史有一个版本号,每当历史被重写(压缩、回滚)就加一;只追加不改变版本号。

4.6.1 入库即截断:工具结果的“中间省略”

历史增长的最大来源是工具输出。一条 npm test 可能吐出五万行。harness 在结果入库的那一刻就按模型档案上的截断策略(按字节或按 token,模型各有上限)处理掉,而不是等发请求时才裁。截断方式是保留头尾、挖空中间

  • 头部保留:命令启动阶段的信息、报错往往在最前面;
  • 尾部保留:最后的错误摘要、退出状态往往在最后面;
  • 中间用省略标记替代,并注明原始规模(“输出已截断,原始 token 数:…”)。

为什么是中间而不是末尾?因为对话是流式增长的,模型永远在历史的尾部工作——最近的信息最有价值;而开头承载着“这个命令在干什么”的语境。中间的大段重复日志(编译进度、逐条测试用例)信息量最低。截断预算还会乘一个 1.2 的序列化系数,给 JSON 包装留出余量。

4.6.2 发请求前的规范化:三对不变量

历史入库后并不直接等于请求内容。每次采样前,历史要过一道规范化(normalize)

  1. 每个工具调用必须有对应结果,每个结果必须有对应调用。 中断、重试可能留下“调了工具但结果还没回灌”或“结果成了孤儿”的半成品——规范化会补齐占位或移除孤儿,保证模型永远看不到悬空的工具调用(这对某些模型是硬错误);
  2. 按目标模型能力剥离不支持的模态。 当前模型不吃图片?历史里的图片内容剥掉;不吃音频?音频剥掉。同一份历史要能安全地发给任何模型(第 3 章的兜底档案原则在这里闭环:不知道模型会什么,就只给文本);
  3. 系统角色消息不外发。 内部标记为 system 的 item 不进入请求。

4.6.3 token 计量:服务端为权威,本地做估算

窗口管理的前提是知道“现在用了多少 token”。精确数字只有服务端有(响应结束时上报真实用量),但两次响应之间新追加的 item(刚回灌的工具结果、刚注入的片段)服务端还没见过。harness 的办法是双轨制

  • 服务端上报的用量覆盖“上一个模型产出 item 之前”的全部内容,这是权威值;
  • 之后本地追加的 item,用字节启发式估算(约 4 字节/token)补上;
  • 特殊内容特殊折算:加密思维链按密文长度的 3/4 再减常数估算(密文比明文膨胀);图片按视觉补丁数折算(而不是按 base64 的字节数——一张图片编码后几兆字节,实际只占一两千 token);音频按时长折算。

估算只用于“该不该压缩、还剩多少”这类触发判断;账单和精确统计永远以服务端数字为准。估算刻意保守、从粗,因为它的成本极低(序列化长度即可),而精确分词器既慢又需要随模型更新。


4.7 上下文窗口:三道防线

现在面对那个终极问题:历史滚到窗口装不下了,怎么办?

先算清楚两条线(都来自模型档案,第 3 章):

  • 有效窗口 ≈ 模型窗口 × 95%。要给系统指令、工具定义和模型自己的输出预留头部空间,不能把窗口算到顶;
  • 自动压缩阈值 ≈ 窗口的 90%(可配置调低,不能调高)。到这条线就该动手了——必须在真正撞墙之前留出“压缩本身还需要空间”的余量。

计量口径还有一个讲究:可以选择按全量计数,也可以只按窗口起点之后的增量计数(prefill 基线优先采用服务端实测值,本地估算兜底)。后者让“压缩后保留的前缀”不占用新窗口的预算,判断更贴近真实剩余空间。

Codex 没有把“压缩”当作唯一手段,而是布置了三道纵深防线:

flowchart TD
    A["历史持续增长"] --> B{"剩余 ≤ 提醒阈值?"}
    B -->|"是"| C["防线一:注入预算提醒<br/>(每个窗口只提醒一次)<br/>模型可自行收尾 / 主动开新窗口"]
    B -->|"否"| A
    C --> D{"模型调用<br/>new_context 工具?"}
    D -->|"是"| E["防线二:模型自助滚动窗口"]
    D -->|"否"| F{"到自动压缩阈值<br/>或撞窗口硬上限?"}
    F -->|"是"| G["防线三:自动压缩<br/>(轮次前 / 轮次中)"]
    E --> H["新窗口:全量重建世界状态"]
    G --> H
    H --> A

防线一:提醒。 剩余 token 跌破阈值时,harness 以开发者角色注入一条提醒(“本上下文窗口还剩 N token”,每个压缩窗口只投一次,防止刷屏),模型可以据此自己加快收尾、少读大文件。模型还可以主动调用两个工具自助查询和处理:get_context_remaining(查询剩余 token)和 new_context(主动开一个新上下文窗口——工具描述里特别声明:“不清理、不重置、不影响任何环境状态”)。这句话点破了上下文管理的世界观:环境状态在机器上(文件、进程、权限),上下文只是模型的工作记忆;换一本笔记本,世界并没有变。

防线二:模型自助滚动窗口。 模型判断对话告一段落时,可以主动调用 new_context。harness 不做摘要,直接开新窗口:历史替换为全量重建的世界状态(当前环境、权限、指令……重新注入一遍)加上需要保留的少量前端开发者消息,窗口编号加一。这是最轻量的“翻篇”。

防线三:自动压缩。 到达阈值(或模型服务端直接返回“上下文超长”错误)时,harness 强制压缩。触发时机有两个:轮次开始前检查(上一轮结束时已经超限);轮次中每次采样+工具回灌后检查(长工具输出把窗口撑爆)。触发原因也不止“超长”一种:

触发原因 场景
上下文到限 token 用量越过阈值或硬上限
压缩兼容哈希变化 切换到与旧模型“压缩格式不兼容”的新模型(模型档案声明兼容哈希),先用旧模型压缩再切换
模型降级 切到窗口更小的模型,当前历史在新模型下放不下,提前压缩
用户手动 显式发起压缩(/compact),作为独立轮次运行

压缩有三种实现,按能力自动选择:

  1. 窗口滚动(token budget 模式):不做摘要,逻辑同防线二——新窗口 + 全量重建世界状态。最省、最快,代价是模型失去对话细节(但事实都在世界状态和文件系统里);
  2. 远端压缩:模型服务端提供专门的压缩端点时,把历史发给服务端做摘要,服务端返回压缩后的历史(近期消息在约 64k token 预算内原样保留,较早的内容被摘要替代)。省客户端算力,压缩质量由服务端统一迭代;
  3. 本地摘要压缩(兜底):服务端不支持时,harness 自己跑一次模型采样完成摘要。提示词写得很直白——“你在做一次上下文检查点压缩,为另一个将要接手任务的 LLM 写一份交接摘要”,要求包含:当前进展与关键决策、重要约束与用户偏好、待办事项、继续工作所需的关键数据。

本地压缩产出的替换历史由三部分组成:近期的真实用户消息(按 20k token 预算从新到旧保留,超出部分整条不选)、一条以固定前缀开头的摘要消息(“另一个模型已经开始解决这个问题并留下了思考摘要……在此基础上继续,避免重复劳动”),以及重建的环境上下文。这里有一个非常细腻的摆放规则:轮次中压缩时,环境上下文插在最后一条真实用户消息之前,摘要保持在历史最末尾——因为模型被训练为“压缩后看到的最后一条是摘要”;而轮次前/手动压缩时不插环境,改为清空状态基线,让下一个常规轮次自己全量重建。

压缩流程还有几个值得驻足的工程细节:

  • 压缩本身也可能超长。 历史太满时,连“请总结这段历史”的请求都发不出去。此时 harness 从最旧的 item 开始逐条丢弃后重试(从头删既保住缓存前缀,又保住最近的对话),直到压缩请求能发出去;
  • 压缩前后有钩子。 外部 hook 可以在压缩前/后投反对票中止压缩(第 11 章),压缩和普通轮次一样可被中断;
  • 压缩是可见的历史事件。 历史里会留下压缩标记 item,UI 上显示为一次压缩记录;压缩后还会给用户一条忠告式警告:“长线程和多次压缩会降低模型准确性,尽量开新线程”;
  • 窗口有身份。 每个压缩窗口有编号和 UUID(首个窗口、上一个窗口、当前窗口),注入到上下文里。模型由此知道“我经历过几次压缩、现在处在哪个窗口”,这些 ID 同时用于遥测和服务端缓存隔离;
  • 旧模型压缩失败有兜底。 按理应在切换模型前用旧模型压缩,但旧模型已不可用时,自动用当前模型重试一次。
sequenceDiagram
    participant T as 轮次循环
    participant H as 历史
    participant M as 模型
    T->>H: 采样后检查:超限?
    H-->>T: 已到压缩阈值
    T->>M: 压缩请求(完整历史 + 交接摘要提示词)
    Note over M: 若超长:从最旧 item 逐条丢弃重试
    M-->>T: 交接摘要
    T->>H: 替换历史:近期用户消息 + 摘要<br/>+ 重建的世界状态
    Note over H: 窗口编号 +1,基线重置<br/>全量快照持久化
    T->>M: 用压缩后的历史继续采样

注意整个过程中对话事实没有丢:磁盘上的完整事件流(rollout)依然保留着全部原始 item,压缩替换的只是“发给模型的工作历史”。用户随时可以回看、可以从旧点分叉(fork,第 1 章);模型的工作记忆变薄了,但会话的完整档案还在。


4.8 小结:上下文管理的五条设计原则

  1. 对话与事实分离。 历史是只追加的流水账(谁在什么时候说了什么),世界状态是此刻的事实快照(目录、权限、指令、模式)。事实的更新走差分注入并显式声明替换语义,绝不让模型在矛盾的旧快照里猜哪条有效。环境状态在机器上,上下文只是视图——这是“开新窗口不丢世界”的根本前提。

  2. 一切注入皆片段。 每个注入上下文的内容都是带角色、带标签、有大小上限的结构体;标签同时服务模型(结构化信号)和 harness(自我识别);片段可合并、可替换、可在压缩后重建,对扩展读历史则隐形。扩展和 hook 注入也必须走同一套接口,没有裸字符串特权。

  3. 有界是硬约束。 项目指令 32KB 预算截断、工具输出入库即头尾截断、任何片段都有 token 上限、窗口按 95% 折算、阈值按 90% 预留——上下文里的每一类内容都有明确的尺寸天花板,且默认值宁保守勿冒进。

  4. 事实多版本共存:权威值与估算值分开。 token 计量以服务端上报为权威、本地字节估算补增量;状态基线以持久化快照为准、历史标签扫描兜底;指令来源(模型档案/用户自定义)全程留痕。估算可以粗,决策依据必须可追溯。

  5. 超长是渐进处理的,不是突然死亡。 提醒 → 模型自助开窗口 → 自动压缩(滚动 / 远端摘要 / 本地摘要三级实现)→ 压缩中超长则逐条丢旧重试,每一级都比上一级多付代价、少留情面;而所有重写都只动“工作历史”,完整事件流永不丢失,可回看、可分叉、可恢复。

留给读者思考的几个问题

  • 世界状态用“差分片段 + 替换声明”更新,而不是靠模型自己从历史里推断最新状态——如果省略替换声明、只追加新值,模型在长历史中会犯什么错?这和第 2 章“事件陈述事实”的原则有什么关系?
  • AGENTS.md 刻意以“用户”角色注入,而环境/权限信息以“开发者”角色注入——角色选择如何被当作一种“注意力编程”手段?什么内容适合哪种角色?
  • 工具输出截断选择“留头留尾挖中间”,而压缩选择“留近期消息 + 摘要替代早期”——两者保留信息的策略为什么不同?(提示:工具结果的消费场景和成段对话的消费场景有何差异?)
  • 本地压缩把摘要放在历史最末尾、环境上下文插在倒数第二条之前——为什么模型“被训练为最后看到摘要”这件事能成立?如果把摘要放在开头会怎样?(→ 第 5 章 LOOP)
  • 状态快照持久化用“全量一次 + 后续 merge patch”,这和第 3 章 WebSocket 增量请求的“严格前缀校验”面临的是同一类什么风险?补丁 apply 出错时为什么安全策略是“重发”而不是“忽略”?(→ 第 10 章)

下一章我们进入 LOOP:把前面所有零件组装起来——agentic 主循环每一圈的具体步骤、模型回复如何驱动分支、以及循环本身在哪些地方被刻意设计成“不自由”。

分类:Agent Harness标签:#agent #harness #codex