news 2026/9/5 19:19:02

caveman cavemem 持久化记忆系统详解:SQLite 存储、BM25 召回与 Token 预算控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
caveman cavemem 持久化记忆系统详解:SQLite 存储、BM25 召回与 Token 预算控制

caveman cavemem 持久化记忆系统详解:SQLite 存储、BM25 召回与 Token 预算控制

【免费下载链接】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 项目的mem/模块(cavemem)实现了跨会话的持久化 Agent 记忆:记忆原文存入本地 SQLite 作为唯一事实来源,召回用确定性 BM25 打分并在保守阈值下过滤,命中项再经压缩引擎处理后注入——既保证tokens_added等成本数据是诚实的推断值,又让被压缩丢弃的细节可通过 CCR(内容恢复)句柄完整还原。读完本文,你可以理解 cavemem 的五个核心操作(remember/recall/supersede/history/forget)的底层实现、MCP 工具与 CLI 的完整用法、单写者 SQLite 并发纪律,以及围绕"失败即不召回、永不丢记忆、全程 inferred"设计的一系列诚实性不变量。

模块定位与核心设计

cavemem 解决的是 Agent 在多个会话之间"记不住事"的问题。它的设计可以概括为三层分工:

  1. 持久层:本地 SQLite 数据库保存原文记忆(raw memories),这是 source of truth。原文在任何引擎调用之前就已落盘,因此压缩失败、引擎异常都不可能导致记忆丢失。
  2. 召回层recall用确定性 BM25 对全部现行记忆打分,低于保守阈值(DefaultThreshold = 0.1,见 mem/store.go)的命中直接丢弃——离题查询召回"空",而不是猜测注入噪声。
  3. 压缩层:每个召回命中通过 engine 的Compress路径压缩,命中携带recovery_handleRecover可取回字节级一致的原文。压缩只发生在召回时刻,是瞬时的。

整个模块输出的tokens_added、匹配分数与basis字段一律标注为inferred(推断值),组件从不声称verified节省。相关背景文档可参考根目录 CLAUDE.md 与 mcp/CLAUDE.md。

目录布局

模块布局与实现职责(源自 mem/CLAUDE.md):

路径职责
mem/store.goRemember/Recall/Supersede/History/Forget/Recover,基于 SQLite + engine;旧 schema 就地迁移
mem/bm25.go确定性分词器 + BM25 打分器(分数非负,使阈值有意义)
mem/cmd/cavemem/MCP 服务器 +remember/recall/supersede/history/forgetCLI 子命令(JSON 输出)
mem/js/ 与 mem/py/薄的 TS 与 Python 客户端,shell 调用二进制(镜像库,不重新实现任何逻辑)

存储层:memories 表、内容寻址 ID 与单写者纪律

表结构与版本链字段

核心 schema 定义在 mem/store.go:

CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, text TEXT NOT NULL, created_at TEXT NOT NULL, valid_from TEXT NOT NULL, valid_until TEXT, supersedes TEXT, superseded_by TEXT );
  • id是内容寻址的:memID(text)取文本 SHA-256 的前 8 字节十六进制,前缀mem_(见 mem/store.go)。因此记住相同文本两次是幂等的——INSERT OR IGNORE保留首次created_at,只存一行。
  • valid_until IS NULL表示"现行版本"。all()查询与Count()都只取现行记忆,这实现了"current-only recall"不变量:被取代的版本永远不进入正常召回。
  • supersedes/superseded_by构成版本链,供History审计。

旧版库通过migrateMemorySchema就地升级(mem/store.go):PRAGMA table_info检测缺失列,逐个ALTER TABLE ADD COLUMN(SQLite 单语句无法加多列),把遗留created_at回填为valid_from,并创建三个索引。

数据目录与打开方式

Open(mem/store.go)默认在~/.caveman/mem下创建mem.db(记忆)与ccr.db(CCR 恢复库),并尊重CAVEMAN_HOME环境变量;Options.InMemory用于测试。目录权限为0o700

单写者纪律是这个模块最重要的工程细节:

  • mem.dbccr.db均以SetMaxOpenConns(1)+SetMaxIdleConns(1)打开(mem/store.go);
  • DSN 由ccr.SQLiteDSN提供,携带busy_timeout(5000)journal_mode(WAL)。代理侧的 spend store 也用同一个 DSN,但强制单连接池——mem/CCR 的纪律更严格;
  • 冷启动迁移是多语句 DDL,在多个全新cavemem进程同时启动时(例如 JS 客户端的Promise.all(facts.map(remember))扇出)可能超过单次busy_timeout,因此迁移步骤包在ccr.RetryOnBusy中,其内部有 5 秒墙钟预算。而运行期单语句写只依赖busy_timeout

为什么这么严格?据 mem/CLAUDE.md 的 Gotchas 记录:没有这套纪律时,32 个并发写只有 1 个落库,其余 31 个以SQLITE_BUSY静默丢失。并发行为由 mem/store_concurrency_test.go 验证。

召回算法:确定性 BM25 + 保守阈值

BM25 打分器在 mem/bm25.go,要点:

  • 参数:经典 Robertson/Spärck Jones 默认值k1 = 1.5b = 0.75(mem/bm25.go);
  • 分词:小写化后按任意非字母数字字符切分,完全确定性——同一文本永远得到同一 token 序列,保证召回可复现;
  • 停用词:过滤 26 个常见功能词(the/is/where/what 等)。这使召回保守:查询 "where is the deploy key" 只靠deploy/key命中,而不是与无关笔记偶然重叠is/the
  • IDF 形式:采用+1平滑形式log(1 + (n - df + 0.5)/(df + 0.5)),保证分数非负,因此固定阈值 0.1 是有意义的过滤线(mem/bm25.go)。

与查询词零重叠的记忆得分为 0,天然低于阈值——这就是"fail toward nothing":不召回比乱猜更安全。

Recall 全流程:压缩、贪心打包与 Token 预算

Recall(query, RecallOptions)的完整流程(mem/store.go):

  1. 参数归一Limit为 0 取DefaultLimit = 5Threshold为 0 取DefaultThreshold = 0.1TokenBudget为 0 取DefaultTokenBudget = 2000
  2. 打分与过滤:对全部现行记忆做 BM25 打分,保留 ≥ 阈值者,按分数降序(同分按 ID 字典序,保证确定性平局裁决),截断到 limit;
  3. 逐条压缩:按排名顺序调用engine.Compress。引擎失败时本身 fail-closed——结果保留原始字节与原 token 数,召回侧保留该记账而不是替换成 0,避免低估注入成本;
  4. 预算打包
    • TokenBudget = UnlimitedTokenBudget(内部值-1),返回全部压缩命中;
    • 排名第一的命中压缩后仍超过整个预算,只返回其"压缩头 + CCR recovery_handle",绝不返回整段正文;
    • 否则构造contextwindow.Item(Priority 编码 BM25 排名),委托 engine/contextwindow 的确定性打包器Pack在预算内贪心装填——cavemem 与代理网关共享同一份预算实现。

默认 2000 token 预算与 44 万 token 事故

DefaultTokenBudget = 2000的注释直接记录了事故背景:没有预算前,recall 会整体加载并返回每条匹配记忆——一次单条 2.5 MB 记忆的召回产生了tokens_added = 440,000的单项结果(mem/store.go)。

针对这条"超大单命中"路径,headHit(mem/store.go)把压缩文本截断到预算内(truncateToTokens用二分找最长的、token 数不超预算的字节前缀,并回退到 UTF-8 rune 边界),同时保证CCR 句柄存在:如果引擎直通压缩未留句柄,就把原文存入 CCR(Compressor: "cavemem-head")——因为头部截断丢掉了尾部,丢失细节必须可恢复。回归测试 mem/recall_budget_test.go 中的TestRecallOversizedSingleHitReturnsHeadAndHandle精确复现了这一场景并断言:只返回一个 head、tokens_added不超预算、句柄非空且Recover可取回字节级一致原文;TestRecallRespectsTokenBudget则验证多命中贪心打包不越预算;TestRecallRejectsUnknownNegativeTokenBudget验证未知负数预算 fail-closed。

外部token_budget=0哨兵语义

这是一个容易踩坑的约定,值得单独强调:

  • Go 内部:TokenBudget = 0表示"未设置",走安全的 2000 默认;UnlimitedTokenBudget = -1才是无限;
  • 公共接口(CLI / MCP / JS / Python):调用方必须显式传token_budget = 0才请求无限召回。适配器把外部的 0 映射为UnlimitedTokenBudgetexternalTokenBudget,mem/cmd/cavemem/main.go),外部负数一律报错。省略参数永远不解除上限。

Remember 边界:256 KiB 上限与退出码 65

Remember是 fail-closed 的:

  • 空白文本直接拒绝;
  • 超过MaxMemoryBytes = 256 KiB返回ErrMemoryTooLarge,错误文案携带cave_memory_too_large惯用错误 id(mem/store.go、mem/store.go)。设计立场很明确:一条记忆是"待召回的事实",不是文件倾倒;
  • CLI 对该错误以退出码 65(EX_DATAERR)结束进程,日志只走 stderr,JSON 走 stdout。两个薄客户端都导出MEMORY_TOO_LARGE_EXIT_CODE = 65常量(mem/js/index.mjs、mem/py/cavemem.py),调用方可以按退出码分支,无需解析 stderr。

另一个细节:如果某文本只以"已过期历史版本"存在(valid_until非空),Remember会拒绝并报错,而不是把它当成功返回——否则Recall仍会隐藏它,声称"记住了"却不召回就是自欺。

supersede / history / forget:版本链生命周期

  • Supersede(mem/store.go):在单事务中替换一条现行记忆。旧行保留valid_untilsuperseded_by供审计,正常召回排除它。fail-closed 细节包括:目标 id 必须现行(valid_until IS NULL)、替换文本不得与当前相同、替换文本的 id 不得已存在、UPDATERowsAffected必须恰好为 1(防止并发修改导致的竞态覆盖);
  • History(mem/store.go):沿supersedes向前、superseded_by向后遍历,返回包含该 id 的完整版本链(最旧 → 最新)。断链或成环直接报错,绝不返回部分历史;
  • Forget(mem/store.go):按 id 删除,并在同一事务中原子修复相邻血缘指针——被删节点的前驱的superseded_by、后继的supersedes都改接到彼此上。已过期的前驱保持过期:删除现行版本意味着"忘记这个事实",而不是悄悄复活旧版本。对不存在的 id 返回{forgotten: false}而非报错。

MCP 服务器与五个 cavemem_* 工具

不带子命令(或显式mcp)运行时,cavemem启动 stdio MCP 服务器,经mcp.NewServer提供与 caveman-mcp 完全一致的帧格式。工具定义在 mem/cmd/cavemem/main.go:

工具参数返回 / 错误 id
cavemem_remember(text)text(必填){id, created_at, basis:"inferred"}cave_memory_too_large/cave_remember_failed
cavemem_recall(query, limit?, token_budget?)query(必填);limit默认 5;token_budget默认 2000,显式 0 解除上限{hits: [...], basis:"inferred"}cave_recall_failed
cavemem_supersede(id, text)idtext(必填){id, supersedes, created_at, basis:"inferred"}
cavemem_history(id)id(链中任一 id){history: [...], basis:"inferred"}
cavemem_forget(id)id(必填){forgotten: bool}

每个 hit 的结构(Hit,mem/store.go)为:idtext(压缩后的注入文本)、scoretokens_added(推断值)、basisrecovery_handle(可选,指向 CCR)。

MCP 客户端注册配置(见 mem/README.md):

{ "mcpServers": { "cavemem": { "command": "cavemem" } } }

CLI 用法

构建并直接操作(命令契约见 mem/cmd/cavemem/main.go):

go build -o cavemem ./mem/cmd/cavemem ./cavemem remember "the deploy key lives in vault under ops/deploy" ./cavemem recall "where is the deploy key" # JSON: { hits: [...], basis: "inferred" } ./cavemem recall "full migration context" 5 0 # 显式 0 token 预算 = 不限量 ./cavemem supersede mem_xxxxxxxx "deploy key moved to vault ops/deploy-v2" ./cavemem history mem_yyyyyyyy # 最旧 → 现行 ./cavemem forget mem_xxxxxxxx ./cavemem # 启动 stdio MCP 服务器

CLI 细节:

  • remember <text>|--stdin--stdin模式用io.LimitReader(stdin, MaxMemoryBytes+1)读入,防止超大输入撑爆进程;超限走退出码 65;
  • recall <query> [limit] [token_budget]:两个数值参数均为非负整数,负数报参数错误;
  • recover <handle>:将某个召回命中的recovery_handle解析为字节级一致原文并写 stdout(针对 cavemem 自己的 CCR 库~/.caveman/mem/ccr.db;注意与全局caveman retrieve读取的是不同 CCR 库);
  • 只有remember/recall/supersede/history/forget/recover会打开数据目录,help与未知子命令不会。

JS / Python 薄客户端

两个客户端是严格的"外壳":shell 调二进制,不在 TS/Python 侧重新实现任何验证、召回或打分逻辑(这正是 mem/CLAUDE.md Conventions 一节强调的约定)。

  • 二进制解析:优先环境变量CAVEMEM_BIN,否则从 PATH 找cavemem
  • remember一律走remember --stdin,把记忆文本通过 stdin 传递而不是塞进 argv——避开操作系统命令行长度限制,也不会在 argv 中暴露文本;
  • JS 客户端(mem/js/index.mjs)用execFile并设maxBuffer: 32 MiB;对超大输入,二进制可能在读完MaxMemoryBytes+1后关闭 stdin,此时写完的 EPIPE 不是第二个错误,不能覆盖退出码 65 契约;
  • Python 客户端(mem/py/cavemem.py)仅用标准库,并双向锁定 UTF-8subprocess.run(..., encoding="utf-8")——否则 Windows 上 locale 的 ANSI 代码页会让remember("cafe\u0301")在到达 Go 二进制之前就抛UnicodeEncodeError,召回非 ASCII 记忆时返回乱码;
  • API 镜像:remember(text)recall(query, limit?, token_budget?)supersede(id, text)history(id)forget(id),其中token_budget=0同样是不限量哨兵。

对应测试:mem/js/tests/client.test.mjs 与 mem/py/tests/test_cavemem.py。

诚实性不变量速查

把 mem/CLAUDE.md 的 Gotchas 一节完整对照源码后,可以汇总为七条工程不变量:

不变量含义源码依据
byte-safe write原文先落 SQLite,压缩只在召回时临时发生,记忆永不丢mem/store.go
single-writer store单连接 +busy_timeout(5000)+ WAL;迁移走 5 秒预算的RetryOnBusy,否则 32 并发写丢 31mem/store.go
bounded recall默认 2000 推断 token 总预算;超大单命中返回"头部 + 恢复句柄",杜绝 440k token 单项召回mem/store.go
bounded remember单条 ≤ 256 KiB,超限 fail-closedcave_memory_too_large,CLI 退出码 65mem/store.go
fail toward nothing低于阈值或零词重叠 → 空召回,绝不猜测mem/bm25.go
current-only recall被取代版本仅经History可审计,不进正常召回mem/store.go
inferred-only / reversible成本与分数均为 inferred;每个压缩命中携带 CCR 句柄,Recover返回字节级一致原文mem/store.go

构建与测试

按 mem/CLAUDE.md 的 Conventions:

make product-build PRODUCT=mem # 构建 Go 核心 make product-test PRODUCT=mem # 运行 Go 核心测试 make test # 根级测试,额外运行镜像的 JS/Python 包装器测试

核心 Go 测试覆盖:召回预算与哨兵语义(mem/recall_budget_test.go)、并发写(mem/store_concurrency_test.go)、存储行为(mem/store_test.go)、BM25 打分(mem/bm25_test.go)以及 CLI 分发表(mem/cmd/cavemem/main_test.go)。

小结

cavemem 把"Agent 长期记忆"做成了一个纪律严明的本地系统:SQLite 单写者存储保证原文不丢,确定性 BM25 + 0.1 阈值保证"宁可空召回、不注入噪声",2000 token 默认预算 + 头部截断 + CCR 句柄保证注入成本诚实且可恢复,supersede/history/forget的版本链保证事实更新可审计。所有数字标注 inferred,MCP、CLI、JS、Python 四个面共享同一份 Go 实现——这套设计对任何需要给 Agent 加"可信记忆层"的系统都有参考价值。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 19:11:31

SpringBoot+Vue招聘系统实战:架构设计、核心模块与避坑指南

简介&#xff1a;这是一套面向Java全栈开发者与高校计算机专业学生的完整企业级招聘系统实战项目&#xff0c;聚焦人力资源数字化管理场景&#xff0c;解决传统招聘流程中职位发布低效、简历筛选粗放、面试调度混乱等痛点。资源包含前后端分离的完整源码&#xff08;64个Java后…

作者头像 李华
网站建设 2026/9/5 19:08:04

Roo Code 实战:5种AI模式让VS Code里的AI编码助手各就各位

Roo Code 实战&#xff1a;5种AI模式让VS Code里的AI编码助手各就各位 【免费下载链接】Roo-Code Roo Code gives you a whole dev team of AI agents in your code editor. 项目地址: https://gitcode.com/GitHub_Trending/ro/Roo-Code Roo Code 是一款装进 VS Code 的…

作者头像 李华