Agent 记忆系统设计实战:让 AI 拥有长期记忆的工程方案
引言
大模型的上下文窗口再大,也装不下一个用户三个月的对话历史。更本质的问题是:LLM 本身是无状态的------每一次调用都是"失忆"的重新开始。如果你做过 Agent 产品,一定遇到过这些场景:
- 用户上周说过"我用的是 Go 技术栈",这周 Agent 又推荐了 Python 方案
- 用户明确说过"别再给我发邮件提醒",Agent 三天后又忘了
- 跨会话任务进行到一半,新会话里 Agent 对之前的进度一无所知
这些问题的根源都一样:缺少一个设计良好的记忆系统。上下文窗口是"工作台",不是"档案室"。本文从工程视角出发,讲清楚 Agent 记忆系统的分层设计、核心机制和落地代码。
一、先分清四种记忆
心理学把人类记忆分成几类,这个模型可以直接映射到工程架构上:
| 记忆类型 | 人类对应 | 工程实现 | 生命周期 |
|---|---|---|---|
| 工作记忆 | 正在思考的内容 | 当前对话的 messages 上下文 | 单次会话 |
| 情景记忆 | 经历过的事件 | 对话历史、任务执行记录 | 长期 |
| 语义记忆 | 学到的知识 | 用户画像、事实三元组、知识库 | 长期 |
| 程序记忆 | 形成的技能 | 沉淀后的 prompt 模板、工具使用偏好 | 长期 |
很多团队只做了第一步------把对话历史塞进向量库,然后发现效果很差。原因在于:原始对话是情景记忆,用户真正需要检索的往往是语义记忆。用户说"我下个月要搬家",三个月后 Agent 应该记住的是"用户于 2026 年 9 月搬家"这个事实,而不是那句原话。
所以记忆系统的核心流水线是:原始对话 → 抽取 → 结构化存储 → 检索 → 回注上下文。
二、写入策略:不是什么都要记
记忆系统最容易犯的错误是"全都存"。存储便宜,但检索成本和噪声会指数级上升。实践中我们用三层过滤:
python
from enum import Enum
class MemoryType(Enum):
FACT = "fact" # 事实:用户偏好、属性、约束
EPISODE = "episode" # 事件:任务进度、重要决策
PATTERN = "pattern" # 模式:反复出现的行为规律
async def extract_memories(self, turn: DialogTurn) -> list[Memory]:
"""用 LLM 从对话轮次中抽取候选记忆"""
prompt = f"""从以下对话中提取值得长期记住的信息。
只提取满足以下条件之一的内容:
1. 用户的稳定属性或偏好(技术栈、沟通习惯、工作时段)
2. 用户表达的明确约束("不要..."、"必须...")
3. 重要的任务进展或决策
4. 对话中没有但可以合理推断的长期事实
对话内容:
{turn.text}
输出 JSON 数组,每条包含 type/content/confidence 字段。
如果没有值得记忆的内容,返回空数组。"""
...
关键设计点:
- 置信度阈值:confidence 低于 0.7 的直接丢弃,宁可漏记不可错记
- 去重合并:新记忆入库前先检索相似记忆,相似度 > 0.9 时走更新而非新增
- 冲突处理:用户三个月前说"喜欢简洁回复",昨天说"解释详细一点"------以时间戳新的为准,并保留版本链
三、检索策略:混合检索是标配
纯向量检索在记忆场景下有个致命弱点:时间感知缺失。"用户的手机号"这类记忆必须取最新版本,而向量检索只关心语义相似度。生产环境的标配是三路混合:
python
def retrieve(self, query: str, user_id: str, top_k: int = 5) -> list[Memory]:
# 1. 向量召回:语义相关性
vec_hits = self.vector_store.search(
query_embedding=embed(query),
filter={"user_id": user_id},
top_k=20,
)
# 2. 关键词召回:精确匹配(人名、项目名、日期)
kw_hits = self.keyword_index.search(query, filter={"user_id": user_id}, top_k=20)
# 3. 结构化查询:显式规则(如"用户偏好"直接查 profile 表)
profile = self.profile_table.get(user_id)
# 融合排序:语义相似度 × 时间衰减 × 重要性
now = time.time()
scored = []
for mem in merge_dedupe(vec_hits, kw_hits):
recency = math.exp(-mem.updated_at_days_ago / 30) # 30天半衰期
score = 0.6 * mem.similarity + 0.25 * recency + 0.15 * mem.importance
scored.append((score, mem))
return [m for _, m in sorted(scored, reverse=True)[:top_k]]
这里的时间衰减公式值得展开说:exp(-days/30) 意味着 30 天没被访问的记忆权重降到 37%。但它只能作为排序因子,不能直接删除------用户一年前提到的过敏史,语义上仍然高度相关,向量相似度会把它拉回来。
四、最小可用实现:150 行代码的 Memory Manager
下面的实现用 SQLite + 本地 Embedding,零外部依赖,可以直接跑在 Agent 项目里:
python
import sqlite3, json, math, time
class MemoryManager:
def __init__(self, db_path: str = "agent_memory.db"):
self.db = sqlite3.connect(db_path)
self.db.execute("""CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL, -- fact / episode / pattern
content TEXT NOT NULL,
importance REAL DEFAULT 0.5,
confidence REAL DEFAULT 1.0,
embedding TEXT, -- JSON 数组
version INTEGER DEFAULT 1,
superseded_by INTEGER, -- 被哪条记忆取代
created_at REAL, updated_at REAL,
access_count INTEGER DEFAULT 0
)""")
self.db.execute("CREATE INDEX IF NOT EXISTS idx_user ON memories(user_id, type)")
def remember(self, user_id: str, mem_type: str, content: str,
importance: float = 0.5, confidence: float = 1.0):
emb = json.dumps(embed(content))
now = time.time()
# 冲突检测:找语义高度相似的旧记忆
dup = self._find_similar(user_id, emb, threshold=0.92)
if dup:
self.db.execute(
"UPDATE memories SET superseded_by=NULL, version=version+1, "
"content=?, updated_at=?, embedding=? WHERE id=?",
(content, now, emb, dup["id"]))
else:
self.db.execute(
"INSERT INTO memories (user_id, type, content, importance, "
"confidence, embedding, created_at, updated_at) "
"VALUES (?,?,?,?,?,?,?,?)",
(user_id, mem_type, content, importance, confidence, emb, now, now))
self.db.commit()
def recall(self, user_id: str, query: str, top_k: int = 5) -> list[str]:
"""组装回注上下文的记忆块""
rows = self.db.execute(
"SELECT * FROM memories WHERE user_id=? AND superseded_by IS NULL",
(user_id,)).fetchall()
now = time.time()
q_emb = embed(query)
scored = []
for r in rows:
sim = cosine(q_emb, json.loads(r[6]))
days = (now - r[9]) / 86400
score = 0.6 * sim + 0.25 * math.exp(-days / 30) + 0.15 * r[4]
scored.append((score, r))
hits = sorted(scored, reverse=True)[:top_k]
# 更新访问计数(供后续重要性评估)
for _, r in hits:
self.db.execute("UPDATE memories SET access_count=access_count+1 WHERE id=?", (r[0],))
self.db.commit()
return [f"- [{r[2]}] {r[3]}" for _, r in hits if _[0] > 0.35]
使用方式非常直接:
python
memory = MemoryManager()
# 每轮对话后异步抽取写入
for mem in await extract_memories(turn):
memory.remember(user_id, mem.type, mem.content, mem.importance)
# 每轮对话前检索回注
memories = memory.recall(user_id, user_message)
system_prompt += f"\n\n## 用户记忆\n" + "\n".join(memories)
五、生产环境的四个坑
1. 记忆污染。 用户开玩笑说"我其实是一只猫",Agent 认真记下来了,之后每次对话都围绕猫展开。防线:写入前的 LLM 抽取要加"忽略戏谑、比喻、假设性表述"的指令,且高重要性记忆需要多次出现才固化(类似人类短期记忆→长期记忆的巩固过程)。
2. 记忆膨胀与检索退化。 记忆条数超过几千条后,"先全量取再重排"的方式会拖垮延迟。方案:按 user_id 分区 + 预算控制(每次回注上下文的记忆 token 不超过 500),重要性定期重算------长期不被访问且重要性低的记忆降级归档。
3. 多用户隔离。 记忆系统一旦串了用户数据就是重大事故。除了查询层的 user_id 过滤,建议在存储层就按 user_id 做分区键,并在集成测试中加入"隔离性测试用例"。
4. 可解释性。 用户会问"你为什么知道这个?"------每条记忆必须保留来源(source_turn_id),支持"查看/修改/删除我的记忆",这既是产品信任问题,也正在成为合规要求(数据可删除权)。
六、要不要用现成方案?
如果不想从零造轮子,目前有几类选择:
- Mem0 / Zep:开箱即用的记忆层,提供 REST API,适合快速验证
- Letta(原 MemGPT):把记忆管理做成 OS 式分页,适合长任务 Agent
- LangGraph / LlamaIndex 的 Memory 模块:与编排框架深度绑定,适合已用该栈的团队
但我的建议是:先用 150 行代码的极简版跑通业务闭环,理解自己业务的记忆读写模式后,再决定是否引入重型方案。记忆系统的难点从来不在存储,而在于"什么该记、什么时候想起来、记错了怎么办"------这些是业务问题,不是框架问题。
总结
Agent 记忆系统的工程要点浓缩成四句话:
- 分层建模:工作记忆靠上下文,长期记忆靠"抽取→结构化→检索"流水线
- 写入有门槛:置信度过滤 + 去重合并 + 冲突以新替旧
- 检索要混合:向量 + 关键词 + 时间衰减 + 重要性加权
- 治理不能少:防污染、控膨胀、强隔离、可解释
记忆是 Agent 从"工具"变成"伙伴"的分水岭。当你的 Agent 能在第三十次对话时自然地说出"上次你提到的那个 Go 项目进展如何了?"------用户留下的那一刻,就是记忆系统的价值证明。