NexusMem作者分享核心洞察:Agent能获取已提交代码,却无法获知开发者尝试过但失败的方案,并展示如何用BM25+向量检索+信号权重解决。
我独自构建 NexusMem 已经好几周了,都是大段时间。它是一个本地优先的 AI 编程 Agent 记忆引擎:将你的 git 历史、shell 命令(带退出码)、项目文档,以及可选的助手对话记录索引到磁盘上的 SQLite 数据库,然后通过 MCP 或 CLI 返回经过排序、符合 token 预算的切片。无需账号、无需云端、无需遥测。
一句话卖点:你的 Agent 已经能读取 git log。它读不了你上周二尝试的四个没成功的方案——而那才是真正值得记住的部分。
本周,有两个陌生人出现,开始修复一些我没让他们修的东西。这是个不错的借口,来写写它做了什么、以及为什么做。
编程 Agent 从两个地方获取上下文:你粘贴进去的内容,以及它们能 grep 到的东西。两者都不记得过程。Git 告诉 Agent 发了什么,但它不会说你在最终成功的方案之前尝试了哪三种方法,也不会说你在调试时哪些 shell 命令退出了非零。这个信息大概只存在一天,在你的终端回滚里,然后就消失了。
NexusMem 的答案刻意做得很无聊:读取磁盘上已有的内容(git log、shell 历史、markdown 文档),将它们规范化为同一种节点形状,建立索引,排序得足够好,让一个查询返回正确的五条而不是正确的五十条。
检索侧是 SQLite FTS5 上的 BM25,加上如果能访问嵌入模型就过一遍 sqlite-vec 的向量通道,两者用 Reciprocal Rank Fusion(RRF)融合。RRF 只根据排名位置融合,不看原始分数——这是使用它的全部意义,因为 BM25 的代价和向量距离活在无关的尺度上,只有位置是它们唯一能达成一致的。
在融合排名之上,两个先验调整分数:信号(一个 fix: commit 排名高于 chore:;一个退出非零的 shell 命令排名高于成功退出的)和时效性。两者都是真实信号。两者也差点把整个系统搞砸。
在这个工具自己的仓库上做内测,一个关于 PowerShell hook 的查询返回了两个同一天提交的无关 fix: commit,排在第 3 和第 4 位,而真正回答了查询的那个 commit 排在第 6 位。先验是各自独立设了上限的——每个最多只能推翻 2 倍的相关性差距——但分数把它们相乘,所以一个新鲜的、高信号的 commit(这描述了大多数活跃工作日的样子)可以推翻 4 倍。修复方法不是加大上限,而是让它们共享一个预算:先验现在在两者之间分配一个预算,推导出的结果是每个值恰好价值 √2,而不是凭感觉断言。
我只是因为不断对自己的工具运行真实查询、批判性地阅读输出,而不是在纸上相信排名数学,才发现了这个。这才是构建这个工具真正在做的事:内测,找到它自信地出错的情况,写一个在修复前失败、修复后通过的测试。
README 有一个"Where it breaks"(它在哪里出问题)部分,我一直努力让它保持真实,而不是粉饰太平。几个例子:
没有安装 hook 的 shell 历史没有目录上下文,所以它会被归因到你恰好运行 sync 的那个仓库。
没有空格词边界的语言(日语、中文)无法获得有用的 BM25 召回——它们完全依赖向量通道。
rebase 会让那些在重写历史中不再存在的 commit 的节点搁浅。
也有一个数字我本来想放在开头但没有:最初的目标是将 API token 消耗比发送完整上下文减少 70% 以上。但在整个仓库端到端测量下来,接近 40%。>70% 的数字描述的是打包数学在其自己的候选集上显示的结果,这是一个真实的数字,但是比"Agent 实际少读了多少"这个问题的答案更乐观。README 直截了当地说明了这一点,而不是悄悄地报告那个更友好的数字。
我是独自构建的,连续几周长时间迭代。本周,第一次,一个我不认识的人开了一个 issue,要求为 MCP 服务器添加端到端 stdio 传输覆盖——现有的测试只练习了内存传输,这无法证明协议组帧在真实进程边界上能否保持。他们先在评论里描述了方法,然后提交了一个 PR,将实际的构建好的 CLI 作为子进程启动,并断言写入 stdout 的每一行都解析为 JSON-RPC。CI 在他们的第一轮中抓到了一个真实的 Windows only bug(一个 .cmd 垫片在 Windows 上需要 shell: true 才能启动)——他们在不到一小时内修复了,合并很干净。
同一天另一个人 fork 了仓库,没有先开 issue,找到了一些我没有发现的东西:PowerShell hook 总是用 \n 行结尾插入它的块,但 Windows 编辑器编写的配置文件按惯例是 CRLF,所以安装 hook 会静默地把一个 CRLF 文件变成混行结尾的文件——之后移除它会留下一行孤零零的裸换行。这是一个足够微妙的 bug,找到它意味着真正在读代码,而不是走马观花。
这两件事都不需要我。这一点值得好好体会——项目变得足够清晰明了,让别人能够第一次尝试就正确地扩展它。
npx nexusmem init
npx nexusmem sync
nexusmem query "windows spawn failure"
只需要 Node 22+ 和 git。Ollama 是可选的,只影响语义搜索——没有它 BM25 也能完全正常工作。
如果你想戳戳看,仓库在这里,现在也有一个 CONTRIBUTING.md,如果你发现有什么值得修复的话。