本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
模块:T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座
来源:xueai.miyang.cn(小山学堂 · 洛小山)
本集为两节合辑(素材 8006 字),已做取舍。
统一主线:两节都在讲同一件事 —— 不同消费方要用不同的数,别让一个口径伺候所有人。
| 节 | 决策不信什么 | 决策信什么 |
|---|---|---|
| 压缩双路径 | 插件的返回值 | 账本上的世代号 |
| Token 计量 | 界面上的百分比 | 自己在边界上现算的数 |
上下文快满了就主动收拾,真撑爆了就先收拾再重试。重试前先对一遍世代号,收拾没起效就不许重试。
决策用重放(measure()),展示用投影(projectedTokens)。
| pressure(预防) | context-overflow(兜底) | |
|---|---|---|
| 挂载点 | agent/pre-step,每个 Step 开始前 | agent/request-error,请求报错之后 |
| 触发条件 | measure().totalTokens ≥ thresholdTokens | 错误码 恰为 CONTEXT_WINDOW_EXCEEDED |
| 阈值 | 容量的 0.8 | 不看阈值,保留预算直接清零 |
| 保留尾巴 | 给最近对话留 16% 原文 | 强制做一次真实缩减 |
| 失败语义 | 松 —— 记一句日志就继续走 | 严 —— 必须给出决定(重试 or 保留原始错误) |
| 重试上限 | — | 默认 1 次,每条成功回复清零 |
出处:compaction-basic/src/index.ts L147–165、L179–223;config.ts L20、L23、L93(均可按 provider+model 组合覆盖)。
generation = surface.replaceGeneration;插件说什么不重要,账本上的数字变没变才重要。
出处:index.ts L191、L218–222;surface.ts L136–142(定义)、L361–371(全库唯一加一处)。
重放 measure()(决策用) | 投影 projectedTokens(展示用) | |
|---|---|---|
| 回答什么 | 此刻发请求会有多大 | 给用户看个占用率 |
| 要求 | 必须准,可以贵 | 必须便宜、持久、重连后立即可用 |
| 代价 | 每次 O(surface) | 普通持久投影,便宜 |
| 谁用 | 只有压缩这类决策方,在自己的请求边界调 | UI 状态行 |
只做主动阈值:每次请求前量一下,超过八成就收拾。听起来够了,实际不够 —— token 数是估出来的,估算和 provider 真实计数总有出入。
一条超大的工具结果突然塞进来 → 测量还没到阈值,请求已经超限,provider 直接拒绝 → 没有任何补救逻辑接手,turn 就地报错终止,用户看到的是一次莫名其妙的失败。
一次算错就直接撞墙,没有第二道防线。
上排是预防,下排是兜底。 两条路各自独立,一条失效了另一条照常工作。
这是可靠性工程的通则:备份和恢复是两套系统,限流和熔断是两道闸门。预防路径追求便宜、常跑、失败无所谓;兜底路径追求可靠、少跑、失败必须有交代。
compaction 是一个开放接缝,第三方可以接自定义后端。假设某个后端每次都报告成功,但从来没真正改过模型可见的内容:
只看返回值就重试 → 请求原样超限 → 再报错 → 再压缩 → 再重试 —— 每一圈都是白花的 API 钱,死循环烧到天亮。
surface 是会话日志里模型可见事件的实时投影(模型眼里的那份对话)。replaceGeneration 是它身上的一个只读计数器,记这份对话被替换过几次。
整个代码库只有一处会让它加一:一段旧消息真的被摘要替换、真的落盘的那一刻。没有任何 API 能把它改小或重置。
世代号前进了 ⟺ 至少发生过一次真实的、已落盘的替换(数学等价)。
就算收拾中途抛了异常,只要前面的免费剪枝已落盘、世代号已前进,这份进展照样够格授权重试。
它不看过程是否顺利,只看账本上有没有真实进展。异常也算,只要证据在;反过来,过程顺利但账本没动,也不放行。两边用的是同一条标准 —— 凭证据。
出处:index.ts L195–208。
设重试上限为 1,装一个自定义后端:每次都报告成功,但从不真正替换。第一次溢出报错发生了。
用单调递增的版本号证明状态确实变了 —— 数据库乐观锁用了几十年,Git commit 链、分布式系统的 epoch 都是它的变体。
把信任问题变成算术问题:执行者可以撒谎,账本不会。 只要系统里存在不受信任的扩展点,重试之前核对一个改不了的计数器,永远是最便宜的防线。
| 主动路径 | 防循环烧钱 | |
|---|---|---|
| DSH | pressure(0.8)+ overflow 兜底 | 世代号对账,0 次无效重试 |
| Grok Build | 主动 85% 触发,另有默认关闭的 two-pass 预摘要 | 已核对材料中未见等价机制(保留未知项) |
| Claude Code | 最厚:裁剪 / 微压缩 / 折叠 / 全量摘要四道工序 | 计数熔断:连续失败 3 次停手 |
3 来自真实事故:源码注释记载曾有 1279 个 session 连续失败 50 次以上,全球每天浪费约 25 万次 API 调用。出处:study/chapters/03-context-management.md L78–81、L121–124。
| 类型 | 止损线来源 | |
|---|---|---|
| Claude Code | 计数器止损 —— 先允许问题发生几次,再靠上限兜住 | 拿事故数据校准 |
| DSH | 结构性证明 —— 让无效重试从机制上发不出去 | 推导出来的 0 |
前者的3需要事故喂出来;后者的0是推导出来的。
决策用的数要准,可以贵。展示用的数要便宜,可以糙。
measure()每次调用把持久日志的当前尾部折叠成一份不可变快照:
代价:每次 O(surface) → 只有压缩这样的决策方在自己的请求边界调它。
projectedTokens普通持久会话投影状态,只有两个各自后者胜的字段:
| 字段 | 含义 | 出处 |
|---|---|---|
pressureTokens | 最近一次请求报告的提示词侧规模(输入 + 缓存读写,不含输出) | usage-projection.ts L70–72 |
contextWindow | 来自最新一条 request/context 日志记录 | 同文件 |
分子分母各写各的,从不凑成一次原子观测。
光有 pressureTokens 有个尴尬:它只在请求报 usage 时更新,Turn 流式期间一动不动,更看不见压缩 —— 压缩替换了一大段 surface,状态行上的数却纹丝不动,用户会以为压缩没干活。
所以 fold 顺手带一份 surface 的运行总量,公布的是样本加上此后 surface 的有符号变动。
源码注释写得很直白:occupancy answers for the next request rather than the last one(占用率回答下一次请求的大小,而不是上一次)。
效果:压缩刚落盘、一个新请求都没发,投影已经掉下来了。
时序细节:usage 样本在同一事件加入 surface 之前盖章(stamped BEFORE),所以 assistant/message 锚定的是它自己那次请求看到的 surface,增量的起点不会错位。
出处:usage-projection.ts L150–161。
view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
...contextWindow === undefined ? {} : { contextWindow },
...pressureTokens === undefined ? {} : { pressureTokens },
...pressureTokens === undefined || sampledSurfaceTokens === undefined
? {}
: { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },
题眼是第三个展开项:pressureTokens + surfaceTokens - sampledSurfaceTokens —— 样本加上取样之后 surface 的净变动,再用 Math.max(0, …) 兜住下界。两个来源字段缺一个,对应的输出干脆不出现。
出处:usage-projection.ts L198–204。
pressureTokens 一直缺席 → 第二、三个展开项都不出现 → UI 干脆不显示占用率(Agent Note 明确:只有压力与容量都已知时才显示占用率);measure() 退化为 estimated 启发式锚,照常给数。换模型时,新 contextWindow 立刻生效,pressureTokens 还是上一个路由的旧样本 → 占用率此刻是近似值,直到下一个请求报 usage。
文档明说这是取舍,不当 bug 处理。辩护词原文(设计笔记 L35):
*确实需要同一边界精确数字的消费方,应在自己的请求边界调用 ctx.tokenMeter.measure(),那里两个值同时可得,而不是读取该投影。*
pressureTokens 只算提示词侧(inputTokens + 缓存读写,不含输出)—— 描述的是发出去的请求有多大,和计费总量 tokenUsage 是两个投影单元,别混。
⚠️ 最要紧的一句:harness 中没有任何环节依据占用率百分比做决策,压缩直接读取 measure()。UI 上那个数再漂亮,也进不了决策函数的参数表。
| 口径 | 特点 | |
|---|---|---|
| Grok Build | 一个数走天下 —— 专门开纯函数 crate 当唯一口径 | 决策与展示天生一致,永远不打架;代价是决策也只能用粗启发式(BYTES_PER_TOKEN = 4,图片一张记 765),provider 真实 usage 不参与占用率计算 |
| Claude Code | 展示口径自己也要防坑 | 子 Agent 进度的 input_tokens 是逐轮累计(只留最新)、output_tokens 是增量(累加)—— 直接相加会把 input 重复计好几遍;压缩阈值用缓冲区常量(autocompact 预留 13000、手动 /compact 预留 3000) |
| DSH | 彻底分开 | 决策读 measure(),展示读投影,并在文档里承认展示那份就是近似值 |
出处:xai-token-estimation/src/lib.rs L3–19;study/chapters/06-task-system.md L169–181。
三家其实答了同一道题:占用率这个数给谁用、错了谁买单。 Grok 选了永远一致的粗数;Claude Code 给展示计数打了防重复的补丁;DSH 把两个消费方彻底分开,然后在文档里承认展示那份就是近似值。
measure(),拿到的总量大约是多少、锚点是哪一类。最后用设计笔记 L35 那句原文,解释这两组数为什么允许不相等、真需要精确值的消费方该怎么办。measure() 而非投影?pressureTokens(不含输出)与 tokenUsage(计费总量)有没有被混用?measure()。quizFiles)一律不进入口播稿,仅作为集页下方的文字自测卡渲染。撞墙了被动救,预防和兜底各走各的触发器。 溢出重试的凭证是 replaceGeneration 的单调前进,插件的返回值不算数 —— 要证据,别信口头汇报。
决策用重放,measure() 在自己的边界现算,准而贵;展示用投影,pressureTokens 加 surface 净变动,糙而便宜持久。 两个数可以不一样,占用率的非原子性是写进文档的设计决策。看到 UI 上的百分比,记住没有任何决策读它。
*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*