首页
/ caveman-shrink 深度解析:如何用结构保持 + 有损压缩削减 MCP/OpenAI 工具目录的 Token 开销

caveman-shrink 深度解析:如何用结构保持 + 有损压缩削减 MCP/OpenAI 工具目录的 Token 开销

2026-09-06 12:16:26作者:袁立春Spencer

在 Caveman 项目中,caveman-shrink 是一个专注于工具目录(tool catalog)压缩的独立产品:当 MCP 或 OpenAI 接口注册了几十个带冗长描述、示例和 schema 注解的工具时,这份目录本身就会占据可观的上下文空间。本文基于 shrink/README.md 展开,覆盖其三个子命令的完整用法、五条核心保证,并结合 shrink/shrink.go 与底层 engine/compressors/toolschema.go 的源码,讲清楚"哪些字节必须逐字节保留、哪些字节可以安全丢弃、丢掉的字节如何恢复"。读完后,你将掌握:如何用 lint 预估压缩收益、用 shrink 生成压缩目录、用 recover 跨进程恢复原始字节,以及压缩器的确定性算法细节。

它解决什么问题

Agent 类应用(Claude Code、OpenAI 函数调用、MCP 客户端等)每轮请求都会携带完整工具定义。目录中大量字节其实对"模型选择哪个工具、如何构造参数"没有直接贡献:examplestitle$comment$schema 等注解,以及描述里大段的叙述性文字。caveman-shrink 的定位是:

  • 丢弃注解膨胀(examples、titles、comments、schema markers);
  • 缩减长描述,但保留可识别的约束句(constraint-bearing sentences);
  • 逐字节保留结构选择面(工具名/参数名/枚举/required)和参数构造值(defaultconst$ref 目标);
  • 明确声明描述缩减是 model-visible 且有损的——结构保持并不能保证模型选中同一个工具;所有数字均为 inferred(推断值)。

这一点在 shrink/shrink.go 的包注释中被反复强调,是整个产品"诚实性约定"的起点。

CLI 用法:三个子命令 + npx 启动器

1. 压缩目录(stdin → stdout)

# 压缩目录;推断的比例报告输出到 stderr
cat tools.json | caveman-shrink > tools.min.json

不带子命令时默认就是 shrink 行为;caveman-shrink shrink 是等价的显式写法(见 shrink/cmd/caveman-shrink/main.go)。压缩结果写入 stdout,一份 JSON 报告(tokens_beforetokens_afterratiobasisrecovery_handle 等字段,结构对应 shrink/shrink.goResult)输出到 stderr。

2. 恢复原始字节

# 从 shrink stderr 报告中打印的 handle 恢复精确原始字节
caveman-shrink recover ccr_... > tools.original.json

3. 只看不压:lint

# 查看每个工具的缩减量,不实际提交压缩
caveman-shrink lint tools.json

lint 输出一个按工具展开的表格(TOOL / BEFORE / AFTER / RATIO,外加 TOTAL 行与 basis 标注),实现见 main.go。仓库自带一个可直接试跑的示例目录:shrink/testdata/catalog.json,包含 search_filesrun_command 两个带 titleexamples$schema 注解的工具定义。

npx 零依赖启动

npx -y caveman-shrink lint tools.json

MIT 协议的 npm 启动器(shrink/package.json,bin 入口为 bin/caveman-shrink.mjs)在首次运行时下载匹配的 BSL-1.1 二进制,校验密钥签名的 checksum 清单 + 工件 SHA-256,并缓存在 ~/.caveman/bin。不需要 Go 工具链,也不需要全局安装 Caveman。授权边界是双层结构:npm 启动器文件本身适用 MIT(LICENSE.launcher),下载到的 Go 源码与官方二进制适用 BSL 1.1,见 shrink/BINARY_LICENSE.md 以及仓库根的 LICENSE.BSLLICENSING.md

五条核心保证(Guarantees)逐条拆解

README 列出的保证并非营销话术,每一条都能在源码和测试中找到对应实现。

1. 结构选择面(Structural selection surface)

压缩后的目录暴露与原始目录完全相同的 names / params / enums / required。这一点由 SelectionProfile 函数提取为可比较的结构(shrink/shrink.goToolProfile{Params, Enums, Required}),并有专门的不变量测试 TestStructuralSelectionSurfaceInvariant 断言 SelectionProfile(input) == SelectionProfile(Shrink(input).Output)shrink/shrink_test.go)。

但文档同时明确:这只是结构检查,不能证明模型会选中同一个工具——描述是 model-visible 的,行为等价需要模型评测(model eval)。这种"结构等价 ≠ 行为等价"的区分贯穿整个 shrink 模块(shrink/AGENTS.md 称之为"two correctness boundaries")。

2. 参数构造保留(Argument-construction preservation)

  • 短描述完整保留:低于预算阈值的描述原文不动(阈值 smallDescBudget = 160 字节,即 maxDescLen(80) * 2,见 toolschema.go);
  • 长描述保留"导语 + 全部识别出的约束句",其余叙述句丢弃;
  • defaultconst、内部 $ref 目标逐字节存活。

为什么 default 必须保留?源码注释给出了精确理由:"an agent that omits it relies on it"——代理省略参数时依赖默认值,丢失它会静默改变调用行为(toolschema.gokeepSchemaKeys)。

约束句的识别靠一个故意偏宽的正则 constraintRetoolschema.go),覆盖:

类别 匹配词示例
义务/禁止 mustmust notcannotshallforbiddennot allowed
校验词 require*reject*disallow*invalid
基数约束 exactly oneonly one ofat leastat mostmutually exclusiveunique
数值边界 max/min/maximum/minimum/range/betweengreater thanno more than
格式锚点 format*iso[- ]?\d*rfc[- ]?\d*absolute(能命中 RFC3339ISO8601

匹配策略是宁可多保留(over-keep):多留一句只损失一点压缩率,而漏掉一个约束句会产生无效工具调用和重试循环,代价远高于省下的字节。对应的防回归测试是 TestShrinkPreservesArgumentConstraints,用 put_object 工具验证 "The path must be an absolute path."、"The format must be RFC3339."、"Provide exactly one of body or body_b64." 这类句子在压缩后仍然存在(shrink/shrink_test.go)。

3. Fail-open(失败即透传)

成功的压缩是一次 S4 有损变换(安全等级 S4,见 toolschema.goSafetyClass());而任何解析问题或压缩结果不比原文更小时,输入原样透传ratio: 0、不产生 handle。CLI 层还有一道兜底:即使库返回错误,也会把原始字节写到 stdout 并在 stderr 输出 {"ratio":0,"basis":"inferred","note":"passed through: ..."}main.go),保证管道下游永远拿到可解析的合法目录。

4. 有界输入(Bounded input)

stdin 上限 32 MiB,超限以 cave_input_too_large 报错,不会无限缓冲。实现是 io.LimitReader(r, maxBytes+1) 加长度校验(main.go,常量 maxStdinBytes int64 = 32 << 20)。

5. 可逆(Reversible,跨进程)

一次真正发生压缩的 shrink,会先把精确原始字节提交到持久化 CCR(Compressible Context Recovery)存储,再返回 handle;之后的任意进程都能用 caveman-shrink recover 解析它。存储路径解析顺序在 shrink/shrink.godefaultCCRPath() 中:

  1. 环境变量 CAVEMAN_CCR_DB
  2. CAVEMAN_HOME/ccr.db
  3. ~/.caveman/ccr.db(父目录不存在时自动创建)。

值得强调的是,这个共享存储与 engine CLI、MCP 服务器、网关是同一个 store——handle 在 shrink 进程里签发,在另一个进程里可解。源码注释特别记录了历史教训:早期用内存 store 时"handle 在 Shrink 返回的瞬间就不可解析了,所以可逆保证在每次调用上都是假的",回归测试 TestShrinkRoundTripsThroughAFreshStore 专门用一个全新打开的 store 实例取回字节并与原文逐字节比对来守门(shrink/shrink_test.go)。

支持哪些目录形态

extractToolsshrink/shrink.go)处理三类信封:

  • MCP{"tools": [...]}
  • OpenAI[{ "function": {...} }, ...](自动解包 function 嵌套)或扁平数组;
  • {"functions": [...]},以及单个工具对象。

参数 schema 兼容两种键名:inputSchema(MCP)和 parameters(OpenAI)(schemaOfshrink/shrink.go)。无法解析出任何带 name 的工具时报 no named tools found

压缩器算法细节:一个描述如何被缩减

结构压缩的实体在 engine 侧的 toolschema 压缩器中,shrink 只是薄封装——Shrink() 调用 engine.Compress(input, Options{Mode: ModeCompress, Type: "toolschema"})shrink/shrink.go),并约定"结构性压缩器住在 engine(compressors/toolschema.go),shrink 复用、从不分叉"(shrink/AGENTS.md)。具体算法:

Schema 键的三分类处理toolschema.go):

分类 行为
丢弃 examplesexample$commenttitle$schema 直接删除(可通过 CCR 恢复)
逐字保留 enumrequireddefaultconst 不递归、不截断
用户定义键空间 properties$defsdefinitionspatternPropertiesdependentSchemasdependentRequireddependencies 子键是用户命名,必须全部存活——一个叫 "title" 的 property 不是元数据,只有其下的 schema 值被压缩

描述缩减compressDescriptiontoolschema.go):

  1. 长度 ≤ 160 字节 → 整体保留,不做任何句子裁剪;
  2. 否则按句子切分,保留顺序为:每句先判断是否含约束标记(constraintRe),是则完整保留且不受长度上限约束;第一句非约束句作为"导语"保留,并在 rune 边界处截断到 80 字节(maxDescLen),必要时回退到最近词边界避免输出半个词或 U+FFFD 替换字符(capRunes);其余非约束句丢弃。
  3. 算法是确定且幂等的:对自己的输出再压缩一次得到相同字符串。

句子切分本身也做了防误判:只在"句点 + 空白 + 大写字母"处断句,并排除单字母首字母("A.")、含内嵌句点 token("e.g."、"i.e.")以及 etc/vs/no/fig 等缩写词表中的词,从而保住 "Node.js"、"v1.2"、"3.14" 这类 token(toolschema.go)。

token 计数口径Lint 与报告中的所有数字使用 engine 的默认离线计数器,basis 恒为 inferred——shrink/shrink.go 的注释明确"never verified, never re-projected"。比例计算 ratio(before, after)after >= before 时归零,即只有真正变小的压缩才被认可(shrink/shrink.go)。

在 Caveman 体系中的位置

shrinktoolschema 压缩器的专用产品面。需要注意两个边界(shrink/AGENTS.md):

  • 托管网关(managed gateway)的适配器把工具数组放在冻结的 prompt-cache 前缀里,从不交给它压缩;网关的 S2 工具搜索/延迟路径也不是压缩。因此 shrink 面向的是本地工具目录缩减,本地缩减一律记为 inferred
  • 构建与测试约定:make product-build PRODUCT=shrink / make product-test PRODUCT=shrink;npm 侧测试为 node --test tests/*.test.mjsshrink/package.json),另有 tests/package.test.mjstests/package.test.mjsshrink/cmd/caveman-shrink/main_test.go 覆盖 CLI 行为。

快速上手小结

# 1. 预估收益(不动原文件)
npx -y caveman-shrink lint shrink/testdata/catalog.json

# 2. 实际压缩,stderr 看报告(含 recovery handle)
cat shrink/testdata/catalog.json | caveman-shrink > tools.min.json

# 3. 事后需要原文时
caveman-shrink recover <handle> > tools.original.json

适用前提与限制:输入为单个合法 JSON 目录(MCP / OpenAI / functions 形态之一),stdin 不超过 32 MiB;压缩是有损的 S4 变换,结构等价不构成行为等价,是否接入生产链路应由你自己的模型评测决定;所有 token 数字均为离线推断值,不应作为计费依据。相关文档可继续参考 shrink/CLAUDE.mdshrink/AGENTS.md

登录后查看全文
热门项目推荐
相关项目推荐