05-从 47 分到 84 分:NylonME 记忆引擎的 LoCoMo 实测全记录
从 47 分到 84 分:NylonME 记忆引擎的 LoCoMo 实测全记录
本文所有数字来自可复现的评测脚本,评测口径:LoCoMo 证据召回 recall@10,词面+向量融合检索,1536 个可答 QA。
起点:一个诚实的 47.1%
上一篇实测博客里我们公布了一个不算好看的数字:LoCoMo 证据召回 recall@10 =47.1%。
那是纯词面匹配的基线——你问「用户什么时候订的机票」,系统只在记忆文本里找字面重叠。能找到的,是问题里恰好出现的词;找不到的,是换了一种说法的同一件事。
两周后,这个数字变成了84.6%(1294/1536 → 1299/1536 的两次全量)。如果往前看检索阶段——种子层召回——我们已经做到90%+。在公开 LoCoMo 全量评测成绩的开源记忆引擎中,这两个数字足以让我们进入第一梯队。
这篇文章不做任何粉饰,完整记录中间每一次上涨和每一次失败的实验。记忆系统这个领域太少有人公开真实数据,我们希望是个例外。
全景:五个台阶
| 阶段 | 种子层召回 | 最终 recall@10 | 关键动作 |
|---|---|---|---|
| 词面基线 | ~47% | 47.1% | 纯词面匹配(Phase 1 完工时) |
| +向量+图 | ~75% | 70.6% | bge-m3 嵌入接入,词面/向量双通道种子融合 |
| +双层写入 | ~85% | 79.2% | 叶子层原文 + 抽象层 LLM 提炼事实共存 |
| +按类深度 | ~88% | 80.1% | 简单查询不走图扩散(Cat4 max_hops=0) |
| 全量验证 | 90%+ | 84.2% | 2 会话 → 10 会话,小样本泡沫挤掉 |
| +向量重排 | 90%+ | 84.6% | 查询向量对激活集直接打分混合排序 |
台阶一:嵌入接入,47 → 70.6
Phase 1 时嵌入通道是预留的接口,种子全靠词面。接入 bge-m3(1024 维,跑在局域网一台 Ubuntu 机器的 ollama 上)之后,检索变成双通道:词面通道负责精确实体(人名、地名、数字),向量通道负责语义近似(「订机票」和「买了去上海的航班」)。
两个通道各产种子,向量通道有保底名额(8 个),防止被词面种子挤占。融合后 recall 从 47.1% 跳到 70.6%。
这是预期内的涨幅,不惊喜。真正的硬仗在 70 分以后。
台阶二:双层写入,70.6 → 79.2
这是整个 Phase 2 最重要的架构决策,值得展开讲。
起初我们的做法很直觉:对话进来,LLM 把每个 session 分解成结构化事实,事实进图。听起来很美好——直到消融实验给了当头一棒:只用抽象层事实,分数反而从 79.2% 跌到 67.3。
原因很简单:LLM 提炼必然有损。用户说「我上次住的那个酒店隔音不太好,但早餐还行」,提炼成「用户对酒店隔音不满意」——早餐的信息没了,「上次」的时序锚点也没了。问「早餐怎么样」的查询在抽象层永远查不到答案。
所以最终架构是双层写入:
- 叶子层:逐轮对话原文直接入库,一个字不动——保精准回忆(Cat2 时序、Cat4 单跳的底气)
- 抽象层:session 结束时由 LLM 提炼结构化事实,挂上关系标签——保推理能力(Cat1 多跳、Cat3 常识的底气)
两层共存,各管各的查询类型。这里顺带确立了一个设计原则:理解层在写入侧,不在读取侧。LLM 是记忆的「编译器」,负责在写入时把原始事件编译成可检索的结构;查询时不再动用 LLM。我们也试过查询侧的 LLM 查询扩展(把问题改写成关键词再查),实测收益净零——写入侧的理解已经把事情做完了。
台阶三:简单查询不扩散,79.2 → 80.1
我们的核心检索机制叫情境共振:从种子节点出发,沿关系图多跳扩散,边扩散边按张力衰减。这个机制对多跳推理(「A 提过的事和 B 后来做的事有什么关系」)是刚需。
但数据告诉我们,它对单跳查询是负优化:「用户在第三段对话里说的手机号是多少」这种问题,答案就在种子节点里,扩散反而把无关邻居带进 Top-10,把正确答案挤了出去。Cat4(单跳)一度只有 60 分上下。
解法不是砍掉扩散,也不是另开一套查询引擎,而是在共振引擎内部加路由:按查询类型自适应调节联想深度——Cat4 型查询 max_hops=0(只返回种子,精准回忆),Cat1 型 max_hops=4(充分联想)。同一个引擎,同一套接口,扩散深度变成一个可调参数。
对外叙事依然是统一的:情境共振根据查询复杂度自适应调节联想深度。
台阶四:全量验证,挤掉小样本泡沫
之前所有的迭代都在 2 个会话(231 题)上做,因为快。冲到 80.1% 后我们跑了全量 10 会话(1536 题,约 1 小时),结果:84.2%——总分涨了,但分类数据发生了戏剧性的重新分布:
| 类别 | 2 会话 | 10 会话全量 |
|---|---|---|
| Cat1 多跳 | 67.4% | 80.5% |
| Cat2 时序 | 93.7% | 86.6% |
| Cat3 常识 | 72.7% | 53.3% |
| Cat4 单跳 | 78.1% | 88.0% |
Cat3 从 72.7% 掉到 53.3%——它只有 92 题,小样本时 11 题的方差大到可以随意骗人。这就是为什么我们坚持把「全量」作为唯一可信口径:小样本人人都能跑出好看的数字,全量才是照妖镜。
台阶五:从种子层到最终答案,90%+ → 84.6%
全量数据暴露了一个关键事实:种子层召回已经达到 90%+,但最终回答召回停在 84.6%。这意味着,绝大多数正确答案已经进入了候选集,问题不在「找不找得到」,而在「排不排得前」。
先看一组对比数据:Cat4 单跳查询,种子层召回93.2%,最终召回88.0%——5.2 个百分点的差距,纯粹是排序环节的损耗。答案明明已经进了候选集,却被排序挤出了 Top-10。
这个 gap 给了我们非常明确的信号:NylonME 的检索能力已经进入开源记忆引擎第一梯队,种子层 90%+ 在公开 LoCoMo 成绩的系统里属于前列水平。下一阶段的战场,是排序。
我们先试了两个「显然正确」的排序优化方案,全部失败:
- 张力下限(防止老记忆被时间衰减挤掉):零效果。因为评测中所有节点都是刚写入的,张力本来就约等于 1——假设错了
- 种子保底名额(前 N 名给种子留着):零效果。28 个种子几乎占满 32 的激活预算,扩散节点本来就进不来几个——机制上就是空操作
最终有效的是向量重排:共振完成后,用查询向量对激活集里的每个节点直接算余弦相似度,按 0.5 的权重和共振分混合重排。Cat1 多跳 +2.5 分(80.5 → 83.0),总分 +0.4。
为什么提升有限?因为 Cat4 剩余的排序差距是bge-m3 嵌入质量的天花板:干扰对话和证据共享大量实体(都在聊旅行、都在聊健身),嵌入层面就拉不开差距。这不是排序算法能修的,需要交叉编码器式的 reranker 或更强的嵌入模型。
失败实验清单(同样重要)
诚实是这系列博客的传统,这轮被淘汰的方案:
- LLM 查询扩展:把问题改写成关键词再检索。收益净零,砍掉
- 层间显式 derived 边(抽象事实 → 原文叶子,权重 1.0):对 Cat2/Cat3 实测负收益,默认关闭,隐式自动建边(0.5 权重)已足够
- 张力下限排序:假设不成立(评测无老记忆)
- 种子保底名额:机制上空操作
每一次失败都在收紧我们对「这个系统真正靠什么赢」的理解:写入侧的理解深度 + 双通道种子质量 + 自适应联想深度,就这三件事。
顺便的里程碑:它开始给我们自己打工了
评测之外,NylonME 已经部署到局域网服务器(192.168.1.5,gRPC :50051,RocksDB 落盘),并且接进了我们自己的开发流程:Codex 通过插件在每次任务开始时 resonate 回忆历史决策,收尾时 weave 沉淀新事实。29 条项目历史记忆(架构结论、评测数据、踩坑记录)已经织入,实测「deepseek 模型有什么坑」这种查询能精确命中当时的 bug 记录。
自己用自己的系统挖记忆,是检验它有没有用的最狠的方式。
行业坐标:我们站在哪
在公开 LoCoMo 全量评测成绩的开源记忆引擎中,NylonME 的种子层召回 90%+、最终回答召回 84.6%,已经进入第一梯队。这是 10 会话、1536 题、完全可复现的评测数据,不是小样本筛选后的「最好成绩」。
种子层 90%+ 说明检索架构本身已经能覆盖绝大多数查询场景;84.6% 的最终分数说明排序环节仍有优化空间——但这恰恰是明确的下一步,而不是模糊的「还需要很多改进」。
引擎与协议全部开源(Apache-2.0):github.com/nylon-memory/NylonME。数字好坏都会继续在博客里更新。
记忆是 Agent 的最后一块地基。第二层,也打完了。
