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/cavemancaveman 项目的mem/模块cavemem实现了跨会话的持久化 Agent 记忆记忆原文存入本地 SQLite 作为唯一事实来源召回用确定性 BM25 打分并在保守阈值下过滤命中项再经压缩引擎处理后注入——既保证tokens_added等成本数据是诚实的推断值又让被压缩丢弃的细节可通过 CCR内容恢复句柄完整还原。读完本文你可以理解 cavemem 的五个核心操作remember/recall/supersede/history/forget的底层实现、MCP 工具与 CLI 的完整用法、单写者 SQLite 并发纪律以及围绕失败即不召回、永不丢记忆、全程 inferred设计的一系列诚实性不变量。模块定位与核心设计cavemem 解决的是 Agent 在多个会话之间记不住事的问题。它的设计可以概括为三层分工持久层本地 SQLite 数据库保存原文记忆raw memories这是 source of truth。原文在任何引擎调用之前就已落盘因此压缩失败、引擎异常都不可能导致记忆丢失。召回层recall用确定性 BM25 对全部现行记忆打分低于保守阈值DefaultThreshold 0.1见 mem/store.go的命中直接丢弃——离题查询召回空而不是猜测注入噪声。压缩层每个召回命中通过 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.goCREATE 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.goPRAGMA table_info检测缺失列逐个ALTER TABLE ADD COLUMNSQLite 单语句无法加多列把遗留created_at回填为valid_from并创建三个索引。数据目录与打开方式Openmem/store.go默认在~/.caveman/mem下创建mem.db记忆与ccr.dbCCR 恢复库并尊重CAVEMAN_HOME环境变量Options.InMemory用于测试。目录权限为0o700。单写者纪律是这个模块最重要的工程细节mem.db与ccr.db均以SetMaxOpenConns(1)SetMaxIdleConns(1)打开mem/store.goDSN 由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.5、b 0.75mem/bm25.go分词小写化后按任意非字母数字字符切分完全确定性——同一文本永远得到同一 token 序列保证召回可复现停用词过滤 26 个常见功能词the/is/where/what 等。这使召回保守查询 where is the deploy key 只靠deploy/key命中而不是与无关笔记偶然重叠is/theIDF 形式采用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参数归一Limit为 0 取DefaultLimit 5Threshold为 0 取DefaultThreshold 0.1TokenBudget为 0 取DefaultTokenBudget 2000打分与过滤对全部现行记忆做 BM25 打分保留 ≥ 阈值者按分数降序同分按 ID 字典序保证确定性平局裁决截断到 limit逐条压缩按排名顺序调用engine.Compress。引擎失败时本身 fail-closed——结果保留原始字节与原 token 数召回侧保留该记账而不是替换成 0避免低估注入成本预算打包若TokenBudget UnlimitedTokenBudget内部值-1返回全部压缩命中若排名第一的命中压缩后仍超过整个预算只返回其压缩头 CCR recovery_handle绝不返回整段正文否则构造contextwindow.ItemPriority 编码 BM25 排名委托 engine/contextwindow 的确定性打包器Pack在预算内贪心装填——cavemem 与代理网关共享同一份预算实现。默认 2000 token 预算与 44 万 token 事故DefaultTokenBudget 2000的注释直接记录了事故背景没有预算前recall 会整体加载并返回每条匹配记忆——一次单条 2.5 MB 记忆的召回产生了tokens_added 440,000的单项结果mem/store.go。针对这条超大单命中路径headHitmem/store.go把压缩文本截断到预算内truncateToTokens用二分找最长的、token 数不超预算的字节前缀并回退到 UTF-8 rune 边界同时保证CCR 句柄存在如果引擎直通压缩未留句柄就把原文存入 CCRCompressor: cavemem-head——因为头部截断丢掉了尾部丢失细节必须可恢复。回归测试 mem/recall_budget_test.go 中的TestRecallOversizedSingleHitReturnsHeadAndHandle精确复现了这一场景并断言只返回一个 head、tokens_added不超预算、句柄非空且Recover可取回字节级一致原文TestRecallRespectsTokenBudget则验证多命中贪心打包不越预算TestRecallRejectsUnknownNegativeTokenBudget验证未知负数预算 fail-closed。外部token_budget0哨兵语义这是一个容易踩坑的约定值得单独强调Go 内部TokenBudget 0表示未设置走安全的 2000 默认UnlimitedTokenBudget -1才是无限公共接口CLI / MCP / JS / Python调用方必须显式传token_budget 0才请求无限召回。适配器把外部的 0 映射为UnlimitedTokenBudgetexternalTokenBudgetmem/cmd/cavemem/main.go外部负数一律报错。省略参数永远不解除上限。Remember 边界256 KiB 上限与退出码 65Remember是 fail-closed 的空白文本直接拒绝超过MaxMemoryBytes 256 KiB返回ErrMemoryTooLarge错误文案携带cave_memory_too_large惯用错误 idmem/store.go、mem/store.go。设计立场很明确一条记忆是待召回的事实不是文件倾倒CLI 对该错误以退出码 65EX_DATAERR结束进程日志只走 stderrJSON 走 stdout。两个薄客户端都导出MEMORY_TOO_LARGE_EXIT_CODE 65常量mem/js/index.mjs、mem/py/cavemem.py调用方可以按退出码分支无需解析 stderr。另一个细节如果某文本只以已过期历史版本存在valid_until非空Remember会拒绝并报错而不是把它当成功返回——否则Recall仍会隐藏它声称记住了却不召回就是自欺。supersede / history / forget版本链生命周期Supersedemem/store.go在单事务中替换一条现行记忆。旧行保留valid_until与superseded_by供审计正常召回排除它。fail-closed 细节包括目标 id 必须现行valid_until IS NULL、替换文本不得与当前相同、替换文本的 id 不得已存在、UPDATE的RowsAffected必须恰好为 1防止并发修改导致的竞态覆盖Historymem/store.go沿supersedes向前、superseded_by向后遍历返回包含该 id 的完整版本链最旧 → 最新。断链或成环直接报错绝不返回部分历史Forgetmem/store.go按 id 删除并在同一事务中原子修复相邻血缘指针——被删节点的前驱的superseded_by、后继的supersedes都改接到彼此上。已过期的前驱保持过期删除现行版本意味着忘记这个事实而不是悄悄复活旧版本。对不存在的 id 返回{forgotten: false}而非报错。MCP 服务器与五个 cavemem_* 工具不带子命令或显式mcp运行时cavemem启动 stdio MCP 服务器经mcp.NewServer提供与 caveman-mcp 完全一致的帧格式。工具定义在 mem/cmd/cavemem/main.go工具参数返回 / 错误 idcavemem_remember(text)text必填{id, created_at, basis:inferred}cave_memory_too_large/cave_remember_failedcavemem_recall(query, limit?, token_budget?)query必填limit默认 5token_budget默认 2000显式 0 解除上限{hits: [...], basis:inferred}cave_recall_failedcavemem_supersede(id, text)id、text必填{id, supersedes, created_at, basis:inferred}cavemem_history(id)id链中任一 id{history: [...], basis:inferred}cavemem_forget(id)id必填{forgotten: bool}每个 hit 的结构Hitmem/store.go为id、text压缩后的注入文本、score、tokens_added推断值、basis、recovery_handle可选指向 CCR。MCP 客户端注册配置见 mem/README.md{ mcpServers: { cavemem: { command: cavemem } } }CLI 用法构建并直接操作命令契约见 mem/cmd/cavemem/main.gogo 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, MaxMemoryBytes1)读入防止超大输入撑爆进程超限走退出码 65recall 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 找cavememremember一律走remember --stdin把记忆文本通过 stdin 传递而不是塞进 argv——避开操作系统命令行长度限制也不会在 argv 中暴露文本JS 客户端mem/js/index.mjs用execFile并设maxBuffer: 32 MiB对超大输入二进制可能在读完MaxMemoryBytes1后关闭 stdin此时写完的 EPIPE 不是第二个错误不能覆盖退出码 65 契约Python 客户端mem/py/cavemem.py仅用标准库并双向锁定 UTF-8subprocess.run(..., encodingutf-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_budget0同样是不限量哨兵。对应测试mem/js/tests/client.test.mjs 与 mem/py/tests/test_cavemem.py。诚实性不变量速查把 mem/CLAUDE.md 的 Gotchas 一节完整对照源码后可以汇总为七条工程不变量不变量含义源码依据byte-safe write原文先落 SQLite压缩只在召回时临时发生记忆永不丢mem/store.gosingle-writer store单连接 busy_timeout(5000) WAL迁移走 5 秒预算的RetryOnBusy否则 32 并发写丢 31mem/store.gobounded recall默认 2000 推断 token 总预算超大单命中返回头部 恢复句柄杜绝 440k token 单项召回mem/store.gobounded remember单条 ≤ 256 KiB超限 fail-closedcave_memory_too_largeCLI 退出码 65mem/store.gofail toward nothing低于阈值或零词重叠 → 空召回绝不猜测mem/bm25.gocurrent-only recall被取代版本仅经History可审计不进正常召回mem/store.goinferred-only / reversible成本与分数均为 inferred每个压缩命中携带 CCR 句柄Recover返回字节级一致原文mem/store.go构建与测试按 mem/CLAUDE.md 的 Conventionsmake product-build PRODUCTmem # 构建 Go 核心 make product-test PRODUCTmem # 运行 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的版本链保证事实更新可审计。所有数字标注 inferredMCP、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),仅供参考