caveman-shrink:压缩 MCP/OpenAI 工具目录的 Token 工程——caveman 项目中 fail-open 与可逆压缩的设计拆解
caveman-shrink 是 caveman 仓库中专用于压缩 MCP/OpenAI 工具定义目录(tool catalog)的产品:它在工具清单撑满上下文窗口之前,删掉注解类元数据、把冗长描述缩减为"引导句 + 全部约束句",同时保证名称、参数、枚举、required 以及 default/const 等参数构造值逐字节存活。读完本文,你将掌握它的 CLI 与 npm 启动器用法、基于 engine toolschema 压缩器的底层压缩规则,以及"结构选择面不变量 + 有界 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。
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