Caveman 本地运行时架构解析:多进程边界、Engine 四操作契约与 Fail-Closed 设计
本文以 Caveman 仓库的 架构文档 为主体,拆解其"小而多进程"的本地运行时设计:一条 loopback HTTP 请求从编码 Agent 到模型供应商的完整路径、七个进程各自的传输与职责、~/.caveman 下的存储布局、Caveman Engine 的稳定操作边界,以及贯穿全局的失败行为契约。读完本篇,你可以完整理解 Caveman 如何做 base-URL 替换而不改动任何 Agent 代码,以及"宁可转发原始字节、也不发明结果"这一 fail-closed 原则在源码中的具体落点。
本地请求路径:一次 base-URL 替换
Caveman 的本地运行时由一组小型进程构成,它们之间通过**文档化的文件、stdio、loopback HTTP 和 Model Context Protocol(MCP)**相互连接。每个进程只拥有一个明确的边界,因此任何一处失败都可以回退(fallback),而不会"发明"出一个结果来。
原文档给出的请求路径如下:
flowchart TD
A["Existing coding agent or provider SDK"] -->|"provider request"| P["caveman-proxy on loopback"]
P --> R["provider route and credential mapping"]
R --> E["Caveman Engine"]
E --> C["CCR SQLite store"]
E -->|"original or recoverable transform"| R
R --> U["Selected model provider"]
U -->|"response and usage counters"| P
P --> D["Local usage SQLite store"]
P --> A
M["caveman-mcp in agent"] -->|"retrieve handle"| C
代理的本质是一次 base-URL 替换:Agent 代码和供应商请求格式保持原样不动。从 proxy 包文档 可以看到这一点被明确写进包注释——standalone 模式把供应商 base URL 换成本地监听器,应用与托管网关相同的"字节安全"(byte-safe)供应商原生转换(record 模式恒为直通),并把每笔真实花费记录到本地存储,且一律标记为 inferred,从不标记为 verified。
供应商适配器的工作顺序是:匹配允许的路由 → 保持或解析凭证 → 检查请求体 → 应用已启用的转换 → 转发上游 → 解析用量 → 写入本地一行记录。默认监听地址在 proxy/doc.go 中定义为 127.0.0.1:8787,caveman start 在此启动代理,caveman wrap 再把 Agent 指向它。
一个值得注意的安全约束:proxy 配置加载逻辑 会拒绝绑定空主机名、通配符或非 loopback 地址——standalone 代理没有任何入站认证机制,因此配置测试 config_test.go 专门验证了非 loopback 监听地址会被 Load 直接拒绝。
进程模型:七个进程,各守一个边界
| 进程 | 传输 | 职责 |
|---|---|---|
caveman |
终端 | 安装、配置、启动、检查 |
caveman-proxy |
loopback HTTP | 供应商路由、转换、用量捕获、本地 native runtime |
caveman-engine |
stdin/stdout CLI | 直接压缩、检测、恢复、TOON、Pixel 与 eval 命令 |
caveman-mcp |
MCP over stdio | 在 Agent 内提供五个 Engine 工具 |
cavemem |
CLI 或 MCP over stdio | 持久记忆与排序召回 |
caveman-browse |
MCP over stdio 加 Chrome DevTools Protocol | 可访问性快照与浏览器操作 |
caveman-shrink |
stdin/stdout CLI | 工具目录(tool catalog)压缩与恢复 |
二进制定位规则
JavaScript CLI 定位各进程二进制的方式是三级查找:
- 显式的环境变量覆盖
CAVEMAN_*_BIN(如CAVEMAN_PROXY_BIN、CAVEMAN_ENGINE_BIN、CAVEMAN_MCP_BIN、CAVEMAN_BROWSE_BIN、CAVEMAN_SHRINK_BIN); - 然后是
PATH; - 最后是
~/.caveman/bin。
某个二进制缺失时,只禁用由它支撑的命令,其余功能不受影响。这一规则在各启动器中都有对应实现,例如 browse/bin/caveman-browse.mjs 优先读取 CAVEMAN_BROWSE_BIN,并明确拒绝指向 npm launcher 本身而非 Go 二进制的配置。
Wrap 模式更严格:如果把一个 Agent 指向一个不存在的代理,路由会直接断掉。因此交互式运行时提供直接启动代理的选项,非交互式调用方则必须自行确保代理已在运行。
五个 Engine 工具在 Agent 内的形态
caveman-mcp 暴露的工具集在 mcp/engine_tools.go 中精确定义,恰好五个、大小写敏感:
caveman_compress:压缩大文本或工具输出;有损(S4)但可逆,返回压缩文本、推断的 token 比率和一个recovery_handle;caveman_retrieve:按 handle 取回被丢掉的原始内容。工具描述里明确写着它是"最后手段"而非分页 API——省略标记本身就携带从被替换单元精确计算出的事实(如"… 340 rows elided (caveman): all state=charged …"),许多问题根本不需要调用它;caveman_stats:本会话的聚合压缩统计,本地口径,永远是推断值;caveman_toon_encode/caveman_toon_decode:把均匀/表格型 JSON 重编码为 TOON 再放入上下文,是面向 Agent 的无损重编码器。
MCP 服务端框架本身(mcp/server.go)有几个与"绝不损坏恢复数据"直接相关的设计:入站行与单条结果都有 16 MiB 上限,超限以 cave_payload_too_large 拒绝而非无限缓冲;而 caveman_retrieve 通过 ExemptResultCap 标记豁免结果大小上限——因为恢复必须返回逐字节精确的原始内容,若对超大原文 fail closed,被省略的内容将永远无法恢复。
存储布局:~/.caveman 下的默认本地状态
| 路径 | 内容 |
|---|---|
bin/ |
经过校验的配套二进制 |
caveman.db |
本地请求用量、前缀替换缓存、trials 与 learn 数据 |
ccr.db |
精确恢复载荷与带类型的工作记忆对象 |
caveman.yaml |
代理模式、loopback 监听器、供应商端点、优化器开关 |
receipts/ |
本地 native-agent 运行回执(产生时) |
连接型 CLI 状态存放在 ~/.caveman-cloud。凭证优先使用 macOS Keychain,不可用时回退到 owner-only 文件;配置文件只存指针和非机密设置。删除与权限细节见 security and privacy。
源码层面可以确认两份数据库各自的定位:
- 用量库
caveman.db由 proxy/internal/store/store.go 维护,是一个纯 Go SQLite 库,"每个被代理的请求持久化一行"。requests表的 schema 包含了大量缓存与转换追踪字段(cache_prefix_sha256、transform_trace、session_correlation_basis等),且文档注释强调:它记录的每一笔节省值都由生命周期标记为inferred——standalone 是单租户、自证的,从不声称verified。 - 恢复库
ccr.db由 engine/ccr 维护。CCR(Caveman Context Recovery)保存每一个有损(S4)压缩的精确原始字节,以 handle 为键,retrieve(handle)按字节返回原文,引擎压缩的任何东西都不会被销毁。handle 是内容寻址的(原文的 sha256),这使得引擎幂等——同一载荷压缩两次得到同一个 handle,只存储一次。
CCR 存储还有一层平台分裂:宿主平台用本地 SQLite(store_sqlite.go),js/wasm 环境用纯 Go 内存 map(store_wasm.go),因为 modernc.org/sqlite 不为 js/wasm 编译。两者暴露相同的类型与方法,引擎对此无感知。
Engine 边界:四个稳定操作与一个无网演练
Engine 对外暴露四个稳定操作,这是 proxy、SDK、CLI、MCP 服务器和浏览器构建共享的契约(engine/engine.go 的包注释同样如此声明):
Compress:检测、路由、转换、计数并持久化恢复;Retrieve:按 handle 返回精确的原始字节;Detect:确定性地对一个载荷做内容分类;Stats:聚合 CCR 存储的行数据。
Simulate 运行同一套检测器和压缩器但不存储任何字节,报告真实压缩是否需要 CCR。它的估算不能授权一次真实转换。
从 engine.go 的实现看,Compress 是 fail-closed 的:record 模式下永不转换;没有匹配的压缩器、解析出问题、结果不比原文小、或有损结果无法建立恢复时,一律原样直通(无 handle、零比率)。关键的次序约束在代码注释中写明:只有当 CCR 写入成功后才发布转换后的元数据——"调用方绝不能在持久化不可用时收到无 handle 的转换字节"。若 store.Put 失败,所有结果字段保持直通并返回错误。
Simulate 与 Compress 有一处刻意差异:对无 store 的有损(S4)压缩器,Compress fail closed 为直通,而 Simulate 仍会报告预期的缩减量并以 Recoverable=false 表明 CCR 是该转换得以发射的前置条件。这就是文档所说"其估算不能授权 live 转换"的实现基础。
Retrieve 的实现还有一个容易忽略的细节:CCR store 持有两个 ID 空间——压缩产生 blob handle(ccr_…),native runtime 的工具输出掩码产生类型化 OBJECT id(显示为 ccr://<id>)。因此 Retrieve 先查 blob 表,未命中再落到 object 表,都未命中才失败——因为一个无法解析的 handle 会诱发重复的检索尝试,而失败的检索结果本身又可能符合掩码条件。
压缩器本身是纯字节转换,不接触网络、存储或 token 计数;引擎在每次压缩器调用外围提供这些控制。类型化的 CCR 对象(FileObservation、SearchResult、CommandResult 等 13 种,见 store.go)还带 currentness(current/stale/archived)与 lifecycle(hot/warm/cold/archived)两个封闭枚举,未知取值一律 fail closed——适配器可以把未知载荷保留在 CCR 之外,但不得为它们发明检索语义。
恢复路径:确定性替换与显式缺失
两条恢复路径按会话类型划分:
- 非流式 API-key 请求可以使用代理侧处理(在受支持时);
- 流式和订阅认证的 Agent 会话需要 Agent 侧的 MCP 恢复路径。CLI 在对外宣告该路径之前,会检查
caveman-mcp是否存在且已为所选 Agent 安装。
转换后的块会成为后续请求前缀的一部分。Caveman 保存确定性的原文到替换文映射,使同一源块在后续轮次中产生相同的替换字节。替换缓存未命中或写入失败时,返回原始字节。CCR 侧对此有对应的预算控制:当新恢复会超出本地库配置的载荷预算时返回 ErrBudgetExceeded(store.go)——既有 handle 保持完整可检索,调用方必须直通。
Agent-native 事件与本地关联
受支持的宿主集成可以把生命周期事件发送到本地代理拥有的用户专用 Unix socket 或 Windows 命名管道。native runtime 在 Unix 侧暴露"每连接一个请求"的 JSON 协议,Windows 侧提供同等的有界 JSON 协议。native runtime 归一化事件、记录任务契约与决策,并可以把大型工具输出移入类型化的 CCR 对象。
会话标记是本地关联数据:代理在供应商捕获与转发之前验证并剥离它们。无效或有歧义的关联不产生任何关联——从用量库 schema 可见,每行请求默认就是 session_correlation_basis = 'uncorrelated'(store.go)。
失败行为:本地数据路径永远让位于正确的供应商流量
| 失败 | 行为 |
|---|---|
| 未知运行时模式 | 使用 record |
| 未知路由 | 返回 404 |
| 转换输入格式错误 | 转发原始 body |
| 转换输出不比原文小 | 转发原始 body |
| CCR 不可用或已满 | 转发原始 body;不发布 handle |
| 供应商/模型不支持的转换 | 转发原始 body |
| 未知安全级别 | 不运行转换 |
| 缺少供应商价格 | 标记为 unpriced;不猜测 |
| 某路径缺少所需的恢复 MCP | 该路径保持不压缩 |
| 代理端口上有外来进程 | 不重启、不信任它 |
供应商错误仍以供应商错误的形态到达调用方。一次转换失败不会变成合成成功,也不会变成客户端解析错误。这套行为表在源码中逐条有对应落点:record 模式直通在 engine.go,"不比原文小则直通"在 engine.go(after >= before 即放弃并声明零比率),CCR 写入失败保持直通同样在 engine.go。
源码地图
各进程与文档的对应关系(原架构文档的 source map,已转换为仓库根路径):
- Engine:engine/
- 本地代理与适配器:proxy/
- CLI:packages/cli/
- 恢复 MCP:mcp/
- 记忆:mem/
- 浏览器:browse/
- 工具目录压缩器:shrink/
- Agent 配置文件:agents/profiles/
- 公共 schema:packages/shared/contracts/
小结
Caveman 的架构可以概括为三句话:进程间以文档化的文件与本地传输相连,每个进程只守一个边界;Engine 用四个稳定操作加一个无网演练定义了所有调用方共享的契约;所有本地数据路径的失败都统一收敛为"转发原始字节、不发明结果"。理解这套边界划分后,你可以从 engine/engine.go 与 engine/ccr/store.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 StartedRust0623
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