跳到主要内容
路径文档

上下文系统

一句话版:模型看到什么 = agent-loop 在请求前把系统提示词 + 工具 schema + 派生会话历史装配成一段受控上下文;超长时走**压缩(compact)**而不是截断。context/ 包只提供额外的上下文注入源,不是装配层。

这是继 Agent 主循环 之后的第二篇核心。你理解"每次模型请求到底发了什么、为什么有时慢/费 token",就靠这一篇。

一、谁在装配上下文(纠正常见误解)

很多人以为"上下文是 context 包组装的":不对。真实分工:

职责归属
装配主体每次 step 把系统提示词 + 工具 schema + 派生消息组装成请求agent-loop(assembleContextFor / buildRequest + agent/request 装配瀑布)
系统提示词注册插件贡献有序的提示词段、工具 schema、命名变量,并渲染core/system-prompt(ctx.systemPrompt)
工具清单呈现决定以 native/code/both 哪种方式给模型core/tools + core/agent-tool-presentation
会话历史事件溯源日志 → surface 消息投影core/session
额外注入源向请求注入额外模型可见上下文(不定义工具)context/*
压缩超限时折叠历史compaction/*

一句话记住:装配是 agent-loop 的事,context/* 只是"可选的额外喂给模型的话",system-prompt 是提示词段的注册中心

二、系统提示词:注册中心 ctx.systemPrompt

@deepseek-ai/dsh-system-prompt系统提示词组装注册表。插件贡献有序的段、工具 schema、命名变量;agent-loop 每步组装一次,渲染成完整模型提示。

6 个核心入口

入口作用
ctx.systemPrompt.section({ name, order, text, complete? })贡献一段提示词,按 order 升序排列
ctx.systemPrompt.context(provider)贡献有序动态上下文;每次 eligible assembly 求值并形成带来源的 user-role 快照
ctx.systemPrompt.suppressRuntimeContext()在当前 scope 抑制全部动态上下文;dispose 后恢复
ctx.systemPrompt.tools(provider)贡献工具 schema(每次 assembly 用当时的 context 求值)
ctx.systemPrompt.variable(name, provider)贡献提示词变量,段文本用 {{name}} 引用
ctx.systemPrompt.assemble(context?)做一次完整装配(global 层 + scope 层,过 waterfall)

order:段怎么排位

段按 order 升序拼接。源码约定的 order 带:

-100 harness identity(固定"You are an AI agent powered by DeepSeek Harness.")
0 deployment persona(仅一个,来自 config)
100–199 工具指引(tool:bash / tool:read …)
  • includeHarnessIdentity: true(默认) 注入固定的 You are an AI agent powered by DeepSeek Harness.
  • includeRuntimeContext: true(默认) 求值动态 context provider;设为 false 时丢弃 provider 与 waterfall 追加的上下文,但底层沙箱、审批、委派等服务继续执行
  • deployment:persona 是 config 里的 ONE 段(全局部署人格);一个 agent-scoped 贡献可以遮蔽
  • 一个 complete: true 的段会在装配瀑布后变成"唯一提示词段";同层多于一个 effective complete → 装配失败

scope 分层

和工具一样,systemPrompt调用上下文的作用域分层:

  • agent.ctx 里注册的段/变量/工具 → 只对该 agent 生效,并遮蔽同名全局项
  • 全局注册 → 全体可见

这让"给特定 agent 一套人设/工具"成为可能(见 Agent 预设与 Persona)。

变量渲染:严格,不是橡皮筋

renderPrompt(assembly) 做严格的 {{variable}} 插值,然后丢弃空段、用空行拼接。fail-loud:

  • 未注册的变量引用、已注册但无值的引用、残缺 {{…}} 组 → 抛错(宁可失败也不发畸形提示)
  • 孤立 {{(后面没有 }})则原样通过
  • 替换后的值不会被二次扫描

严格是为了稳定:系统提示词逐字节一致是 KV cache 复用的前提(见下)。

装配会过 system-prompt/assemble waterfall:监听器可以协作地改动/替换装配(受 scope 过滤),之后才应用 complete 段约束和 runtime-context suppressor。动态上下文与系统提示词段是两条独立通道,只在有内容时成为带来源的 user-role 快照。

想给 agent 加"当前日期"这类动态事实?注册一个 variable provider,在任何段里用 {{date}} 引用:比把硬编码拼进 persona 干净得多。agent loop 已注册 providermodelcwd 三个变量。

三、工具 schema 进装配

工具的 schema 是装配的一部分:README 原话是"what the model is told it can do is one coherent thing":即使 adapter 在线上把 schema 作为独立 wire 字段传输,在装配层面它和提示词是一个整体。

  • ToolRuntime自动把自己注册为 tool provider,把可见工具 schema 交进 systemPrompt
  • toolOrder config 可显式排工具呈现顺序(列出 name,用 '<unlisted-tools>' 占位余下),错误配置会 fail-loud
  • 呈现方式(native / code / both)由 ctx.tools.presentAs + core/agent-tool-presentation 决定,见 工具执行

四、请求头(request header):一次请求的"名片"

每次模型请求都有一个请求头,记录这次请求的路由与装配:

{ config: { provider, model, reasoningEffort, maxTokens },
system: <组装后的系统提示词>,
tools: <呈现给模型的工具清单> }

request/header 有几个值得懂的语义:

  • reason:initial / resume / change。header 与上一次逐字节相同时不重发 change 事件:这是不失效 prefix-cache 的开关
  • 压缩摘要调用复用会话当前请求头(对齐 KV cache);但"辅助调用(子 agent 等)复用同头"无源码依据:子 agent 有独立 header
  • 配套 request/context 事件,携带 provider/model/contextWindow 路由元数据
  • request/header 记录在会话日志里:你随时可以在 JSONL 里看某条消息到底用哪个模型、系统提示词拼成了什么样

这就是 Agent 主循环 里"adapter-default 标记"落地的地方:请求头记录哪些字段来自 adapter,下一 waterfall 去掉它们让当前 route 重新物化默认,保证 HMR 不串味。

五、Context 注入源:context/*

context/ 组的产品插件往请求里加模型可见的额外上下文,但不定义工具:

ctx key角色
session-referencectx.sessionReferenceResolver其它会话的有界快照
time-context当前时间 / 经过时间
tmux-contexttmux 位置上下文
agent-instructions工作区指令上下文

agent-instructions 在默认 demo bundle 里;其余 opt-in。它们是"可选给模型喂的料",不参与系统提示词的 order 装配

六、token 计量(token-meter):压缩的判断依据

compact 怎么知道"该压了、压多少"?靠 @deepseek-ai/dsh-token-meter 挂载的 ctx.tokenMeter 服务(默认挂载于 base bundle)。它是一个**可重放(replay-aware)**的计量单例:每次测量都基于持久化日志的最新消费位点(consumed-log revision),所以压缩和其它对压力敏感的插件能共享同一套账目,而不必依赖 CompactionEngine

估算是"固定启发式",不是精确 tokenizer

计量器没有任何配置项,故意只用一条固定启发式:每 4 个字符 ≈ 1 token,再加上 role / block / 请求信封字段的结构开销。任何配置 key 都会被拒绝——模型容量属于拥有精确 provider/model 路由的 adapter,通过 ctx.llm.resolveModelInfo().context 拿。

两个入口

操作返回说明
ctx.tokenMeter.measure(session, requestHeader?){ totalTokens, surfaceTokens, nodes[], … }一个消费位点上同步一次,返回一份深不可变的分离快照
ctx.tokenMeter.estimateMessage(message)单条消息的估算用同一条固定启发式为一条消息计价
  • totalTokens = 请求 + 响应的压力;surfaceTokens = 只算表面的启发式总量,恰好等于 nodes[].tokens 之和。
  • requestHeader 覆盖只影响压力字段;表面字段仍描述当前会话。
  • 每次调用都克隆定位节点,所以测量复杂度是 O(surface)。

provider usage 的复用规则

能复用 provider 上报的真实用量,条件是最新一次成功调用的规范请求信封(provider / model / tools / 前缀 / 调用配置)与当前测量的信封逐字节一致,且其总量不低于那次调用的完整启发式锚点;之后的成功调用会替换更早的锚点。否则就退回到"对整个信封 + 表面"做完整启发式估算。表面变化(含压缩后的收缩替换)都相对锚点带符号累加,负增量也正确。

会话投影(session projections):给 UI 看的三个量

当组合层提供 ctx.sessionProjections 时,计量器通过一个可选子 fiber 注册三个单元:

投影内容
tokenUsage完整持久化日志的 uncachedInputTokens / outputTokens / cacheReadTokens / cacheWriteTokens(输入/缓存读/缓存写/输出四桶不相交,reasoning 不再二次计入)
contextPressure可选 pressureTokens(provider 上报的最新提示词大小)、可选 projectedTokens、可选 contextWindow(最新 request/context 的路由容量)
contextBreakdown启发式的 systemTokens / toolsTokens / messageTokens——上下文的构成,而非 provider 计费的大小

关键在 projectedTokens:下一次请求的提示词要花多少 = provider 采到的样本 + 自采样以来表面增减的启发式重计价(夹在 0)。只有增量被估算,所以它既锚定 provider,又在内容落地(或压缩阴影化)的瞬间反应。这正是它存在的理由:压缩摘要走 ctx.llm.stream() 直接调用、自己不上报任何 usage,所以 pressureTokens 在压缩后、直到下一整轮完成前仍报告压缩前的提示词——occupancy 显示读的是 projectedTokens

近似是刻意的。occupancy 字段是独立的 last-wins 记录,不是对某次请求的原子观察;切换模型会让新容量先配对上一条路由的旧样本。occupancy 百分比是给人看的参考值,不是计费记录、也不是门禁输入——harness 里没有任何地方拿它做决策,压缩读的是 measure()。CJK 文本和 JSON schema 在"4 字符/ token"下会严重低估,所以 contextBreakdown 三者不会加出 projectedTokens

七、上下文压缩(compact):不截断,折叠

长会话不用"砍掉开头",而是折叠:

折叠 = 把一段历史"阴影化"(shadowed)
→ 用摘要/报告替换模型可见面
→ 完整日志仍留在持久化层(事件溯源:日志是唯一真源)
→ 模型看到的是 surfaceOp: replace 之后的面

机制要点:

  • CompactionEngine 三个入口:compactIfNeeded / compactNow / compactRegion
  • 事件序列:compaction/startcompaction/summarycompaction/end;带 compaction/end-seed 孤儿锁与恢复(崩溃时不会留下"压缩到一半")
  • 表面约束:surfaceOp(append/replace)只在 user/message assistant/message tool/result 上合法;compact 事件本身是 log-only,不入表面
  • 压缩压力挂在 agent/pre-step,规范的溢出修复挂在 agent/request-error(见 事件系统Agent 主循环)

结果裁剪(compaction-tool-result-pruner):先减料,再摘要

@deepseek-ai/dsh-compaction-tool-result-pruner 提供 ctx.toolResultPruner,是 compaction-basic 的可选配套(默认挂载于 base bundle;web 模式 bundle 显式禁用)。它不是压缩后端、也不是面向模型的工具,而是一次model-free、可重放的裁剪:把超预算的 tool/result 表面节点改写成"有界头 + 固定省略标记 + 有界尾",完整原始事件仍留在 append-only 会话日志里。

  • 触发时机:compaction-basic 在压力或规范溢出达标之后、选范围之前读它(ctx.get('toolResultPrune'));低于压力的 step 检查从不裁剪。
  • 改写方式:每处超预算结果被替换成一个新 append 的 tool/result,带 { surfaceOp: { op:'replace', start: originalSeq, end: originalSeq }, sourceEventSeqs:[originalSeq] };替换只改 content,保留 turn/step/callId/错误字段/meta
  • 裁剪后 remeasure:compaction-basic 再经 ctx.tokenMeter 重测,压力降到安全线就跳过摘要;否则对裁剪后的表面做摘要。
配置默认含义
thresholdChars8192文本合计超过该 Unicode 码点数才裁剪
headChars4096保留的头部码点数
tailChars1024保留的尾部码点数
  • measureContent(blocks)text 块的 Unicode 码点;pruneContent(blocks) 返回有界替换,或内容已在阈值内时返回 null。非文本块保持原相对位置;切片不会劈开 UTF-16 代理对(但可能劈开多码点的 grapheme 簇)。
  • 每次输出严格等于"head + 标记 + tail",且严格小于触发输入,所以第二遍不会再次产出替换。

第三方 tool-rewind 的 SHRINK "摘要必须比被折叠区域短" 属仓库外插件,其语义无法从当前源码验证:当生态参考即可,别当成 DSH 内置。

八、三视角:模型 / token / KV cache

一次组装模型看到token 效应KV cache 效应
系统提示词identity + persona + 各插件段(严格插值后)identity 固定费 token;persona/段文本每请求重付identity/persona/段/order 逐字节不变才前缀复用
工具 schema可见工具(经 toolOrder/restriction)schema 每 step 重付schema 变化自首个改动 token 起失效
会话历史surface 消息(不含原始 chunk/边界)随表面消息增长;多工具轮次每 step 重发累积普通增长 append-only;surface 替换/压缩使前缀失效

实操含义:想省 token/保缓存,保持系统提示词与工具 schema 稳定;任何"动态拼 persona"都会让前缀缓存从改动处失效,费一次全量重算。

九、源码佐证(事件层)

{ "type": "user/message", "seq": 9, "data": {...} }
{ "type": "request/header", "seq": 11, "data": { "header": { "config": {...}, "system": "{{system}}", "tools": "{{tools}}" } } }
{ "type": "request/context","seq": 12, "data": { "provider": "deepseek-official", "model": "deepseek-v4-flash", "contextWindow": ... } }

十、验证

# 看一个会话的请求头(默认 zstd 压缩、两级目录)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep "request/header" | head -1

# 看装配后的系统提示词(含 identity + persona + 各段)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -o '"system":.*' | head -1

# 看变量渲染是否生效(严格插值,畸形会 fail)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -i "model\|cwd" | head

下一步