deepseek-harness 学习手册
上下文工程M4 · 把 token 当公开契约
柱二独家70 min

把 token 当公开契约

每个包 README 必须写清模型看见什么、花多少 token、对 KV cache 有什么影响——而且 CI 会卡。

最后核对于 v0.1.1-rc.2 · commit b150a551b8 · 2026-08-22

这是全站最没法从别处学到的一节。

大多数 agent 项目把「模型看见了什么」当成实现细节:它散落在几十个拼 prompt 的地方,谁改了谁知道,review 时靠人肉记忆。dsh 把它提升成了每个包的公开契约,并且用 CI 卡住——一个包的 README 如果不交代它对模型上下文的影响,门禁就红。

这一节讲三件事:这份契约要求写什么、CI 怎么卡、以及为什么「模型读的」和「文档渲染的」在这个仓库里不可能分叉

4.1 Model Experience 契约

先看它想解决什么问题。决策记录里的原话(.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md):

「一个包的 README 可以把 API 和运行机制讲清楚,却回答不了那些真正主宰 agent harness 行为与成本的问题:这个包里有什么会进入模型请求?在什么条件下进?那些 token 会停留多久?后续请求还能不能复用 KV cache 前缀?

「在插件架构里,这种遗漏尤其难审计。一个 consumer 可能把后端结果变成一条工具消息,一个策略插件可能把成功换成错误,compaction 可能删掉旧历史,一个 agent-scoped 的注册可能只改变一个 agent 的 prompt 或 schema 而别的 agent 毫无变化。只读那些名义上面向模型的包,会漏掉真实的上下文影响;而把每个依赖的源码都读一遍,对例行 review 来说太贵了。

三个字段,各自回答什么

字段回答的问题写作要求
What the model sees 这个包让什么文本进入模型请求?在什么条件下?对哪些 agent 可见? 稳定的、包自己拥有的文本必须逐字引用——system prompt 散文和长字面量用一个带标题的 H5 + markdown 代码块;短字面量内联,并用具名占位符标出插值处。工具 schema 链接到生成式 catalog 的锚点,只写组合与配置的差异。
Token effect 这些 token 是每次请求都花,还是按调用花?会保留多久?有没有上限? 不要求写具体数字。决策记录里明确否决了「要求数值 token 计数」这个方案——精确数字取决于 tokenizer、适配器序列化、配置和运行时数据。稳定的契约是增长形状:每请求固定 / 每调用条件性 / 保留 / 被替换 / 有上限 / 零直接影响。
KV Cache effect 后续请求还能不能复用已有的缓存前缀?从哪一个 token 开始失效? 必须区分四种:append-only 增长稳定的重复前缀替换更早的 token独立的模型请求;并且要点名每一个本包拥有的配置、作用域、生命周期、compaction 或路由变化,只要它可能在新内容追加之前改动请求。
一个措辞上的洁癖,很值得学

契约里写死:「不会失效」的意思是「这个包保住了一个原本就可复用的前缀」,不是「某个 provider 承诺会命中缓存或保留多久」。

这是在防一类很常见的文档谎言——把「我没破坏它」写成「它一定有效」。两者差着一整个外部系统的可靠性。

零影响的包怎么写:审计过的短式

如果一个包对模型上下文没有影响,或者某条路径完全由别的包渲染,它用被验证器审计过的短式:一句以 None, as Indirectly, through 开头的话,后面跟一个 KV Cache effect 小节和一段散文。

为什么不允许「零影响就直接省略」?决策记录给的理由很干净:「不受约束的缺席,在『审计过的零』和『忘了写文档』之间是有歧义的。」

只有模型无关的通用包才能整节省略,而且必须在验证器里登记理由。这份名单就写在门禁脚本源码里,是可 review 的审计证据:

scripts/verify-package-readme-model-experience.tsts
/**
 * 公开契约与模型无关的通用包。它们的 README 完全省略 Model Experience;
 * 理由留在这里作为可 review 的审计证据,
 * 这样「缺一节」就不会被误当成「忘了写文档」。
 */
const NO_MODEL_EXPERIENCE_SECTION: Readonly<Record<string, string>> = {
  'packages/core/scope': '模型无关的注册与生命周期原语;上下文选择由面向模型的 consumer 拥有。',
  'packages/util/brand': '纯类型原语,编译后被擦除。',
  'packages/util/home-paths': '只解析 harness 自己的宿主路径。',
  'packages/util/launch-environment': '只解析宿主环境变量。',
}

代码 4.1 全仓 227 个包,能整节省略的只有四个。名单是硬编码的白名单,加一个包进去要过 review。(散文为便于阅读作了翻译,结构与键名照原文。)

4.2 CI 到底卡什么

verify-package-readme-model-experience 会自己发现所有包的 manifest,然后校验:

  1. 三种分类是否成立(完整结构式 / 审计过的短式 / 整节省略)
  2. 末尾小节的顺序——Model Experience 必须紧挨在 ## Known Limitations and Deferred Work 之前
  3. 字段标题的层级与顺序精确匹配(H4,且必须是那三个、按那个顺序)
  4. 每个字段下有非空的散文段落
  5. 逐字文本块归属正确(H5 拥有)
  6. 具体的字面证据存在——不能只写抽象描述
  7. 工具 catalog 链接带锚点

它跑在 doc-sync 聚合门禁里。这个门禁一共 28 个叶子检查,Model Experience 只是其中一个。

这条门禁划得很清楚:它不管对不对

决策记录最后一句:「Review 仍然拥有覆盖度、链接相关性和事实准确性。」

换句话说,机器卡的是结构与完整性——你不能不写、不能写错格式、不能少字段;内容是不是真的,还是人来判断。这个分工值得抄:把可机械检查的不变量接进门禁,把判断留给人,不要试图用 lint 规则去表达品味。

4.3 生成式文档链:模型读的和文档渲染的不可能分叉

光有「必须写」还不够——写下来的东西会过期。dsh 的第二招是:凡是能从代码推出来的,一律生成,并且给生成物配一个新鲜度门禁。

生成物内容生成 / 校验命令
docs/tool-catalog.md每个模型可见工具的精确 schemagen-tool-catalog / verify-tool-catalog
docs/config-catalog.md每个插件的配置字段gen-config-catalog / verify-config-catalog
docs/cordis-api/Cordis 服务方法签名与事件(含 @modegen-cordis-api / verify-cordis-api
docs/persistence-catalog.md持久化格式gen-persistence-catalog / verify-persistence-catalog
docs/module-graph.md模块图 / 文档图 / scoped eventsverify-module-graphverify-doc-graphsverify-scoped-events

这条链最有意思的地方在 tool-cordis 这个包。它给模型的 cordis_inspect 报告,渲染的是 src/api-catalog.ts——workspace 里所有 Cordis 声明的生成式投影:方法签名、JSDoc、事件与分发模式、以及签名引用到的类型。而这份投影docs/subsystems 用的是同一次 AST 遍历

README 里那句话是这一整节的最好总结:

「所以模型读到的数据,和渲染出来的文档,不可能分叉。」

一个现场发现:手写散文确实会过期,生成物不会

本站在 commit b150a55 上核对时发现的一处不一致

packages/extensions/tool-cordis/README.md 开头写的是「五个面向模型的工具」,并列出 cordis_inspect / define / run / stop / undefine

而生成式的 docs/tool-catalog.md 在同一个 commit 上列出的是七个:inspect 一侧已经拆成了 cordis_inspect_list / cordis_inspect_query / cordis_inspect_self,源码 src/index.ts 与之一致。

以生成式 catalog 为准。这不是在挑刺——恰恰相反,它是这一节论点的现场证据:被门禁盯着的生成物跟上了代码,手写的散文没跟上。本站 M6 按七个动词讲。

4.4 KV cache 前缀稳定性:哪些操作会打掉缓存

把 KV cache 单列成一个字段,是因为它的失效规则很反直觉:请求前缀从第一个变动的 token 开始,后面全部作废。所以「在 system prompt 开头加一行」和「在对话末尾追加一句」的代价差着一整个数量级。

照着契约里那四种分类,可以整理出一张实操表:

你做的事对前缀的影响代价
追加一条消息 / 一条工具结果 / 一条注入提醒append-only:新内容跟在可复用前缀后面不打掉任何已有缓存
工具集合不变、prompt section 不变前缀稳定:整段可重复复用
挂载 / 卸载一个注册了 prompt 或工具的插件改写前缀:从第一个变动的贡献开始失效贵。cordis_run / cordis_stop 就在这一档
切换 agent scope,让某些工具 schema 隐藏或出现改写前缀:从第一个变动的 schema token 开始失效
compaction 触发,历史被摘要替换替换更早的 token最贵,但换来的是不触上限
调用一次辅助模型(如生成 session 标题、web search 的服务端检索)独立的模型请求,跟主对话的前缀无关另算一笔账
目录

本页

M4 · 把 token 当公开契约