Files
deepseek-harness/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
T
Tianyi Cui 7be78c7d4c Merge latest origin/master into compact-post-step-overflow-recovery
# Conflicts:
#	docs/agent-lifecycle.md
#	docs/architecture.md
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/rfc/INDEX.md
#	docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md
#	docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
#	docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md
#	docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
#	docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md
#	docs/rfc/implemented/feature/2026-07-07-session-prefix.md
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/src/config.ts
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/src/summarizer.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/compact/compact-basic/tests/compact-loop-repro.spec.ts
#	packages/compact/compact/README.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/cancel.spec.ts
#	packages/llm/llm-deepseek/src/adapter.ts
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/src/stream.ts
#	packages/llm/llm-pi-ai/tests/convert.spec.ts
#	packages/llm/llm/README.md
#	packages/llm/llm/src/index.ts
#	packages/llm/llm/tests/service.spec.ts
#	scripts/gen-doc-graphs.ts
2026-07-19 12:06:23 +08:00

7.2 KiB
Raw Blame History

RFC: 回放式 token 计量服务

Status: implemented

English | 中文

问题

上下文压力并不只对压缩有用。压缩后端、溢出保护或未来的请求策略插件都可能需要回答同一个问题:持久请求占用了已配置上下文窗口的多少容量?如果把该折叠逻辑留在 dsh-compact-basic 内部,就会重复实现回放逻辑,使未加载压缩的调用方无法使用计量,并诱使调用方复用陈旧的核算结果。

提供方 usage 也不是完整答案。它只描述某个精确请求信封下的一次成功调用,而当前表层之后还可能增长、缩小或被替换。会话也可能切换提供方与模型,旧日志可能缺少分片来源,usage 字段还会分别报告输入、缓存读取、缓存写入、输出与推理计数。因此,可用的服务必须把最新精确锚点与保守的启发式重新定价结合起来,并公开每个结果已经消费的日志修订号。

决策

一个具体的 LLM 家族服务

@deepseek-ai/dsh-token-meter 是 packages/llm/ 下的单个具体包,并注册 ctx.tokenMeter。在第二种实现出现之前,它不会被拆成接口与后端。TokenMeterService 本身公开 contextWindow、measure(session, requestHeader?) 与 estimateMessage(message);消费方直接调用这个单例服务。

服务只有一个 contextWindow,默认值为 128,000 token,并允许配置为正整数。估算采用固定的每 token 四个字符启发式规则,并加上结构开销。服务不提供模型 profile、密度设置、分词器后端或语言专用策略。

逐会话回放折叠

每个会话都有一个隔离的增量折叠。活跃折叠通过 session/event 前进;每次读取都会追到持久日志尾部,因此监听器顺序、种子会话与服务重载不会改变答案。折叠跟踪规范的完整请求头快照、步骤边界、表层追加与替换、assistant usage,以及 assistant 分片来源。下一个畸形事件会以事务方式失败并保持未读,不会让状态只修改一半。

measure(session, requestHeader?) 只同步一次折叠,并在返回标量压力的同时给出逐位置节点价格。totalTokens 仍表示请求与响应压力;surfaceTokens 是仅针对表层的启发式总量,并等于 nodes[].tokens 之和。requestHeader 覆盖只改变压力定价,表层字段始终描述当前会话。estimateMessage(message) 不依赖会话状态,直接应用固定启发式规则。每个结果都是一个分离且深度不可变的快照,只携带一个 logRevision。每次计量都会复制当前节点,因此成本为 O(surface)。

只有当待计量的规范请求信封等于最近一次成功调用的锚点时,服务才复用提供方 usage。提供方、模型、系统提示词、前缀、工具或调用配置任一变化都会触发完整的启发式重新定价。表层变化相对匹配锚点保留有符号增量,包括缩小替换后的负值。后续成功请求会替换先前锚点,提供方或模型切换时也一样。

Usage 会对互不重叠的输入、缓存读取、缓存写入与输出 bucket 求和,不会再次加入推理计数。每次成功模型调用都会记录 assistant/message,包括无内容调用与达到 token 上限的调用,并带上精确的更早分片 seq。显式空来源列表表示已知为空的提供方流;旧日志中缺失的来源则保守地把持久 assistant 输出视为提供方输出。

compact-basic 消费计量,但不拥有计量

dsh-compact-basic 要求 ctx.tokenMeter;CompactService 不增加 token 方法或类型。配置、区域事务与摘要器分别保留在独立模块中,服务自身注册自动监听器,而 summarize() 仍是唯一的子类 hook。单例计量器一致用于压力、保留、被遮蔽内容、来源以及非缩小摘要拒绝的定价。

自动压缩的每次阈值与保留联合决策只使用一次统一计量。区域事务先追加持久 compact/start 锁,再执行一次计量,并在异步摘要完成后再次计量;期间任何持久追加都会改变 logRevision,从而阻止替换。

压缩策略采用服务级默认值:阈值比例 0.8、保留尾部 floor(contextWindow × 0.16)、summarizationProvider: ''、summarizationModel: ''、maxTokens: 8192、compactionRetries: 1、maxOverflowRetries: 1 与 auto: true。顶层 thresholdRatio 与 retainTokens 覆盖压力策略;保留值必须小于最终阈值。摘要提供方与模型必须同时设置或同时为空;空组合先解析最近记录的请求目标,再使用 AgentOptions 中的组合。

自动压力检查运行在 agent/post-step,并计量 agent/request 实际所选提供方/模型产生的规范持久信封。没有请求头的会话尚无已完成的路由请求可供判断,因此不执行工作;任意路由目标都可使用这个单例估算器。规范化溢出恢复使用同一计量结果强制选择范围,并且只有在表层替换得到证明后才重试。

测试

单元测试覆盖固定估算、信封失效与锚点替换、回放边界、不可变快照、已路由压力、收敛、溢出 generation 证明与回滚。真实 Loader/Include fixture 验证零配置 token-meter 与 compact-basic 按依赖顺序加载的路径。

考虑过的替代方案

  • 把估算保留在 CompactService 内——不予采纳,因为计量拥有独立于压缩的消费方与回放语义;它还会强迫每个压缩器暴露同一套无关 API。
  • 立即把 token meter 拆成接口与启发式后端——不予采纳,因为目前只有一种实现。单个具体服务保留未来接缝,同时避免推测性的包与配置。
  • 保留模型键控的窗口与密度 profile——不予采纳,因为当前部署只有一种上下文策略与一个估算器。模型注册表、未知模型错误和可配置密度只增加分支,却没有第二种行为可供选择。
  • 保留独立的标量与表层计量——不予采纳,因为消费方必须为一次决策执行两次读取并匹配修订号。仅读取标量可以避免在低于阈值时复制节点,但拆分 API 会在消费方引入竞态窗口;统一快照接受 O(surface) 复制成本,以换取结果一致性。
  • 在不同信封之间移用提供方 usage——不予采纳,因为模型、工具、前缀与调用配置都是请求事实。不匹配时会重新定价完整当前请求。

后果

  • Token 压力拥有一个可供压缩与未来插件共享的回放感知所有者。
  • 默认值让内置组合只需两个零配置插件条目即可使用;部署需要时只覆盖一个上下文容量。
  • 固定启发式定价仍然只是提供方行为的估计,并不是精确分词器或请求序列化器。
  • 每次计量都会复制当前的位置表层,因此成本为 O(surface),低于阈值即可结束的压力检查也不例外。
  • 遇到畸形持久边界时,计量会明确失败。这会把损坏的回放转化为具名集成错误,而不是让压力静默漂移。
  • post-step 压力检查读取精确记录的路由、工具与前缀边界;对于在成功 usage 锚点出现前就被拒绝的请求,提供方溢出分类仍是由适配器维护的兜底路径。