上一章讲了会话的事件溯源。这一章看它最大的受益者:上下文工程。两个问题:① 模型每次请求看到的系统提示词,是怎么由几十个插件各贡献一段拼出来的?② 上下文快满了怎么办?
答案分别是:dsh-system-prompt 的段/变量机制,和 dsh-compaction-basic + dsh-compaction-tool-result-pruner + dsh-spill-policy 的三件套压缩体系。全部有本机实测数据支撑。
第 1 章说过,本机实测的系统提示词是 9927 字符。它不是一个人写的,而是装配出来的。dsh-system-prompt 的机制:
工具包拥有自身的跨调用指导(tool:bash、tool:read 等);此插件拥有 harness:identity 与 deployment:persona。 —— dsh-system-prompt/README.zh.md
每个插件调 ctx.systemPrompt.section({ name, order, text }) 贡献一段,dsh-system-prompt 按 order 排序拼装。
| order | 段 | 内容 |
|---|---|---|
| -100 | harness:identity | 固定文本 You are an AI agent powered by DeepSeek Harness. |
| 0 | deployment:persona | 部署人格(本机实测由 web-app patch 配置) |
| 100–199 | 工具引导 | 各工具包的跨调用指导 |
【来源:dsh-system-prompt/README.zh.md;lib/index.js】
【来源:dsh-system-prompt/lib/index.js:263】
段文本里写 {{name}},变量由 provider 在组装时提供值。agent loop 注册了 model、provider、cwd 三个变量。web-app 的 persona 段就是一个活例子 [实测]:
严格插值:未知引用、已注册但无值的引用、格式错误的 {{…}} 组——都抛异常。README 说:"明确失败胜过交付格式错误的提示词"。
工具 schema 的排列独立于段顺序,由 toolOrder 配置决定:
toolOrder 是 ToolSchema.name 列表,必须恰好包含一个 '<unlisted-tools>' 标记,已列工具按列表位置排,未列工具按名称字典序插入该标记位置;缺席则全部按字典序。
字典序用 code-unit 比较,"locale-independent, so the order is identical on every machine"。
从本机会话日志的 request/header 事件提取完整系统提示词,逐段标注来源 [实测]:
| 段落 | 来源 | 内容示例 |
|---|---|---|
| 身份 | dsh-system-prompt | You are an AI agent powered by DeepSeek Harness. |
| checkout 提示 | dsh-web-app 部署层 | 实现 checkout 位置、GUI URL、HMR 提示 |
| 角色 | persona(web-app patch) | You are a coding agent powered by the claude-opus-5 model. Your working directory is … |
| 环境 | 部署层 | # Environment + OS/Shell 说明 |
| 行为准则 | 部署层 | # Acting with care / # Working faithfully |
| 沟通 | 部署层 | # Communication(中文默认、file_path:line 格式) |
| 工具使用指南 | dsh-tool-fs 等各工具包 | read/write/edit 的完整行为约定 |
| 任务追踪 | dsh-tool-todo | todo 的维护规则 |
| 目标 | dsh-tool-goal | goal 工具的使用说明 |
| 委派 | dsh-tool-subagent / workflow / ralph | 各自的使用判据 |
| 工作区策略 | 部署层注入 | Python workspace policy |
怎么验证? 这段系统提示词就在你自己的会话日志里 —— 找到最新的 request/header 事件,读 data.header.system。你能亲眼看到它是"装配"出来的:段与段之间是拼接痕迹,不是一篇手写文档。
dsh-agent-instructions 负责加载 AGENTS.md / CLAUDE.md 链(你正在读的这份仓库规约就是通过它进来的)。
loader 先读取 $DSH_HOME/AGENTS.md,随后针对项目根目录到 agent.session.header.cwd 的每个目录,先读取每个现有基础候选文件(AGENTS.md、CLAUDE.md),再读取每个现有本地 overlay 候选文件(AGENTS.local.md、CLAUDE.local.md)。 —— dsh-agent-instructions/README.zh.md
配置要点:
maxBytes 必填 —— 这是 dsh 对"指令膨胀"的硬约束:每个部署都必须显式选择预算,超过就按优先级裁剪(先丢较宽泛的,再截断最具体的,并发出可见通知指明被省略的路径)。
同级去重:同一目录里 CLAUDE.md 若只是 AGENTS.md 的字节级复制(去首尾空白后 SHA-1 相同),只渲染一次。
基线的注入发生在每个会话第一次符合条件的 agent/pre-step(第 5 章那个 waterfall):
当下游决策让非空的第一步批次进入时,插件会将基线折入最终批次、紧随已领取的直接提示词之后。
嵌套文件发现跟随结构化文件系统活动:
观察第一方 read、write、edit 调用成功后产生的不可变 tools/result。每个已接受的 touch 都会检查新达到的后代 scope 以及之前加载的每个 scope。
三种转换:新文件 → 新增;已改变 → 替换;消失或成为重复项 → 移除通知。
为什么不跟 shell cd?因为每次本地 bash 调用都启动新 shell,解析任意 shell 语法也不可靠。
新 scope 的注入模板(实测):
安全细节:内容里出现字面 </system-reminder> 会转义,"仓库控制的文本无法关闭插件控制的框架"。
上下文快满了怎么办?先要知道"多满"。dsh-token-meter 负责计量:
它从持久日志为每个会话推进一个隔离 fold,因此压缩与其他压力敏感插件可以共享计量,无需依赖 CompactionEngine。 —— dsh-token-meter/README.zh.md
meter 不是每次重新估算全部上下文,而是沿日志增量折叠:
只有当最新成功调用的规范请求 envelope 与已测量 envelope 匹配,且其总量不低于该调用的完整启发式锚点时,才会复用提供方用量;后续成功会替换较早锚点。否则会对当前 envelope 与表层进行完整估算。表层变更保持相对于匹配锚点的带符号值,包括缩减替换后的负 delta。 —— dsh-token-meter/README.zh.md
翻译:提供方报告过真实用量(usage)的位置成为锚点,之后只对锚点以来的增减做启发式计价。压缩造成的 replace 产生负 delta —— 压力读数在压缩落地的瞬间就下降。
每个 token 按四个字符估算,再加上角色、块与请求 envelope 字段的结构开销。任何配置键都会被拒绝。
这是固定启发式,无配置。模型容量来自适配器的 resolveModelInfo().context。
部署会在 LLM 适配器上配置容量,并在 dsh-compaction-basic 上配置压缩策略。
容量归适配器、计量归 token-meter、策略归 compaction-basic —— 三者独立可替换(第 4 章 seam 思想的又一次体现)。

配图说明:token-meter 重放感知计量(提供方 usage 锚点 + 4 字符/token 启发式);三个递进手段 —— ① spill-policy(溢出到文件,模型见首尾预览 + 路径,read 工具跳过)、② tool-result-pruner(裁剪单个结果:head 4096 + marker + tail 1024,surface 上 1→1 遮蔽,幂等)、③ compaction-basic(压力达 80% 触发整段摘要:回放前缀复用 KV cache,单个 user/message 的 replace 遮蔽,八个固定小节)。共同铁律:只追加不删除。
压力高了怎么办?dsh 有三个递进的手段。
配置(默认值):
| 键 | 默认 | 含义 |
|---|---|---|
| thresholdChars | 8192 | 合并文本超过此 Unicode 码点数时剪枝 |
| headChars | 4096 | 保留的开头 |
| tailChars | 1024 | 保留的末尾 |
裁剪后插入固定标记:
"重放安全"的技术含义:
每个超出预算的工具结果都会被一个新追加的 tool/result 替换,其携带 { surfaceOp: { op: 'replace', start: originalSeq, end: originalSeq }, sourceEventSeqs: [originalSeq] }。原始事件仍可用于持久化、回放和精确日志检查。 —— dsh-compaction-tool-result-pruner/README.zh.md
日志只追加,surface 上做 1→1 遮蔽。第二次扫描不会发出替换(幂等),且头+标记+尾严格小于触发输入。
已知限制:字符预算不是 token 预算(不同提供方 token 密度各异,真正是否缓解压力由 token-meter 判定);剪枝只基于语法(保留头尾,不解释中间哪行语义重要)。
第 7 章讲过:超长纯文本结果溢出到 ctx.spillStore,返回预览 + 路径。maxInlineBytes 省略即完全禁用(无默认值)。
与 pruner 的分工:pruner 是"截断保留头尾",spill 是"完整存文件只留预览"。pruner 保语义(头尾原文),spill 保完整(可随时 read 回)。
默认配置:
| 键 | 默认 | 含义 |
|---|---|---|
| thresholdRatio | 0.8 | 上下文压力达到窗口 80% 时触发 |
| retainRatio | 0.16 | 逐字保留的近期尾部比例 |
| maxTokens | 8192 | 摘要调用的生成上限 |
| compactionRetries | 1 | 压力仍高时额外尝试 |
| auto | true | 是否注册步骤边界压力监听 |
触发流程(源码 compactIfNeeded,要点):
摘要调用是一次直接的 ctx.llm.stream(),不是 agent loop 的 step:
该调用会逐字回放会话自身的系统提示词、工具与已遮蔽区域消息(包括图片引用),并将压缩指令作为最后一条 user 消息追加,从而复用提供方的热前缀 cache,而非使它失效。 —— dsh-compaction-basic/README.zh.md
这是第 6 章"KV Cache 视角"的正面应用:摘要器故意回放前缀以命中缓存。
摘要的八个固定小节(要求模型"EXACTLY 保留每个小节,顺序不变,空的写 (none),永不丢小节"):
框定标记:
前置一段固定的英文前导("This is an automatically generated checkpoint…"),把压缩结果定位为"已确立的背景,不要复述"。
事务五步(compaction/* 事件不上 surface):
如果在 compaction/start 与 compaction/end 之间崩溃,会留下可检测的遗留锁,而不是虚假声称压缩已完成但表层从未被遮蔽的 compaction/end。
收敛校验:拒绝不能缩小源内容的摘要。
| 手段 | 时机 | 模型看到 | 数据安全 |
|---|---|---|---|
| spill-policy | 工具结果生成时 | 预览 + 文件路径 | 完整文本在 spill 存储 |
| tool-result-pruner | 压力检查/溢出时 | 头 + 标记 + 尾 | 原始事件仍在日志 |
| compaction-basic | 压力达 80% 时 | 摘要检查点 | 被遮蔽事件仍在日志 |
共同点:只追加,不删除。 三个手段都遵守事件溯源铁律 —— 原始事件永远在日志里,压缩/裁剪只是 surface 层的变化。这就是第 8 章"日志是真源"的红利。
上下文工程是 dsh 设计密度最高的领域:
下一章进入安全边界:沙箱与权限 —— fail-closed 的四层防线。