MemT 1.0 里,写入和检索是两个独立环节:写入决定“记了什么、以什么粒度记”,检索决定“这次问法能不能把它捞出来”。记忆效果不理想时只把总开关打开或关掉,等于同时动了两个变量,改完既不知道是存错还是取错,也没法判断下次该动哪一侧。比较稳妥的做法是先让两侧都有可观察的输入输出,再用控制变量法一次只动一侧。
处理 MemT 1.0 记忆不准的问题,先把写入侧(写入原文、粒度、来源轮次、记忆 id)和检索侧(查询原文、top_k、过滤条件、命中条目与得分)分别落日志,再做两次单变量对比:固定检索条件只换写入粒度,固定写入内容只换查询表述。只有两侧都留痕,才能判断是“存得不对”还是“取不出来”。如果当前日志里没有命中条目和得分,建议先补日志再调参,否则任何改动都无法归因;线上调整返回条数前,需要结合其他调用方的行为一起确认影响面。
分别记录写入与检索的输入输出
这一步的目标不是调效果,而是让两侧行为都能被看见。建议在调用 MemT 的封装层(业务代码里统一包一层写入函数和检索函数)打结构化日志,而不是依赖页面上的最终回答。下面的骨架是通用形态,字段名可按你自己的 SDK 替换,关键是四类信息不能缺:写入侧要有原文、粒度、来源轮次;检索侧要有查询原文、返回条数、命中条目和得分。
// 写入侧:每次写入调用返回后落一条
{
"side": "write",
"ts": "<ISO 时间>",
"turn": "<会话 id + 轮次>",
"fact": "<写入原文>",
"granularity": "sentence | paragraph | structured",
"source": "<来源渠道>",
"mem_id": "<系统返回的记忆 id>"
}
// 检索侧:每次检索调用返回后落一条
{
"side": "retrieve",
"ts": "<ISO 时间>",
"query": "<查询原文>",
"top_k": 5,
"filters": "<时间范围 / 标签等>",
"hits": [
{"mem_id": "<id>", "score": "<系统返回分值>", "text": "<命中原文片段>"}
]
}
字段较多时可先按下表核对一遍,缺哪列补哪列:
| 侧 | 字段 | 记录什么 | 填写样例(占位) |
|---|---|---|---|
| 写入 | fact | 实际写入的文本原文 | 用户希望周报在周五下午提交 |
| 写入 | granularity | 单句 / 段落 / 结构化 | sentence |
| 写入 | turn + ts | 来源会话与轮次、时间 | session-A / 第 12 轮 |
| 写入 | mem_id | 系统返回的记忆标识 | mem_xxx |
| 检索 | query | 查询原文,不要只记改写后的 | 周报什么时候交 |
| 检索 | top_k / filters | 返回条数与过滤条件 | 5 / 不限时间 |
| 检索 | hits[].score | 命中条目及其分值 | mem_xxx / 见系统返回 |
落完日志后先做一次自查:拿一条刚写入的 mem_id,用它的原文当查询去检索,看能否命中。命中不了,说明问题在写入或索引侧,后面的对比才有意义。
固定检索侧只改写入内容看变化
这一组只回答一个问题:写进去的东西本身,是否适合被检索到。做法是把检索条件完全锁死(同一条 query、同一个 top_k、同一组 filters),只改变同一条事实的写入粒度,分三次写入,并记录命中情况。注意三次写入要用不同的会话或标记隔开,避免混在一起归因不了。
同一条事实的三种粒度可以是:
- 细粒度单句:
用户希望周报在周五下午提交 - 中粒度段落:把上下文一起写进去,例如交代谁在什么场景下提的、是否变更过
- 结构化字段:
{"对象":"周报","字段":"提交时间","值":"周五下午","来源轮次":12}
| 写入粒度 | 命中条数 | 首条得分 | 首条是否直接可用 | 备注 |
|---|---|---|---|---|
| 单句 | 待填 | 待填 | 待填 | 待填 |
| 段落 | 待填 | 待填 | 待填 | 待填 |
| 结构化 | 待填 | 待填 | 待填 | 待填 |
判断口径建议这样定:如果某一粒度的首条直接可用、而另外两种命不中,说明写入粒度对这条事实影响明显,先把这类事实的写入方式统一;如果三种粒度都命不中,问题更可能在检索侧或索引未生效,转去做第二组对比。段落粒度容易把关键信息稀释,结构化粒度依赖写入方的字段约定,各有适用场景,不宜一概而论。
固定写入侧只改检索条件看变化
这一组只回答另一个问题:写进去的内容没问题,是不是查询表述没匹配上。做法是保持写入内容不变(就用上一组里表现最稳的那条记录),只改检索侧的输入,逐条记录命中差异。一次只改一个维度,别同时改表述又改条数。
- 原句查询:直接用用户当时的问法
- 同义改写:换一种说法,不加新信息
- 加限定词:补上主体、时间范围等限定
- 去口语词:去掉“那个、帮我看看”之类的填充词
- 只改 top_k:从 1 调到 3、5、10,观察目标条目是否出现在靠后位置
| 检索变体 | query | top_k | 目标条目是否命中 | 首条得分 | 备注 |
|---|---|---|---|---|---|
| 原句 | 待填 | 待填 | 待填 | 待填 | 待填 |
| 同义改写 | 待填 | 待填 | 待填 | 待填 | 待填 |
| 加限定词 | 待填 | 待填 | 待填 | 待填 | 待填 |
| 调大 top_k | 待填 | 待填 | 待填 | 待填 | 待填 |
如果调大 top_k 后目标条目出现但排名靠后,这属于排序或匹配质量问题,不是记忆没写进去;如果换几种表述都命不中,而用原文查询能命中,说明查询侧的改写策略需要单独看。线上直接放大 top_k 会把更多无关条目带进上下文,需要结合下游对上下文体量的要求一起确认。
把两侧观察结果整理成对照清单
两组对比做完,就可以把现象和排查顺序固化下来,下次遇到类似问题不用重新推演。下表是通用顺序,建议按行从上往下走,确认一行再走下一行:
| 现象 | 先查哪侧 | 验证动作 |
|---|---|---|
| 检索返回空,或全是无关条目 | 先查写入 | 用该事实的原文当 query、适当放大 top_k 检索一次,看记录是否存在 |
| 写入日志里查不到这条事实 | 先查写入 | 检查调用是否真的触发写入、是否被长度或格式规则截断 |
| 能命中但内容答非所问 | 先查写入 | 换段落或结构化粒度重写同一条事实,固定检索条件再测一次 |
| 同一问题换个问法结果差异很大 | 先查检索 | 固定写入,横向比较原句、同义改写、加限定词的命中结果 |
| 目标条目总是排在后面 | 先查检索 | 只调 top_k 看排名位置,确认是排序问题而非缺失 |
| 两条记忆互相矛盾 | 先查写入 | 按来源轮次和时间排序,确认是否缺少新旧覆盖规则 |
执行上的两条边界:一是同一轮排查只动一侧,动完再测,不然结果无法归因;二是把“开/关”只当成最后的手段,先确认写入覆盖、粒度选择和查询改写这三处具体配置,再决定是否需要整体关闭。日志字段和对比表格可以按团队习惯裁剪,但写入原文、查询原文、命中条目与得分这四项建议一直留着,它们是判断存错还是取错的唯一依据。