“加了记忆功能之后,模型开始胡编。”这通常不是记忆模块写错,而是边界没定:什么进长期存储、什么只活一轮、写进去的事实凭什么被信任。三者没定,记忆库就成了装满猜测的黑箱,而检索每次都会忠实地把猜测喂回去。
记忆不是“让模型更懂我”,而是一个带容量、带过期、带错误率的数据系统,与缓存同构。按顺序拆:存什么 → 何时写 → 如何存 → 如何忘 → 如何取 → 如何评估。
1. 四种记忆与各自的载体
| 形态 | 内容 | 生命周期 | 载体 | 失效模式 |
|---|---|---|---|---|
| 工作记忆 | 当前推理所需的中间结论 | 单次任务 | 上下文窗口 | 被后续内容挤掉 |
| 会话记忆 | 本次对话的意图、已确认条件 | 单会话 | 会话状态 + 摘要 | 摘要把限定条件丢掉 |
| 长期事实记忆 | 跨会话稳定的事实(偏好、约束、实体属性) | 周/月 | 结构化库 + 向量索引 | 过时、冲突、污染 |
| 流程经验 | 某类任务的做法(步骤、工具偏好、坑) | 长期 | 提示片段 / 工作流模板 | 过拟合到旧场景 |
四者混在一个存储里是最常见的起点错误:读写频率、一致性要求、清理策略都不同。
2. “存什么”的判据是下次会不会用到
不是“信息有没有价值”,而是下一次任务是否可能用到它。有价值但不复用的信息,存了只会污染检索。
| 信息 | 有价值 | 会复用 | 结论 |
|---|---|---|---|
| 输出格式偏好(表格/要点) | 是 | 是 | 存,作为稳定偏好 |
| 项目技术约束(版本、禁用项) | 是 | 是 | 存,带 scope |
| 用户今天的情绪 | 是 | 否 | 不存 |
| 一次性的临时口径 | 是 | 否 | 只进会话记忆 |
写入门槛(三条同时满足)
1 可复用:同类任务未来还会出现
2 可验证:有来源或用户确认过
3 可作用:会改变下一次的输出或动作
第三条最易被忽略:不改变任何决策的事实只是占容量的噪声。
3. 写入时机:显式指令与自动抽取
| 方式 | 精度 | 召回 | 噪声来源 | 适用 |
|---|---|---|---|---|
| 显式指令(“记住…”) | 高 | 低 | 几乎无 | 用户主动设定的偏好与约束 |
| 自动抽取 | 低 | 高 | 把闲聊、假设、模型自己的推测当事实 | 需要覆盖大量隐式信息时 |
自动抽取的问题不在抽得少,而在抽得随意:假设句、反问、模型自己的推测,都会被写成肯定陈述。折中是两级流水线:先写候选池,满足重复出现或用户确认才晋升。
对话 → 抽取器 → 候选池(未生效)
├ 重复 ≥ 2 次 → 晋升
├ 用户确认 → 晋升
└ 超期未确认 → 淘汰
# 记忆写入策略(按实现核对字段名)
extract:
mode: candidate # off | candidate | direct
require_source: true
max_candidates_per_turn: 3
promote:
min_observations: 2
require_confirmation: false
ttl_days: 30
4. 事实记忆的结构
一条事实至少要有五个字段,缺一个就会在某个场景失效。
| 字段 | 作用 | 缺失后果 |
|---|---|---|
| subject | 归属主体(用户/项目/团队) | 张冠李戴 |
| predicate + value | 断言内容 | 无法比较新旧 |
| valid_from / valid_to | 生效区间 | 旧规则覆盖新规则 |
| source | 来源(对话/文档/系统) | 无法判权威、无法纠错 |
| confidence | 置信度 | 低质事实与确认事实同权 |
{
"subject": "user:u1024",
"predicate": "preferred_output_format",
"value": "markdown_table",
"valid_from": "2026-03-01",
"source": "explicit_instruction",
"confidence": 0.95,
"scope": "project:clarity"
}
scope 是实践中救场最多的字段:同一人在不同项目的约束常常相反,没有 scope 必然串味。
5. 冲突处理:覆盖需要判据
| 情形 | 处理 | 理由 |
|---|---|---|
| 用户显式更正 | 新事实生效,旧的写 valid_to |
显式指令优先级最高 |
| 同一 predicate 两个值,来源不同 | 按来源权威排序(系统 > 文档 > 对话推断) | 权威差异是真实存在的 |
| 来源相同、时间不同 | 取时间晚者,保留历史 | 时间是最便宜的判据 |
| 来源与时间都相同 | 并列保留,降低置信度 | 无法裁决时不要伪造裁决 |
| 值互相矛盾但 scope 不同 | 各归其 scope,不视为冲突 | 大部分“矛盾”其实是串域 |
最常见的事故是后写覆盖:没有判据,最后写入的永远赢,一次口误就能永久污染一个事实。
6. 遗忘机制:三种策略各有代价
| 策略 | 触发 | 代价 | 适用 |
|---|---|---|---|
| 容量淘汰 | 每主体条目上限 | 挤掉低频但关键的事实 | 个人助手类,容量压力大 |
| 时间过期 | TTL / valid_to | 长期有效的偏好被误删 | 时效性强的事实(职位、价格) |
| 纠错删除 | 显式删除或负反馈 | 需要产品入口与审计 | 错误事实、敏感信息 |
retention:
max_items_per_subject: 200 # 典型量级,按实测容量调
ttl_default_days: 180
ttl_override: {preference: never, employment: 365}
decay:
enabled: true
half_life_days: 90
keep_floor: 0.3 # 低于此值只降权不删
遗忘的目的不是省空间,而是缩短错误事实的存活时间。容量与 TTL 是手段,纠错入口才根治——大部分错误来自抽取,不会自己过期。
7. 检索记忆:三种方式,错一种就串味
| 方式 | 适合 | 风险 |
|---|---|---|
| 按时间(最近 N 条) | 会话续接、短期上下文 | 久远但关键的约束被漏掉 |
| 按相似度(向量) | 事实语义模糊、需模糊匹配 | 相似但不同主体/项目的事实被拉进来 |
| 按结构化查询(键值、条件) | 明确 predicate(偏好、版本) | 需要 schema,冷启动成本高 |
“检索错记忆比没有记忆更糟”:没有记忆时模型会问一句,检索错了它不问,直接按错约束执行。顺序必须是先 scope 后相似度,阈值宁高不低。
recall(subject, query):
pool = store.where(subject == s AND scope IN {global, current_scope})
key = pool.exact(predicate_of(query)) # 结构化优先
return key if key else pool.vector(query, top=5, min_score=0.8)
8. 记忆污染与纠错入口
污染是自我强化的:错误事实写入 → 下次命中 → 据此生成 → 生成内容又被抽成新事实。三轮之后原始来源已不可见。
| 环节 | 必须留下的东西 |
|---|---|
| 写入 | source、写入时的原始片段 id |
| 检索 | 本次命中了哪些记忆 id(进日志) |
| 生成 | 输出是否依赖记忆(可标注) |
| 纠错 | 用户可查看、可删除、可更正 |
纠错入口(最小可用)
1 用户侧:列出"关于我的事实",逐条可删/可改
2 运营侧:按 source=candidate 批量清理
3 数据侧:删除时写 tombstone,防止重新抽取复活
第 3 条最易漏:删了事实但保留原对话,下一轮抽取会把它原样写回。删除必须带墓碑。
9. 隐私与隔离
| 类别 | 处理 |
|---|---|
| 证件号、银行卡、健康信息 | 不进长期记忆;需要时即时取用,用完不落库 |
| 第三方的个人信息 | 不进长期记忆(不属于该用户可授权范围) |
| 密钥、口令 | 不进记忆,走凭据服务 |
| 多用户环境 | namespace = tenant + user,检索前强制过滤 |
隔离失效的典型症状是“A 用户问出了 B 的偏好”——测试环境几乎不出现,因为测试数据常只有一个人。
10. 记忆与上下文的预算竞争
| 预算项 | 建议占比 | 挤占后果 |
|---|---|---|
| 系统提示与工具定义 | 固定 | 削减会破坏行为一致性 |
| 记忆片段 | 小且固定上限 | 过大则挤压检索结果 |
| 检索证据 | 主要弹性项 | 被挤压 → 答非所问 |
| 历史对话 | 可压缩(摘要) | 压缩过度丢限定条件 |
| 输出预留 | 固定 | 不足则回答被截断 |
记忆高置信、低覆盖,证据低置信、高覆盖。紧张时优先保记忆,但不能让它吃掉检索空间——约束对了、事实没有,照样错。
11. 评估:有/无记忆的对照
| 指标 | 含义 | 对照做法 |
|---|---|---|
| 首轮正确率 | 不需要用户重复说明的比例 | 同一批任务,开/关记忆各跑一遍 |
| 重复说明次数 | 用户重申同一约束的次数 | 统计会话内重复率 |
| 纠正次数 | 用户纠正模型的比例 | 纠正是负向信号,绝对值看趋势 |
判定表
开记忆后首轮正确率↑ 且 纠正次数↓ → 记忆有效
开记忆后正确率持平 但 纠正次数↑ → 记忆在引入错误约束
两者都无变化 → 记忆没被用上,删掉它
最后一行值得写进决策记录:从不被命中的记忆,成本是存储、延迟与污染风险,收益为零。
12. 排错:六种“记忆引起的怪事”
| 现象 | 原因 | 处理 |
|---|---|---|
| 模型突然用上没提过的偏好 | 旧事实未过期、scope 未过滤 | 检查 valid_to 与 scope 条件 |
| 同一问题两次会话答案不同 | 记忆检索结果不稳定(阈值附近) | 提高阈值,或改结构化优先 |
| 用户说“我没这么说过” | 抽取把假设句写成事实 | 关闭 direct 模式,走候选池 |
| 删了记忆但行为不变 | 原对话仍在,被重新抽取 | 写 tombstone,排除已删来源 |
| 记忆越多回答越慢 | 检索记忆挤占了证据预算 | 给记忆设固定小配额 |
| A 用户看到 B 的偏好 | namespace 过滤缺失 | 检索前强制 tenant+user 过滤 |
13. 边界:什么时候不要做记忆
| 场景 | 更合适的做法 |
|---|---|
| 会话很短、任务不重复 | 把上下文管好就够了 |
| 事实极少且稳定 | 写进系统提示 |
| 强合规场景 | 事实由系统录入,不由对话抽取 |
判断标准只有一句:记忆的收益来自复用。没有复用,它只是把上下文问题搬到了更难排查的地方。
14. 自测
- 用户说“记住我用表格输出”,这条该进哪一层记忆?为什么不是会话记忆?
- 自动抽取为什么必须先进候选池?不进会发生什么?
- 同一 predicate 出现两个值,按什么顺序裁决?都裁不了时怎么办?
- 删除一条事实后行为没变,最可能漏了哪一步?
- 为什么“先 scope 过滤再相似度”不能反过来?
- 开记忆后首轮正确率不变、纠正次数上升,说明什么?