一个客服Agent上线初期回答得头头是道,运行几个月后却开始把A用户的订单信息说给B用户,把上周的临时规则当成今天的政策。这类现象在业内常被称作记忆混淆,它不是模型能力问题,而是记忆系统的架构缺陷:写入时没有分区,检索时没有过滤,所有历史记忆堆在同一个池子里被随意召回。本文从混淆根源讲起,给出记忆分区与检索过滤的完整落地思路。

记忆混淆到底是怎么产生的
要解决问题,先要定位问题。Agent记忆混淆通常来自三个层面。第一层是主体混淆:不同用户、不同租户的记忆被写入同一个集合,检索时没有用户标识过滤,相似问题会召回别人的记忆。比如用户A问“我的会员什么时候到期”,向量检索找出的相似片段可能来自用户B的会员信息,Agent据此生成的回答自然是错的。
第二层是时间混淆。记忆库里的历史记录没有时间衰减机制,三年前的偏好和今天的指令权重相同。当用户说“按老规矩处理”时,Agent可能召回的是早已废弃的“老规矩”,因为那条记忆的向量相似度反而更高。
第三层是语义混淆。工作记忆(当前会话上下文)、情景记忆(历史会话记录)、语义记忆(沉淀下来的知识)混存一处。Agent在执行任务时把背景知识、过期对话、当前指令全部塞进提示词,模型无法区分哪些是事实、哪些是历史,混淆就此发生。
记忆分区:从存储端划清边界
记忆分区的核心思想是:写入时就确定记忆属于谁、属于哪个场景,而不是检索时再补救。最常用的分区维度有三个。
第一个维度是按主体分区,即用户ID或租户ID。每个用户的记忆写入独立的命名空间,物理上可以对应向量数据库不同的collection,也可以是同一个collection内的分区键。以Milvus为例,可以按user_id做partition key,写入和检索都限定在同一分区内,天然杜绝跨用户召回。
from pymilvus import Collection
# 写入时强制带上分区键,记忆只落在该用户的分区
collection.insert([
{
"id": "mem_1001",
"user_id": "user_A", # 分区键
"session_id": "sess_ab12",
"content": "用户A的会员到期日为3月1日",
"memory_type": "episodic", # episodic / semantic / procedural
"timestamp": 1735689600,
"embedding": embed("用户A的会员到期日为3月1日"),
}
])
# 检索时同样限定分区,检索范围天然收窄
results = collection.search(
data=[embed(query)],
anns_field="embedding",
param={"metric_type": "IP", "params": {"nprobe": 10}},
expr='user_id == "user_A"', # 主体过滤,硬边界
limit=5,
)
第二个维度是按会话分区。当前会话的工作记忆与跨会话的长期记忆分开存储,工作记忆生命周期随会话结束而清理或归档,长期记忆则通过摘要沉淀。这样Agent执行任务时,当前上下文不会被几个月前的对话碎片污染。
第三个维度是按类型分区。参考认知科学的分类,把记忆拆成情景记忆、语义记忆、程序记忆三类,分别建索引。事实类问题只查语义记忆区,流程类问题只查程序记忆区,检索目标明确后混淆概率大幅下降。
检索过滤:在召回端收紧口径
分区解决了“去哪里找”,过滤解决“找到之后留哪些”。即使分区正确,同一分区内仍可能堆积上千条记忆,需要多层过滤来精筛。
第一层是元数据过滤。给每条记忆打上结构化标签,比如时间、来源、置信度、状态,检索时用布尔表达式先筛掉明显不合条件的记录。常见做法是只召回状态为active的记忆,或者只召回最近一段时间内的记录。
# 组合过滤:状态有效 + 时间窗口 + 记忆类型
expr = (
'user_id == "user_A" and '
'memory_type == "semantic" and '
'status == "active" and '
'timestamp >= 1704067200' # 只查近期记忆
)
results = collection.search(
data=[query_vec],
anns_field="embedding",
param={"metric_type": "IP"},
expr=expr,
limit=10,
)
第二层是时间衰减打分。在相似度得分上叠加时间衰减因子,越新的记忆权重越高。一个实用的衰减公式是 score = similarity × exp(-λ × days),λ取0.01到0.1之间,业务变化快的场景取大值。这样即使旧记忆相似度略高,也会因为时间惩罚被排到后面。
第三层是相关性重排。先用向量检索粗召回20到50条候选,再用交叉编码器模型对候选逐一精排,取前5条进入提示词。粗排保证召回率,精排保证精度,两级漏斗比单次top-k检索准确率高出一截。
import math
from datetime import datetime, timezone
def decay_score(similarity: float, mem_ts: float, lam: float = 0.05) -> float:
"""相似度叠加时间衰减"""
days = (datetime.now(timezone.utc).timestamp() - mem_ts) / 86400
return similarity * math.exp(-lam * days)
# 粗召回后按衰减分重排,取前5条
candidates = collection.search(data=[query_vec], anns_field="embedding",
expr='user_id == "user_A"', limit=30)
reranked = sorted(
candidates,
key=lambda m: decay_score(m.similarity, m.timestamp),
reverse=True,
)[:5]
落地时的常见坑与注意事项
第一个坑是只在检索端过滤、不在写入端分区。有人图省事把所有记忆塞进一个collection,靠expr过滤兜底,数据量上来后过滤性能急剧下降,而且一旦某处代码漏写过滤条件就会直接泄露跨用户数据。分区应该在写入时就确定,过滤只是第二道防线。
第二个坑是记忆状态管理缺失。用户改了偏好,旧记忆没有标记为deprecated,新旧两条同时被召回,Agent输出自相矛盾。建议引入版本号或生效状态字段,写入新记忆时将同主题旧记忆置为inactive,保持同一事实只有一条生效记录。
第三个坑是过度分区导致召回不足。如果按会话粒度建独立索引,跨会话的长期记忆就查不到了。合理策略是会话级工作记忆用短期存储,长期记忆走全局语义索引加严格过滤,两级配合而不是一刀切。
整体来看,记忆分区划清存储边界,检索过滤收紧召回口径,两者配合才能让Agent的记忆既丰富又不串线。先用主体、会话、类型三个维度做好分区设计,再叠加元数据过滤、时间衰减与重排,基本可以消除绝大多数记忆混淆问题。