caveman-shrink:压缩 MCP/OpenAI 工具目录的 Token 工程——caveman 项目中 fail-open 与可逆压缩的设计拆解
【免费下载链接】caveman🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman
caveman-shrink 是 caveman 仓库中专用于压缩 MCP/OpenAI 工具定义目录(tool catalog)的产品:它在工具清单撑满上下文窗口之前,删掉注解类元数据、把冗长描述缩减为"引导句 + 全部约束句",同时保证名称、参数、枚举、required以及default/const等参数构造值逐字节存活。读完本文,你将掌握它的 CLI 与 npm 启动器用法、基于 enginetoolschema压缩器的底层压缩规则,以及"结构选择面不变量 + 有界 CCR 可逆恢复"这两条正确性边界的实现与测试证据。
定位:toolschema 压缩器的专用产品面
caveman-shrink 本身不是一套独立的压缩算法。根据 shrink/CLAUDE.md,它是对 engine 中toolschema压缩器的一个薄 Go 封装(thin wrapper),结构压缩器本体位于 engine/compressors/toolschema.go,shrink 只复用、绝不 fork。这一分工在 shrink/CLAUDE.md 的 Conventions 一节中是明确约定:
- 构建/测试:
make product-build PRODUCT=shrink/make product-test PRODUCT=shrink; - shrink 是
toolschema压缩器的专用产品面。engine 的 API/CLI 调用方也可以本地强制走该压缩器(通过engine.Options.Type = "toolschema"),但在 caveman 的受管网关(managed-gateway)路径中,工具数组被保留在冻结的 prompt-cache 前缀里,从不交给它处理;网关上另一条 S2 的 tool-search/deferral 路径也不是压缩。本地缩减的统计口径一律记为inferred; - 文档同时给出一条未来的商业化前提:如果将来要为这个变换开辟计费路由,必须先有 cache-vs-schema 的成本证明、稳定的逐字节前缀输出,以及一个 eval 门禁。
压缩行为本身分三层(均来自 shrink/CLAUDE.md 首段与 engine/compressors/toolschema.go 的类型注释):
- 丢弃注解元数据——
examples、example、title、$comment、$schema这类对工具选择没有信息量、却最占字节的 JSON-Schema 注解键; - 缩减自由文本描述——只保留引导句和所有携带约束的句子(规则见后文"两条正确性边界");
- 逐字节保留两类 token——选择 token(工具名、参数名、类型、
enum、required)和参数构造值(default、const、内部$ref目标)。
它在 engine 的安全分级里属于 S4:lossy、模型可见的字节会被改变。这一点可以从 engine/safety/safety.go#L48 的注册表看到:S4: {Class: S4, ByteSafe: false, RequiresCCR: true, Reversible: false}——S4 类要求依赖 CCR(上下文压缩恢复存储)作为安全网。
目录布局与三种入口形态
shrink/CLAUDE.md 的 Layout 一节列出了 shrink 的三块组成,对应仓库中的实际路径:
| 组成 | 路径 | 职责 |
|---|---|---|
| 库 | shrink/shrink.go | Shrink(目录 → 压缩,fail-open 的 S4 变换,持久化 CCR 支撑)、Recover(handle → 原始字节)、Lint(逐工具的 inferred token 缩减报告)、SelectionProfile(结构选择面——即"必须存活的东西") |
| CLI | shrink/cmd/caveman-shrink/main.go | caveman-shrink/shrink(stdin→stdout)、lint <file>、recover <handle> |
| npm 启动器 | shrink/bin/caveman-shrink.mjs + shrink/package.json | npx caveman-shrink的 MIT 启动器,围绕预构建二进制工作 |
库入口 shrink/shrink.go 的核心 API 值得逐一说明:
Shrink(input []byte, opts ...Option) (Result, error)——以Type: "toolschema"强制路由到工具的 schema 压缩器(shrink/shrink.go#L117-L124)。它是 fail-open 的:任何解析问题、或结果不更小,都原样返回输入,Ratio为 0、无 handle。Result——Output、TokensBefore、TokensAfter、Ratio、Basis(恒为inferred)、ContentType、RecoveryHandle(仅当真正压缩时非空)。Recover(handle, opts...)——从与Shrink相同的持久化 store 读取原始字节;未知 handle 返回ccr.ErrNotFound(engine/ccr/store.go#L27-L29),恢复过程从不猜测。WithStore(s *ccr.Store)/WithStorePath(path string)——两个 Option 用于指定恢复存储:前者由调用方持有生命周期,后者打开指定路径(":memory:"仅在一次调用内有意义)。不提供 Option 时使用默认的共享持久化 store:CAVEMAN_CCR_DB环境变量,否则~/.caveman/ccr.db。Lint(input) (Report, error)——只测量、不提交:对每个工具用 engine 的默认离线 counter 计数,并刻意取"压缩前后较小者"(若压缩结果不更小就保留原值),保证 Lint 的数字不会虚报。SelectionProfile(input) (map[string]ToolProfile, error)——提取每个工具的选择面:参数名列表、每参数的enum值、required列表;description被刻意排除,因为那正是 shrink 要压缩的对象。它支持三种目录形态(shrink/shrink.go#L240-L284 的extractTools):MCP 的{"tools":[…]}、OpenAI 的{"functions":[…]}或扁平数组(含{function: …}嵌套),以及单个裸工具对象;schema 字段则兼容 MCP 的inputSchema与 OpenAI 的parameters。
默认存储路径的解析在 shrink/shrink.go#L79-L95:依次尝试CAVEMAN_CCR_DB→CAVEMAN_HOME/ccr.db→~/.caveman/ccr.db,并会自动创建父目录(0o700),使一台机器上的第一次 shrink 就能成功。
CLI 实操:shrink、lint、recover 三个子命令
CLI 入口在 shrink/cmd/caveman-shrink/main.go。子命令分派逻辑是:显式shrink、lint、recover,help/-h打印用法,不带子命令时把整个调用当作对 stdin 的 shrink——这使得它可以直接插进管道。
基本用法
# 压缩目录(stdin → stdout);inferred 的 ratio 报告写到 stderr cat tools.json | caveman-shrink > tools.min.json # 从 shrink stderr 报告里打印的 handle 恢复精确的原始字节 caveman-shrink recover ccr_... > tools.original.json # 不承诺压缩、只查看逐工具的缩减量 caveman-shrink lint tools.json以上三条即 shrink/README.md 的 Use 一节。仓库自带一个可直接拿来试手的示例目录 shrink/testdata/catalog.json:两个工具(search_files、run_command),带title、$schema、examples、enum、default等注解,正好覆盖压缩器会处理的所有注解类型:
caveman-shrink lint shrink/testdata/catalog.json cat shrink/testdata/catalog.json | caveman-shrink有界输入:32 MiB 上限
CLI 对 stdin 有硬性上限。shrink/cmd/caveman-shrink/main.go#L23 定义maxStdinBytes int64 = 32 << 20,readBoundedInput用io.LimitReader(r, maxBytes+1)读取,超限即返回cave_input_too_large错误——更大的目录直接失败,而不是无界缓冲(shrink/README.md 将其列为 Bounded input 保证之一)。
fail-open 的字节安全语义
shrink/cmd/caveman-shrink/main.go#L46-L63 的runShrink体现了"失败也不改字节"的原则:Shrink返回错误时,CLI把原始输入原样写到 stdout,并在 stderr 输出{"ratio":0,"basis":"informed"…,"note":"passed through: …"};成功时 stdout 是压缩后的目录,stderr 是Result的 JSON(其中recovery_handle字段是后续recover的凭据)。测试侧 shrink/shrink_test.go#L174-L186 的TestShrinkByteSafeOnMalformed锁死了库层行为:喂入{not a valid catalog这样的坏 JSON,Shrink不返回 error,输出与输入逐字节相等,且ratio=0、无 handle。
npm 启动器:无需 Go 工具链
shrink/package.json 声明包名caveman-shrink、"bin": {"caveman-shrink": "bin/caveman-shrink.mjs"}、Node>=18、MIT 许可。shrink/bin/caveman-shrink.mjs 是一个 shim:通过ensureBinary定位预构建的 Go 二进制(支持CAVEMAN_SHRINK_BIN环境变量覆盖),stdio: "inherit"地 exec 它并透传退出码;找不到二进制时以退出码 127 失败。按 shrink/README.md,MIT 启动器首次运行会下载匹配的 BSL-1.1 许可二进制,校验密钥签名的 checksum 清单与工件 SHA-256,并缓存在~/.caveman/bin——因此不需要 Go 工具链,也不需要全局安装 Caveman:
npx -y caveman-shrink lint tools.json许可是拆开的:npm 启动器本身是 MIT(shrink/LICENSE.launcher 对应LICENSE.launcher文件),下载到的二进制受 shrink/BINARY_LICENSE.md 中注明的 BSL-1.1 条款约束。shrink/CLAUDE.md 标题中的 "commercial Go core + MIT launcher" 指的就是这个结构。
引擎侧压缩规则:哪些字节会掉,哪些字节必活
shrink 的全部压缩决策发生在 engine/compressors/toolschema.go 的toolSchemaCompressor中。理解四个键集合和描述缩减算法,就理解了 shrink 的完整行为面。
元数据丢弃集与必保留集
engine/compressors/toolschema.go#L28-L45 定义了两张核心表:
// 直接丢弃:schema 注解元数据(可经 CCR 恢复) var schemaMetaDrop = map[string]bool{ "examples": true, "example": true, "$comment": true, "title": true, "$schema": true, } // 逐字节保留:承载"参数构造"语义的键(不递归、不截断) var keepSchemaKeys = map[string]bool{ "enum": true, "required": true, "default": true, "const": true, }源码注释里有一个值得注意的设计动机:default被刻意排除在丢弃集之外,因为它是"agent 省略参数时所依赖的值"——丢掉default会静默改变调用行为;const同理(它钉死了唯一合法值)。
第三张表 userDefinedKeys(properties、$defs、definitions、patternProperties、dependentSchemas、dependentRequired、dependencies)标记了键名本身由用户控制的位置:一个叫title的属性名是用户命名,不是 schema 元数据,必须逐字存活;压缩器只递归压缩这些键下面的 schema 值。compressSchema(engine/compressors/toolschema.go#L191-L230)用一个inUserKeys布尔位区分这两种语境。
描述缩减:小额预算整体保留 + 约束句全保留
compressDescription(engine/compressors/toolschema.go#L269-L289)的规则是:
- 小额预算整体保留。常量
maxDescLen = 80(L18),smallDescBudget = maxDescLen * 2 = 160字节(L260)。描述若 ≤160 字节,整句保留、一个句子都不剪——源码注释解释了为什么:短描述是常见情形,为一个省不了多少字节的小描述冒"漏掉无标记约束"的风险不值得。 - 大描述 = 引导句 + 全部约束句。对超过预算的描述,先按句号切句(
splitSentences),然后遍历:带约束标记的句子永不丢弃、永不截断;第一个非约束句作为引导句保留,按 rune 边界截到 80 字节(capRunes保证不拆 UTF-8 字符、尽量退回词边界);其余非约束句丢弃(可经 CCR 找回)。
约束标记是一个刻意写得宽的正则 constraintRe,匹配整词/短语,涵盖四类语义:
- 义务/禁止:
must、must not、cannot、can't、shall、require*、reject*、disallow*、forbidden; - 合法性判断:
invalid、not allowed、not permitted; - 数量/互斥/边界:
exactly one、only one of、one of、at least、at most、mutually exclusive、only、unique、case-sensitive、max、min、maximum、minimum、range、between、greater/less/more/fewer than、no more/less than、over、above、below、under、beyond、exceed*; - 格式锚点:
format*、iso[- ]?数字*、rfc[- ]?数字*、absolute(后两者允许尾随数字串,以便RFC3339、ISO8601命中)。
源码注释明确给出了取舍原则:匹配宁多勿少(over-keep)——多留一句只损失一点 ratio,而丢掉一个约束句会造出非法工具调用和重试循环,代价远超 shrink 省下的字节。
句子切分splitSentences(engine/compressors/toolschema.go#L296-L330)专门处理了英文缩写陷阱:只有当句号后跟空白+大写字母、且句号不是某个缩写(e.g、i.e、etc、vs等,见 abbrevSet)的结尾时才切句。这保证 "e.g. /srv/x" 不会被截成 "e."——shrink/CLAUDE.md 里把这一反例写得很直白:把 "Target path, e.g. /srv…" 截成 "Target path, e." 会产生非法调用,而结构 profile 根本看不见这类错误。
信封与 schema 的分流
压缩器还区分"provider 工具信封"与裸 JSON Schema:compressDocument(engine/compressors/toolschema.go#L112-L133)识别tools数组、function嵌套、单工具对象;compressToolEnvelope对工具级description走描述缩减,对声明为 schema 值的字段才进入compressSchema。MCP 注解里模型可见的title等信封字段不受 schema 元数据规则影响。
两条正确性边界:结构选择面与参数有效性
shrink/CLAUDE.md 用"Two correctness boundaries"一节划定了这个产品能证明什么、不能证明什么,这是全文最核心的诚实性声明。
边界一:结构选择面不变量
不变量是:
SelectionProfile(input)==SelectionProfile(Shrink(input).Output)恒成立。
这证明了工具名、参数名、enum值、required列表逐字节存活。但它不能证明模型会选中同一个工具——因为描述是模型可见的,而长描述的缩减是有损的。行为等价需要模型 eval 固件,当前结果因此保持inferred。
库级测试TestStructuralSelectionSurfaceInvariant(shrink/shrink_test.go#L105-L127)就是这条不变量的执行体:对压缩前后分别取SelectionProfile并reflect.DeepEqual,同时验证mode参数的 3 个 enum 值一个不少。
边界二:参数有效性(偏向 over-keep)
Agent 是从描述里构造工具参数的,所以"选择面存活"并不等于"参数合法"。shrink 的保证(偏保守地多留)是两条:
- 一个已经装得下小额预算(≤
maxDescLen * 2字节)的描述整体保留——不剪任何句子,因此即便约束句没有使用任何被识别的标记词,它也活下来; - 对真正大的描述,保留引导句 + 每个带约束标记的句子(完整保留,见上一节的标记清单)。
丢default、RFC3339 或边界规则会产生结构 profile 看不见的非法调用;为此 engine 侧有 call-validity 一致性测试(golden fixture + 复现的 under-keep 用例,位于engine/compressors测试中),把已识别的约束 token 钉死在行为里。shrink 产品层的对应测试是TestShrinkPreservesArgumentConstraints(shrink/shrink_test.go#L134-L172),断言压缩输出必须仍然包含:
"must be an absolute path"(绝对路径规则)"exactly one of body"(互斥约束)"format must be RFC3339"(时间戳格式)"e.g. /srv/x"(缩写不被截成 "e.")
并且长描述里的填充散文(This trailing clause is filler…)应当被丢弃。shrink/CLAUDE.md 的总结语是产品的行为准则:"Over-keep is the rule — never knowingly under-keep."
Reversible for real:持久化 CCR 恢复存储
shrink/CLAUDE.md 的 Gotchas 第三条("reversible for real")描述了一次真实的缺陷修复:早先的实现在返回时关闭的内存store 里铸造 handle,导致"Shrink 一返回,handle 就永远无法解析"——可逆性声明在每次调用上都是假的。现在的设计是:
- 先提交、后发布。一次真正压缩的 shrink 会把精确的原始字节写入持久化store——engine 的
Compress在发布变换后的字节之前提交恢复行(shrink/shrink.go#L112-L116 的注释)。因此任何非空的RecoveryHandle都能稍后、跨进程、经Recover/caveman-shrink recover解析。默认 store 是共享的CAVEMAN_CCR_DB/~/.caveman/ccr.db,与 engine CLI、MCP server、网关使用同一个。 - store 打不开则不承诺可逆。若持久 store 无法打开,库的
Shrink返回 error,CLI 捕获后转发原始字节:没有 handle,也就没有可逆性声明。
两条测试锁死这个语义。TestShrinkRoundTripsThroughAFreshStore(shrink/shrink_test.go#L76-L99)是"可逆性门禁":第一次Shrink用WithStorePath(dbPath),然后用同一路径全新打开一个 store 实例(模拟一个recover进程看到的状态)取回字节,与原始 catalog 逐字节比较——注释特别指出,旧实现下handle != ""的自证检查恰好掩盖了这个 bug。TestShrinkReturnsPassThroughResultWhenCCRIsUnavailable(shrink/shrink_test.go#L188-L203)则验证:store 已关闭时,Shrink返回 error,且附带一个"完整记账的原始字节直通结果"(Ratio=0、无 handle、TokensAfter == TokensBefore),让库调用方既能安全转发又能感知操作失败。
恢复端同样有边界测试:TestRecoverUnknownHandleFails(shrink/shrink_test.go#L205-L209)断言未知 handle 必须失败——"recovery never guesses"。
诚实性不变量与统计口径
shrink/CLAUDE.md 最后三条 Gotchas 汇总成产品的诚实性不变量,值得逐条对照源码验证:
- S4 lossy、fail-open:成功压缩会改变模型可见的字节(S4 在 engine/safety/safety.go 中
ByteSafe: false);畸形/不可压缩的目录原样直通(ratio:0、无 handle)。直通语义在库层由TestShrinkByteSafeOnMalformed、CLI 层由runShrink的 error 分支共同保证。 - inferred-only:token 数来自 engine 的离线 counter,是估计值;永远是
inferred,从不verified,也从不二次投影。Lint的Report.Basis硬编码为engine.BasisInferred(shrink/shrink.go#L183),TestLintReportsInferredReductions(shrink/shrink_test.go#L211-L230)验证两个工具各自有名、总量确有下降。 - reversible for real:如上节所述,持久 store、先提交后发布、跨进程可解析;store 不可用时宁可报 error 也不发 handle。
ratio 的计算口径也值得说明:ratio(before, after) = (before-after)/before,且当after >= before时直接记 0(shrink/shrink.go#L326-L331)——ratio 永远不会是负数,"压不动"与"没压"在报告里是同一个信号(0)。
小结
caveman-shrink 用很小的表面(一个库文件 + 一个 CLI + 一个 npm shim)实现了工具目录压缩的完整产品语义:压缩决策全部委托给 engine 的toolschema压缩器,产品层负责存储寻址(CAVEMAN_CCR_DB/~/.caveman/ccr.db)、有界输入(32 MiB)、fail-open 直通和跨进程可逆恢复。它的两条正确性边界——结构选择面不变量与偏向 over-keep 的参数构造保真——分别由SelectionProfile对比测试和约束句保留测试钉死,而"全部数字都是 inferred"的口径则避免了结构检查被误读为行为等价。相关深入阅读路径:shrink/CLAUDE.md、shrink/README.md、engine/CLAUDE.md、engine/compressors/toolschema.go、shrink/shrink_test.go。
【免费下载链接】caveman🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考