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 即可。

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

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

小夜