Hy Memory 2.0 检索返回旧条目,多数时候不是向量召回失灵,而是两条记忆都在库里,只是检索把旧的那条排到了前面:写入时间字段没更新、排序按相似度而旧条目措辞更像用户问句、或者过期/覆盖规则没生效。处理顺序建议反过来:先按用户 ID 和主题把冲突的两条同时查出来,确认两条都在;再逐条核对时间字段是否真的在写入;然后看排序依据是相似度还是时间;最后配置过期或覆盖规则,用同一句提问复测。
当存储里同时存在新旧两条记忆而检索总是返回旧的,先不要调相似度阈值。先并列打印两条条目,核对写入时间字段、排序依据与过期/覆盖规则三处,通常能定位到时间戳未更新或排序按相似度这两类原因。适用于可查库、可看日志的自建或托管实例;若时间字段由外部写入方提供,需要与写入方确认字段语义后再改排序。
把冲突的两条记忆同时打出来看
第一步不是改配置,而是确认两条记忆确实都在库里。按用户 ID + 主题(topic / memory_key,字段名以实例为准)查询,不要用检索接口,用列表或按条件过滤的查询接口,避免检索层又只返回一条。下面是一段通用查询骨架,把 USER_ID、TOPIC 换成实际值,命令或 SDK 按你的接入方式替换:
# 通用骨架:按用户与主题列出全部条目(字段名以实例为准)
GET /memories?user_id=USER_ID&topic=TOPIC&limit=50&include_expired=true
# 等价 SQL 形式(表名/列名为占位)
SELECT id, user_id, topic, content,
created_at, updated_at, write_ts, expire_at, version
FROM memory_items
WHERE user_id = 'USER_ID'
AND topic = 'TOPIC'
ORDER BY updated_at DESC;
输出样例只表示结构,不表示具体数值:
[
{"id":"m_1024","content":"用户偏好邮件通知",
"created_at":"T1","updated_at":"T1","write_ts":"T1","version":1},
{"id":"m_1031","content":"用户偏好站内信通知",
"created_at":"T2","updated_at":"T2","write_ts":"T2","version":2}
]
两条都在,说明写入没有失败,问题在检索侧的取舍。只查回一条时,先排除写入是否被 topic 归一化、用户 ID 大小写或前后空格改写,导致第二条落在另一个分区里。
检查每条记忆的时间字段是否真的在更新
记忆类系统里常见的时间字段有 created_at、updated_at、write_ts、event_time、expire_at;具体叫什么名字以你的表结构和接口返回为准,不确定时按占位处理,不要凭记忆猜。逐条核对三件事:字段是否存在且非空;第二条的写入时间是否晚于第一条;更新操作是原地更新还是新插入一条。
# 逐条核对:把每条的时间字段单独打印出来
for item in items:
print(item["id"],
item.get("created_at"),
item.get("updated_at"),
item.get("write_ts"),
item.get("expire_at"))
- 时间字段为空、为 0、为默认纪元时间:说明写入时没赋值,排序会把它们当成同一时刻或最旧。
- 两条的 created_at 都正确但 updated_at 相同:更新走的是“先删后插”以外的路径,或更新语句没有写这个字段。
- 时间字段是字符串而非时间类型:比较时按字典序,跨月份、跨年份的顺序会错。
- 时区不一致:写入方用本地时间、检索方按 UTC 比较,差值稳定出现在同一侧。
这一步的验证方式很直接:用同一主题连续写两次内容不同的记忆,再查一遍时间字段,看第二条是否严格更晚。若是外部系统写入,需要与写入方确认 write_ts 的语义是写入时刻还是事件发生时刻。
看排序:结果按相似度还是按时间排
两条都在、时间也正确,旧条目仍胜出,通常就是排序依据的问题。检索接口常见的配置项有 sort_by(similarity / time / hybrid)、top_k、rerank、recency_weight 一类参数,名称以实例为准。先在调用处或检索服务配置里找到排序依据,再把每条候选的分值打印出来,确认旧条目是相似度更高还是时间更新。
# 打印分值的骨架:字段名以实际返回为准
resp = memory.search(query="通知方式偏好", user_id=USER_ID,
topic=TOPIC, top_k=5, debug=True)
for c in resp.candidates:
print(c.id, c.score, c.updated_at, c.get("recency_score"))
判断规则:旧条目 score 高、updated_at 旧,说明排序基本只看向量相似度;新旧条目 score 接近而旧条目仍排第一,检查是否有 sort_by 默认值、是否被上层缓存了上一次结果。若排序已支持 sort_by=time 或时间加权,把它们作为对照组各跑一次同一句提问,看返回顺序是否变化。
设置过期或覆盖规则后复测同一句提问
排序调完通常还会遇到同类问题:同一主题下条目越积越多,模型看到几条互相矛盾的说法。这类情况适合用覆盖写(同 topic 只保留一条)或设置过期时间,而不是靠提高相似度阈值。下面只是配置骨架,键名要替换成你实例里真实存在的:
# 覆盖/过期规则骨架(键名为占位)
memory.write(
user_id=USER_ID,
topic=TOPIC,
content="用户偏好站内信通知",
mode="upsert", # 同 topic 覆盖旧条目
ttl_days=90, # 或 expire_at=具体时间
updated_at=now() # 确保时间字段被写入
)
复测用同一句提问,前后两次结果填进同一张表再对比,不要凭印象:
复测记录表
提问:通知方式偏好?
复测前:返回 m_1024(邮件),updated_at=T1
复测后:返回 m____(站内信),updated_at=T2
差异点:排序依据 / 过期时间 / 是否只剩一条
如果复测后仍返回旧条目,先确认写入是否真的生效(再查一次列表),再确认检索有没有结果缓存、有没有多副本读到旧数据。ttl 设置过短会让记忆来不及被用到,建议先用较长周期观察,再收紧。
留一条旧数据被正确淘汰的证据
一次复测通过不代表规则稳定,需要在下一篇旧数据自然过期或被覆盖时留下记录。截图或存档前按下面的检查点过一遍:
- 旧条目在查询结果中的状态:已删除、已标记过期,还是仍在但被过滤。
- 旧条目的
expire_at与实际当前时间的比较,确认是按预期时刻淘汰的。 - 新条目的
updated_at与version,确认是覆盖写而不是并列新增。 - 同一句提问的检索日志:候选列表、分值、最终返回的条目 ID。
- 是否开启结果缓存,缓存过期时间与记忆过期时间是否互相拖后腿。
这些检查点都能通过配置、日志或查询结果直接观察到,属于可验证内容。整理处理记录时,把“冲突两条的原始输出、时间字段核对结果、排序依据、复测对比”四样留在一起,下次同类问题可以直接对照,而不用从检索算法重新排查一遍。