Hy Memory 2.0 记忆写入挑时机、检索结果控数量

文章导读
Hy Memory 2.0 的写入频率和召回条数是同一条链路上的两个阀门:写得太勤,条目越堆越像;召回开得太大,历史片段就会盖住当前问题。处理顺序建议是先做写入侧的重复统计和触发条件,再动召回条数——反过来调,常常只是在噪音堆上换了窗口大小,看不出真实差别。
📋 目录
  1. 壹 先数一遍重复条目到底有多少
  2. 贰 给写入加一个触发条件:值不值得记
  3. 叁 限制召回条数并观察回答变化
  4. 肆 写入和召回各改一项的对照实验
  5. 伍 把稳定取值写回配置并记录依据
A A

Hy Memory 2.0 的写入频率和召回条数是同一条链路上的两个阀门:写得太勤,条目越堆越像;召回开得太大,历史片段就会盖住当前问题。处理顺序建议是先做写入侧的重复统计和触发条件,再动召回条数——反过来调,常常只是在噪音堆上换了窗口大小,看不出真实差别。

面对“每轮都写导致重复堆积、召回一多又冲淡当前问题”,可以按这个顺序走:先统计重复条目规模,确认是不是真问题;再给写入加轮次、信息类型、显式指令三类触发条件;最后把召回条数从默认值分三档对比。每轮只改一个参数,用固定问题集复测。重复条目降下来但回答仍跑偏时,才继续收紧召回条数。具体字段名、默认值和取值范围需按实际部署版本核对。

先数一遍重复条目到底有多少

凭感觉调参最容易踩的坑是:你以为在解决重复,实际重复只占很小一部分,真正的痛点是召回把无关条目塞进了上下文。所以先量化。统计口径建议固定在“最近一个可观察窗口 + 同一 scope”,跨会话和跨用户的记忆不要混在一起数。

如果记忆条目落在数据库里,可以先用关键字段去重。前提是写入时已经算过归一化指纹(去首尾空白、统一大小写、去掉时间戳和后缀);没有指纹字段的话,先按原文精确分组粗看,再抽样人工核对。下面的表名和字段名按实际库替换。

-- 规模:总量 vs 去重后数量
SELECT scope,
       COUNT(*) AS total,
       COUNT(DISTINCT content_hash) AS uniq_by_hash,
       COUNT(*) - COUNT(DISTINCT content_hash) AS dup_rows
FROM memory_items
WHERE created_at >= now() - interval '14 days'
GROUP BY scope;

-- 最密集的重复簇,取前 20 组人工看
SELECT content_hash, COUNT(*) AS n,
       MIN(created_at) AS first_seen,
       MAX(created_at) AS last_seen
FROM memory_items
WHERE created_at >= now() - interval '14 days'
GROUP BY content_hash
HAVING COUNT(*) > 1
ORDER BY n DESC
LIMIT 20;

内容相似但不完全相同的近重复,SQL 分组抓不住,用抽样替代:按时间倒序导出最近 100 条,人工标注“同一件事被记了几次”,标到 20 条左右就能判断重复是偶发还是系统性。如果记忆以 jsonl 这类文件形式落地,用命令粗看写入频率也很直接:

# 字段名按实际文件结构调整
awk -F'"created_at": "' '{split($2, a, "T"); print a[1]}' memory.jsonl \
  | sort | uniq -c

判断依据:单日写入条数明显高于对话轮次,且重复簇集中在同一类信息(偏好、环境配置、任务背景),说明是“每轮都写”造成的,不是用户重复交代。

Hy Memory 2.0 记忆写入挑时机、检索结果控数量

给写入加一个触发条件:值不值得记

减少无效写入比事后清理更省事。可选的触发条件通常有三类,可以叠加使用,也可以先只开其中一类观察:

  • 轮次条件:不在每轮写,改为每 N 轮写一次,或会话结束时统一抽取。适合长对话,代价是中途中断会丢一部分上下文。
  • 信息类型条件:只放行偏好、决定、约束、稳定事实;寒暄、确认、重复复述一律不写。这一条对降重复最有效,但需要确认你的版本是否支持按类型过滤。
  • 显式指令:用户明确说“记住这个”时立即写入,不受轮次条件限制。这条要保留,否则会牺牲可控性。

配置位置通常在记忆模块的 write 段,字段名各版本不完全一致,下面的骨架只作对照,落到实际配置文件时逐个核对:

memory:
  write:
    enabled: true
    trigger: on_turn_threshold      # on_every_turn | on_turn_threshold | on_session_end
    min_turns: 3
    types_allow: [preference, decision, constraint, fact]
    types_block: [greeting, smalltalk, ack]
    honor_explicit: true            # 显式指令优先
    dedup_before_write: true        # 写入前查同 scope 近邻
    dedup_window: 50                # 只在最近 N 条内查重,控制写入开销

验证方式:改完配置后重放同一段对话,对比写入日志里的条数;确认“显式指令”路径仍然能写进去,避免把用户主动要求记的内容也拦掉。

限制召回条数并观察回答变化

召回数量参数一般叫 top_k,也有版本叫 limit 或 max_results,位置在检索/recall 段。改之前先确认当前默认值,很多人凭印象以为默认是 5,实际是 10 或更高。

Hy Memory 2.0 记忆写入挑时机、检索结果控数量
memory:
  recall:
    top_k: 6                 # 常见别名:limit / max_results
    min_score: 0.3           # 低于阈值的条目直接丢弃
    scope: [session, user]   # 先 session 后 user,避免跨用户串味
    rerank: false            # 条目少时不必开,先看数量影响

三档取值的对比记录方式:准备 10~20 条固定问题(包含需要历史信息的和纯粹闲聊的),每档跑一遍,逐条记录四项——召回条数、召回的条目里是否包含回答问题所需的那条、回答有没有答非所问、是否需要追问才能补齐背景。用表格记,不要只写“感觉变好了”:

档位top_k召回是否覆盖所需信息回答是否跑偏是否需追问备注
低3逐条填写逐条填写逐条填写偏保守,容易漏掉早期关键信息
中6逐条填写逐条填写逐条填写多数场景的起点
高12逐条填写逐条填写逐条填写噪音明显上升时再往回降

判断方向:如果低档已经能覆盖所需信息,就没必要用高档;只有在低档明显漏信息、且重复条目已经清理过的情况下,才考虑上调。

写入和召回各改一项的对照实验

两个参数同时改,出问题时分不清是谁的锅。用同一段对话、同一组问题做两次单独变更,每次都回到基线再动:

Hy Memory 2.0 记忆写入挑时机、检索结果控数量
组别写入触发召回条数想验证什么观察指标
基线保持原样(每轮写)保持默认值记录当前重复规模和回答质量写入条数、重复簇数量、跑偏轮次
A 组只加上触发条件与基线相同写入收敛后,重复是否下降、关键信息是否丢新增条目数、显式指令是否仍写入、回答缺信息轮次
B 组回到基线只改为三档中的某一档召回条数本身对回答的实际影响召回条数、所需信息是否命中、回答是否离题

观察指标要能落到日志或页面上:写入侧看 memory write 日志的条数和去重拦截记录;召回侧看检索日志里返回的条目和分数。别用“整体感觉更聪明”当结论。

把稳定取值写回配置并记录依据

对照跑完后,把站得住的取值固化,并把“为什么是这个值”一起写进记录,否则过几周没人记得当初的取舍。建议按这个格式留档:

参数改动前改动后对应现象是否保留
write.triggeron_every_turnon_turn_threshold, min_turns=3写入条数下降,显式指令仍能写入保留
write.dedup_window未开开启,查最近 50 条同 scope 近重复被拦截保留
recall.top_k默认值6所需信息仍能命中,回答不再被旧片段带偏待复测确认

复测通过标准建议写成可检查的几条:固定问题集上,需要的背景信息都能召回;不需要历史信息的问题不会被塞进旧条目;用户显式要求记住的内容确实写入并能在后续召回;重复条目不再按天线性增长。任何一条不满足,就回到对应的单项实验再调,不要同时改两项。

最后提醒边界:召回条数不是越小越好,写得越少也不等于越干净。写入过严会丢掉后续要用的约束条件,召回过窄会让多轮任务断片。取值最终取决于你的对话长度、信息密度和用户对“记错”和“忘掉”哪个更敏感,需要结合真实对话回流再确认。