上下文窗口是内存,记忆是硬盘。没有好的写入与检索策略,硬盘越大,噪声越多。
记忆不等于「把历史存起来」
一个常见的错误做法是:把所有对话存进向量库,每次检索 top-k 塞回上下文。结果:
- 检索到大量寒暄和重复内容;
- 旧偏好覆盖新偏好;
- 敏感信息被反复带出;
- 上下文膨胀,模型反而更糊涂。
记忆系统要回答四个问题:存什么、怎么存、何时取、何时忘。
四类记忆
| 类型 | 内容 | 存储 | 例子 |
|---|---|---|---|
| 工作记忆 | 当前任务的目标、计划、观察 | 上下文 / State | 「正在核对第 3 张发票」 |
| 情景记忆 | 具体发生过的事 | 事件表 + 向量 | 「上次用户要求用英文回复」 |
| 语义记忆 | 事实、偏好、领域知识 | 结构化表 + 向量 | 「用户偏好顺丰,不接受放门口」 |
| 程序记忆 | 做事的方法、技能 | 提示模板 / 工作流 | 「退款必须先查政策再确认金额」 |
工作记忆由上下文工程管理;后三类是长期记忆,需要独立的生命周期。
生命周期
大多数团队只做了「写入」和「检索」,忽略了整合与遗忘,结果记忆库逐渐腐化。
存什么:从对话中抽取结构化记忆
不要直接存原始对话。先抽取成原子事实:
{
"type": "preference",
"key": "shipping.carrier",
"value": "顺丰",
"confidence": 0.9,
"source": "conversation:2026-10-12#msg_18",
"observed_at": "2026-10-12T10:32:00Z",
"scope": "user:u_123",
"sensitivity": "low"
}字段解释:
type:preference / fact / episode / procedure;key:可合并的语义键,避免同义重复;confidence:模型抽取的置信度,低置信度不直接生效;source:可追溯,出错时能回查;observed_at:时效判断;scope:用户、租户、项目,防止串数据;sensitivity:决定是否允许写入、是否需要脱敏。
对情景记忆,可以存摘要而不是全文:
{
"type": "episode",
"summary": "用户投诉配送延迟,最终补偿 20 元优惠券",
"outcome": "resolved",
"lesson": "该用户对时效敏感,后续优先发顺丰",
"at": "2026-10-20"
}怎么存:组合多种存储
| 存储 | 适合 | 不适合 |
|---|---|---|
| 关系库 | 精确字段、偏好、权限、审计 | 语义相似检索 |
| 向量库 | 语义回忆、文档片段 | 精确匹配、数值范围 |
| 图数据库 | 实体关系、多跳推理 | 简单键值查询 |
| 对象存储 | 原始对话、文件、附件 | 低延迟查询 |
生产系统通常是组合:
用户偏好 → PostgreSQL(key-value + 版本)
历史事件 → 事件表 + 向量索引(摘要)
领域知识 → 文档库 + 向量 + 倒排
实体关系 → 图(可选)
原始会话 → 对象存储(保留 N 天)检索时先用结构化过滤缩小范围,再做语义检索,最后按规则排序。
写入策略:不是每句话都值得记住
一个实用的打分:
importance = 0.4 × 新颖性
+ 0.3 × 与目标的相关性
+ 0.2 × 用户明确表达
+ 0.1 × 情绪强度低于阈值的内容不写入长期记忆。写入前还要:
- 去重:相同 key 的旧值合并或覆盖;
- 冲突检测:新偏好与旧偏好矛盾时,保留时间更新、置信度更高的,并记录变更;
- 脱敏:手机号、地址、证件号按策略处理;
- 可撤销:记录来源,用户要求删除时能定位。
这会让检索结果充满寒暄、重复和过时信息。长期记忆应该是「提炼后的状态」,不是「聊天记录备份」。
检索:相关性不是唯一维度
一个好的记忆检索分数:
score = 0.55 × 语义相关
+ 0.20 × 时效(越近越高)
+ 0.15 × 重要性
+ 0.10 × 使用频率然后再做:
- scope 过滤:只取当前用户/租户;
- 类型过滤:当前任务需要偏好还是事实;
- 冲突消解:同一 key 只保留最新有效值;
- 预算截断:最多取 N 条、M 个 token。
只按向量相似度取 top-k,常见问题是「语义相近但已经过时」的记忆被反复召回。
遗忘:记忆系统最重要的功能
没有遗忘,记忆会变成负担。策略:
| 策略 | 说明 |
|---|---|
| TTL | 临时偏好、会话上下文 7–30 天过期 |
| 衰减 | 长期未使用的记忆权重下降 |
| 容量上限 | 每个用户最多 N 条,超出按分数淘汰 |
| 用户删除 | 提供「忘记我」接口,级联删除向量与原文 |
| 冲突覆盖 | 新事实替代旧事实,旧版本标记失效 |
| 敏感隔离 | 高敏感记忆单独存储,更短保留期 |
「用户要求删除」不是可选功能。记忆里可能包含地址、偏好、健康信息,必须有删除与导出能力。
一个最小实现
def remember(user_id: str, text: str, kind: str = "fact"):
extracted = extract_memory(text) # LLM + Schema
if extracted.confidence < 0.6:
return None
if extracted.sensitivity == "high":
text = redact(extracted.value)
existing = db.find_memory(user_id, extracted.key)
if existing and existing.value == extracted.value:
db.touch(existing.id)
return existing.id
mem = db.upsert_memory(
user_id=user_id,
key=extracted.key,
value=extracted.value,
kind=kind,
confidence=extracted.confidence,
source=extracted.source,
observed_at=now(),
)
vector.upsert(mem.id, embed(extracted.summary), metadata={"user_id": user_id, "kind": kind})
return mem.id
def recall(user_id: str, query: str, limit: int = 5):
hits = vector.search(embed(query), filter={"user_id": user_id}, top_k=limit * 3)
scored = []
for h in hits:
mem = db.get_memory(h.id)
if not mem.active:
continue
score = 0.55 * h.score + 0.20 * recency(mem.observed_at) + 0.15 * mem.importance
scored.append((score, mem))
scored.sort(reverse=True, key=lambda x: x[0])
return [m for _, m in scored[:limit]]记忆的评估
- 召回率:该记住的是否记住了?(写入测试)
- 准确率:检索出来的记忆是否相关?(检索测试)
- 时效性:是否用了过时偏好?
- 冲突率:同一事实是否存在多个矛盾版本?
- 污染率:是否检索到其他用户/租户的数据?
- 删除有效性:删除后是否还能被检索到?
构造测试集:给 Agent 一段多轮对话,埋入 10 条应记住的事实和 10 条不应记住的噪声,然后在新会话中提问,看它能否正确回忆。
反模式
所有用户共享一个向量库,检索时不加 user_id 过滤。轻则答错,重则泄露隐私。
模型可能把寒暄记为重要偏好。写入前用规则 + 置信度阈值,并允许人工抽检。
记忆库越来越慢、越来越吵。必须有 TTL、容量上限和删除接口。
检索到三页聊天记录,模型要从里面找一句话。应该存提炼后的原子事实。
检查清单
- 区分工作记忆与长期记忆
- 记忆有类型、key、来源、时间、置信度
- 写入前做去重、冲突检测、脱敏
- 检索有 scope 过滤与多维排序
- 有 TTL、衰减、容量上限、用户删除
- 记忆可追溯、可审计
- 有召回率、准确率、污染率测试
- 跨用户/租户严格隔离
小结
- 记忆不是聊天记录备份,而是提炼后的状态。
- 四类记忆对应不同存储与检索方式。
- 写入要有门槛,检索要多维排序,遗忘要可执行。
- 隐私与作用域隔离是底线。
- 用测试集验证「该记住的记住了,不该用的没用」。
下一篇进入 RAG:当记忆来自外部文档时,如何把检索做对。