记住什么是能力,忘掉什么才是设计。一个没有遗忘机制的记忆系统,会从资产迅速变成污染源——错误信息被反复召回、过时结论被当成当前事实、用户的一句玩笑被永久固化。这一章讲清一个记忆系统必须回答的六个问题。
编辑部注 · 本章的核心判据只有一句话:能被解释、能被更正、能被删除的记忆才是资产。
把记忆理解成「把历史存起来、需要时取出来」是最简单的理解,也是失败率最高的理解。因为记忆系统的真正难点不在「存」,而在四件事:什么时候该写、写的是什么级别的信息、冲突时信谁、以及怎么保证用户能把它删掉。
一、先分清三种「记忆」
很多人把三件不同的事都叫记忆,导致设计混乱。它们应该分开存、分开管。
| 类型 | 内容 | 存哪 | 存活期 |
|---|---|---|---|
| 工作记忆 | 当前任务的上下文、中间结果、待办 | 上下文 + 外部状态文件 | 任务结束即释放 |
| 情节记忆 | 发生过什么:任务历史、结论、失败原因 | 结构化日志 / 向量库 | 按需保留,可归档 |
| 语义记忆 | 长期稳定的事实与偏好:用户是谁、习惯什么口径 | 键值或知识表 | 长期,但必须可修改可删除 |
混淆的代价很具体:如果把「用户这次让我用简洁格式」写进语义记忆,下一次任何任务都会被这个偏好影响;如果把「本次任务的目标」写进语义记忆,三个月后它还会被召回。
二、写入时机:三个触发器
写入是记忆系统最容易失控的环节。最常见的事故是写入过多——把每一轮对话都写进去,结果记忆库充满噪音,召回质量急剧下降。
三、冲突消解:两条记忆打架怎么办
这是记忆系统最难的部分,也是面试里最容易问到的深层问题。
| 冲突类型 | 示例 | 处理原则 |
|---|---|---|
| 时序冲突 | 「A 部门负责这个指标」→ 半年后「改由 B 部门负责」 | 新事实取代旧事实,但旧事实要保留为历史版本并标记失效时间,而不是物理删除 |
| 来源冲突 | 用户口述「口径是 X」,系统文档写「口径是 Y」 | 系统文档优先作为事实;用户口述作为补充或标注为待确认 |
| 层级冲突 | 通用偏好「输出详细」vs 本次指令「要简洁」 | 越临时的越优先,本次指令覆盖长期偏好,且不修改长期偏好 |
| 真伪冲突 | 两条记忆语义相反且无时间差异 | 不选边:两条都标为「存在冲突」,在需要时向用户澄清 |
当两条记忆真正矛盾且无法用时序判断,绝大多数系统的做法是「取相似度高的那条」或「取新的那条」。这两种做法都在**假装自己知道答案**。
正确做法是:保留冲突,并在回答时显式暴露它——「关于这个口径,我这里有两条互相矛盾的信息(A:…… B:……),你希望以哪条为准?」 这样做有两个好处:用户得到了正确的服务,而且这次澄清会产出一条高置信度的新记忆,把冲突真正解决掉。
四、让记忆可解释、可更正、可删除
这三件事是记忆系统的底线,也是产品能不能被信任的前提。
from dataclasses import dataclass, field
from datetime import datetime
import hashlib
@dataclass
class Memory:
key: str # 归属实体 + 主题,如 "user:42/pref/format"
content: str
tier: str # fact | preference | inference
confidence: float # 0-1,推断类必须低于 0.8
source: str # 谁说的 / 从哪来的
written_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
superseded_at: str | None = None # 被新版本取代的时间(软删除)
evidence: list[str] = field(default_factory=list) # 依据链
@property
def active(self) -> bool:
return self.superseded_at is None
def to_prompt_line(self) -> str:
"""回喂格式:内容 + 时间 + 层级 + 置信度,让模型自己能判断时效"""
tag = f"[{self.tier}·{self.confidence:.1f}]"
return (f"{tag} {self.content}\n"
f" 记录于 {self.written_at[:10]} 来源:{self.source}")
class MemoryStore:
def __init__(self):
self._items: dict[str, list[Memory]] = {} # key -> 版本链(保留历史)
def write(self, m: Memory) -> str:
"""
同 key 的旧版本不删除,只标记 superseded_at。
这保证了「可解释」——任何时候都能回答「为什么系统会这么认为」。
"""
chain = self._items.setdefault(m.key, [])
for old in chain:
if old.active and old.content == m.content:
return "duplicate"
if old.active and old.content != m.content:
old.superseded_at = m.written_at # 软删除,保留证据
chain.append(m)
return "written"
def recall(self, keys: list[str], limit: int = 5) -> list[Memory]:
"""只返回当前有效版本,且带时间戳"""
out = []
for k in keys:
actives = [m for m in self._items.get(k, []) if m.active]
out.extend(actives)
return out[:limit]
def conflicts(self) -> list[tuple[Memory, Memory]]:
"""检测同 key 下无法用时序消解的并存冲突(理论上不该存在)"""
res = []
for chain in self._items.values():
actives = [m for m in chain if m.active]
if len(actives) > 1:
res.append((actives[0], actives[1]))
return res
def forget(self, key: str, hard: bool = False) -> int:
"""
删除能力是记忆系统的合规底线。
hard=True 用于用户行使「删除权」,彻底清除包括历史版本。
"""
if key not in self._items:
return 0
n = len(self._items[key])
if hard:
del self._items[key]
else:
for m in self._items[key]:
m.superseded_at = m.superseded_at or datetime.now().isoformat(timespec="seconds")
return n
def explain(self, key: str) -> str:
"""可解释性:把一条记忆的完整演变史打印出来"""
chain = self._items.get(key, [])
if not chain:
return f"{key}: 无记录"
lines = []
for m in chain:
state = "生效" if m.active else f"已于 {m.superseded_at[:10]} 失效"
lines.append(f" {m.written_at[:10]} {state} [{m.tier}] {m.content}(来源 {m.source})")
return f"{key} 的演变:\n" + "\n".join(lines)
def memory_fingerprint(text: str) -> str:
"""内容指纹,用于跨库去重"""
return hashlib.sha256(text.strip().encode()).hexdigest()[:16]- 用
explain()输出一条记忆的演变史,检查是否满足「任何时刻的结论都能追溯到来源与时间」; - 构造一个时序冲突(先写 A,再写 A'),验证
recall()只返回 A' 而历史仍可查; - 构造一个真伪冲突(同 key 两条 active),验证
conflicts()能检出,并设计一段话术在回答时暴露冲突。
五、自测
六、小结
| 问题 | 答案要点 |
|---|---|
| 记什么 | 分三层:工作记忆(任务级)/ 情节记忆(历史)/ 语义记忆(长期事实与偏好) |
| 什么时候写 | 显式指令 > 任务结束固化 > 自动抽取(最需谨慎) |
| 写入前校验 | 稳定性、来源可信、非重复、可归属,四条都要过 |
| 冲突怎么办 | 时序 / 来源 / 层级 / 真伪,四类用四套原则;真伪冲突不选边 |
| 怎么召回 | 上限 5 条以内,必须带时间与来源 |
| 底线 | 可解释、可更正、可删除(含硬删除) |
记忆的价值不在于记得多,而在于记得准、说得清、忘得掉。本刊编辑部
下一章处理一个常见的设计抉择:当任务复杂到一定程度,是该开更多 Agent,还是把上下文做得更好?
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
长期记忆你会存什么?什么时候写? 高频
写入时机是这道题的关键:只在信号足够强的时候写——用户显式纠正("以后别用表格")、用户显式声明("我在做招聘")、或者同一偏好被重复观察到多次。绝不能把每一次会话里的临时要求都写进去,否则记忆区会变成噪声区,注入后反而干扰判断。
配套要求:写入带时间戳与来源,支持用户查看与删除,并设置 TTL 或衰减机制。
追问链
- 怎么防止把一次性偏好写进去?
- 记忆检索是走向量还是规则?
记忆和新事实冲突了怎么办?
① 先分类——是"偏好变化"还是"事实变化"?偏好变化(从喜欢表格改成喜欢列表)通常是覆盖;事实变化(换团队了)也是覆盖,但要保留历史区间。
② 带时间与置信度——每条记忆都有生效时间与置信度,冲突时高置信度优先;同等置信度则新的优先,但把旧的标记为 superseded 而不是删除。
③ 不可消解就暴露给用户——如果两条记忆指向矛盾结论(用户说"汇报要简洁",但历史反馈显示他常要求补充细节),不要自己赌,应该在输出时给出选择或直接询问。
核心原则是:记忆系统要能解释自己为什么这么判断,否则出错时无法排查。
怎么证明记忆让效果变好了,而不是让输出变得飘忽?
做法是 A/B:同一批任务分别跑「注入记忆」与「不注入记忆」两条链路,对比任务完成率、人工介入次数、以及目标风格的一致率。要特别关注负向信号:注入记忆后,模型是否开始在不相关场景里生搬硬套旧偏好(过拟合);是否出现"忘了任务本身要求,只记得风格要求"(注意力争夺)。
工程上还有两个必要设计:记忆可关闭(某个任务可以临时不使用历史偏好)和记忆可追溯(输出里能看出用了哪条记忆)。没有这两样,记忆出问题时你会连原因都定位不到。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。