Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
7.8 KiB
Agent Note: 回放式 token 计量服务
Status: implemented
English | 中文
问题
上下文压力并不只对压缩(compaction)有用。压缩后端、溢出保护或未来的请求策略插件都可能需要回答同一个问题:持久请求消耗了多少 token?如果把该折叠逻辑留在 dsh-compaction-basic 内部,就会重复实现回放逻辑,使未加载压缩的调用方无法使用计量,并诱使调用方复用陈旧的核算结果。
提供方 usage 也不是完整答案。它只描述某个精确请求信封下的一次成功调用,而当前表层之后还可能增长、缩小或被替换。会话也可能切换提供方与模型,旧日志可能缺少构成 assistant 消息的分片 seq,usage 字段还会分别报告输入、缓存读取、缓存写入、输出与推理计数。因此,可用的服务必须把最新精确锚点与保守的启发式重新定价结合起来,并公开每个结果已经消费的日志修订号。
决策
一个具体的 LLM(大语言模型)家族服务
@deepseek-ai/dsh-token-meter 是 packages/llm/ 下的单个具体包,并注册 ctx.tokenMeter。在第二种实现出现之前,它不会被拆成接口与后端。TokenMeter 本身公开 measure(session, requestHeader?) 与 estimateMessage(message);消费方直接调用这个单例服务。
服务没有配置。估算采用固定的每 token 四个字符启发式规则,并加上结构开销。服务不提供模型 profile、容量设置、密度设置、分词器后端或语言专用策略。对精确提供方/模型容量的查询由适配器单独负责,具体见路由模型上下文与压缩策略 Agent Note。
逐会话回放折叠
每个会话都有一个隔离的增量折叠。活跃折叠通过 session/event 前进;每次读取都会追到持久日志尾部,因此监听器顺序、种子会话与服务重载不会改变答案。折叠跟踪规范的完整请求头快照、步骤边界、表层追加与替换、assistant usage,以及每条 assistant 消息引用的分片 seq。下一个畸形事件会以事务方式失败并保持未读,不会让状态只修改一半。
measure(session, requestHeader?) 只同步一次折叠,并在返回标量压力的同时给出逐位置节点价格。totalTokens 仍表示请求与响应压力;surfaceTokens 是仅针对表层的启发式总量,并等于 nodes[].tokens 之和。requestHeader 覆盖只改变压力定价,表层字段始终描述当前会话。estimateMessage(message) 不依赖会话状态,直接应用固定启发式规则。每个结果都是一个分离且深度不可变的快照,只携带一个 logRevision。每次计量都会复制当前节点,因此成本为 O(surface)。
只有当待计量的规范请求信封等于最近一次成功调用的锚点时,服务才复用提供方 usage。提供方、模型、系统提示词、前缀、工具或调用配置任一变化都会触发完整的启发式重新定价。表层变化相对匹配锚点保留有符号增量,包括缩小替换后的负值。后续成功请求会替换先前锚点,提供方或模型切换时也一样。
Usage 会对互不重叠的输入、缓存读取、缓存写入与输出 bucket 求和,不会再次加入推理计数。每次成功模型调用都会记录 assistant/message,包括无内容调用与达到 token 上限的调用,并带上精确的更早分片 seq。显式的空 sourceEventSeqs 列表表示已知为空的提供方流;旧日志中缺失的列表则保守地把持久 assistant 输出视为提供方输出。
compaction-basic 消费计量,但不拥有计量
dsh-compaction-basic 要求 ctx.tokenMeter;CompactionEngine 不增加 token 方法或类型。配置、区域事务与摘要分别保留在独立模块中,服务自身注册自动监听器,而 summarize() 仍是唯一的子类钩子。单例计量器一致用于压力、保留、被遮蔽内容、引用的源事件以及非缩小摘要拒绝的定价。
自动压缩的每次阈值与保留联合决策只使用一次统一计量。区域事务会在追加持久 compaction/start 锁后执行计量,在异步摘要完成后再次计量,随后比较分离的表层节点向量。期间发生的表层变更会阻止替换;logRevision 可以因无关的纯日志事实而推进,而不会使未变的选定范围失效。
压缩策略采用服务级默认值:阈值比例 0.8、保留尾部比例 0.16、summarizationProvider: ''、summarizationModel: ''、maxTokens: 8192、compactionRetries: 1、maxOverflowRetries: 1 与 auto: true。顶层字段适用于每个路由目标;modelPolicies 中的精确提供方/模型项可以部分覆盖这些字段。压力检查以所属适配器解析的容量为基准换算这些比例,retainTokens 可以替代 retainRatio;保留值必须小于最终阈值。摘要提供方与模型必须同时设置或同时为空;空组合先解析最近记录的请求目标,再使用 AgentOptions 中的组合。
自动压力检查在请求派生前运行于 agent/pre-step,并计量前一个 agent/request 实际所选提供方/模型产生的规范持久信封。没有请求头的会话尚无已完成的路由请求可供判断,因此不执行工作;任意路由目标都可使用这个单例估算器。规范的溢出恢复流程使用同一计量结果强制选择范围,并且只有在表层替换得到证明后才重试。
测试
单元测试覆盖固定估算、信封失效与锚点替换、回放边界、不可变快照、已路由压力、收敛、溢出 generation 证明与回滚。真实 Loader/Include fixture(测试前置数据)验证零配置 token-meter 与 compaction-basic 按依赖顺序加载的路径。
考虑过的替代方案
- 把估算保留在
CompactionEngine内——不予采纳,因为计量拥有独立于压缩的消费方与回放语义;它还会强迫每个压缩器暴露同一套无关 API。 - 立即把 token meter 拆成接口与启发式后端——不予采纳,因为目前只有一种实现。单个具体服务保留未来的 seam,同时避免推测性的包与配置。
- 把模型键控窗口与密度 profile 放进 meter——不予采纳,因为回放估算不拥有模型路由或容量事实。路由所属适配器公开容量,compaction-basic 则拥有消费方专用的阈值与保留策略。
- 保留独立的标量与表层计量——不予采纳,因为消费方必须为一次决策执行两次读取并匹配修订号。仅读取标量可以避免在低于阈值时复制节点,但拆分 API 会在消费方引入竞态窗口;统一快照接受 O(surface) 复制成本,以换取结果一致性。
- 在不同信封之间移用提供方 usage——不予采纳,因为模型、工具、前缀与调用配置都是请求事实。不匹配时会重新定价完整当前请求。
后果
- Token 压力拥有一个可供压缩与未来插件共享的回放感知所有者。
- 默认值让 meter 成为零配置组合项;部署时在各个路由所属适配器上配置容量,并在 compaction-basic 上配置可选策略覆盖。
- 固定启发式定价仍然只是提供方行为的估计,并不是精确分词器或请求序列化器。
- 每次计量都会复制当前带位置信息的表层,因此成本为 O(surface),低于阈值即可结束的压力检查也不例外。
- 遇到畸形持久边界时,计量会明确失败。这会把损坏的回放转化为具名集成错误,而不是让压力静默漂移。
- 步骤后压力检查读取精确记录的路由、工具与前缀边界;对于在成功 usage 锚点出现前就被拒绝的请求,提供方溢出分类仍是由适配器维护的兜底路径。