caveman-shrink 深度解析:如何用结构保持 + 有损压缩削减 MCP/OpenAI 工具目录的 Token 开销
在 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 客户端等)每轮请求都会携带完整工具定义。目录中大量字节其实对"模型选择哪个工具、如何构造参数"没有直接贡献:examples、title、$comment、$schema 等注解,以及描述里大段的叙述性文字。caveman-shrink 的定位是:
- 丢弃注解膨胀(examples、titles、comments、schema markers);
- 缩减长描述,但保留可识别的约束句(constraint-bearing sentences);
- 逐字节保留结构选择面(工具名/参数名/枚举/required)和参数构造值(
default、const、$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_before、tokens_after、ratio、basis、recovery_handle 等字段,结构对应 shrink/shrink.go 的 Result)输出到 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_files 和 run_command 两个带 title、examples、$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.BSL、LICENSING.md。
五条核心保证(Guarantees)逐条拆解
README 列出的保证并非营销话术,每一条都能在源码和测试中找到对应实现。
1. 结构选择面(Structural selection surface)
压缩后的目录暴露与原始目录完全相同的 names / params / enums / required。这一点由 SelectionProfile 函数提取为可比较的结构(shrink/shrink.go:ToolProfile{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); - 长描述保留"导语 + 全部识别出的约束句",其余叙述句丢弃;
default、const、内部$ref目标逐字节存活。
为什么 default 必须保留?源码注释给出了精确理由:"an agent that omits it relies on it"——代理省略参数时依赖默认值,丢失它会静默改变调用行为(toolschema.go 的 keepSchemaKeys)。
约束句的识别靠一个故意偏宽的正则 constraintRe(toolschema.go),覆盖:
| 类别 | 匹配词示例 |
|---|---|
| 义务/禁止 | must、must not、cannot、shall、forbidden、not allowed |
| 校验词 | require*、reject*、disallow*、invalid |
| 基数约束 | exactly one、only one of、at least、at most、mutually exclusive、unique |
| 数值边界 | max/min/maximum/minimum/range/between、greater than、no more than |
| 格式锚点 | format*、iso[- ]?\d*、rfc[- ]?\d*、absolute(能命中 RFC3339、ISO8601) |
匹配策略是宁可多保留(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.go 的 SafetyClass());而任何解析问题或压缩结果不比原文更小时,输入原样透传,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.go 的 defaultCCRPath() 中:
- 环境变量
CAVEMAN_CCR_DB; CAVEMAN_HOME/ccr.db;~/.caveman/ccr.db(父目录不存在时自动创建)。
值得强调的是,这个共享存储与 engine CLI、MCP 服务器、网关是同一个 store——handle 在 shrink 进程里签发,在另一个进程里可解。源码注释特别记录了历史教训:早期用内存 store 时"handle 在 Shrink 返回的瞬间就不可解析了,所以可逆保证在每次调用上都是假的",回归测试 TestShrinkRoundTripsThroughAFreshStore 专门用一个全新打开的 store 实例取回字节并与原文逐字节比对来守门(shrink/shrink_test.go)。
支持哪些目录形态
extractTools(shrink/shrink.go)处理三类信封:
- MCP:
{"tools": [...]}; - OpenAI:
[{ "function": {...} }, ...](自动解包function嵌套)或扁平数组; {"functions": [...]},以及单个工具对象。
参数 schema 兼容两种键名:inputSchema(MCP)和 parameters(OpenAI)(schemaOf,shrink/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):
| 分类 | 键 | 行为 |
|---|---|---|
| 丢弃 | examples、example、$comment、title、$schema |
直接删除(可通过 CCR 恢复) |
| 逐字保留 | enum、required、default、const |
不递归、不截断 |
| 用户定义键空间 | properties、$defs、definitions、patternProperties、dependentSchemas、dependentRequired、dependencies |
子键是用户命名,必须全部存活——一个叫 "title" 的 property 不是元数据,只有其下的 schema 值被压缩 |
描述缩减(compressDescription,toolschema.go):
- 长度 ≤ 160 字节 → 整体保留,不做任何句子裁剪;
- 否则按句子切分,保留顺序为:每句先判断是否含约束标记(
constraintRe),是则完整保留且不受长度上限约束;第一句非约束句作为"导语"保留,并在 rune 边界处截断到 80 字节(maxDescLen),必要时回退到最近词边界避免输出半个词或 U+FFFD 替换字符(capRunes);其余非约束句丢弃。 - 算法是确定且幂等的:对自己的输出再压缩一次得到相同字符串。
句子切分本身也做了防误判:只在"句点 + 空白 + 大写字母"处断句,并排除单字母首字母("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 体系中的位置
shrink 是 toolschema 压缩器的专用产品面。需要注意两个边界(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.mjs(shrink/package.json),另有tests/package.test.mjs、tests/package.test.mjs与 shrink/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.md 与 shrink/AGENTS.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00