toolshrink:按语义压缩 Agent 工具输出

前言

在 DeepSeek Harness(DSH)里,工具调用返回的大段文本会进入模型上下文。Harness 默认按体积截断:保留开头和结尾,中间丢弃。Codex、pi 等 Agent 宿主也常见类似做法。

这种截断不看内容形状。测试输出里 5,000 行通过、中间 3 条失败时,头尾截断往往只留下通过记录,模型会误判测试全部通过。unclecode 的 toolshrink 针对这类问题:先识别输出形态,再保留携带信息的部分,而不是单纯按位置切分。

这是什么

toolshrink 是一个 DSH 插件,也可作为 Node.js 库单独使用。维护者为 unclecode,仓库地址:github.com/unclecode/toolshrink。项目在 SkillHub 社区目录的分类为「模型推理」,当前 GitHub 星标约 10。

一句话定位:Cut large agent tool output by what it means, not by where it was cut.(按语义压缩,而非按截断位置。)插件内置 13 种内容感知 reducer;无匹配时回退到按尺寸的截断策略,保证结果始终落在预算内。

与默认截断的差异

README 给出一个 vitest 运行示例:输入 31,958 字符、805 行,预算 2,000 字符。

方式 输出大小 模型能读到什么
head+tail 截断 1,904 字符 主要是摘要
toolshrink 255 字符 哪条测试失败、原因、行号,以及摘要

被移除的内容会在输出末尾标注数量,例如 ... 15,903 characters, 401 lines omitted ...。若配置了 spill 存储,完整原文会写入磁盘并附带 locator,不会静默丢失。

核心功能:13 种 cut

每个 cut 识别一种文本形态,按顺序尝试;第一个匹配的 cut 执行。都不匹配时使用 size 回退。各 cut 遵循四条规则:不返回半行、不拆分 UTF-16 代理对、明确标注移除量、二次调用不再改变结果。

Cut 识别对象 保留 丢弃
diff git diff、patch 变更行、文件与 hunk 头、两侧各 1 行上下文 未变更上下文
json 单个 JSON 值 结构、长数组每数组 3 条样本、宽对象每对象 5 个键、计数 重复记录
tests vitest、jest、pytest、cargo test、go test 失败项及说明、摘要 通过的测试
build tsc、cargo、gcc、webpack、esbuild 错误与警告及代码帧、摘要 构建进度
stacktrace Node、Python、Java、Ruby 堆栈 消息与用户代码帧 依赖库帧(计数标注)
log 带时间戳的日志 错误与警告及前序行、结尾 常规行
tree find、ls -R、文件列表 目录结构、每目录 8 条、计数 拥挤目录的其余条目
repeat 重试风暴、进度刷屏 每种模式 2 条样本 + 省略说明 连续近重复行
lint eslint、ruff、clippy 每条规则的数量与示例位置、最差文件 同规则重复出现
install npm、pip、pnpm、cargo 安装 摘要、版本、弃用、漏洞、错误 下载进度
csv CSV、TSV、管道表格 表头、开头 5 行、末尾 2 行、行列计数 中间行
gitlog git log(两种格式) 最新 15 条提交、总数、作者及计数 更早提交
size 兜底 bash:末尾;grep/read:开头;未知:头尾 其余部分(计数标注)

新增 cut 只需一个实现共享接口的文件,无需 fork 整个项目。

安装与启用

DSH 采用「一切皆插件」的架构;SkillHub(skillhub.cn)是面向中国用户的 Skills 社区目录,与 DeepSeek / 幻方无官方从属关系。安装前建议浏览仓库源码并确认 MIT 许可证(见 package.json)。插件以当前 dsh 进程权限运行。

一条命令安装到 web profile:

dsh plugin --profile web add github:unclecode/toolshrink

包内带有 dsh.bundle manifest,下次启动时自动挂载,默认字符预算为 50,000。可在用户层 ~/.dsh/cordis.patch.yml 调整:

- id: toolshrink
  config:
    maxChars: 20000
    log: /tmp/toolshrink.log

本地开发时,可 clone 后执行 npm install && npm run build,再通过 insert 挂载适配器文件:

- insert:
    - id: toolshrink
      name: /path/to/toolshrink/adapters/harness/toolshrink.mjs
      config:
        maxChars: 50000        # 超过此字符数触发压缩(默认 50000)
        maxLines: 2000         # 或超过此行数(默认 2000)
        maxLineChars: 0        # 单行长度上限,0 表示关闭(默认 0)
        disable: [json]        # 跳过指定 cut(默认无)
        spillDir: ~/.dsh-toolshrink   # 完整原文存放目录
        log: /tmp/toolshrink.log      # 每次 cut 一行日志,省略则静默

日志行格式示例:bash 64151 -> 2942 via tree+size

典型用法

作为库调用

当前版本为 0.1.0(ESM,main 指向 ./dist/index.js)。

import { shrink, FileSpillStore } from 'toolshrink'

const out = shrink(bigText, { tool: 'bash', command: 'npm test' }, {
  budget: { maxChars: 20_000 },
  spill: new FileSpillStore({ dir: '/tmp/spills' }),  // 可选
})

out.content   // 交给模型的文本
out.reduced   // 输入未超预算时为 false
out.strategy  // 如 "tests"、"diff+size"、"size:tail"、"none"
out.note      // 人类可读的一行说明
out.stats     // inputChars、outputChars、keptLines、droppedLines 等

第二个参数 hint 可选:tool 影响 size 截断方向,command 帮助识别测试与 diff,path 帮助识别 JSON 与日志。

自定义 cut

一个 cut 文件默认导出三个成员,文件名即 cut 名:

// mycut.mjs
export default {
  name: 'mycut',
  detect(text, hint) {
    return hint.command?.startsWith('kubectl') ?? false
  },
  reduce(text, hint, budget) {
    const content = text.slice(0, budget.maxChars)
    return {
      content,
      reduced: true,
      strategy: 'mycut',
      note: 'kept the part I know matters',
      stats: {
        inputChars: text.length, inputLines: 0,
        outputChars: content.length, outputLines: 0,
      },
    }
  },
}

加载并使用:

import { shrink, loadReducers } from 'toolshrink'

const mine = await loadReducers('/path/to/my-cuts')
shrink(text, hint, { extra: mine })           // 优先于内置 cut 尝试
shrink(text, hint, { only: ['tests', 'diff'] }) // 限制并排序
shrink(text, hint, { disable: ['json'] })       // 跳过指定 cut

Spill:完整原文可恢复

启用 spill store 后,压缩前会将完整原文落盘,压缩文本末尾附带 locator,例如:

[full output saved as spill:bash-d63d2aebb643: directories sampled to 8 entries each]

store.load('spill:bash-d63d2aebb643') 可字节级还原原文。默认文件存储会在 24 小时后清理;存储后端可替换。

适用场景与注意

适合谁

  • 在 DSH 或自建 Agent 中频繁调用 shell、测试、构建、lint 等工具,输出常超上下文预算的开发者。
  • 需要可审计截断(明确省略量 + 可选 spill)而非静默丢数据的场景。

使用时注意

  • 无 cut 匹配时仍走 size 回退,行为与「按位置截断」接近;可通过 hint 与自定义 cut 提高识别率。
  • README 提到:在 3,000 字符预算下,60,000 字符的 find 结果被头截断后,模型看到省略标记会主动改用聚合查询——这是设计预期,依赖模型读懂 marker。
  • 安装与运行权限等同于 dsh 进程;spillDir 会写入本地磁盘,注意路径与磁盘占用。
  • 仓库 TODO 列出计划中的 cut(如 semanticsql),尚未实现,勿当作现有能力。

结尾

toolshrink 把 Agent 工具输出的截断从「按字节切」改成「按形态留」:测试留失败、diff 留变更、日志留错误。对 DSH 用户来说,一行 dsh plugin add 即可挂载;需要更细控制时,改 YAML 或写自定义 cut 即可。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜