不想维护一堆记忆 Markdown:用 llmem 把 AI 编码 Agent 的项目经验留在本地 SQLite
AI 编码工具最令人沮丧的失败,并不总是生成了一段错误代码。更常见的是:同一个仓库里已经决定过的命名方式、迁移顺序、接口边界和排障结论,换一次会话就要重新解释;跨项目时,Agent 又把旧项目的习惯带到不该出现的地方。
把这些信息塞进每个项目的 AGENTS.md、CLAUDE.md 或一组 Skill 文件当然可行,但它们更适合稳定、需要人工审阅的规则。日常开发中不断涌现的「为什么这样改」「这个模块的坑是什么」「下次遇到什么错误先查哪里」则更像可检索的工作记忆。llmem 是一个面向 Claude Code、Cursor 及其他 MCP 客户端的本地持久化记忆服务:它把数据放在单个 SQLite 文件中,用 BM25 词法检索取回内容,不依赖向量数据库、embedding 服务或外部 API。
这篇文章不把 llmem 描述成“永不遗忘”的万能大脑。它更适合英文技术笔记、项目决策和可检索的工程事实;对于跨语言语义改写、模糊概念联想,基于 embedding 的系统仍可能更有优势。理解这条边界,才能让本地优先的取舍真正发挥作用。
从“把所有上下文塞进提示词”转向按需恢复
每次启动 Agent 都把整份项目历史塞进上下文,成本和噪声都会增长。更可控的办法是拆成两层:
- 启动层:只恢复少量高优先级记忆,例如当前项目约束、正在推进的目标和关键原则;
- 检索层:当用户提到鉴权、数据库迁移或某个子系统时,再从历史中找相关记录。
llmem 的 memory_context 用于前者,memory_relevant 和 memory_search 用于后者。记忆还可以带 type、标签与 project scope。scope 解决了一个很实际的问题:同一台电脑上的全局偏好可以被所有项目看到,但某个仓库的架构约束只应在对应 scope 查询时出现。
例如,团队约定把“支付状态变化必须经由 outbox”写成项目范围内的事实,而把“回答前先给出风险与验证步骤”写成全局原则。两者不必混在一个越来越长的规则文件里。
为什么是 BM25,而不是默认上向量库
llmem 的检索核心是 BM25,并可选用 WordNet 同义词扩展。BM25 根据查询词在文档中的出现频率、词的稀有度和文档长度排序。对于错误码、接口名、目录名、迁移编号、配置键这类工程语料,它往往很直接:查询里出现的精确词会成为强信号。
这也带来清晰代价。若记忆写的是 “rotate session cookie”,而查询改成完全不同措辞的“刷新登录令牌”,只靠词法相关性未必能找到;README 也明确将“重度同义改写”视作它相对 embedding 检索的弱项。因此,实践中应把记录写得可搜:保留模块名、错误信息、接口路径、决策关键词,而不是只写“之前修好了”。
本地 BM25 的收益同样具体:没有每次检索的模型调用,也没有需要维护的远程向量库。对希望把代码库经验留在开发机、又不想把内部决策送到第三方服务的个人开发者或小团队,这是有意义的隐私与运维边界,而不是性能宣传语。
最小运行与 MCP 接入
项目提供 Makefile 目标。先在本地构建并启动服务:
make run make run-bg make status
默认服务端口是 9980,SQLite 数据库位置是 ~/.llmem/data.db。在 MCP 客户端配置中加入 Streamable HTTP 地址:
{
"mcpServers": {
"llmem": {
"url": "http://localhost:9980/mcp"
}
}
}
这里的“本地”并不自动等于安全:任何能访问该端口的本地进程都可能成为攻击面。若你调整了监听方式、使用容器或让端口跨机器暴露,应重新评估网络边界;不要因为数据文件是 SQLite 就把 HTTP 服务直接暴露到公网。
启动后可先检查健康状态:
curl http://localhost:9980/v1/health curl http://localhost:9980/v1/stats
REST API 与 MCP 并存的价值在于分工。Agent 在会话中通过 MCP 调用工具;脚本、cron 或运维检查则可以用 HTTP 端点执行健康检查、导出和统计,不必伪装成一次 Agent 对话。
记录时给未来检索留下锚点
一条记忆至少应包含可判断的结论、适用范围和检索锚点。下面的 REST 示例存入一项项目决策;字段名和路径来自项目 README:
curl -X POST http://localhost:9980/v1/memories \
-H "Content-Type: application/json" \
-d '{
"text": "#decision payments: refund state changes write an outbox row in the same database transaction; do not publish directly from the request handler.",
"type": "decision",
"label": "payments refund outbox",
"scopes": ["payments-service"]
}'
随后可以把检索范围收窄到同一项目:
curl 'http://localhost:9980/v1/memories/search?q=refund+outbox&type=decision' curl 'http://localhost:9980/v1/memories/context?scope=payments-service&per_category=3'
注意:README 描述的 scope 过滤是以创建时的 scopes 数组和查询时的 scope 参数配合工作的;没有 scope 的全局记忆会在各范围查询中出现。因此,不要把临时调试日志当全局事实存入。更稳妥的约定是:全局只放长期偏好和跨仓库原则,业务决定必须附项目 scope,排障记录附模块名与错误关键词。
例如,把“迁移失败”写成一条记忆几乎没有复用价值;把“PostgreSQL 迁移在 production 卡住时,先检查锁等待与应用版本,再决定是否回滚;不要直接删除 schema_migrations 记录”写入,则包含了场景、动作顺序和禁止项。对 API 集成也一样:记录实际 endpoint、认证方式和出现过的响应错误,比记录“已接通某服务”更利于 BM25 召回。
此外,记忆需要有生命周期。一次性实验的输出可以在确认结论后删除;仍在变化的需求可标成 note 并在稳定后升级为 decision;已经被仓库规则文件吸收的结论,应删除记忆或在文本中明确“以仓库文档为准”。否则 Agent 虽然能记住,检索到的却可能是被后来决定推翻的历史。
防止记忆库变成另一种垃圾场
持久化不代表所有内容都值得永久保存。llmem 提供候选查询、手工合并和自动合并机制,用于处理高度相似的记录。先用 dry run 看看会发生什么:
curl -X POST http://localhost:9980/v1/consolidation/auto \
-H "Content-Type: application/json" \
-d '{"min_similarity": 0.95, "dry_run": true, "max_consolidations": 10}'
确认候选合理后,再移除 dry_run 执行合并。阈值不能照抄:过低会把看似相近但条件不同的故障经验混为一谈;过高则几乎不会清理重复项。对包含版本号、环境和日期的事故记录,宁可保留两条并加清楚标签,也不要过早合并。
项目还支持用 LLMEM_AUTO_CONSOLIDATE_INTERVAL 开启周期性自动合并。更建议先观察一段时间:检查合并后是否仍能取回关键决策,尤其是带否定条件的记录,例如“仅在无外部副作用时可重试”。自动整理应减少重复,而不应抹平边界条件。
适合谁,以及不适合谁
llmem 的强项是把 AI 编码过程中的局部、可验证经验留在开发机:SQLite 文件便于备份,MCP 工具使 Agent 可以读写,BM25 让精确工程词汇有可预期的检索路径。它尤其适合不愿维护外部向量服务、希望避免按查询付费、且主要处理英文项目记录的工作流。
但它不是共享知识库、权限系统或版本控制的替代品。团队长期规范仍应留在仓库可审阅文件中;机密信息不应仅因“本地”就随意写入记忆;需要跨语言语义召回时,也应评估 embedding 或混合检索方案。最实用的组合不是用 llmem 取代规则文件,而是让规则文件保存必须遵守的契约,让 llmem 保存会话间需要重新找到的工程证据。
如果要把它纳入日常工作流,可以从一个仓库、两类记忆开始:将反复出现的架构决定记录为 decision,将可重复排查的错误路径记录为 fact。每周用统计端点查看记忆量,并抽查几次真实检索是否返回了正确 scope 的内容;检索质量不好时,先改善记录中的模块名、接口名和异常文本,再考虑增加更多自动化。这样能避免一开始就把每句对话都写入库中,也能在本地检索与规则文件之间维持可解释的责任边界。
重要的是把导出也放进备份策略。llmem 提供 GET /v1/export 与 POST /v1/import;在更换电脑、重装开发环境或做数据库维护前,先导出并验证备份内容,才能让“持久化”不止停留在单一磁盘文件上。备份文件同样可能包含项目决策和内部上下文,应按与源码或配置快照相当的敏感性处理。
当 Agent 的“记忆”被限定为可搜索、可分范围、可整理的本地数据后,目标就不再是制造一个神奇助手,而是减少下一次重复解释同一件事的成本。