作者构建了一个 MCP 记忆服务器,将「模型提议、确定性代码决策」作为核心规则,解决 LLM 自行管理记忆导致的幻觉记忆问题。
几乎所有的 AI 记忆库都有一个漏洞,一旦你看到这个漏洞就无法忽视:那个会产生幻觉的模型,也被你安排来负责记录。
你把一段对话交给 LLM,让它记住重要的内容。它阅读、它决定、它把它的解读写入长期记忆,仿佛那就是事实。三个会话后,你的应用自信地"知道"了用户从未说过的东西——而你甚至找不到这条信息的来源。狐狸守着鸡舍,然后还要事后写事故报告。
我在这个问题上花了一整年时间,很惭愧。最终我构建的东西——刚刚作为 MCP 服务器发布——完全颠覆了这种安排。模型不再决定该记住什么。它只能提议。确定性代码(里面没有任何模型和提示词)来决定真正写入的是什么。
这篇文章是诚实版本:它做了什么、它的真正强项在哪里、它的弱点在哪里,以及发布前夜它对我撒谎、我差点就这样 ship 的故事。
规则只有一句话:模型提议,确定性代码裁决,没有任何依据的东西不写入。
具体来说,你的 agent 不是直接断言一个事实。它提出一个主张,同时引用它所依据的精确文本。然后一道闸门检查该引用是否真正支持这个主张。看它如何拒绝我试图绕过证据滑入自己的解读:
remember(claim="Priya joined Acme in 2019 under duress.",
evidence="Priya Raman joined Acme in 2019 as a logistics analyst.")
REFUSED (asserts_more_than_evidence) — the claim adds something the evidence does not say.
claim : Priya joined Acme in 2019 under duress.
evidence: Priya Raman joined Acme in 2019 as a logistics analyst.
"under duress"(被迫)在证据中完全找不到,所以这条主张永远不会进入存储。而且我没办法通过辩论说服它,因为这道闸门不是我能够施加说服力的对话——它是一个函数。这才是关键。提示词注入、越狱、模型只是自信地犯错——这些在这里都帮不了你,因为在裁决方没有任何模型可以被你糊弄。
这是反幻觉检查被前置到写入时的实现方式——此时代价低廉且永久有效——而不是寄希望于稍后读取时有一个重排序器或 LLM 裁判来 catch 问题。
当一条主张被确认时,它被写入——同时存储它所来自的字节范围:
ADMITTED — Dana Kim has a cat named Pepper.
grounding : grounded_verbatim
receipt : bytes [0:71] of sha256:b410428a2b58…
这个收据(receipt)是我最引以为傲的部分。它不是指向某个 near 答案的 chunk 的引用。它是(document_hash, byte_start, byte_end)——精确的字节跨度。它是可以被验证的:
verify_receipts
→ receipts re-verified: 1/1 checkable — verified
verify_receipts 会对源文档重新哈希,并重新切片精确的字节范围。如果有人在存储的事实下面编辑了文档,收据不会悄悄地保持绿色——它会失败:
→ receipts re-verified: 0/1 checkable
FAILED — Dana Kim has a cat named Pepper.
A failure means the source document changed after the claim was bound to it.
一个能被抓到对你撒谎的记忆,比一个自信地 smooth 的记忆更有价值。大多数 RAG 系统中的"引用"都是 smooth 那种:它们指向一个大致正确的 chunk,即使底层文本已经漂移也看起来没问题。字节范围收据是你能够证伪的出处——这是唯一值得拥有的出处。
问它一些它存储内容之外的问题,它不会即兴发挥一个看似合理的答案。它精确地告诉你它的知识边界在哪里,以及它确实知道什么:
> What is Dana Kim's salary?
ABSTAINED (unknown_predicate) — no claims ground "salary"; 2 claims about Dana Kim exist,
grounding: named, pepper, cat, plays, weekends, basketball
A refusal, not an empty result: the substrate is telling you it has nothing rather than guessing.
Next: ask about one of: named, pepper, cat, plays, weekends — or commit a claim grounding "salary".
注意这个拒绝会重定向。大多数调用者都是 agent,而 agent 无法浏览存储来想出该问什么——所以一个赤裸裸的"没有"就是交互死亡的地方。说出有依据的术语名称和下一个动作,把死胡同变成可用信号。(这个重定向,附带说一句,是我发布前一天修复的东西之一——下面会详细说。)
如果你构建的东西会接触真实用户的数据,你迟早会遇到"删除我——并证明你做到了"。forget(subject) 就是为这个而构建的:
forget(subject="Priya Raman")
→ ERASED Priya Raman — certificate issued
nodes removed : 3
bystanders surviving : 1
精确闭包意味着 subject 被完全移除。旁观者存活意味着其他所有人的事实都完好无损——删除 Priya 不会悄悄损害与她一同被提及的人们的记录。而且你得到的是签名证书作为产物,而不是你希望起作用但实际上可能没有的软删除。这是告诉合规官"相信我"和递给他们一张收据之间的区别。
这里是我停止推销并开始对你坦诚的地方,因为我认为这是更有用的模式。
这个想法是 solid 的。模型提议,代码裁决,字节范围收据,可证明的擦除。我已经反复锤炼过,它站得住脚。这是保证,是我愿意用这个项目做赌注的部分。
代码还年轻。它在 PyPI 上发布时才两天,而且它年轻的程度和两天的代码年轻程度完全一样。我精确地知道这一点,因为发布前夜发生的事情。
我就要发帖了。在发帖之前,我终于做了我应该始终做但很少做到的事情:我用自己的包的方式安装它——就像一个完全的陌生人会做的那样——在干净的环境中,通过真正的协议让真实的客户端说话——然后我开始试图破坏它。
几分钟后它就对我撒谎了。
我让它记住"Ada Lovelace wrote the first algorithm."它高兴地返回 ADMITTED。它什么都没存储。写入路径在报告成功的同时悄悄把事实丢在了地上——这是一个记忆系统能有的最糟糕的 bug,而且它就坐在前门。
原因几乎很可笑。分类传入主张的防火墙通过拼写识别动词——任何以 -s、-ed、-ing 结尾的东西。所以它根本没听说过"wrote"。或者"went"。或者"built"。英语中每个不规则过去式对它都是不可见的,任何建基于其上的句子都被作为无意义的片段丢弃。十六个完全普通的句子中有九个被拒绝。而那些通过的大多靠运气——"Marcus Webb sold his bookshop"之所以存活只是因为 Marcus 以 s 结尾。
那天晚上我发现了三个阻断发布的 bug。动词那个。路径遍历输入抛出了原始 traceback 而不是干净的错误。一个腐败的 store 文件导致服务器每次调用都崩溃,而不是隔离自己。
我修复了所有三个,写了回归测试以防止它们悄悄回来,然后才切到了你今天会安装的发布版本。
更深层的教训是令人不舒服的那个。"报告 ADMITTED 但什么都没存储"这个 bug 是这个代码库中我第三次犯同样类型的错误:计算一个决策,然后忽略它。一个验证结果被计算然后丢弃。一个弃权标志被计算然后丢弃。现在是一个摄取结果被计算然后丢弃。计算了一个检查然后不尊重它,比根本不检查更糟糕,因为它看起来像是做了检查。这个模式现在是我积极寻找的东西。
catch 所有这些的工具——一个通过 stdio 驱动已安装二进制文件的测试工具,覆盖 Python 3.9 到 3.13,抛出畸形 JSON-RPC、46KB payload、路径遍历、空字节、腐败的 store 文件,以及三个服务器同时锤击一个 store——这是应该在 0.1.0 版本就存在的东西。它现在存在了,并且每次发布都会运行。
所以当你发现一个 bug 时——你会的——这不是项目崩溃了。这是循环在起作用。它在发布前夜 catch 了三个。它会 catch 你的。
Recall 是柔软的那一半,我宁愿你从我这里听到,而不是在最初十分钟发现它并感到被骗。
在一个 410 题的集合上(答案确实在存储中),它在默认安装下仍然拒绝约 37%(如果添加可选的语义编码器则约 25%)。这很高,我公布这个数字而不是隐藏它,因为替代方案——在不应该回答时悄悄回答——正是这个项目存在要避免的精确失败。这个权衡是刻意的:它优化的是永远不存储无依据的事实,以及每个存储的事实都是可证明的,而不是原始 recall 率。如果你想要一个最大 recall 的检索器,这是错误的工具,我会在评论中告诉你。
在这个在公共场合犯错的话题上:几个月前我撤回了这个项目的自己的基准测试。它报告了满分——零个错误断言——结果证明它在测量一个空数据库。722 个记忆存储中有 721 个是空的,每一个答案都是相同的拒绝字符串,而一个"I don't know"总是正确答案的语料库让一个空存储看起来完美无瑕。那次撤回及其原始数据在 repo 中是公开的。如果你在自己的工作中评估弃权或依据,这个失败模式值得你花五分钟,无论你是否接触过我的代码。
uvx fireweed-mcp
或者将它接入 MCP 客户端:
claude mcp add fireweed -- uvx fireweed-mcp
{
"mcpServers": {
"fireweed": { "command": "uvx", "args": ["fireweed-mcp"] }
}
}
零依赖。无 API key,无云,无模型下载,无 GPU。服务器中没有任何东西运行推理,所以它不关心你的 agent 背后是什么——llama.cpp、Ollama、前沿 API、什么都行。存储是一种开放格式,有仅用标准库的参考读取器,所以你的数据是这个项目的生命周期之后以及我这个人生命周期之后仍然存在的。
许可证,放在前面以免有人觉得被骗:FSL-1.1-ALv2。源码可用,不是 OSI 开源——免费用于任何事情除了构建竞争产品,并且在 2028 年转换为 Apache-2.0。我宁愿在一段你真正会读的段落中说这些,而不是让你在 LICENSE 文件中找到它并合理地认为我在耍花招。
Repo、撤回和原始数据:https://github.com/Starksood/fireweed-mcp
我会看评论。你能做的最有用的事情是把它接到某个东西上,试图打破它,然后告诉我怎么做到的。比用户更快发现 bug 是整个游戏——你比我更擅长破坏我的代码。