关于 cuba-memorys
MemoryIndustry(原名 cuba-memorys)是面向编码 Agent 的长期记忆 MCP 服务,使用 PostgreSQL 和 pgvector 构建可搜索、可纠正的知识图谱。它提供多种检索方式、代码图谱及可篡改检测的审计,并可通过共享数据库或同步在机器间共享记忆。
主要功能
- 01PostgreSQL 与 pgvector 持久化
- 02BM25、向量与重排检索
- 03可纠正的长期记忆知识图谱
- 04带篡改检测的审计记录
- 05CLI 管理及多客户端共享守护进程
- 06共享数据库或 Git 同步记忆
适用场景
- 01跨编码会话检索代码库上下文
- 02在多台机器间共享 Agent 记忆
- 03审查记忆来源及审计记录
- 04通过 CLI 配置并诊断 MCP 客户端
MCP 相关
向 MCP 客户端提供长期记忆与代码图谱工具。可用 stdio 启动独立进程,也可运行共享守护进程,通过 loopback HTTP 服务多个客户端。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y cuba-memorysDATABASE_URLPostgreSQL connection URL (e.g. postgresql://user:pass@localhost:5432/brain)CUBA_PROJECT_FILTERSet to 'off' to disable v0.8 project scoping filters (admin/debug only).CUBA_SYNC_DIRDirectory used by cuba_sync export/import (default ./.cuba-memorys/).CUBA_JUDGEcuba_juez backend selector: claude_cli | anthropic_api | heuristic | auto (default auto).CUBA_JUEZ_CLISubprocess CLI used by ClaudeCodeJudge (default 'claude').CUBA_JUEZ_MODELModel name passed to the judge backend (default 'claude-haiku-4-5').ANTHROPIC_API_KEY密钥API key for AnthropicApiJudge (only used when feature 'anthropic-api' is built in).CUBA_REM_RELATION_BATCHIsolated entities the REM cycle scans for relations per run (default 5, 0 disables).CUBA_RERANKER_PATHDirectory holding the bge-reranker ONNX model; enables cross-encoder reranking (+93% nDCG on GPU).CUBA_RERANK_FIXED_SHAPEPad reranker batches to a constant 512 tokens. Defaults on for GPU builds, off on CPU.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
MemoryIndustry
原名 cuba-memorys。同一守护进程、同一组 cuba_* MCP 工具,新产品名称。
面向 AI 编码 Agent 的长期记忆。 一个 MCP 服务,为你的 Agent 提供知识图谱,让它能够搜索、推理并接受纠正——这样它就不会在会话之间忘记你的代码库。
使用 Rust 编写。由 PostgreSQL + pgvector 提供支持。31 个 MCP 工具(设置 CUBA_DOCS=1 时为 32 个)、28 条 CLI 命令。以下所有数字均来自一个真正测量其声称指标的基准测试(自 v0.12 起)。(之前的基准测试并没有做到这一点。参见测量结果,以及那个说谎的基准测试。)
安装
pip install memory-industry # or: npm install -g memory-industry
claude mcp add memory-industry -- memory-industry
# Previous names still install the same binary:
# pip install cuba-memorys
# npm install -g cuba-memorys
这就是全部的设置步骤。首次运行时,它会通过 Docker 部署 PostgreSQL 18 + pgvector 容器并初始化架构。Docker 必须处于运行状态。 cuba-memorys 命令仍是一个二进制别名。
Cursor / Windsurf / VS Code / Zed
{
"mcpServers": {
"memory-industry": {
"command": "memory-industry"
}
}
}
无需设置 DATABASE_URL。也可以运行 cuba-memorys setup(或 memory-industry setup),它会为找到的每个客户端写入配置;之后运行 cuba-memorys setup check,检查各客户端配置是否存在不一致。这才是真正会导致故障的问题(两份配置、两个嵌入维度,搜索却有一个悄然失效)。
使用自己的 PostgreSQL
{
"mcpServers": {
"memory-industry": {
"command": "memory-industry",
"env": { "DATABASE_URL": "postgresql://user:pass@localhost:5432/brain" }
}
}
}
需要启用 vector 和 pg_trgm 扩展。cuba-memorys doctor 会告知你是否缺少任何扩展。
使用一个共享守护进程,而不是每个客户端运行一个进程
stdio 会为每个客户端启动独立进程,而且每个进程都会加载各自的模型副本——嵌入模型、重排器和 NLI 加起来要占好几个 GB。三个编辑器窗口就意味着三份副本;在一台 16 GB 的笔记本电脑上,这就占满了整台机器。
serve 只加载一次模型,并通过环回 HTTP 响应每个客户端的请求。这也是 2026-07-28 MCP 规范最终确定的模式:无需会话握手,每个请求都包含完整信息。
cuba-memorys serve # 127.0.0.1:8787 by default
cuba-memorys serve 127.0.0.1:9000 # or pick the address
memory-industry serve 是相同的命令。让每个客户端都连接到它,并为每个客户端分配独立的 Mcp-Client-Id,以便各自的会话保持分离——否则,一个窗口中的 jornada start 会成为下一个窗口的活动会话:
{
"mcpServers": {
"memory-industry": {
"type": "http",
"url": "http://127.0.0.1:8787/mcp",
"headers": { "Mcp-Client-Id": "editor-window-1" }
}
}
}
GET /health 会报告运行时长、数据库可访问性,以及已连接过的客户端数量。status 有三个值——ok、starting、degraded——而 HTTP 状态码始终是 200:如果返回 503,就会把一个仍能提供词法搜索、也仍能通过唯一可解释问题所在的端点提供服务的守护进程移出轮转。请根据 status 设置告警,而不是根据状态码。
GET /identity 和 POST /pair 用于让客户端确认它连接到的是正确的守护进程,并与之配对。GET /identity 会向任何人返回 {service, host_id, name};如果请求中提供了凭据 ID 和 nonce,还会返回一份证明,只有持有该凭据哈希的主机才能计算出这份证明(如果凭据已撤销或未知,则返回 revoked 或 unknown,不提供证明)。POST /pair 会兑换配对码,并返回一份只有知道该配对码的主机才能计算出的 MAC。两者都会拒绝带有转发标头或外部 Origin 的请求;/pair 的请求大小最多为 4 KiB,所有失败都会以相同的 401 {"error":"pairing failed"} 响应,并将失败计入与 /mcp 相同的按地址限流计数器(每分钟 10 次)。
在 Authorization: Bearer 标头中提供 CUBA_HTTP_TOKEN 后,响应还会包含 runtime:mode、resource_tier、嵌入器、重排器和 NLI 的 {state, device, reason} —— device 表示会话实际打开的位置(gpu,或在 GPU 提供程序拒绝启动时为 cpu,且 reason 中会说明提供程序给出的原因);对于尚未打开会话或从未打开过会话的模型,值为 null —— gpu{build, degraded, placement},其中 placement 仅反映计划中的设置,而提供程序拒绝启动时 degraded 也会变为 true;以及 llm{configured, backend, model, base_url} —— 无需阅读源码,就能借此查明重排器实际运行的位置。不提供令牌时,这些内容都不会返回,graph_db.last_error 也不会返回,因为其中可能包含内部主机和端口。无论是否提供令牌,都不会输出密钥、DATABASE_URL 或模型路径。
只要绑定到回环地址以外的地址,就必须提供令牌 —— 默认情况下,守护进程会在没有身份验证的情况下提供整个图谱;如果绑定到可路由地址,令牌长度必须至少为 32 个字符。
端口会先打开,然后加载模型,只有全部加载完毕后,守护进程才会宣布自身并开始提供服务。先绑定端口可以快速诊断问题:同一端口上的第二个守护进程会立即失败,而不是先花两分钟加载,之后才发现端口已被占用。加载期间到达的连接会在内核积压队列中排队,因此客户端等待后会得到实际响应,而不是收到连接被拒绝的错误。如果加载时间超过 MEMORY_INDUSTRY_WARM_BEFORE_SERVE_SECS(180 秒),守护进程仍会开始提供服务,/health 会显示 starting,所有要求重排的搜索结果都会标记为降级。在 stdio 模式下,如果客户端等待 30 秒后放弃,却没有关闭 stdin,过去会留下一个被遗弃的多 GB 进程,并继续占用它已加载的所有模型;现在,如果在 MEMORY_INDUSTRY_HANDSHAKE_TIMEOUT_SECS(60 秒)内没有收到握手,stdio 就会退出(设置为 0 或 off 可禁用)。
这个队列只有在客户端能比加载过程等得更久时才有帮助:如果客户端自己的等待时限更短 —— 同步对等端会在 CUBA_HANDLER_TIMEOUT_SECS(30 秒)后放弃 —— 调用仍会被中断,只不过得到的是超时而不是连接被拒绝。这样的诊断更好,但并不是设置队列原本要提供的实际响应。/health 用于判断守护进程处于哪种状态,它回答两个不同的问题。只要它有响应,就表示端口正在提供服务;如果即将调用的操作不会触及模型(例如同步获取操作只会移动行),这就足够了。响应中的 ready: true 则表示模型也已完成加载;在进行要求重排的搜索前,或者不希望首次搜索承担模型加载开销时,应等待此状态。第一种等待有时限,第二种没有:无论模型处于什么状态,守护进程都会在 MEMORY_INDUSTRY_WARM_BEFORE_SERVE_SECS 规定的时间内打开端口;但如果预热超过这个上限,守护进程就会继续提供所有服务,并在所需时间内一直保持 ready: false。因此,守护进程运行正常并能响应请求时,ready: true 仍可能迟迟不会出现。如果等待时没有为自己设置时限,健康的机器也会让你一直等下去。
语义嵌入与模型(推荐)
没有模型时,嵌入基于哈希:结果是确定性的,但在语义上毫无意义。搜索仍可通过词法和 BM25 分支正常工作,但不会理解*含义*。只需一条命令,即可在任何操作系统上安装模型和 ONNX 运行时——无需 Shell 脚本,也无需手动设置 ORT_DYLIB_PATH:
cuba-memorys models all # embeddings + NLI + reranker + runtime
cuba-memorys models embed # just the embeddings model (~113 MB)
cuba-memorys models all --gpu # GPU runtime, if you have one
cuba-memorys doctor # confirms what loaded
所有文件都会放在 ~/.cache/memory-industry/(或重命名前安装的 ~/.cache/cuba-memorys/)中,并会自动找到。只有运行 models 时才会下载模型——不会在你不知情的情况下下载任何内容。下载会先写入 <dir>.downloading,只有在所有文件下载完成并通过验证后才会变为 <dir>,因此下载失败不会留下空目录供加载器选用。
将旧缓存迁移到新名称下。 此版本仍会读取 ~/.cache/cuba-memorys/ 根目录,但每个文件会自行决定使用哪个根目录;在新名称下创建任意一个条目(启动时创建的 pgpass_app,或运行 delete 创建的 undo/)就足以让 models 重新下载旧根目录中已有的内容。当旧根目录中仍有内容时,doctor 会发出警告。只需一条命令即可迁移:
memory-industry cache migrate # the plan: touches nothing
memory-industry cache migrate --apply # moves it
仅存在于旧根目录中的条目会被迁移;两边都有相同条目的,会删除旧副本。内容不同的条目会保留二进制当前读取的副本——对于 pgpass_app,是正在使用的 pgpass 旁边的副本;对于模型目录,则是实际包含模型的副本——另一份会在新根目录中另存为 <name>.legacy-YYYYMMDD,绝不会被删除。若某个文件正被另一个进程占用(在 Windows 上,运行中的守护进程已加载的运行时也算),则会跳过该文件并以非零状态退出:停止守护进程后重新运行。已迁移的内容会保留,再次运行时会提示无需迁移。
bge-m3(1024-d)在西班牙语上的表现优于 e5-small,但不再声称差距有多大(旧的 +21 nDCG 数值来自有问题的基准测试)。它需要进行维度迁移(scripts/migrate-embedding-dim.sh 1024),并设置 CUBA_EMBED_MODEL=bge-m3 CUBA_POOLING=cls。
模式:local · red · completo
CUBA_MODE 是一个预设项,可同时设置数据库、模型和出站网络,让你只需选择一个名称,而不用逐一配置十几个环境变量:
CUBA_MODE | 数据库 | 功能 | 出站网络 |
|---|---|---|---|
local(默认) | 本机上的 Docker | 按安装情况提供嵌入和 NLI | 无 |
red | 共享的托管 Postgres(设置 DATABASE_URL 并使用 sslmode=require) | + 每个节点的来源信息、机器间实时同步 | 无 |
completo | 由 DATABASE_URL 决定 | + 重排器(如有 GPU 则使用 GPU)+ cuba_docs | cuba_docs |
两台机器,共用一份记忆。 让两台机器都指向同一个托管 Postgres(Neon 或 Supabase 免费层都支持 pgvector,足以容纳 36 MB 语料库很多次),分别通过 CUBA_NODE_NAME 设置名称,并使用 CUBA_MODE=red。一台机器写入的内容,另一台都能读取;每条记忆都会记录其来源机器(origin_node)。如果没有共享数据库,cuba_sync 可以通过 git 仓库完成相同的工作——请参阅通过 git 在机器间同步。不要将自己的 Postgres 端口暴露到互联网——请使用提供 TLS 的托管服务商,或 Tailscale 这样的专用网络。
共享时实现真正的隔离。 共享数据库正是行级安全不再只是摆设的地方。以管理员角色运行一次 cuba-memorys secure,即可创建启用了 RLS 和追加式审计强制执行的非超级用户 cuba_app,然后使用 CUBA_SKIP_MIGRATIONS=1 将运行时指向该用户。cuba-memorys doctor 会报告运行时角色是否为超级用户(超级用户会绕过所有这些保护)。
获取 GPU 运行时只是其中一半:二进制文件本身必须使用 --features cuda 构建,否则 gpu::configure() 不会注册任何提供程序,重排序器也会在 CPU 上运行。这并非假设——以下是在 6 核笔记本电脑上测得的 50 个候选项重排序开销,使用 cargo run --release --example rerank_bench 测量:
| 构建方式 | 50 个候选项,长度混合 | 是否在 20 s 预算内? |
|---|---|---|
CPU,with_intra_threads(2) | 106,9 s | 否——已计算分数,随后被丢弃 |
| CPU,物理核心数 | 61,0 s | 否 |
--features cuda | 4,1 s | 是 |
无论哪种方式,排序结果都相同——CPU 和 GPU 对每个候选项的判断一致,分数仅在小数点后第五位有所不同。在任意机器上运行 rerank_bench,即可查看重排序器是否能在该机器的预算内完成,还是在悄悄丢弃计算结果;cuba-memorys doctor 则会报告当前构建是否包含 GPU 提供程序。
**本节以前曾写道“每个模型都会悄悄在 CPU 上运行”,这让人以为使用 --features cuda 构建后,三个模型都会在 GPU 上运行。实际上只有重排序器会。**嵌入器以动态量化的 INT8 格式随附,这意味着它包含 96 个 DynamicQuantizeLinear,后接 144 个 MatMulInteger——CUDA 提供程序没有为这两种算子注册内核,因此无论如何构建,ONNX Runtime 都会将它们分配到 CPU 上。为该会话注册 CUDA 毫无益处,却占用了 VRAM 分配区,而模型从未在其中执行计算:**模型的全部 544 MB 权重都留在主机 RAM 中,却占用了 374 MiB。**NLI 交叉编码器则面临相反的问题——它采用 FP32,并因此受限于此,因为上游文档指出 mDeBERTa 不支持 FP16,而 INT8 构建会返回确信无疑的错误蕴含结果。
因此,现在会按模型而不是按进程决定放置位置,且只有重排序器会请求使用 GPU。在这次测量所用的 6 GB 显卡上,守护进程的 VRAM 占用从 5228 MiB 降至搜索时的 2950 MiB、空闲时的 0——采用下方 占用空间 中的两个可选步骤后,还可降至 1460 MiB。
各个环境变量(CUBA_DOCS、CUBA_RERANKER_PATH……)始终会覆盖预设值。
它实际做什么
大多数记忆服务都是附加了嵌入的键值存储。这个服务对记忆分为四类,因为心理学文献指出,它们是四种不同的东西,且衰减方式各不相同:
| 存储内容 | 如何增强 | |
|---|---|---|
| 语义记忆 | 关于实体的事实——“所有端点都是异步的” | 访问(Hebbian/BCM、Oja 1982) |
| 情景记忆 | 包含参与者和时间的事件——“我们在周二发布了 v2” | 幂律衰减(Tulving 1972、Wixted 2004) |
| 程序记忆 | 此处如何做事——有成效记录的操作流程 | 成功,而非访问(ACT-R) |
| 工作记忆 | 与当前会话绑定的临时笔记 | 随会话结束而清除 |
程序记忆使用独立的数据表,而不是作为第九种观察类型,有一个具体原因:ACT-R 将陈述性记忆(通过访问强化)与程序记忆(通过成功强化)区分开来。如果把操作流程作为观察记录,那么一个因为总是失败而被频繁查阅的流程,其重要性反而会不断上升。它按 Wilson 下界排序,因此,1/1 次成功得分为 0.21,而 47/50 次成功得分为 0.84——侥幸成功一次不会胜过长期的成功记录。
检索
对三种信号进行混合 RRF 融合(k=60,Cormack 2009)——全文搜索、BM25(ts_rank_cd)和 pgvector HNSW——并通过熵路由加权,随着查询的 Shannon 熵升高,权重会从偏重关键词转向偏重语义。
默认以 compact 格式返回答案:键名缩写,内容截断至 1200 个字符。令牌数减少 30%,nDCG 还略有提升——在 221 个带 ID 评分的问题上进行测量,提升 +0.0090,配对 95% 区间为 [+0.0024,+0.0166]。格式实际上无法改变文档的排序;它改变的是,在评分之前,有多少文档能保留在响应令牌预算内。默认 5000 令牌预算下,verbose 格式占用 5286 个令牌,尾部内容会被截掉;compact 格式占用 3723 个令牌,可以完整保留。传入 "format": "verbose" 可查看每个分支的完整得分明细。
真正有效的验证
cuba_faro mode=verify 会根据存储的内容核验声明。过去,它通过与检索到的证据的余弦相似度为声明打分,但这行不通——相似度衡量的是文本在谈论什么,而不是它断言了什么。“cuba-memorys 是用 Rust 编写的”和“……用 Java 编写的”在向量空间中几乎相同。在实时语料库上的测量结果显示,错误声明得分为 0.61,正确声明为 0.59。
蕴含判断与相似度是不同的问题,需要某种能够阅读内容的机制。现在,本地交叉编码器会判定每条证据——supports / contradicts / unrelated——置信度则根据判定结果计算,并按对应证据的相似度加权。同一语料库,改进后:
| 声明 | 之前(余弦相似度) | 现在 |
|---|---|---|
| “用 Rust 编写”(正确) | 0.59 | 0.995 · 已验证 |
| “用 Java 编写”(错误) | 0.61 | 0.00 · 矛盾 |
| “最好的西班牙海鲜饭要用藏红花”(无关) | 0.45,附带 10 条“证据” | 0.00 · 未知,无证据 |
主题相关不等于支持,unrelated 不计入任何一方。
判定器是本地运行于 ONNX 上的 mDeBERTa-v3-base-xnli:支持 100 种语言,每次判定约 50 ms,无需 API 密钥、无需联网、无需付费。这一点很重要——该语料库约 75% 为西班牙语,而大家首先会选用的仅支持英语的 NLI 模型,会在四条记忆中有三条上悄无声息地失效。运行 cuba-memorys models nli 安装;cuba-memorys doctor 会告诉你它是否已加载。
如果没有它,验证会回退到 LLM(通过采样使用 MCP 客户端自身的模型、本地 claude CLI,或 Anthropic API);如果这些都不可用,则会如实返回 unknown,而不是捏造判定结果。
它不会做两件事。它不会依据薄弱证据确认声明:蕴含置信度必须超过 0.80,而矛盾判定只需达到 0.60,因为确认错误记忆与怀疑正确记忆的代价并不相同。而在无法判定时,它会明确说明,而不是返回数值最大的那个结果——对三分类头取 argmax,会轻易把明明错误的声明标为 supports,而它确实曾这样做过。
经过校准的弃答机制
分布外检测门会拒绝语料库无法回答的查询。阈值不是一个魔法常数:通过 Ledoit-Wolf 协方差收缩和保形分位数,使用 cuba-memorys calibrate --dataset <questions.jsonl> --apply 根据你自己的语料库进行校准并持久化(必须提供数据集——否则命令会拒绝执行)。(理论上的 χ² 阈值拒绝了 100% 可回答的查询。在这里,无分布假设校准并非可有可无。)
通过 git 在机器间同步
CUBA_MODE=red 可让两台机器共用一个数据库。cuba_sync 是另一种方式,适用于彼此永远无法连接的机器:图谱会写出为可提交的 JSON 文件,并可在另一端读回。
cuba-memorys sync export # write the bundle under .memory-industry/ (or $CUBA_SYNC_DIR)
cuba-memorys sync import # read one back in
cuba-memorys sync diff # entities on disk vs entities in the database
cuba-memorys sync status # which bundles this machine has already imported
cuba-memorys hook install # export after every commit, import after every checkout, merge and rebase
同样的四种操作是 cuba_sync action=export|import|diff|status。一个捆绑包包含每个实体对应的一个 JSON 文件,文件中包含该实体的观察记录;此外还有 episodes/YYYY-MM/、errors/、decisions/、relations.json、projects.json、tombstones.json 和 manifest.json——内容包括当前活动项目以及所有未绑定到项目的内容,除非你传入 --scope all。除非你指定 --with-embeddings,否则不会包含嵌入向量:它们占用大部分字节,而且可以重新计算。一个捆绑包只会导入一次,清单哈希涵盖其中每个文件的内容——因此,未更改的捆绑包会被跳过,而手动编辑过的实体文件会被视为新捆绑包,而不是被悄悄忽略。导出时只会写入内容有变化的文件:如果上次导出后数据库没有任何写入,导出不会改动任何内容,包括 manifest.json;因此,它的 exported_at 表示捆绑包上次发生变化的时间。
**hook install 也会在合并和变基后导入。**它会写入四个钩子:post-commit 导出,post-checkout 导入,post-merge 导入,post-rewrite 在变基后导入。运行 git merge 时,git 不会运行前两个钩子;运行 git rebase(在含有自身提交的分支上执行 git pull --rebase 时会运行)时,则不会运行 post-merge。因此,如果没有最后两个钩子,合并或变基后的捆绑包就永远不会写入数据库,而下一次提交会将数据库导出并覆盖它,丢掉另一方的行。githooks(5) 指出,post-rewrite“由重写提交的命令调用(使用 --amend 调用的 git-commit,以及 git-rebase)”,其第一个参数为 amend 或 rebase;在 amend 后它不会执行任何操作,因为被 amend 的提交是这台机器自己的导出。git 在合并时停止后,不会运行 post-merge;结束合并的提交有第二个父提交,而在该提交上,post-commit 会先导入解决冲突后的结果,再进行导出。之前就安装了?请再次运行 hook install。
**hook install 会记录它解析出的目录。**每个钩子都会将 CUBA_SYNC_DIR 设为 install 解析出的目录——相对路径会相对于仓库根目录解析,而不是相对于你运行命令的目录——因为在 IDE、GUI 客户端或 cron 作业中进行的提交会使用其自身的环境;如果没有这个固定值,提交就会将内容导出到合并驱动程序未查找的位置。更改变量后,再次运行 hook install:它会替换自己的代码块,而不是保留第一次写入的代码块。.gitattributes 中的行只会以 git 能识别的方式写入:相对于工作树顶层,使用 /,且不带 . 或 ..(名称中含空格时会加引号);它会替换先前安装留下的任何行。以前,./dir、绝对路径或 Windows 上的 data\sync 会按原样写入,无法匹配,却会被报告为 added。即使 CUBA_SYNC_DIR 位于仓库之外,钩子仍会安装——它们会从共享文件夹导入并向其中导出——但不会安装合并驱动程序,也不会写入任何行,因为 git 不会合并工作树之外的文件;install 会说明原因:.gitattributes: skipped — <path> is outside this repo; git never merges it。
**合并驱动程序会合并它识别的内容,其余部分留给你处理。**实体、关系、项目、片段、错误和决策会根据它们在同步目录下的位置进行识别(如 entities/<file>、relations.json、episodes/<YYYY-MM>/<file>,等等),无论该目录叫什么,并按 id 合并。tombstones.json 会合并为一个以 (table_name, row_id) 为键的集合:保留双方的删除记录,各保留一次;如果双方都删除了同一行,则以较晚的删除记录为准。facts.json、procedures.json、artifacts.json 和 source_trust.json 会按导入冲突时使用的键合并——fact_id、id、(path, project_id)、source——如果双方都包含某行,则保留较新的那一行:已关闭的事实优先于未关闭的事实;过程采用较新的 updated_at(其成功和失败计数取两者中的较大值);制品采用较新的 CRDT 时钟;来源的每项信任计数都取最高值。manifest.json 会在双方的 project_id、with_embeddings、embedding_dim、embedding_model 和 schema_version 一致时采用你的一方——导入会依据这些字段执行操作,但不会检查它们是否与文件匹配;其余内容都是派生数据,下次 sync export 时会重写。如果其中任一字段不同,就会产生冲突,提示信息会指出字段名称。embeddings.bin.zst、任何其他文件,以及双方任一侧无法完整解析或包含此版本不认识的字段的文件,都会导致程序以非零状态码退出,git 因而会保留冲突。以前,程序会在不写入文件的情况下以 0 状态码退出,git 会将其视为“已合并”:保留你的一方,丢弃另一方,且不留下任何标记。
删除现在会传播,并会在可能连带删除其他内容时停下。 删除一行会记录一个墓碑,接收端只删除其中列明的 ID。此前,删除并不是到达得慢——而是被撤销了:对端仍保留该行,将它导出,它便在下一轮往返中重新出现。实体墓碑最危险,因为删除一个实体会级联删除所有依附于它的内容。只有当这台机器上该实体下没有发送方未列明的观察或事件时,才会应用该墓碑;否则会暂缓应用,并在 tombstones_withheld 中报告。对端有 3 个子项的实体墓碑,不应在本端删除 300 个子项。
而且,数据包不能悄悄清空你的数据。 如果其中的墓碑会删除至少 25 行,并且超过本机观察总数的 10%,导入就会拒绝执行,并要求设置 confirm=true。远程清空和大量合理清理看起来一模一样,区别只在于这是否是你的本意。绝对数量下限和比例同样重要:对于只有一条观察的数据库,单看比例会要求用户确认删除这一条;而一个在日常整理时就会触发的保护机制,大家学会通过传入 confirm=true 来绕过它后,就形同虚设了。
conflict=merge 并不会合并内容,现在会明确说明这一点。 merge 和 skip 是同一种策略:本地缺少的行会被导入;对于已存在但内容不同的行,则保留先前已有的版本,并丢弃传入文本。变化在于不再默不作声——导入会统计这些行,并在 diverged 中报告,附上它们的 ID 和处理说明。conflict=overwrite 会采用传入版本,并将被替换的版本保留在 previous_versions 中(最多保留最新的 20 个);如果内容发生变化,还会清除嵌入向量,让该行不再能凭借它已不再包含的含义被检索到。
无论采用哪种策略,计数器都会合并。 实体的 importance 和 access_count,以及关系的 strength,都不是由一端复制到另一端的值:每台机器都会根据自身的强化操作和遍历情况独立累积。两者取较高值,这样便具有幂等性——重复导入同一个数据包不会让数值膨胀。(求和更符合“双方都计数了”的直觉,但重复导入时会翻倍,因此不如不会破坏数值的规则可靠。)
识别哪台机器是哪台。 每个安装实例会在首次迁移时,在自己的数据库中生成一个 UUID——一行记录,重启后保持稳定,并且按设计唯一;清单会携带它,因此数据包可以注明由哪台机器生成。CUBA_NODE_NAME 的含义保持不变:它是存储在 origin_node 中、人类可读的标签。它不是身份标识,也不可能充当身份标识,因为两台机器都叫 pop-os 是最有可能发生的情况。
时钟为对端所需的变化而前进,本地杂动不会让它动。 观察的 version 只会在其内容、类型、可信度、证据等级或标签实际发生变化时递增,除此之外不会递增。衰减会改变 importance 和 last_accessed;reembed 会替换向量。如果这些操作也会让时钟前进,每次导出都会发送一张其实并未变化的图,两台机器就会不停地互相传递无关内容。用相同内容重写一行也不会让时钟前进,因此幂等的重新导入不会凭空制造本不存在的冲突。
旧数据包仍可导入。 格式为 SCHEMA_VERSION 2:现在会一并传递 version、updated_at、origin_node、previous_versions、evidence、verification 和 trust,因为比较时钟的冲突规则要求文件中包含时钟信息。此前写入的数据包仍可导入——所有新增字段都会使用默认值;而 v1 观察会被设为 asserted,这是对从未声称具有更高可信度的文件最诚实的解读。
任何看起来像凭据的传入包内容都会存为 quarantined,而非受信任内容——在你通过 cuba_eco 将其提升之前,不会提供给 cuba_faro 和 cuba_expediente——因为导入操作会从任何拥有推送权限的人都能写入的代码仓库中读取 JSON。
只读的对等节点。 CUBA_PEER_TOKEN 只能使用另外五个动词,除此之外什么也不能做。pull 会将数据包放在响应中返回,而不是写入任何位置,并按文件分页(limit、offset),每页最多 3 MB——如果分页期间 manifest_hash 发生变化,就中止操作,因为这意味着此节点在传输过程中被写入,分页内容描述的是两个不同的状态。notify 是对等节点令牌唯一可以执行的写入操作:用一段简短的 summary(最多 2000 个字符)说明另一台机器有了新发现,并标记 node_id/node_name;它会显示在下一次 cuba_jornada start 和 status 中,并在导入带有其 manifest_hash 的数据包时自动关闭——它本身永远不会进入图谱。conflicts 会列出两台机器意见不一致的行及其两种文本,resolve id=… keep=ours|theirs|both 则会关闭其中一项:keep=both(默认值)会保留本机文本为当前文本,并将另一方的文本存入 previous_versions,不丢弃任何内容;而 theirs 还会清除嵌入向量,因为它描述的是当前已不存在的文本。fetch 是另一半,在本机上运行:它会通过 HTTP 分页获取对等节点的 pull 数据,保存文件,并使用与任何数据包相同的验证流程导入;同时记录对等节点的清单哈希,因此如果内容没有变化,下一次 fetch 会在开启事务前停止。默认情况下,导出时不包含嵌入向量,而 pull 默认包含嵌入向量——收到无向量文本的对等节点在重新生成嵌入向量之前无法搜索刚收到的内容;而在没有 GPU 的机器上,重新生成会很慢且只能顺序进行——如果数据包的模型或维度与本机不匹配,就会拒绝该数据包,而不会悄悄将其他向量空间中的向量填入索引。
出现故障时,它会告诉你
$ cuba-memorys doctor
[ ok ] migrations 49 aplicadas, ninguna dirty
[ ok ] embedding_dim runtime 1024-d == columna vector(1024)
[ ok ] runtime_role 'cuba_app' sin superuser — RLS y audit efectivos
[ warn ] binary_freshness 4 proceso(s) MCP corren un binario más viejo que el de disco
之所以需要这个功能,是因为混合搜索引擎的故障模式不是崩溃,而是向量分支停止工作、搜索悄悄退化为词法搜索,却没有任何症状。现在,如果嵌入维度不匹配,服务器会拒绝启动;如果某个分支发生故障,搜索响应中会设置 degraded: true。
CLI:无需 LLM 介入的记忆
共有 23 个命令。运行 memory-industry --help 可查看所有命令。
serve | 为所有客户端提供一个共享的 HTTP 守护进程,而不是每个编辑器窗口各运行一个进程(并各自加载一份模型) |
search <query> · save · delete · export | 通过 Shell 读取和写入知识库 |
dashboard | 查看其中内容的独立 HTML 页面 |
doctor | 健康检查:架构、维度、配置一致性、陈旧进程 |
recall | 会话启动时注入上下文——通过 setup hook 接入 |
reembed | 重新编码需要处理的内容(默认仅处理过期行,而非全部内容) |
calibrate | 根据语料库重新计算弃答阈值 |
link | 根据 NPMI 共现关系自动关联实体 |
dedupe | 清理以不同名称指代同一事物的实体——见下文 |
sync · hook | 将图谱写成可提交的 JSON,并在另一台机器上读回——见在机器之间通过 Git 同步。hook install 会将其接入 Git |
skills <dir> | 将过程导出为 Claude Code Skills |
eval | 检索基准测试——nDCG@10(含置信区间)、MRR、召回率、令牌成本 |
setup | 将其接入 MCP 客户端;setup check 可检查这些客户端 |
dedupe — 因为不同的字符串就是不同的实体
cuba_alma create 使用 ON CONFLICT (name) 插入。因此,一个项目会被拆分成 Mapupita-Web、Mapupitta-Web(拼写错误)、Mapupita Web、mapupita……搜索其中一个时,找不到其他名称。在一个包含 266 个实体的真实图谱中,158 个实体(59%)没有任何关系——对于 PageRank 和多跳检索来说,它们根本不存在。
决定是否合并的不是嵌入向量质心。这看似是显而易见的办法,但实际上行不通:M-Codes Reference Guide 和 G-Codes Reference Guide 的质心余弦相似度为 0.811。在一个围绕单一领域的语料库中,质心相似度衡量的是领域,而不是实体——阈值设为 0.80 会把两本不同的 CNC 指南合并,且无法撤销。
因此,--apply 只合并有确凿依据的对象(统一大小写和分隔符后完全相同)。拼写错误和近似匹配会显示出来,逐个通过 --judge 判断。旧名称会写入 brain_entity_aliases,因此不会丢失:查询旧名称仍会解析到对应实体。
31 个工具
工具以古巴文化命名。cuba-memorys 会公开所有工具,也可以设置 CUBA_TOOL_PROFILE=lean,只公开 13 个日常核心工具,以及 cuba_tools + cuba_call——31 个工具中有 15 个,目录缩小 49%,且没有丢失任何功能,其余工具可按需调用。
知识图谱 — cuba_alma(实体)· cuba_cronica(观察、事件、时间线)· cuba_puente(类型化关系、遍历、链接预测)· cuba_ingesta(批量导入)
搜索 — cuba_faro(混合 RRF、验证、MMR 多样化、OOD 弃答)
错误记忆 — cuba_alarma(报告)· cuba_remedio(解决)· cuba_expediente(搜索过往错误;如果某种方法此前失败过,会发出警告)
会话与决策 — cuba_jornada(会话生命周期、差异)· cuba_decreto(架构决策)· cuba_proyecto(按项目隔离)· cuba_pre_compact(在 /compact 后保留状态)
程序性 — cuba_receta(按 Wilson 下置信界排序的配方)
认知 — cuba_reflexion(差距检测)· cuba_hipotesis(溯因推理)· cuba_contradiccion(语义冲突)· cuba_juez(大模型裁判)· cuba_centinela(前瞻性触发器)· cuba_calibrar(贝叶斯校准、来源可信度)
维护 — cuba_zafra(衰减、剪枝、合并、PageRank、Leiden 社区)· cuba_eco(RLHF 反馈)· cuba_vigia(健康状况、漂移、中心性)· cuba_forget(GDPR 擦除)· cuba_archivo(CFR-21 哈希链审计日志)· cuba_pizarra(工作记忆)· cuba_sync(通过 git 在机器间进行便于版本控制的导出/导入,支持传播删除操作并防止远程擦除)
元工具 — cuba_tools(发现)· cuba_call(调用)
配置
| 变量 | 默认值 | 作用 |
|---|---|---|
CUBA_MODE | local | local / red(共享云数据库) / completo(全部功能 + GPU)。用于预设其余配置。 |
CUBA_NODE_NAME | $HOSTNAME / $COMPUTERNAME | 此机器的易读标签,写入 origin_node,并在配对中的客户端调用 GET /identity 时显示。回退值为 $HOSTNAME,但 shell 不会将其导出给子进程,因此在 Linux 上,除非设置此变量,否则 origin_node 会保持为空。这不是此安装实例的身份标识:身份标识是其数据库中生成的 uuid,因为两台机器很容易选用相同的名称 |
DATABASE_URL | 自动(Docker) | PostgreSQL 连接。使用 red 模式时,请设置此变量(外部连接 + TLS)。 |
ONNX_MODEL_PATH + ORT_DYLIB_PATH | 自动(~/.cache) | 语义嵌入。运行 cuba-memorys models 即可完成设置。 |
RUST_LOG | cuba_memorys=info | 日志级别,由 tracing 的 EnvFilter 读取。日志写入 stderr — 使用 stdio 传输时,stdout 是 JSON-RPC 通道,在那里打印任何其他内容都会破坏客户端通信。使用 cuba_memorys=debug 查看每个处理器的详细信息,使用 sqlx=debug 查看每条查询。 |
CUBA_EMBED_MODEL · CUBA_EMBEDDING_DIM · CUBA_POOLING | multilingual-e5-small · 384 · mean | 设置为 bge-m3 · 1024 · cls 可使用更强的西班牙语模型 |
CUBA_QUERY_PREFIX · CUBA_PASSAGE_PREFIX | query: · passage: | 分词前添加的指令前缀。E5 的训练使用了这些前缀;bge-m3 则没有 — 切换模型时,请将两者都设为空字符串,否则每个向量都会基于模型从未以这种形式见过的文本计算 |
CUBA_CHUNK_THRESHOLD_CHARS · CUBA_CHUNK_CHARS | 1800 · 1400 | 内容超过阈值时,会拆分为指定字符数的块(重叠 200 个字符)。CUBA_CHUNK_CHARS 的最小值为 200。非正整数值会回退为默认值 |
CUBA_EMBED_CONCURRENCY | 1 | ONNX 嵌入会话周围信号量允许的许可数。首次使用时设定一次 |
CUBA_TOOL_PROFILE | full | lean → 31 个工具中启用 15 个,工具目录缩小 49%,功能不变。这 15 个是日常核心工具(包括 whoami/artefacto/contexto);其余工具仍可通过 cuba_call 调用 |
CUBA_JUDGE · MEMORY_INDUSTRY_JUDGE | auto | nli / mcp_sampling / claude_cli / heuristic / 命名的 OpenAI 兼容提供商。优先使用 MEMORY_INDUSTRY_* 名称;CUBA_* 仍然可用 |
CUBA_JUEZ_CLI · MEMORY_INDUSTRY_LLM_CLI · CUBA_JUEZ_MODEL · MEMORY_INDUSTRY_LLM_MODEL | claude · claude-haiku-4-5 | 离线评判器要调用的 CLI,以及它请求的模型。CUBA_JUEZ_CLI / MEMORY_INDUSTRY_LLM_CLI 也决定自动选择路径:如果该名称不在 PATH 中,就没有 CLI 评判器,选择会回退 |
CUBA_JUEZ_TIMEOUT_SECS · MEMORY_INDUSTRY_LLM_TIMEOUT_SECS | 30 | 每次评判的时间预算,适用于 CLI 和 API。无法解析为整数的值会保留默认值 |
CUBA_JUEZ_MAX_PAIRS · MEMORY_INDUSTRY_LLM_MAX_PAIRS | 5 | cuba_juez 每次调用发送的候选对数 |
MEMORY_INDUSTRY_LLM_PROVIDER · CUBA_LLM_PROVIDER | 未设置 | 命名的云端/本地预设(deepseek、qwen、ollama 等)或 openai_compat |
MEMORY_INDUSTRY_LLM_BASE_URL · CUBA_LLM_BASE_URL | 未设置 | OpenAI 兼容的 /v1 基础 URL(Ollama、vLLM、供应商网关) |
MEMORY_INDUSTRY_LLM_API_KEY · CUBA_LLM_API_KEY | 未设置 | 该基础 URL 的 Bearer 凭证。供应商回退变量:OPENAI_API_KEY、DEEPSEEK_API_KEY、DASHSCOPE_API_KEY(以及其他预设密钥) |
CUBA_NLI_PATH | ~/.cache/cuba-memorys/models-nli | 本地蕴含模型(cuba-memorys models nli) |
CUBA_NLI_ESCALATE · MEMORY_INDUSTRY_NLI_ESCALATE | 关闭 | 将 NLI 无法判定的主张交给大模型处理。可提高召回率,但每条约需 12 秒 |
MEMORY_INDUSTRY_GRAPH_DB · CUBA_GRAPH_DB | off | 可选的图投影:falkor / neo4j / off。Postgres 仍是事实来源 |
MEMORY_INDUSTRY_GRAPH_URL · CUBA_GRAPH_URL | 未设置 | 图端点(FalkorDB 使用 redis://…) |
MEMORY_INDUSTRY_GRAPH_NAME · CUBA_GRAPH_NAME | memory_industry | Falkor/RedisGraph 图键。gate 会将其设为未设置/off,以免临时写入落到实时图中 |
MEMORY_INDUSTRY_ENTITY_FACTOID | on | 对提及实体的事实查询,额外启用一条 RRF 排序路径。设置为 off / 0 / false 可保持混合排序不变 |
CUBA_RERANKER_PATH · CUBA_RERANK_TIMEOUT_SECS | ~/.cache/…/reranker · 20 | 交叉编码器重排器(nDCG 提升 93%);在 CPU 上超出预算时会回退到 RRF |
CUBA_RERANK_INTRA_THREADS | 物理核心数(GPU 上为 2) | 每次重排推理使用的 ONNX 线程数。超过物理核心数反而会变慢 — 提高之前请使用 rerank_bench 测量 |
CUBA_RERANK_LENGTH_BUCKETING | 开启(固定形状时关闭) | 将长度相近的候选项分批,避免填充造成额外计算。分数不变 |
CUBA_RERANK_CHUNK | 16 | 每次前向传递处理的候选项数。使用固定形状时,每批都会填充至 512 个 token,因此这是 GPU 内存池的主要调节项:16 → 2938 MiB,4 → 2364 MiB。分数不变 — 一次较长的搜索分别使用 16 和 4 时,返回结果逐字节相同 |
CUBA_RERANK_CONCURRENCY | 1 | 重排器会话周围信号量允许的许可数。会话由互斥锁保护,因此提高此值只会让调用排队,而不会让它们并行运行 |
CUBA_RERANK_BUCKET | 512 | 将填充后的序列长度向上取整到此值的倍数。只接受 0 或不大于 512 的 2 的幂 — 其他值会保留默认值。0 表示填充到最长候选项的长度 |
CUBA_RERANK_FIXED_SHAPE | 重排器在 GPU 上运行时开启 | 将每批填充到相同的 512-token 形状。0 / off / false 会禁用;其他任何值都会启用。它还会反转 CUBA_RERANK_LENGTH_BUCKETING 的默认状态,因为所有批次大小都相同时,长度分桶已无用武之地 — 而且正是它让 CUBA_RERANK_CHUNK 成为 VRAM 的主要调节项 |
CUBA_EMBED_DEVICE · MEMORY_INDUSTRY_RERANK_DEVICE · CUBA_RERANK_DEVICE · CUBA_NLI_DEVICE | cpu · gpu · cpu | 按模型指定运行设备。只有重排器能从 GPU 中获益;INT8 嵌入器无法使用 GPU,而 FP32 NLI 不值得占用 VRAM。资源计划只考虑重排器,因此强制在 GPU 上运行的嵌入器或 NLI 会使用自身的 CUDA 会话,不受我们的内存池上限约束(SameAsRequested 只会按请求大小增长)— 这是在 0.30 中作出的决定,并非疏忽。设置为 gpu/cpu,即可在不重新构建的情况下进行设备对比测试。操作人员需要设置的是重排器,因此它也支持 MEMORY_INDUSTRY_* 名称;另外两个模型只保留旧名称 |
MEMORY_INDUSTRY_GPU_MEM_LIMIT_MB · CUBA_GPU_MEM_LIMIT_MB | 测量得到的空闲 VRAM 减去 512 MiB | 限制 CUDA 内存池,并将 arena_extend_strategy 固定为 SameAsRequested。默认值(NextPowerOfTwo)会在每次扩展时将预留空间翻倍,这就是 1.65 GB 的权重会占用 5+ GB VRAM 的原因。上限针对每个会话。通常无需设置此变量:资源计划会测量显卡,只有 main 会应用计划。如果既未设置此变量也没有计划(例如测试、示例或库调用),就完全不会传递上限,ONNX Runtime 会使用显卡 — 没有内置数值,因为曾经使用的数值(2048 MB)不足以容纳 4.23 GiB 的重排器。如果设置的值低于模型所需下限,就会输出带有这两个数值的 ERROR 并提高上限,因为低于下限的上限节省不了 VRAM — 只会把故障推迟到加载中途。若要减少显存占用,请将重排设备设为 cpu |
CUBA_EMBED_INTRA_THREADS | 逻辑核心数的一半,最多 4 | 每次嵌入使用的 ONNX 线程数。在 12 个线程的环境中测得:每次查询 1 个线程 → 94.8 ms,2 个 → 52.3 ms,4 个 → 35.8 ms,6 个 → 68.1 ms,12 个 → 155.4 ms |
CUBA_IDLE_SHUTDOWN_SECS | 0(关闭) | 所有客户端在此时长内都没有请求后退出。与 systemd .socket 单元配合使用,使下次调用重新启动守护进程 — 参见 占用空间 |
MEMORY_INDUSTRY_WARM_RERANKER · CUBA_WARM_RERANKER | 开启 | 启动时加载交叉编码器,而不是等到首次搜索时再加载。现在可选择关闭(0/off/false/no):延迟加载并不会省去开销,只会把开销移到一个预算为 20 秒的请求中,而该请求还得等待读取 1.1 GB 数据 |
CUBA_HTTP_ALLOWED_ORIGINS | 未设置 | 允许调用 /mcp 和 /panel 的额外浏览器来源,以逗号分隔。没有 Origin 的请求始终通过 — 所有 MCP 客户端都属于这种情况,它们都不是浏览器。带有 Origin 的请求来自网页,而只有守护进程自身的网页才应访问此服务:在默认文档所述的环回地址且未设置令牌时,DNS 重绑定会让操作人员访问的任何网站都能驱动守护进程。任何位置都不会发送 CORS 标头,这是有意为之:浏览器拒绝的预检请求正好起到了作用 |
MEMORY_INDUSTRY_WARM_BEFORE_SERVE_SECS · CUBA_WARM_BEFORE_SERVE_SECS | 180 | serve 绑定地址后,等待模型就绪并开始响应前的最长时间。超出预算后仍会开始提供服务,/health 会报告 ready:false,要求重排的搜索则会标记为降级 — 如果守护进程一直不开启端口,就无法询问它在做什么 |
MEMORY_INDUSTRY_BIND_WAIT_SECS · CUBA_BIND_WAIT_SECS | 300 | 如果绑定失败是因为地址尚未分配给此机器,serve 会持续重试的时长 — 这会发生在 Windows 启动后不久,守护进程在网卡获得固定 IP 地址前就已启动。每 5 秒重试一次,之后以 is not assigned to this machine 退出,让监管程序重新启动它。0 表示只尝试一次;非秒数值会使用默认值 300。端口已被占用时会立即失败,不会重试 |
CUBA_HTTP_ADDR · CUBA_HTTP_TOKEN | 127.0.0.1:8787 · 未设置 | serve 使用的地址,以及它要求的 Bearer 令牌。绑定到环回地址以外的任何地址时都必须提供令牌;绑定到可路由地址时,令牌长度必须至少为 32 个字符。如果已有程序占用该地址,doctor 会发出警告 |
MEMORY_INDUSTRY_CLIENT_ID · CUBA_CLIENT_ID | 未设置 | 当客户端未发送 Mcp-Client-Id 时,作为 stdio/whoami 使用的工作区标签。HTTP 仍要求每个工作区都提供该标头;守护进程只有一个进程,不能通过此环境变量区分聊天 |
CUBA_PANEL | 未设置 | 设为 1 后,serve 也会响应 GET /panel:这是编译进二进制文件的控制页面,可读取守护进程状态、已连接客户端、近期调用和未解决问题。它本身不携带任何数据 — 所有显示内容都通过 POST /mcp 向守护进程请求,并使用与任何 MCP 客户端相同的 Bearer 令牌,因此无需保护第二个端点。默认关闭 |
CUBA_PANEL_PUBLIC | 未设置 | 未设置时,/panel 会拒绝携带转发标头(Forwarded、X-Forwarded-For、CF-Connecting-IP 及另外 6 个)的请求 — 这是 HTTP 代理的特征。Cloudflare 隧道连接到 127.0.0.1,因此无论哪种情况,客户端地址都是环回地址,只有标头能区分两者。它无法检测的情况:原始 TCP 转发(ssh -L、socat、ngrok tcp)不会添加标头,与本地请求无法区分,因此此设置只能阻止 HTTP 代理,不能证明请求来自本地。若要主动公开面板,请设为 1 |
CUBA_PEER_URL | 未设置 | cuba_sync action=fetch 使用的另一台守护进程的默认地址,例如 https://brain.example.net。这只是回退值:首次成功获取后,会按对等端名称记住地址 |
CUBA_PEER_TOKEN | 未设置 | 另一台与此机器同步的机器使用的第二个 Bearer 令牌。它只能访问同步操作 — 不能使用 cuba_forget、cuba_zafra prune 或 cuba_sync import — 因此对等端可以读取此节点知道的内容,但无法写入或删除任何行。必须与 CUBA_HTTP_TOKEN 不同,后者也用于隧道;如果两者相同,serve 会拒绝启动 |
MEMORY_INDUSTRY_REQUIRE_ACCOUNTS | 未设置 | 设为 1(也接受 true、yes、on)后,绑定到其他机器可访问地址的守护进程将停止接受 CUBA_HTTP_TOKEN 对 /mcp、/events 和 /events/ticket 的访问:它会返回 401,并说明不再接受共享令牌以及如何配对设备。账户凭据(memory-industry account pair 和 memory-industry bridge pair 生成的 mi1.…)以及 CUBA_PEER_TOKEN 仍然有效;/health 也仍会向共享令牌提供完整视图,因此不会让监控失明。因该开关而拒绝的共享令牌不会计为登录失败。在环回地址上,只有共享令牌可用于访问,因此此开关会被忽略,且守护进程会记录警告。如果启用此选项但没有有效凭据,doctor 会失败 |
MEMORY_INDUSTRY_DOCTOR_DEEP_SECS · CUBA_DOCTOR_DEEP_SECS | 300 | doctor --deep 等待单个模型打开、并将其判定为故障前的时长。加载在阻塞线程中运行,因此无响应的模型不会卡住工具:预算用尽时会以 fail 状态返回并附带原因,而不是跳过 |
MEMORY_INDUSTRY_HANDSHAKE_TIMEOUT_SECS · CUBA_HANDSHAKE_TIMEOUT_SECS | 60 | 如果没有收到 MCP 握手,stdio 会退出,而不会为已放弃连接的客户端继续占用模型。单位为整秒,数字前后的空格会被忽略。0(也包括 00、+0)或 off 表示禁用。其他值 — 60s、1.5、-5 — 都会保留 60 秒默认值,并针对该值记录警告;过去这类值会悄无声息地关闭看门狗 |
CUBA_HANDLER_TIMEOUT_SECS | 30 | 单次工具调用的时间上限。这也是 cuba_ingesta 中大模型提取操作所用的预算,即此值的 60% — 提高此值可让提取过程进行更长时间 |
CUBA_DOCS | 关闭 | 设为 1 可启用 cuba_docs,这是唯一会离开本机的工具。未设置时,甚至不会公布此工具。 |
CUBA_COMPACT_CHARS | 1200 | 紧凑截断(测得的拐点) |
CUBA_OOD_THRESHOLD | 已校准 | 覆盖弃答阈值 |
CUBA_BITEMPORAL | 开启 | 将观测镜像到 brain_facts |
CUBA_AUDIT_KEY | 未设置 → ~/.cache/cuba-memorys/audit_key | cuba_archivo 哈希链的 HMAC 密钥。没有密钥时,哈希链使用纯 SHA-256,任何有权写入该表的人都可以重新计算 — 条目仍会保持一致,伪造也无法被发现 |
CUBA_APP_ROLE | 开启 | 迁移完成后,连接池会以权限受限的 cuba_app 角色重新连接。设为 0 / off / false 会保留管理员连接 — 超级用户连接会在整个会话期间保持启用 |
CUBA_PROJECT_FILTER | 未设置(过滤开启) | 设为 off(不区分大小写)可禁用按项目进行的范围限制:RLS 范围变为 *,所有项目的记忆会同时可见。其他任何值都会保留过滤功能 |
CUBA_QUARANTINE_INFERENCE | 关闭 | 设为 1 / on / true 后,会将所有 source=inference 的内容存储为 quarantined,而不是 trusted,除非调用方明确设置了信任级别 |
CUBA_PG_BIND | 127.0.0.1 | 受管理 Postgres 容器发布端口时使用的主机地址。设置为环回地址以外的任何地址都会让数据库暴露到网络中 |
CUBA_RANDOM_PAGE_COST · CUBA_IO_CONCURRENCY | 1.1 · 200 | 连接池的每连接规划器设置。允许范围分别为 0.1–10.0 和 ≤ 1000;超出范围时使用默认值 |
CUBA_REM_AUTOLINK | 开启 | 设为 0 / off / false 可阻止 REM 周期在实体之间创建 NPMI 共现边 |
CUBA_GATE_MIN_FREE_GB | 8 | gate 开始编译前要求的可用磁盘空间。低于此值时会拒绝启动并说明原因。在分区使用率达 98% 时运行,曾以 collect2: fatal error: ld terminated with signal 7 [Bus error] 失败,并将 3 个测试二进制文件报告为“could not compile” — 输出中没有任何内容提到磁盘,因此看起来像是代码错误 |
CUBA_GATE_SWEEP_BELOW_GB | 20 | 可用磁盘空间低于此值时,gate 会在运行前清理构建产物。cargo 从不删除早前编译生成的二进制文件 — 每次编辑都会生成新的哈希,旧文件则会留下 — 因此 target/debug/deps 会无限增长;这里曾达到 64 GB |
CUBA_GATE_SWEEP_DAYS | 7 | 构建产物必须达到的清理最小存留时间。刻意按存留时间而不是大小判断:本次运行所需的任何内容都是今天生成的 |
CUBA_REM_FIRST_DELAY_SECS | 300 | 启动后首次运行 REM 整合前的等待时间。过去使用的是 REM_INTERVAL — 4 小时 — 因为循环会消耗间隔的第一个 tick,而该 tick 会立即完成。stdio 进程通常不会运行那么久,因此此模式下周期根本不会运行;每次机器重启后计数器又会归零 |
CUBA_REM_RELATION_BATCH | 5 | REM 周期每轮执行关系扫描的实体数。0 表示跳过扫描。未设置时会自适应调整:仍有 50 个或更多实体等待时为 20,队列清空后回到 5 — 以每 4 小时处理 5 个计算,226 个待处理实体需要一周 |
CUBA_REM_SCAN_TIMEOUT_SECS | 90 | 单个实体关系扫描的时间预算 |
CUBA_REM_EXTRACTION_BATCH | 5 | 在关系扫描后,REM 周期每轮对多少条观测运行 cuba_ingesta auto_extract。0 表示跳过。发现的内容始终以 trust=quarantined 写入 — 这是图中唯一完全无人值守的写入器,因此在 cuba_eco action=promote 手动批准之前,cuba_faro 不会显示其生成的任何内容 |
CUBA_REM_BACKFILL_LIMIT | 100 | REM 周期每轮为多少条缺少嵌入的观测补充嵌入。0 表示禁用回填;负值会保留默认值 |
CUBA_SYNC_DIR | 未设置 → 工作目录下的 .memory-industry;如果该目录已存在而首选目录不存在,则使用 .cuba-memorys | cuba_sync 导出/导入的根目录,也是范围限制边界:如果 --dir 指向此根目录之外的路径,就会被拒绝。因此,设置此变量即可将同步目标设到其他位置,而无需使用 ../ 越界。hook install 会解析一次此变量(相对路径以仓库根目录为基准),并将解析结果写入钩子;更改此变量后请重新运行该命令 |
CUBA_UNDO_DIR | ~/.cache/memory-industry/undo;如果该目录已存在,则使用 ~/.cache/cuba-memorys/undo | 破坏性 CLI 命令写入撤销快照的位置。显式设置时,会在解析主目录之前读取,因此即使机器未定义 HOME 或 USERPROFILE,也能正常使用 |
占用情况
记忆服务属于基础设施:即使你不使用它,它也会运行。在测试所用的 6 GB 笔记本 GPU 上,它过去从启动起就会占用 5228 MiB 显存——占显卡显存的 93%——导致其他 GPU 程序无法启动。NVIDIA 驱动在创建通道时会返回 NV_ERR_NO_MEMORY,这也是游戏或 GPU 加速终端无法运行时遇到的问题。
下面的四项数据中,两项是随附的默认值;另外两项需要添加配置行。此表将它们区分开来,而不是把最佳数据说成是默认就有的。
| 之前 | 0.20.0 默认值 | CUBA_RERANK_CHUNK=4 | + 融合工件 | |
|---|---|---|---|---|
| 搜索时的显存占用 | 5228 MiB | 2950 MiB | 2364 MiB | 1460 MiB |
| 空闲时的显存占用 | 5228 MiB | 0 — 进程已退出 | 0 | 0 |
| 冷启动到开始响应 | 11,1 s | 0,027 s | 0,027 s | 0,027 s |
| 搜索耗时,预热后 | 5,90 s | 5,25 s | 3,73 s | 1,70 s |
| 嵌入一个查询 | 52,3 ms | 35,8 ms | 35,8 ms | 35,8 ms |
默认值列中的所有内容都是随附代码的一部分。CUBA_RERANK_CHUNK=4 是一个环境变量。最后一列还需要使用下文所述的重新构建的重排序器。这些改动没有移除任何功能。
四项改动让它达到了现在的状态:
按模型而非按进程分配。 只有重排序器使用 GPU 加速——INT8 嵌入器无法使用 GPU,而 FP32 NLI 偶尔才会运行,并且可以容忍 150-400 ms 的延迟,不值得为它占用 1 GB 显存。arena 上限按会话计算,因此在 6 GB 显卡上,三个会话都请求 CUDA 就意味着超额分配 3 倍,迟早会失败。
不再不断翻倍的 CUDA arena。 ArenaExtendStrategy::NextPowerOfTwo 是 ONNX Runtime 的默认值,它会按 2 的幂次预留,而不是按会话所需的大小预留。
重排序器在收到首个批次时才加载。 使用套接字激活时,守护进程的启动次数远多于执行重排序的次数,而且很多次启动只会响应 save。
无人请求时不运行守护进程。 CUBA_IDLE_SHUTDOWN_SECS 加上 systemd .socket 单元:套接字占用端口,守护进程在收到首个实际连接时启动,并在空闲时间结束后退出。它会通过常规路径关闭——serve 返回,后台 drain 刷新正在进行的嵌入写入,sqlx 关闭连接池——因为直接退出进程会悄无声息地丢失这些写入。
systemd 单元对
# ~/.config/systemd/user/memory-industry.socket
[Socket]
ListenStream=127.0.0.1:8787
Accept=no
[Install]
WantedBy=default.target
# ~/.config/systemd/user/memory-industry.service (abridged)
[Service]
Type=exec
ExecStart=%h/.local/bin/memory-industry serve 127.0.0.1:8787
# Every knob — CUBA_IDLE_SHUTDOWN_SECS, the per-model devices — lives in the env file, never here.
EnvironmentFile=%h/.config/memory-industry/memory-industry.env
# An idle shutdown exits 0 — Restart=always would bounce it straight back up.
Restart=on-failure
这两个单元以及包含所有键的环境文件均由二进制文件生成,并在 packaging/ 中进行版本管理:memory-industry setup service 会打印配置方案,--apply 则会安装这些文件。ExecStart 必须指向你实际安装的二进制文件——使用 command -v memory-industry 查询——路径错误会导致 status=203/EXEC。较早版本发布的 cuba-memorys.service / cuba-memorys.socket 组合已在 0.28.0 版从仓库中移除;如果你仍安装着它们,请参阅 CHANGELOG 中的 0.28.0 条目。
serve 会接管 systemd 通过 fd 3 传入的套接字(LISTEN_FDS),因此即使守护进程未运行,端口也会保持占用,客户端不会收到连接被拒绝的错误。
该单元还必须绑定到回环地址。使用套接字激活时,.socket 单元的 ListenStream 决定地址,CUBA_HTTP_ADDR 会被忽略,因此 serve 会检查接收到的套接字地址;除非设置了 CUBA_HTTP_TOKEN,否则遇到可路由的地址时会拒绝启动。
VRAM 只是其中一半。模型权重也驻留在主机内存中,而过去无论机器配置如何,这部分内存需求都是固定的。使用 cargo run --release --features cuda --example mem_bench 测量,守护进程已停止,测试设备为配备 6 GB 笔记本 GPU 的机器:
| 阶段 | 增加的 RSS | VRAM | 加载时间 |
|---|---|---|---|
| 进程启动 | 5,5 MiB | 0 | — |
| + PostgreSQL 连接池 | +1,3 MiB | 0 | — |
| + 嵌入模型(bge-m3,CPU) | +862,0 MiB | 0 | 1,72 s |
| + 重排器(融合 FP16,GPU) | +1034,7 MiB | 1460 MiB | 3,73 s |
| + OOD 拟合(n=1811,d=1024) | +37,4 MiB | 0 | 11,45 s |
| 峰值 | 2677 MiB | 1460 MiB |
常驻内存最终稳定在约 1941 MiB;峰值达到 2677 MiB,是因为加载 1,1 GB 的 ONNX 文件时,除了留下的模型权重之外,还会产生瞬时内存开销。必须满足的是峰值,而不是稳定状态。
在进行测量的这台机器上,这没问题。但在 4 GB 笔记本上就不行了;在受 MemoryHigh=4500M 限制的 systemd 单元下,曾观察到 2,56 GiB 被换出到 swap——MemoryHigh 不会终止进程,而是回收内存;回收内存意味着换页。
重新运行测试时,有两个需要了解的注意事项。mem_bench 通过 nvidia-smi --query-compute-apps 统计自身 PID 占用的 VRAM,因为读取 memory.used 会把显卡上其他所有进程的占用也算进去——第一次尝试显示进程启动时占用了 3590 MiB,其实是游戏和桌面 shell 占用的。还要使用守护进程自己的环境运行:如果未设置 CUBA_RERANKER_PATH,它会悄悄加载未融合的产物,在 CPU 上的预热时间会从 3,7 s 增至 131 s。
因此,守护进程现在会在启动时读取机器信息并选择一个级别。这不是凭空新增的机制:这三种降级方式原本就已存在,也都有测试覆盖。
| 级别 | 加载的模型 | 主机 RAM | 需要放弃的功能 |
|---|---|---|---|
| minimal | 无 | ~220 MiB | 语义搜索。BM25、全文搜索和 trigram 仍可提供结果 |
| lean | 嵌入模型 | ~1,1 GiB | 重排和本地蕴含判断 |
| standard | 嵌入模型 + 重排器 | ~2,2 GiB | NLI 判别器,它会切换到自己的备用方案梯级 |
| full | 三者全部 | ~3,3 GiB | 无 |
级别的选择方式。 预算为 min(cgroup 限制,系统可用内存) − 768 MiB 的余量,并且必须以 cgroup 限制为准。在这台机器上,/proc/meminfo 报告可用内存为 7,16 GB,而守护进程的 cgroup 将其限制在 4,39 GiB——如果相信 /proc,就会在内核已经开始换页的限制下加载 2,6 GiB 的模型权重。读取器会从 cgroup 根目录一路遍历到叶节点,并采用找到的最严格的 memory.max 或 memory.high,因为限制通常设在某个祖先节点上。
随后按实测价值依次加载模型:先加载嵌入模型,再加载重排器(nDCG 提升 +93%,因此优先级高于判别器),最后加载 NLI。
该方案只能减少配置。 每个参数的上限都设为守护进程原本使用的值,因此在资源充足的机器上,级别为 full,一切保持不变。降级只会向下进行。
你始终拥有优先权。 手动设置的以下任一项都会保持原样——调节器只会补齐缺失项,不会覆盖已有决定:
CUBA_EMBED_INTRA_THREADS CUBA_RERANK_INTRA_THREADS CUBA_NLI_INTRA_THREADS
CUBA_RERANK_CHUNK CUBA_GPU_MEM_LIMIT_MB CUBA_OOD_FIT_LIMIT
CUBA_DB_MAX_CONNECTIONS
无论预算如何,都可以通过将模型指向一个不存在的路径来强制关闭它——CUBA_RERANKER_PATH=/nonexistent 或 CUBA_NLI_PATH=/nonexistent。调节器自身也使用同样的机制。
要查看它的决定,运行 cuba-memorys doctor:它会报告检测结果和由此生成的方案,并在级别降至 minimal 时发出警告。启动时也会记录该方案,并附上完整的机器检测信息。
重排模型产物
发布的 bge-reranker-v2-m3 ONNX 在任何图融合之前就已转换为 FP16,因此其中穿插着 785 个 Cast 节点。ONNX Runtime 在加载时会挽回一部分(节点数从 2023 降到 897,包含 49 个 SkipLayerNormalization),但它无法融合 Gelu,而且每次冷启动都会重复这项工作。从 FP32 导出版本重新构建并先进行融合:
python -m onnxruntime.transformers.optimizer \
--input model.onnx --output model.onnx \
--model_type bert --num_heads 16 --hidden_size 1024 \
--opt_level 1 --use_gpu --float16
| VRAM | search p50 | load + warm | |
|---|---|---|---|
| 随附的 FP16 | 2364 MiB | 3,73 s | 22,8 s |
| 先融合,再转为 FP16 | 1460 MiB | 1,70 s | 10,2 s |
在真实搜索中,前 10 个结果的顺序完全相同,fused_score 的差异最多为 0,0029;在真实批次形状的合成 logits 上,Pearson ≥ 0,9997,且每个批次的排名都相同。
Attention 不会融合,而且在这里无法解决。 在 FP16 产物和干净的 FP32 产物上,opt_level 为 0、1、2 和 99 时,结果均为 is_fully_optimized: Attention (or MultiHeadAttention) not fused。该导出版本通过动态形状子图(Shape → Gather → Unsqueeze → Concat → Reshape)构建 Q/K/V reshape,而 AttentionFusion 需要一个形状为常量的 Reshape,才能从中读取 num_heads 和 head_size。因此,如果不使用静态形状重新导出,flash/efficient attention 就无法使用——在有人为此花上一下午之前,值得先了解这一点。
测量结果——以及那个撒谎的基准测试
直到 v0.12,本节还保留着这样一句话:“这里的每个数字都是实测值,而非假设值”;但其中的每个数字都是错的。基准测试有三个方面的问题,发现这些问题后,我们撤回了两个已发布的结论。
只有 10 个查询。 95% 置信区间约为 ±0.12;它能检测出的最小效应约为 0.25 nDCG。任何关于更小差异的说法,都只是带着小数点的噪声。
相关性通过子串匹配判断。 只要结果文本包含一个标记词,就算正确——因此,任何提到“postgres”的观察都被算作对任何 postgres 相关问题的正确答案,无论它实际上有没有回答问题。这衡量的是关键词是否出现,而非检索效果,而且会让整个基准测试偏向词法分支、不利于向量分支。
nDCG 是根据检索到的内容,而非实际存在的内容进行归一化。 如果语料库中有 5 篇相关文档,但只找到 2 篇,那么这 2 篇就被当作“理想”排序——这样一来,漏掉 60% 答案的系统也能得到完美的 1.0。(而且,这个文件发布时的 R@10 = 3.125。召回率是一个比例。)
真实数字不是 0.894。在 221 个带有 ID 的查询上,结果是 nDCG@10 = 0.50 [95% CI 0.44–0.56]。系统并没有变差,它从来就不是 0.894。
这造成的影响
-
“交叉编码器重排器毫无收益”——它从来没有运行过。 三个错误接连出现:faro用if let Ok(..)包裹调用并丢弃了错误;它向一个不包含token_type_ids的 XLM-RoBERTa 模型传入了token_type_ids;它把f16logits 当成f32读取。输出与未进行重排时“逐位完全相同”,不是因为重排没有改变结果,而是因为它根本没有运行。现已修复;正在进行正确的测量。 -
关联检索确实会变差——但旧证据(n=10 时为 −0.03)根本无法证明这一点。在新数据集上使用配对自举法(正确的检验方法:两组使用相同的查询)后,置信区间为 [−0.051, −0.018],从未触及零。它没有改善任何查询,却损害了 23 个查询。决策是对的,理由却不对。关键不在于增加更多数据,而在于使用正确的检验方法。
重新诚实评估后仍然成立的结论
默认使用 compact | Token 数减少 30%,nDCG 提升 +0.0090(配对 95% 置信区间 [+0.0024, +0.0166],n=191)。此前的“恰好 0.0000”是在一个测试框架下测得的:5000-token 的响应预算会在评分前截断排序结果,导致 verbose 的尾部被截掉,而 compact 不受影响。此前的“减少 40%”来自有缺陷的基准测试。 |
| 保形弃答 | 捕获了 100% 的分布外查询,误弃答率为 0%。 |
lean 工具配置 | 31 个工具中保留 15 个,目录缩减 49%,没有丢失任何函数。 |
| bge-m3 优于 e5-small | 方向几乎可以确定是对的;撤回 +21.2 的 nDCG 数据——该数据来自有缺陷的基准测试,要重新验证就意味着必须对语料库重新嵌入两次。 |
| 基准测试本身 | 221 个查询(原为 10 个),按文档 id 标注相关性,提供 Bootstrap 置信区间,并在每个结果旁标出最小可检测效应——这样就不会再把 3 个百分点的差异误读为发现。 |
基础理论
| 算法 | 参考文献 |
|---|---|
| RRF 融合(k=60) | Cormack 等(2009) |
| Hebbian + BCM 元可塑性 | Oja(1982);Bienenstock、Cooper 和 Munro(1982) |
| 保形预测 | Vovk(2005);Angelopoulos 和 Bates(2023) |
| Ledoit-Wolf 协方差收缩 | Ledoit 和 Wolf(2004) |
| Mahalanobis 分布外检测 | Lee 等(NeurIPS 2018) |
| Wilson 分数区间 | Wilson(1927) |
| 陈述性记忆与程序性记忆 | Anderson 和 Lebiere(ACT-R) |
| 测试效应 | Karpicke 和 Roediger(Science 2008) |
| 幂律遗忘 | Wixted(2004) |
| 情景记忆与语义记忆 | Tulving(1972) |
| PageRank · Leiden · Brandes | Brin 和 Page(1998);Traag 等(2019);Brandes(2001) |
| NPMI 共现 | Bouma(2009) |
| MMR 多样化 | Carbonell 和 Goldstein(1998) |
| 上下文检索 | Anthropic(2024) |
| 提示词注入聚焦技术 | Hines 等(2024) |
开发
git clone https://github.com/LeandroPG19/Memorys.git
cd Memorys/rust && cargo build --release
# On an NVIDIA machine, build this way instead — without it the reranker spends
# its whole budget for a ranking that gets discarded. It accelerates the
# reranker only; see Footprint for why the other two models stay on the CPU.
cargo build --release --features docs,cuda
./scripts/demo.sh # runs on a throwaway Postgres it removes on exit
./scripts/merge-gate.sh # local CI 100% — sole merge judge (see docs/gate.md)
./scripts/como-el-ci.sh todo # same SIL
./scripts/quality-gate.sh # second judge: CRAP + mutants of rust/src in the diff
cargo run --release --example rerank_bench # does the reranker fit its budget here?
发布由标签驱动:在 LeandroPG19/Memorys 上创建 v* 标签,即会触发 GitHub Release 二进制文件(5 个平台)、PyPI wheel(memory-industry 和 cuba-memorys 别名)、npm(memory-industry 和 cuba-memorys 别名)以及 MCP Registry(io.github.LeandroPG19/memory-industry)的发布。测试会固定 Cargo.toml、package.json、pyproject.toml 和 server.json 中的版本,确保它们不会出现偏差。
许可证
Apache-2.0 — 可使用、修改、发布、销售,也可将其嵌入闭源产品。无需承担著佐权义务。该许可证还明确授予专利权,这正是法务部门关心的部分。
作者
Leandro Perez G. — @LeandroPG19
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。