deepseek-harness/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md

58 lines
6.6 KiB
Markdown
Raw Normal View History

# RFC: 重放式 token 计量服务
Status: implemented
[English](2026-07-15-replay-token-meter-service.md) | 中文
## 问题
上下文压力并不只对压缩有用。压缩后端、溢出保护或未来的请求策略插件都可能需要回答同一个问题:持久请求占用了某个模型多少上下文窗口?如果把该折叠逻辑留在 `dsh-compact-basic` 内部,就会重复实现重放逻辑,使未加载压缩的调用方无法使用计量,并诱使调用方错误复用其他模型的核算结果。
提供方 usage 也不是完整答案。它只描述某个精确请求信封下的一次成功调用,而当前表层之后还可能增长、缩小或被替换。会话也可能切换模型,旧日志可能缺少 chunk 来源,提供方字段还会分别报告输入、缓存读取、缓存写入、输出与推理计数。因此,可用的服务必须把精确锚点与保守的逐模型重新定价结合起来,并公开每个结果已经消费的日志修订号。
## 决策
### 一个具体的 LLM 家族服务
`@deepseek-ai/dsh-token-meter` 是 `packages/llm/` 下的单个具体 package,并注册 `ctx.tokenMeter`。在第二种实现出现之前,它不会被拆成接口与后端。公开入口把精确模型名解析为稳定的 `ModelTokenMeter`;未知名称抛出带 `TOKEN_METER_MODEL_UNCONFIGURED` 的 `TokenMeterError`,而不是继承通用窗口。
内置的 `deepseek-v4-flash` 与 `deepseek-v4-pro` profile 都采用 128,000 token 上下文窗口,以及每 token 四个字符的估算密度。`models` 覆盖按字段合并。自定义名称必须提供 `contextWindow`,而 `charsPerToken` 默认为四。直接构造会报告类型化 profile 错误;Loader 挂载则先应用 package 的 Schemastery 形状校验。
### 绑定模型的重放折叠
每个模型/会话对都有隔离的增量折叠。活跃折叠通过 `session/event` 前进;每次读取都会追到持久日志尾部,因此监听器顺序、种子会话与服务重载不会改变答案。折叠跟踪规范请求头及其增量、步骤边界、表层追加与替换、assistant usage,以及 assistant chunk 来源。下一个畸形事件会以事务方式失败并保持未读,不会让状态只修改一半。
`measure(session, requestHeader?)` 返回标量压力。`measureSurface(session)` 返回用于保留与替换决策的逐位置节点价格。`estimateMessage(message)` 不依赖会话状态,直接应用该 handle 的 profile。结果是分离且深度不可变的快照,并携带 `logRevision`;消费者在一次联合决策前比较标量与表层修订号。
只有当 handle 的模型与规范请求信封都等于成功调用锚点时,服务才复用提供方 usage。系统提示词、前缀、工具或调用配置任一变化都会在请求模型下重新定价完整当前请求。表层变化相对匹配锚点保留有符号增量,包括缩小替换后的负值。其他模型的成功调用会改变共享表层,但绝不会覆盖当前模型的锚点。
Usage 会对互不重叠的输入、缓存读取、缓存写入与输出 bucket 求和,不会再次加入推理计数。每次成功模型调用都会记录 `assistant/message`,包括无内容调用与达到 token 上限的调用,并带上精确的更早 chunk seq。显式空来源列表表示已知为空的提供方流;旧日志中缺失的来源则保守地把持久 assistant 输出视为提供方输出。
### compact-basic 消费计量,但不拥有计量
`dsh-compact-basic` 要求 `ctx.tokenMeter`;`CompactService` 不增加 token 方法或类型。后端拆分为配置、自动触发、区域事务与摘要器模块,而 `summarize()` 仍是唯一的子类 hook。会话模型的 meter 一致用于压力、保留、被遮蔽内容、来源以及非缩小摘要拒绝。
每个已计量模型都会获得默认压缩策略:阈值比例 `0.8`、保留尾部 `floor(contextWindow × 0.16)`、摘要模型 `''`、摘要最大输出 `8192`、一次额外压缩尝试,以及启用自动触发。逐模型压缩覆盖按字段合并 `thresholdRatio` 与 `retainTokens`;保留值必须小于最终阈值。空摘要模型先解析最近记录的实际路由模型,再使用 `AgentOptions.model`。
pre-step 触发器计量临时请求信封:当前提示词与前缀覆盖日志值,最近记录的请求头提供模型、工具及其他调用配置。没有模型的纯路由 agent 会跳过该临时检查,因为 `agent/request` 仍可稍后路由;显式命名未知模型仍然报错。
## 测试
单元覆盖固定 profile、按字段覆盖、自定义与未知模型、信封失效、模型切换、有无 usage 的路径、种子追加/替换重放、有符号增量、来源模式、畸形边界、不可变快照、监听器顺序、重载、压缩默认值、路由回退、保留、收敛与事务回滚。真实 Loader/Include YAML fixture 按依赖顺序加载精确的零配置 token-meter 与 compact-basic package 名称。
## 考虑过的替代方案
- **把估算保留在 `CompactService` 内**——不予采纳,因为计量拥有独立于压缩的消费者与重放语义;它还会强迫每个压缩器暴露同一套无关 API。
- **立即把 token meter 拆成接口与启发式后端**——不予采纳,因为目前只有一种实现。单个具体服务保留未来接缝,同时避免推测性的 package 与配置。
- **给未知模型提供 128,000 token 回退**——不予采纳,因为看似合理但错误的容量会在错误时点触发破坏性策略。未知路由名称会携带精确名称失败。
- **在每个标量结果中复制完整历史**——不予采纳,因为低于阈值的读取很常见。不可变且带修订号的标量与独立表层快照,在不进行 O(history) 复制的情况下保持一致性。
- **在模型或信封之间移用提供方 usage**——不予采纳,因为分词、上下文容量、工具、前缀与调用配置都是模型/请求事实。不匹配时会重新定价完整当前请求。
## 后果
- Token 压力拥有一个可供压缩与未来插件共享的重放感知所有者。
- 默认值让内置 DeepSeek 组合只需两个零配置插件条目即可使用,而自定义模型必须声明唯一不能安全猜测的事实:上下文容量。
- 启发式密度与提供方 usage 仍然只是提供方行为的估计。随着模型演进,维护者必须更新内置 profile 与溢出措辞。
- 遇到畸形持久边界时,计量会明确失败。这会把损坏的重放转化为具名集成错误,而不是让压力静默漂移。
- pre-step 压缩集成可能跳过纯路由的首次检查,也可能错过请求中间件稍后应用的工具或路由变化。