DeepSeek Harness 回放式 Token 计量服务:统一上下文压力核算与压缩策略的消费边界
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
本文基于仓库内 Agent Note《Replay token meter service》展开,讲解 DeepSeek Harness 如何以
@deepseek-ai/dsh-token-meter这一零配置单例服务,为压缩(compaction)、溢出保护与未来的请求策略插件提供统一的"持久请求消耗多少 token"这一答案。读完你将掌握:回放式计量的设计动机、固定启发式估算规则、逐会话增量折叠与锚点复用机制、measure()/estimateMessage()的完整语义,以及dsh-compaction-basic如何消费而不拥有计量。
一、问题背景:为什么计量必须独立于压缩
上下文压力(context pressure)的用途远不止压缩。一个压缩后端、一个溢出保护、乃至未来的请求策略插件,都需要回答同一个问题:当前持久请求到底消耗了多少 token?
原文档明确指出,如果把这套折叠(fold)逻辑留在dsh-compaction-basic内部,会带来三个后果:
- 重复实现回放逻辑——每个需要压力的模块都要各自重放一遍会话日志;
- 未加载压缩的调用方无法计量——计量能力被错误地绑定在了压缩后端上;
- 诱使调用方复用陈旧核算结果——不同时刻、不同路由下的压力口径无法对齐。
同时,提供方 usage 也不是完整答案,因为它只描述"某个精确请求信封下的一次成功调用":
- 当前会话表层之后可能增长、缩小或被替换;
- 会话可能切换提供方与模型;
- 旧日志可能缺少构成 assistant 消息的分片 seq;
- usage 字段还会分开报告输入、缓存读取、缓存写入、输出与推理计数。
因此,一个可用的计量服务必须做到三点:结合最新精确锚点(provider usage)与保守的启发式重新定价(heuristic repricing),并公开每个结果已经消费的日志修订号(logRevision)。
二、核心决策:一个具体的 LLM 家族服务,而非接口抽象
@deepseek-ai/dsh-token-meter是 packages/llm/token-meter 下的单个具体包,通过 Cordis 注册为ctx.tokenMeter。关键决策是:在第二种实现出现之前,不将其拆分为接口与后端。
从 src/index.ts 可以看到TokenMeter继承自 Cordis 的Service,其 API 面非常克制:
export class TokenMeter extends Service { static Config: z<TokenMeterConfig> = z.object({}) // 零配置 measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement estimateMessage(message: Message): number }这个服务没有任何配置项。从validateConfigKeys(src/index.ts)可以看到,任何传入的键都会被直接拒绝:
function validateConfigKeys(config: TokenMeterConfig): void { for (const key of Object.keys(config)) { throw new Error(`TokenMeterConfig: unknown key "${key}" (no settings are supported)`) } }原文档特别强调:没有模型 profile、容量设置、密度设置、分词器后端或语言专用策略。精确的提供方/模型容量属于"路由所属适配器"(route-owning adapter)的职责,通过ctx.llm.resolveModelInfo().context查询(见 token-meter/README.md);而消费方专属的阈值与保留策略则归dsh-compaction-basic所有。这一职责划分对应原文档中"路由模型上下文与压缩策略"的架构决策。
三、固定启发式估算:每 token 四字符 + 结构开销
计量服务的估算核心在 src/estimate.ts,是一个确定性、与路由无关的固定密度启发式:
const CHARS_PER_TOKEN = 4 // 固定文本密度 const BLOCK_OVERHEAD = 4 // 每 block 的 JSON 框架/类型标签开销 export const ROLE_OVERHEAD = 4 // 每条消息的角色字段开销各定价分支如下(对应 estimate.ts):
| 内容类型 | 定价规则 |
|---|---|
text/reasoning | ceil(文本长度 / 4) + BLOCK_OVERHEAD |
tool-call | ceil(name 长度 / 4) + ceil(arguments 长度 / 4) + BLOCK_OVERHEAD |
tool-result | 递归定价content+BLOCK_OVERHEAD |
| 未知 block(merge 扩展)与 image 引用 | 保守的结构化 JSON 价格:BLOCK_OVERHEAD + ceil(JSON 长度 / 4) |
消息级 APIestimateMessage(message)即estimateContent(message.content) + ROLE_OVERHEAD(estimate.ts)。请求信封(header)侧则拆分为系统提示词与工具 schema 两部分:estimateHeader(header) = estimateSystemTokens(header) + estimateToolsTokens(header)(estimate.ts)。
图片路由定价:适配器声明优先
固定启发式并非绝对。measure()在定价时会通过可选的llm服务解析有效信封的提供方/模型,若该路由所属适配器声明了图片请求定价(imageRequestPricing),则每个图片出现处使用"路由声明的视觉 token + 模型可见文本"定价,其余节点保留固定启发式;未声明定价的路由行为不变(见 src/route-pricing.ts)。priceSurface还会校验返回价格数量与图片出现次数一致,数量不匹配即抛错,防止节点被静默错价。
四、逐会话回放折叠:增量、隔离、事务性失败
每个会话在WeakMap<Session, ReplayState>中拥有一个隔离的增量折叠状态(src/index.ts):
interface ReplayState { consumedEvents: number // 已消费事件游标 header: EpochHeader | undefined // 规范请求头快照 surface: MeterSurfaceNode[] // 带位置的表层节点 stepStart: { turn; step; nodes } | undefined // 步骤边界 anchor: MeasurementAnchor | undefined // 最近一次成功调用锚点 }折叠通过session/event前进:活跃会话由服务自身注册的监听器推着走,每次读取都会追平到持久日志尾部。这意味着监听器顺序、种子会话(seeded session)与服务重载都不会改变答案——回放完全由持久日志决定,具有确定性(src/index.ts)。
事务性失败:畸形事件整体回滚
折叠采用plan/commit 两段式(src/surface-fold.ts):planSurfaceTokens只读地执行所有可能失败的步骤(表层替换范围解析、步骤边界校验、锚点校验),commitSurfaceTokens才原地修改。因此下一个畸形事件会事务性失败并保持未读——同一份损坏日志每次重试都以完全相同的方式失败,绝不会让状态只改一半、压力静默漂移。
具体校验点包括:step/start到达时上一步未结束、step/end无匹配的step/start、assistant/message无匹配步骤边界、表层替换范围在现有节点中不存在、sourceEventSeqs引用了不早于 assistant 消息或重复的 seq、引用的源不是同一步骤的assistant/chunk(src/index.ts)。
五、measure() 语义:一次同步、一个快照、O(surface) 成本
measure(session, requestHeader?)是计量服务的主入口(src/index.ts),其行为可概括为:
- 同步一次折叠到当前持久尾部;
- 解析有效信封(未传
requestHeader时用折叠中最新的规范 header); - 用该路由的图片定价给当前表层节点重新计价;
- 返回一个分离、深度不可变的快照。
返回类型定义在 src/types.ts:
interface TokenMeasurement { readonly logRevision: number // 已消费的持久事件数,等于下一个未读事件 seq readonly baseline: TokenMeasurementBaseline // usage / estimated / none 三种锚点 readonly surfaceDeltaTokens: number // 相对锚点的有符号表层增量 readonly totalTokens: number // 请求+响应总压力(非负) readonly surfaceTokens: number // 仅表层的路由计价总量,等于 nodes[].tokens 之和 readonly nodes: readonly TokenSurfaceNode[] // 按 head-to-tail 顺序的带位置节点 }要点:
totalTokens是请求与响应压力;surfaceTokens是仅表层的启发式总量,恒等于nodes[].tokens之和;requestHeader覆盖只改变压力定价,表层字段永远描述当前会话;- 每个结果携带一个
logRevision,调用方可据此判断自己消费的是哪一版日志事实; - 每次计量都会克隆当前节点,因此成本为O(surface)——即便是低于阈值即可结束的压力检查也不例外(原文档"后果"一节明确承认这一代价,换取的是结果一致性,消除了分离 API 在调用方侧的竞态窗口)。
六、锚点与增量:提供方 usage 何时被复用
折叠会追踪每次成功模型调用的 usage 及其引用的 chunk seq。原文档给出了精确的复用条件:
只有当待计量的规范请求信封等于最近一次成功调用的锚点时,服务才复用提供方 usage。提供方、模型、系统提示词、前缀、工具或调用配置任一变化都会触发完整的启发式重新定价。
从源码看(src/index.ts),锚点判定还有一层保守性约束:usage 总额不得低于该次调用按完整路由计价出的启发式价格,否则退回纯估算锚点。表层变化则相对匹配锚点保留有符号增量——包括缩小替换后的负值(surfaceDeltaTokens可以为负,最终totalTokens用Math.max(0, …)钳制为非负)。
锚点替换规则:后续成功请求会替换先前锚点,跨提供方或模型切换时同样如此。
usage 求和的去重规则
usageTokens()(src/index.ts)对互不重叠的输入、缓存读取、缓存写入与输出 bucket 求和,推理计数不会二次加入:
return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0) + usage.outputTokens每次成功模型调用都会记录assistant/message——包括无内容调用与达到 token 上限的调用——并带上精确的更早 chunk seq。sourceEventSeqs的语义分三种(src/index.ts):
- 显式空列表:已知为空的提供方流,按 0 token 定价;
- 缺失(旧日志):保守地把持久 assistant 输出视为提供方输出(无法区分提供方输出与监听器改写);
- 非空列表:从精确引用的 chunk seq 用
BlockAssembler重组提供方内容后再定价,并对 seq 顺序、去重、步骤归属做严格校验。
七、compaction-basic 消费计量,但不拥有计量
dsh-compaction-basic是计量服务的首要消费方。从 packages/compaction/compaction-basic/src/index.ts 可以看到它的依赖注入声明:
export class BasicCompactionEngine extends CompactionEngine { static inject = ['llm', 'tokenMeter', 'sessions'] // ... }架构约束非常清晰:
CompactionEngine不增加任何 token 方法或类型——计量全部经ctx.tokenMeter完成;- 配置、区域事务与摘要各自留在独立模块(
config.ts、region.ts、summarizer.ts); summarize()仍是唯一的子类定制钩子,回放与持久变更策略保持固定,从而保证每一次定价决策(压力、保留、被遮蔽内容、引用的源事件、非缩小摘要拒绝)都使用同一个单例计量器,口径一致。
区域事务:锁定后计量、摘要后复测、比较向量
自动压缩的每次"阈值与保留联合决策"只使用一次统一计量。区域事务(region transaction)的执行顺序为:
- 追加持久
compaction/start锁; - 计量一次;
- 异步摘要完成后再次计量;
- 比较两个分离的表层节点向量。
如果期间发生表层变更(节点向量不同),则阻止替换;但logRevision因无关的纯日志事实推进时,不会使未变化的选定范围失效——这正是logRevision语义与"按节点向量比较"配合的价值所在。
自动压力检查的时机
自动压力运行在agent/pre-step(请求派生之前),计量的对象是前一个agent/request实际所选提供方/模型产生的规范持久信封。原文档强调:
- 无请求头的会话(没有已完成的路由请求可评估)不产生任何工作;
- 任意路由目标都可以使用这个单例估算器;
- 规范的溢出恢复使用同一计量结果强制选择范围,并且只有在表层替换得到证明后才重试;
- 对于在成功 usage 锚点出现前就被拒绝的请求,提供方溢出分类仍由适配器维护作为兜底路径。
八、压缩策略配置详解:默认值、覆盖与校验
原文档给出了压缩策略的服务级默认值,与 config.ts 完全一致:
| 配置项 | 默认值 | 说明 |
|---|---|---|
thresholdRatio | 0.8 | 压力触发阈值比例(对容量换算) |
retainRatio | 0.16 | 保留尾部比例 |
summarizationProvider | '' | 摘要提供方(空 = 未指定) |
summarizationModel | '' | 摘要模型(空 = 未指定) |
maxTokens | 8192 | 摘要最大 token 数 |
compactionRetries | 1 | 压缩重试次数 |
maxOverflowRetries | 1 | 溢出恢复最大重试次数 |
auto | true | 是否启用自动压缩 |
配置解析的关键机制(对应 config.ts):
- 顶层字段适用于每个路由目标;
modelPolicies中的精确provider/model项可以部分覆盖这些字段(resolveTargetPolicy按 provider+model 精确匹配,config.ts); - 压力以容量为基准换算比例:
resolveCompactSpec用适配器解析的contextWindow计算thresholdTokens = floor(contextWindow × thresholdRatio)(config.ts),容量必须是正整数,否则抛出TargetPressureConfigError; retainTokens可以替代retainRatio(二者互斥,同时配置即报错);无论哪种形式,保留值必须小于最终阈值,违规在插件加载时即失败;- 摘要提供方与模型必须成对:同时为空或同时非空(
validateSummarizationPair,config.ts);空组合先解析最近记录的请求目标,再回退到AgentOptions中的组合; thresholdRatio必须是(0, 1]区间内的有限数,maxTokens为正整数,重试次数为非负整数——所有越界与拼写错误的键都在加载期被拒绝,防止默认值掩盖配置错误。
九、测试与验证
原文档列出的测试面与仓库测试文件一一对应:
- 固定估算、信封失效与锚点替换、回放边界、不可变快照、已路由压力、收敛、溢出 generation 证明与回滚——对应 tests/token-meter.spec.ts、tests/route-pricing.spec.ts、tests/turn-usage.spec.ts 等;
- 真实 Loader/Include fixture验证零配置 token-meter 与 compaction-basic按依赖顺序加载的路径——对应 tests/loader-composition.spec.ts,这正是"meter 先于 compactor 注册、compactor 通过
ctx.tokenMeter消费"这一依赖契约的落地验证。
十、替代方案与取舍(原文档决策记录)
原文档记录的五条替代方案及其否决理由,是理解这套设计边界的关键:
- 把估算保留在
CompactionEngine内——否决:计量拥有独立于压缩的消费方与回放语义,还会强迫每个压缩器暴露同一套无关 API; - 立即拆成接口与启发式后端——否决:目前只有一种实现,单一具体服务既保留了未来的 seam,又避免了推测性的包与配置;
- 把模型键控窗口与密度 profile 放进 meter——否决:回放估算不拥有模型路由或容量事实,容量归路由所属适配器,阈值与保留策略归 compaction-basic;
- 保留独立的标量与表层计量——否决:调用方要为一次决策做两次读取并匹配修订号;标量只读虽能避免低于阈值时的节点复制,但会在调用方引入竞态窗口,统一快照以 O(surface) 复制换取一致性;
- 在不同信封之间移用提供方 usage——否决:模型、工具、前缀与调用配置都是请求事实,不匹配时必须重新定价完整当前请求。
十一、后果与适用边界
原文档"后果"一节明确划定了这套方案的边界,作为使用时的约束:
- Token 压力拥有了一个回放感知的统一所有者,压缩与未来插件共享同一核算;
- meter 是零配置组合项:部署时在各自路由所属适配器上配置容量(含图片定价),在 compaction-basic 上配置可选策略覆盖;
- 固定启发式只是提供方行为的估计,不是精确分词器或请求序列化器——尤其注意 CJK 文本与 JSON schema 在每 token 四字符下会明显低估(见 token-meter/README.md 的 Dev Note);
- 每次计量都复制带位置信息的表层,成本 O(surface),低于阈值即结束的压力检查也不例外;
- 遇到畸形持久边界时计量明确失败,把损坏的回放转化为具名集成错误,而非压力静默漂移;
- 步骤后压力检查读取精确记录的路由、工具与前缀边界;对无成功 usage 锚点即被拒绝的请求,提供方溢出分类由适配器维护兜底。
对于想要进一步深入阅读的读者,推荐继续查看 token-meter 包文档(含tokenUsage、contextPressure、contextBreakdown三个 session 投影的语义)、Token meter 子系统文档 与 Compaction 子系统文档。这两个子系统页分别从计量语义与压缩消费方两个视角,与本文形成互补。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考