# TokenSqueezer 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件,用来降低 agent 的 运行成本。它作用于**模型自己产出的内容**,而不是喂给它的输入。 [English](README.md) | **中文** --- ## 先读这一节:到底什么在省 token 这个插件里住着两套**经济性完全不同**的机制,把它们混为一谈是失望的最快来源。 | | 机制 | 省的是 | 为什么 | |---|---|---|---| | **掐住生成** | `brevityInstruction` | **输出** | 模型自己少写,答案仍然完整 | | | `reasoningBrevityInstruction` | **输出** | 思考占模型全部产出的 ~87%,让它"走直线"是这里最大的杠杆 | | | `outputTokenCap` | **输出** | 调用**之前**经 `agent/request` 设下天花板。没生成的 token 不会计费 | | | `reasoningEffort` | **输出** | 钉住思考档位。只在模型广告了该档位时才应用 | | **压缩** | `llm/stream` 改写 | 只省重放 | chunk 到手时,那些 token 已经生成、已经付费 | | | `session/event` 兜底 | 只省重放 | 同上 | | | `answer` / `reasoning` / 各类折叠 | 只省重放 | 同上 | **压缩不能让当前这次回答变便宜** —— 那笔钱已经花掉了。它做的是把上下文压短,让这个回合里 **此后每一次**请求少带一些字节。这种节省会复利,但在发生的那一刻你几乎看不见。 插件**从不改动 `usage`**,所以上报的 token 数字始终是真实的。 --- ## 安装 ```powershell # 1. 把仓库放到一个固定位置,再以 link: 方式装进 profile # (profile 自己管理 node_modules;link: 安装让插件保持可编辑) cd <你的 DSH profile 目录> pnpm add link:<本仓库路径> # 2. 启用。bundle 自带 cordis.patch.yml,会自动插入那一行。 # 在 \profiles\\cordis.patch.yml: - id: tokensqueezer disabled: false ``` **必须重启应用。** 两个半边的重载机制不同,这个不对称值得先知道,否则会白白耗掉一个下午: | | 加载方式 | 运行中改它 | |---|---|---| | **Host 半边**(`index.js`、`lib/`) | loader entry,**应用启动时读一次** | **完全无效**,必须完整重启 | | **Client 半边**(`client.js`) | 模块产物的 revision 由 `mtimeMs`/`ctimeMs`/size 推导 | 立即热重载 | 所以"**界面更新了但行为没变**"是 Host 陈旧的典型症状。`status` 会打印 Host 实际加载的那个文件, 于是这个问题从猜测变成一次比较: ``` Host code: \index.js @ 2026-09-30T04:26:12.732Z. ``` --- ## 工作原理 ### Host 半边 —— 五个表面,各自独立安装 | 表面 | 挂钩 | 作用 | |---|---|---| | 流改写 | `llm/stream`(waterfall) | 把每个 `text`/`reasoning` 块缓冲到 `block-end`,压缩后重新发出。这是唯一在**显示之前**生效的一条腿;由于 loop 是用它收到的 chunk 构造 `assistant/message` 的,它也决定了落库的内容 | | 落库兜底 | `session/event` | 用可重放的 `compaction/prune` + `surfaceOp: replace` 协议,替换掉那些从别的路径进了日志的超预算 `assistant/message`。正常路径上是 no-op | | 输出预算 | `agent/request`(waterfall) | "掐住生成"那一半:在 `prepareCall` 之前设 `maxTokens`、按需钉住 `reasoningEffort` | | 工具结果 | `tools/post-execute`(waterfall) | 在模型读到之前压缩工具结果 —— 受下面的代码保证约束 | | 状态路由 | `webServer.register` | 一条**只读**的 `GET /api/tokensqueezer/status`,让界面能显示一个数字。只答 GET,其余一律 `405`。payload 只有插件自己的计数:无会话内容、无路径、无凭据 | 每一个都是**独立的安装步骤**,所以某个失败会在 `status` 的 `Installed: …` 里报出自己的名字, 而不会把另外四个一起带走。 ### Client 半边 —— 两个座位 - `conversation.composer.dock` —— 输入框下方的读数。每 3 秒轮询一次状态路由并显示本会话的节省量。 拿不到数字时它**如实说明**,而不是显示一个占位符。 - `settings.section` —— 设置页,用官方 primitives 搭成(`Tag`、`Switch`、`SegmentedControl`、 `Input`、`CodeBlock`、`Button`),按"每一项到底省什么"分组,并通过官方 locale 服务本地化。 ### 管线 `lib/engine/output.js` 先把载荷切成"围栏代码段"和"散文段",所以**没有任何代码块会进入语义层**。 其余内容依次经过: 1. **L1 无损** —— ANSI 与控制字符、`\r` 重绘、行尾空白、空行折叠、重复行/块折叠 2. **L2 归一化** —— 按会话 cwd 做路径相对化、可选的符号驻留。两者对可执行种类都跳过 3. **L3 语义** —— 每种检出类型一个压缩器:`json`、`yaml`、`diff`、`stacktrace`、`log`、 `terminal`、`testreport`、`filetree`、`searchresults`、`deptree`、`markdown`、`html`、`code`、 `prose`,另有在**回答尺度**上折叠的 `answer` 与 `reasoning` ### 可逆性 有损层删掉的一切都会按内容哈希(8 位 sha1)存入引用库,并替换成一枚裸标记 `⟦TS:id⟧`,载荷末尾附 一行图例。同一段文本永远得到同一个 id,所以对已压缩内容再压一次是 no-op。模型通过 `tokensqueezer` 工具的 `expand` 动作取回原文。 --- ## 代码保证 **结果是文件内容或补丁的工具,不做语义压缩。** 这条约束在三个地方落实,而有三个地方的原因值得记下来: > 第一版实现调用的是 `engine.compress(text, { lossy: false })`,以为这样就够了。用一段 40 行的 > 重复代码实测,它返回了**三行**。会伤代码的重复行折叠跑在**无损**阶段,所以"关掉有损层"发生在 > 伤害的下游。 因此: 1. **由工具名决定类型。** `toolContentKind()` 把 `read`/`read_file`/`view`/`cat`/`write`/`edit`/ `str_replace` 钉成 `code`,把 `diff`/`patch` 钉成 `diff`。这里用启发式是错的工具:一个被 `detectKind` 恰好读成 `log` 的源码文件,会被折掉重复行。 2. **这些种类根本不进管线**,只走 `losslessCleanup`。`EXECUTABLE_KINDS` **不是**这道防线 —— 它只 保护 L2,而 `compressCode`/`compressDiff` 照样会折代码体。 3. **最终文本必须更短。** 如果连同标记和图例都不比原文短,整条改动被原样丢弃。这正是"统一压缩" 之所以安全而不是自伤的原因:一枚 13 字符的标记加上约 100 字符的图例,否则会把小载荷撑大。 在出货的引擎上实测: ``` read_file,40 行重复代码 1079 → 1079 40/40 行存活 patch,30 条新增行 1231 → 1231 30/30 行存活 含 ANSI 与空行的代码 1122 → 1111 41/41 行存活,转义已剥离 ``` --- ## 配置 每一项都有默认值,什么都不配也能跑。各门槛默认为 **`0`**,所以普通载荷总会进入压缩器 —— 是否保留 结果由上面那条"必须更短"的规则决定。 | 键 | 默认 | 含义 | |---|---|---| | `compressOutput` | `true` | 输出压缩总开关 | | `compressStreamOutput` | `true` | `llm/stream` 那条腿(唯一在显示前生效的) | | `compressDurableOutput` | `true` | `session/event` 落库兜底 | | `compressReasoning` | `true` | 折叠思考块。设 `false` 会在任何压缩器之前短路 | | `compressToolResults` | `true` | 压缩工具结果,受代码保证约束 | | `outputMinChars` | `0` | 可见 `text` 块的门槛 | | `reasoningMinChars` | `0` | 思考块的门槛 | | `toolMinChars` | `0` | 工具结果的门槛 | | `minSavedChars` | `0` | "值不值"门槛。因为有"必须更短"那条规则,`0` 是安全的 | | `segmentMinChars` | `200` | 回答被代码切分后,单个散文段的门槛 | | `reasoningElideMinChars` | `0` | 压缩器内部的思考门槛 | | `reasoningFoldMinChars` | `0` | 压缩器内部的思考折叠门槛 | | `answerElideMinChars` | `0` | 压缩器内部的回答门槛 | | `answerFoldFloor` | `0` | 压缩器内部的回答折叠门槛 | | `outputTokenCap` | 未设 | 生成 token 的硬上限。设太低会截断答案(`finish: max-tokens`)。永远不会把已有的更严上限放宽 | | `reasoningEffort` | 未设 | 强制思考档位。属质量取舍;只在模型广告了该值时应用 | | `brevityInstruction` | `true` | 要求回答简洁。这是插件对文风做的唯一改动 | | `reasoningBrevityInstruction` | `true` | 要求思考简洁 | | `level` | `1` | `0` 只无损,`1` 常规,`2` 激进 | | `relativizePaths` | `true` | 相对化重复的绝对路径。**不作用于代码与 diff** | | `internSymbols` | `false` | 驻留长重复标识符。**不作用于代码与 diff** | | `protectCodeFences` | `true` | 让围栏代码留在语义层之外 | | `toolSkip` | `[]` | 按工具名跳过(这些工具的结果永不压缩) | | `archMaxChars` / `archMaxDepth` | `1200` / `3` | 架构摘要的预算与深度 | `outputTokenCap` 与 `reasoningEffort` 作用于**每一个** agent(**包括 subagent**)—— 这是刻意的, 免得一次委派悄悄超出给它的预算。 --- ## 工具 插件自带验证 —— 因为出问题的从来不是引擎那一层: ```powershell $node = '' & $node scripts\linkcheck.mjs # 每个相对 import 都解析到真实导出 & $node scripts\selftest.mjs # 引擎 9 类样本:122537 → 32812 字符(73.2%) & $node scripts\hosttest.mjs # Host 半边,假 ctx + 和真 session 一样严的假 session & $node scripts\clienttest.mjs # Client 半边,假 loader + 真渲染 & $node scripts\reasontest.mjs # 骨架折叠:骨架保留、引用可逆 & $node scripts\archtest.mjs # 架构摘要 ``` 另有三个诊断脚本,专门回答界面回答不了的问题: | 脚本 | 回答什么 | |---|---| | `scripts/audit.mjs` | 插件**真的**改写已提交的输出了吗?它直接读持久会话日志,逐会话数有多少条 `assistant/message` 带标记 —— 并把回答与思考分开统计。界面是二手视图,这个才是日志 | | `scripts/sessionrepair.mjs` | 哪些行会被 v4 准入拒绝,并提供修复其中两种形状的能力。**默认只预览**,`--write` 才落盘且先备份。它的第二条规则**只在 v4 日志生效**,所以不会去动别的项目里的 v3 会话 | | `scripts/asar-find.mjs` · `asar-extract.mjs` | 出货的 Harness 到底怎么做的。两者只读应用归档的头部再提取指定条目 —— 下面那些契约就是这么**读出来**而不是猜出来的 | `hosttest.mjs` 用的假 session 会强制真规则 —— `assistant/message` 的替换不得带 `sourceEventSeqs`、 必须带 `turn`/`step` 且该 step 仍开着、`surfaceOp` 必须恰好是 `{op, startSeq, endSeq}`。 --- ## 平台约束 下面每一条都是撞出来的,而且每条至少花掉一次重启。它们是 Harness 的性质,不是这个插件的问题。 - **装在 profile 里的 bundle 不能 import `@deepseek-ai/*`。** 那些包躺在应用归档里,Node 的解析 永远走不到,整个 entry 只报一句 `failed to import`。所以 Host 半边是**函数式插件**、不 import 任何框架代码,需要的一点点帮助函数就地内联。 - **cordis 只在上下文 `inject` 了某服务时才让它可解析。** 未声明的服务不只是"取不到"——**以属性 方式读它会抛异常**。这个抛异常在本插件里静默地干掉过一整个安装步骤。`inject` 是一道门,两个半边 都声明了自己需要的(`['tools', 'webServer']` 与 `['slots', 'locale', 'configForms']`)。 - **插件的 `Config` 是两套协议。** cordis 的激活走 **Standard Schema** (`Config['~standard'].validate`,必须同步);DSH 的设置投影要 **schemastery 商标** (`Symbol.for('schemastery')`、`type`、`meta`)。只满足后者时,inspect provider 会报一个健康的 `status: "schema"`,而 entry 实际是 `fiberPhase: "failed"`。**inspect provider 是只读投影, 不是验证器。** - **要让投影出现在客户端,key 必须编译期声明合并进 `SessionProjectionMap`**,并提供 `wire` 编解码器;自建 typert Remote 命名空间则需要代码生成。两者纯 JS 插件都拿不到 —— 所以界面改读 一条只读 HTTP 路由。 - **`SessionReference` 描述的是行为,不是内容 API。** 它有分页、最小条数与生命周期语义,但**没有** 读取已加载消息的公开方法。 - **`agent/request`、`llm/stream`、`tools/post-execute` 都是 waterfall**,可以合法地替换自己的返回 值。而 `agent/assistant-stream` 是 `emit`,永远只能观察、改不了。 --- ## 诚实的局限 - **短内容压不动,而且本就不该压。** 门槛全为 0 之后压缩器照样会被调用,它只是**找不到可删的东西**。 一段 46 字符的回答没有可折的中段,一个 11 字符的工具结果没有冗余。再往下压就不是节省,而是删信息。 - **真实会话上的实测节省是"有限"的** —— 够格的块里省个位数到十几个百分点,而**大约一半的块根本没有冗余**。 大数字在"掐住生成"那几个旋钮里,其中两个(`outputTokenCap`、`reasoningEffort`)默认关闭,因为它们 会改变模型产出。 - **设置页的写入路径尚未端到端验证。** 控件调用的是设置服务的 `set(key, value)` 并把结果(含失败 原文)显示出来,但那个签名是**从文档读来的**,没有在真实 profile 上跑通过。 - **dock 读数依赖状态路由。** 路由不可达时它会明说,绝不显示一个它没有的数字。 - **引用库按会话隔离且有上限。** `expand` 只能解析产生该标记的那个会话;LRU 保持 32 个会话常驻。 原文始终留在持久日志里。 --- ## 目录结构 ``` index.js Host 半边:五个表面、工具、提示词段、状态路由 client.js Client 半边:输入框读数与设置页 cordis.patch.yml bundle 补丁:插入 tokensqueezer 行 package.json bundle 与客户端声明 lib/config.js 插件 Config(Standard Schema + schemastery 商标) lib/session-message.js 仍在依赖的 v4 producer-source 规则 lib/arch.js 架构摘要 lib/engine/ compress.js 编排器 output.js 围栏切分、代码保护、回答尺度接线 detect.js 内容类型识别 textsafe.js L1 无损 refs.js 内容寻址引用库 dedupe.js 行/块折叠 normalize.js 路径与符号归一化 tokens.js token 估算 kinds/ 每种内容类型一个压缩器 locale/ i18n:en.json、zh.json scripts/ 六个套件 + 三个诊断脚本 ``` ## 许可 MIT。