caveman-shrink:在工具目录吃掉上下文之前压缩它 —— MCP/OpenAI Tool Catalog 压缩器的两条正确性边界与 CCR 恢复
本文基于 caveman 仓库中 shrink/AGENTS.md 展开,讲解 caveman-shrink 这个"工具目录压缩器"的完整设计:它如何在不丢失任何选择信号(工具名、参数名、类型、enum、required)的前提下,把 MCP/OpenAI 工具定义目录的 token 成本压下来;它用哪两条正确性边界(结构选择面不变式 + 参数构造保全)来定义"压得安全"的边界;以及它如何依托持久化 CCR 恢复存储做到"真的可逆"。读完本文,你将掌握该工具的库 API 与 CLI 用法、底层压缩算法(元数据剔除、约束句保留、句界切分)的源码细节,以及 fail-open、inferred-only 这两个诚实性约定的工程含义。
一、caveman-shrink 是什么:引擎 toolschema 压缩器的专用产品面
caveman-shrink 是 caveman 项目中专用于压缩 MCP/OpenAI 工具定义目录(tool catalog)的产品。它是一个薄 Go 封装(thin wrapper),底层完全复用引擎中的 toolschema 压缩器,核心行为可以概括为三句话:
- 剔除注释性元数据:
examples、title、$comment、$schema这类不影响模型选工具却最占字节的键被直接丢弃; - 缩减长描述:把冗长的自由文本描述压缩为"引导句 + 所有承载约束的句子",短句则原样保留;
- 逐字节保真关键面:所有选择 token(工具名/参数名、类型、enum、required)和参数构造值(
default、const、$ref目标)逐字节保留。
按 shrink/AGENTS.md 的原话,这个产品输出的所有数字都是 inferred(推断值),绝不标记为 verified。它被定位为 toolschema 压缩器的专用产品面——引擎的 API/CLI 调用方也可以本地强制使用同一压缩器,但 shrink 复用它而绝不 fork 它。
二、代码布局:三个层次各管一事
仓库中 shrink 产品由三个层次组成(见 shrink/AGENTS.md 的 Layout 一节与 shrink/ 目录):
| 层次 | 位置 | 职责 |
|---|---|---|
| Go 库 | shrink.go | Shrink(目录 → 压缩,fail-open 的 S4 变换,持久 CCR 支撑)、Recover(handle → 原始字节)、Lint(逐工具推断 token 缩减报告)、SelectionProfile(结构选择面提取) |
| CLI | cmd/caveman-shrink/ | caveman-shrink / shrink(stdin→stdout)、lint <file>、recover <handle> |
| npm 启动器 | bin/caveman-shrink.mjs + package.json |
npx caveman-shrink 的 MIT 许可启动器,首次运行下载预构建二进制 |
库的存储配置通过 functional options 完成:WithStore 传入调用方自持的 *ccr.Store(生命周期归调用方),WithStorePath 打开指定路径的存储(支持 ":memory:" 临时库)。两者都不给时,默认落到 DURABLE 共享存储——环境变量 CAVEMAN_CCR_DB,否则 ~/.caveman/ccr.db,与引擎 CLI、MCP server 和 gateway 使用同一个库。defaultCCRPath() 会先 MkdirAll 建好父目录,保证一台新机器上的第一次 shrink 就能成功。
这个默认值不是随手选的:shrink.go 的注释明确指出,旧行为使用"返回即关闭的内存存储",结果签发出来的 handle 从来都解析不了——"可逆"的保证在每次调用上都是假的。换成持久共享存储后,Shrink 签发的 handle 可以在另一个进程里通过 Recover 解析,"持久化才是全部意义所在"。
三、CLI 实战:三条命令 + npx 免安装入口
shrink/README.md 给出的标准用法(CLI 入口):
# 1. 压缩目录(stdin → stdout);推断的比率报告打到 stderr
cat tools.json | caveman-shrink > tools.min.json
# 2. 从 stderr 报告里打印的 handle 恢复原始字节
caveman-shrink recover ccr_... > tools.original.json
# 3. 只测量、不提交的逐工具缩减报告
caveman-shrink lint tools.json
几个 CLI 细节值得注意:
- 无参数即 shrink:main() 中,不带子命令的调用整体按 stdin→stdout 的 shrink 处理,
caveman-shrink shrink与之等价。 - stderr 报告是 JSON:
runShrink把 Result(tokens_before、tokens_after、ratio、basis、content_type、recovery_handle)序列化后写到 stderr,stdout 只留压缩后的目录本体——管道里可以放心重定向。 lint输出对齐表格:TOOL / BEFORE / AFTER / RATIO四列逐工具列出,末行是 TOTAL 并标注basis: inferred(runLint)。- 32 MiB 输入上限:stdin 通过
io.LimitReader(r, maxBytes+1)读取,超限直接以cave_input_too_large报错,而不是无界缓冲(readBoundedInput)。
不装 Go 工具链、不装全局 Caveman 也能用:MIT 许可的 npm 启动器首次运行下载对应平台的 BSL-1.1 二进制,校验密钥签名的 checksum 清单与产物 SHA-256,缓存在 ~/.caveman/bin:
npx -y caveman-shrink lint tools.json
许可模型见 BINARY_LICENSE.md:MIT 只覆盖 npm 启动器文件;caveman-shrink 的 Go 源码与官方二进制受 BSL 1.1 约束(对应仓库根的 LICENSE.BSL 与 LICENSING.md)。
仓库自带一份可直接当输入示例的目录:shrink/testdata/catalog.json,包含 search_files(带 enum/default/title/$schema/examples 等各种可压项)与 run_command 两个工具,caveman-shrink lint shrink/testdata/catalog.json 即可复现逐工具报告。
四、压缩到底改变了什么:toolschema 压缩器源码解析
shrink 自己不做任何压缩逻辑,一切发生在引擎的 toolschema 压缩器。它的 ContentType 是 "toolschema",SafetyClass 是 S4(engine/safety/safety.go 中登记为 ByteSafe: false, RequiresCCR: true——即"改变模型可见字节、必须写 CCR")。shrink 库通过把 engine.Options.Type 固定为 "toolschema" 来强制路由(schemaType 常量);引擎的 Detect 不会自动路由到它,因此代理的通用 JSON 路径不受影响。
4.1 元数据剔除与键级保留
toolschema.go 用三张键表精确区分三类 schema 键:
schemaMetaDrop(直接丢弃):examples、example、$comment、title、$schema——注释性元数据,不影响工具选择,字节最多;keepSchemaKeys(逐字保留、不再递归):enum、required(选择面)与default、const(参数构造意义——agent 省略参数时就靠default继承,丢了它等于静默改变调用;const钉死唯一合法值,同样危险);userDefinedKeys(键名是用户数据):properties、$defs、definitions、patternProperties、dependentSchemas、dependentRequired、dependencies——这些键下的子键是用户自定义的名字(属性名、定义名、正则、依赖属性名),必须全部原样存活;一个叫title的属性是用户命名,不是元数据,不会被误删。只有这些键下的 schema 值继续压缩。
这个"用户键 vs schema 词汇"的区分是 compressSchema 的核心:遍历时用 inUserKeys 标志切换处理策略。
4.2 描述压缩:短句不动,长句留"引导句 + 约束句"
compressDescription 的规则:
- 短句整体保留:描述长度不超过
smallDescBudget(=maxDescLen * 2= 160 字节)时一字不动。注释写明理由:省下那点字节不值得冒"标记词集没认出的约束句被误删"的风险——短描述是常见情况,应该直接通过; - 长描述切句重组:保留第一句非约束句作为"选择引导"(引导句按 rune 边界截断到
maxDescLen= 80 字节,见 capRunes,绝不把 UTF-8 字符切碎成替换符,且尽量回退到词边界,避免发出半截单词),再按原顺序追加每一句承载约束的句子(约束句不受 80 字节上限约束——丢掉它会直接产出非法工具调用); - 约束识别靠 constraintRe 正则:覆盖义务词(must/cannot/shall/require*/reject*)、禁止词(forbidden/invalid/not allowed)、数量约束(exactly one/one of/at least/at most/mutually exclusive)、界限词(max/min/range/between/over/above/exceed*)、格式锚点(format*/iso*/rfc*(允许尾随数字,
RFC3339、ISO8601能匹配)/absolute)。匹配刻意偏向多留:多留一句只损失一点比率,丢一句约束会换来一次非法调用加重试循环,代价远高于省下的 token; - 句界切分很保守:splitSentences 只在"句号 + 空白 + 大写字母"处断句,并有缩写守卫(
e.g.、i.e.、etc.、Node.js以及v1.2、3.14这类尾点都不会被误判为句界),保证e.g. /srv/x这种片段完整存活。
整个变换是确定且幂等的:对自己输出再压一次得到同一字符串。
4.3 工具信封与 schema 的边界
compressDocument 先区分 provider 信封与裸 JSON Schema:{"tools":[…]}、{"functions":[…]}、OpenAI 的 {function: …} 嵌套和裸数组都走 compressToolEnvelope,其中信封层的 description 走描述压缩路径,但信封层的 MCP annotations.title、厂商元数据等键不受 schema 元数据删除规则影响(它们是模型可见的工具元数据);只有声明为 schema 值的字段(如 inputSchema/parameters)才跨入 compressSchema。
五、正确性边界一:结构选择面逐字节不变
shrink/AGENTS.md 定义的第一条正确性边界是:
SelectionProfile(input) == SelectionProfile(Shrink(input).Output)恒成立。
SelectionProfile 提取每个工具对"宿主决定要不要调用它"有结构性影响的面:参数名列表、每参数 enum 取值、required 列表——描述被刻意排除在外(描述正是被压缩的对象)。profileOf 对参数名和 required 做排序,保证比较确定。
这条不变式证明的是什么、不证明什么,文档说得非常克制:它证明结构选择面(names/params/enums/required)逐字节存活;它不证明模型会选到同一个工具——描述是模型可见的,而长描述压缩是有损的。行为级选择等价需要 model-eval 夹具来验证,结果一律保持 inferred。
测试侧有双重证据:
- TestStructuralSelectionSurfaceInvariant:对压缩前后目录各取一次 profile,
reflect.DeepEqual断言相等,并抽查search_files.mode的 3 个 enum 值仍在; - extractTools 同时支持 MCP(
{"tools":[…]})、OpenAI({function: …}数组或扁平)和{"functions":[…]}三种目录形态,schema 分别从inputSchema(MCP)和parameters(OpenAI)下取(schemaOf)。
六、正确性边界二:参数构造保全(选择面之外还要保住"能构造出合法参数")
第二条边界:光保住选择面不够——agent 的参数是从描述里读出来的。shrink/AGENTS.md 的表述:短描述整句保留;长描述保留引导句 + 每个被识别的约束句(must / cannot / required / 界限 / format / ISO / RFC / absolute 等);default、const 和内部 $ref 目标存活。golden 与对抗性一致性测试把这些"构造面"钉死,原则是多留比重试更安全(over-keep is safer than a retry)。
shrink_test.go 中的 TestShrinkPreservesArgumentConstraints 是这条边界的验收用例:构造一个 put_object 工具,描述含填充散文与真实规则,断言压缩输出中下列片段必须存活:
| 断言片段 | 对应的参数构造规则 |
|---|---|
must be an absolute path |
绝对路径规则 |
exactly one of body |
互斥约束(body / body_b64 二选一) |
format must be RFC3339 |
时间戳格式规则 |
e.g. /srv/x |
缩写完整保留(不会被截成 e.) |
同时断言 This trailing clause is filler 这类无规则填充散文应该被丢掉——即压缩真的发生了(ratio > 0),而不是空转。
七、fail-open 与"真的可逆":CCR 恢复存储
7.1 S4 有损 + fail-open 语义
shrink/AGENTS.md 的 Gotchas 第一条:这是 S4 有损、fail-open 变换。成功压缩会改变模型可见字节;任何解析问题、或结果并不更小,输入都原样通过(ratio: 0、无 handle)。Shrink 的调用链是 Shrink → engine.Compress(input, Options{Mode: ModeCompress, Type: "toolschema"}),失败时保留引擎返回的"完整记账、字节相同的透传结果"连同错误一起返回给库调用方。TestShrinkByteSafeOnMalformed 验证非法 JSON 输入字节不变、ratio == 0、无 handle;TestShrinkReturnsPassThroughResultWhenCCRIsUnavailable 验证 CCR 存储不可用时返回"记账完整的原始字节透传"(tokens_after == tokens_before)。
这里有个文档明示的库/CLI 行为差:存储打不开会让库的 Shrink 返回错误,而 CLI 捕获该错误并转发原始字节、不附 handle(runShrink 在 err != nil 时直接 os.Stdout.Write(input),stderr 打印 passed through 说明)——字节安全优先于失败退出。
7.2 恢复路径:内容寻址的持久 handle
可逆性的实现全在 engine/ccr:每次成功的有损压缩,先把原始字节精确提交进持久共享 CCR store,再发布变换后的字节并返回 handle。handle 是内容寻址的(原文字节的 sha256),因此压缩同一 payload 两次得到同一 handle、只存一份(store.go 包注释)。存储实现按平台分裂:宿主平台用本地 SQLite(store_sqlite.go),js/wasm 用纯 Go 内存 map(store_wasm.go),接口一致,引擎无感。
Recover 的行为边界同样有测试钉死:未知 handle 返回 ccr.ErrNotFound——恢复从不猜测(TestRecoverUnknownHandleFails)。跨进程可解析性由 TestShrinkRoundTripsThroughAFreshStore 把关:它特意新开一个同路径的 store 实例(正是新进程里 recover 命令看到的场景),取出字节与原始目录逐字节比对——测试注释直言,旧内存存储时代这个不变式"每次调用都是假的",而一个 handle != "" 的自说自话检查把它藏了十年。
另外注意 ErrBudgetExceeded:本地存储的 payload 预算超限时,新恢复被拒绝在发布有损字节之前,既有 handle 不受影响,调用方必须透传——这正对应 7.1 的透传测试。
八、inferred-only:为什么所有 token 数都是估计值
shrink/AGENTS.md 的第二条 Gotcha 是硬性约定:token 数一律来自引擎的离线计数器(tokens.Default()),因此永远是 inferred,绝不 verified,也绝不对结果做二次投影(re-project)。从源码看这一点贯彻得很彻底:Result 与 Report 都带 Basis 字段且恒为 inferred(Shrink、Lint);Lint 对每个工具独立跑 NewToolSchema().Compress 并只在下压后计数更小时采纳后值,逐工具汇总出总体比率;即使压缩器对某工具返回 ok 但没省字节,该工具的 after 也保持等于 before。CLI 的 lint 表尾 (basis: inferred) 标注与 stderr 报告里的 "basis":"inferred" 都是这条约定的外露接口。
九、与托管网关的关系,以及产品边界
shrink/AGENTS.md 的 Conventions 一节划清了 shrink 在整个 caveman 体系中的位置,值得逐条对照理解:
- 结构压缩器住在引擎里:实现唯一在 engine/compressors/toolschema.go,shrink 复用它、绝不 fork——产品层只有存储配置、目录解析、profile 提取与 CLI 包装;
- 托管网关的适配器不碰它:managed-gateway 适配器把工具数组留在冻结的 prompt-cache 前缀里,从不交给该压缩器;网关另有的 S2 工具搜索/延迟路径不是压缩,两者不要混谈;
- 本地缩减保持 inferred:本地跑 shrink 得到的数字不进任何已验证账目;
- 计费化的前置条件:文档明确,未来若要为这个变换开一条计费路由,需要三样东西——cache-versus-schema 的成本证明、字节级一致的稳定前缀输出、以及一道 eval 闸门。这是把"有损 S4"从本地优化升级成计费功能所需的诚实门槛。
十、构建、测试与验证入口
- 文档约定的构建/测试命令(见 shrink/AGENTS.md):
make product-build PRODUCT=shrink与make product-test PRODUCT=shrink; - Go 侧一致性测试集中在 shrink_test.go,覆盖:压缩确实发生(
ratio > 0、basis == inferred)、跨新 store 的恢复往返、结构选择面不变式、约束句保全、非法输入字节安全、CCR 不可用透传、未知 handle 必败、lint 报告完整性——测试统一用WithStorePath(t.TempDir()…)指向临时库,避免写脏共享的~/.caveman/ccr.db(tmpStore); - npm 启动器侧测试为
node --test --test-force-exit tests/*.test.mjs(package.json),对应 shrink/tests/ 下的package.test.mjs。
小结
caveman-shrink 的价值不在"省了多少 token"(那个数字永远是 inferred),而在它把"有损压缩"这个危险操作关进了两条可测试的边界里:结构选择面逐字节存活(SelectionProfile 不变式 + 测试),参数构造规则不丢失(约束句识别 + 多留策略 + golden 测试)。再叠加 fail-open 透传、内容寻址的持久 CCR 恢复(跨进程 recover)、inferred-only 记账,它给出了一个可复制的范式:当你想对模型可见的输入做有损变换时,先写下不变式,让测试替你证明它,而不是让 ratio 报表替你承诺它。若要进一步深入,建议按 shrink/AGENTS.md 末尾的指引阅读引擎总览 engine/CLAUDE.md 与压缩器实现 engine/compressors/toolschema.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