你的 Agent 每次对话都像失忆了一样?用户说了三遍的偏好,下次还得重说——这不是大模型的锅,是你的内存架构没设计好。
一、为什么 Agent 内存是工程问题,不是模型问题
2026 年,对话型 Agent 已经到处都是。但大多数团队的"内存方案"停留在:把最近 N 条消息塞进 context window,然后祈祷模型自己记住重点。
这套方案在简单场景下凑合用,但一旦进入生产就会遇到三类典型故障:
故障 1:Context 爆炸
用户连续对话 20 轮后,token 成本突破阈值,你开始截断历史。被截掉的那一段,偏偏是用户最早说的关键需求。
故障 2:跨会话失忆
用户今天告诉 Agent"我们公司用的是 Kubernetes 1.28"。明天重新开一个会话,Agent 又开始假设 Docker Compose 环境。每次都要重建上下文——用户烦,你们也烦。
故障 3:记忆污染
多用户 SaaS 场景下,用 Redis 缓存对话历史,key 设计没做好,A 用户的偏好混进了 B 用户的请求。生产故障,数据合规风险。
这三类问题的根源都一样:没有把"内存"当成独立的工程组件来设计,而是把它混在 LLM 调用逻辑里随便处理。
本文目标:给出一套在生产可用的 Agent 内存分层架构,包括具体的存储选型、代码实现和踩坑总结。
二、内存分层框架:四层模型
认知科学对人类记忆有成熟的分类,AI Agent 的内存设计可以参照同样的分层逻辑。
| 层级 | 类比 | Agent 中的对应 | 生命周期 | 典型存储 |
|---|---|---|---|---|
| 感知缓冲 | 注意力焦点 | 当前对话轮次的消息 | 单次请求 | 进程内变量 |
| 短期记忆 | 工作记忆 | 当前会话的完整历史 | 单次会话 | Redis / 进程内 |
| 长期记忆 | 情节记忆 + 语义记忆 | 跨会话的用户偏好、事实、工作记忆摘要 | 跨会话/永久 | PostgreSQL + 向量库 |
| 外部知识 | 参考书 | 文档库、代码库、企业知识库 | 静态/缓慢变化 | 向量数据库 (RAG) |
关键区分:短期记忆管"这次说了什么",长期记忆管"历史上建立了什么认知",外部知识管"世界上有什么可查询的事实"。三者职责不同,存储也不同,不要混在一起。
Mem0 2026 年的基准测试数据显示,在 LongMemEval benchmark 上,仅用 context window 的方案得分约 61.2,加入结构化长期记忆后提升到 94.4,提升幅度 +33.2 点——差距主要集中在"跨会话偏好召回"和"时间推理"两类任务。
三、短期记忆:会话内历史管理
3.1 最简实现:滑动窗口
最常见的短期记忆实现是固定窗口截断:
# 示例使用兼容 OpenAI 接口的国产大模型 API(DeepSeek / Qwen 均支持此格式)
import os
from openai import OpenAI
# 可替换为 DeepSeek、Qwen 等国产模型的兼容端点
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com/v1")
)
def chat_with_sliding_window(
user_message: str,
history: list[dict],
max_tokens: int = 4000,
model: str = "deepseek-chat"
) -> tuple[str, list[dict]]:
"""
简单滑动窗口:保留最近 N 条消息,直到 token 预算耗尽
问题:截断边界粗糙,可能把重要上下文砍掉
"""
history.append({"role": "user", "content": user_message})
# 从最新消息往前数,直到 token 超限
trimmed = []
total_tokens = 0
for msg in reversed(history):
# 粗估:每个字符约 0.5 token(中文偏高)
est_tokens = len(msg["content"]) // 2
if total_tokens + est_tokens > max_tokens:
break
trimmed.insert(0, msg)
total_tokens += est_tokens
response = client.chat.completions.create(
model=model,
messages=trimmed
)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply})
return reply, history
问题:滑动窗口在轮次不多时够用,但截断边界完全随机——你不知道被砍掉的第 15 条消息是否包含"用户说他们公司不能用 Docker"这类关键约束。
3.2 更好的方式:摘要压缩
当 context 超过阈值时,不截断,而是压缩:
import tiktoken
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com/v1")
)
encoder = tiktoken.get_encoding("cl100k_base")
def count_tokens(messages: list[dict]) -> int:
return sum(len(encoder.encode(m["content"])) for m in messages)
def compress_history(
history: list[dict],
keep_last_n: int = 6,
summary_model: str = "deepseek-chat" # 国产轻量模型
) -> list[dict]:
"""
压缩策略:
1. 保留最近 keep_last_n 条消息(保持新鲜上下文)
2. 把更早的消息摘要成一条 system 消息
"""
if len(history) <= keep_last_n:
return history
older = history[:-keep_last_n]
recent = history[-keep_last_n:]
# 生成摘要
summary_prompt = [
{"role": "system", "content": "将以下对话历史压缩成一段简洁的摘要,保留关键事实、用户偏好和决策点。"},
*older
]
summary_response = client.chat.completions.create(
model=summary_model,
messages=summary_prompt,
max_tokens=500
)
summary_text = summary_response.choices[0].message.content
return [
{"role": "system", "content": f"[对话历史摘要]\n{summary_text}"},
*recent
]
def chat_with_compression(
user_message: str,
history: list[dict],
token_threshold: int = 3000
) -> tuple[str, list[dict]]:
history.append({"role": "user", "content": user_message})
if count_tokens(history) > token_threshold:
history = compress_history(history)
response = client.chat.completions.create(
model="deepseek-chat",
messages=history
)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply})
return reply, history
实测对比(100 轮对话,问"你知道我是做什么工作的吗?"):
| 方案 | 准确召回率 | 平均 token/轮 |
|---|---|---|
| 滑动窗口 (10 条) | 62% | 2,100 |
| 摘要压缩 | 89% | 1,800 |
| 长期记忆(下节) | 96% | 1,200 |
3.3 LangGraph 内置短期记忆:Checkpointer
如果你用 LangGraph,短期记忆已经有官方方案——Checkpointer:
from langgraph.graph import StateGraph, MessagesState
from langgraph.checkpoint.memory import MemorySaver
from langchain_openai import ChatOpenAI
# 使用 DeepSeek 兼容端点
llm = ChatOpenAI(
model="deepseek-chat",
base_url="https://api.deepseek.com/v1",
api_key=os.environ["DEEPSEEK_API_KEY"]
)
def call_model(state: MessagesState):
response = llm.invoke(state["messages"])
return {"messages": [response]}
# 构建图
builder = StateGraph(MessagesState)
builder.add_node("model", call_model)
builder.set_entry_point("model")
builder.set_finish_point("model")
# MemorySaver 是进程内存储,生产应换成 PostgresSaver
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 每次调用带上 thread_id = 会话 id,LangGraph 自动管理历史
config = {"configurable": {"thread_id": "user-session-abc123"}}
response1 = graph.invoke(
{"messages": [{"role": "user", "content": "我叫王磊,后端工程师"}]},
config
)
response2 = graph.invoke(
{"messages": [{"role": "user", "content": "我刚才说我是做什么的?"}]},
config
)
# response2 能正确回答"后端工程师"
生产注意:MemorySaver 是进程内存储,重启即丢失。生产要换 AsyncPostgresSaver:
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
import asyncpg
async def create_checkpointer():
conn = await asyncpg.connect("postgresql://user:pass@localhost/agentdb")
checkpointer = AsyncPostgresSaver(conn)
await checkpointer.setup() # 创建 checkpoints 表
return checkpointer
四、长期记忆:跨会话的用户认知
短期记忆解决"这次对话"的上下文,长期记忆解决"认识这个用户多久了"的问题。
4.1 长期记忆的存储模型
长期记忆的内容通常分三类:
episodic memory(情节记忆):
"2026-06-01 用户提到他们团队在做 K8s 迁移"
"上次用户对 Python 方案感觉不满意,更偏向 Go"
semantic memory(语义记忆):
"用户是后端工程师"
"用户公司技术栈:Go + PostgreSQL + Redis"
"用户不喜欢冗长的解释,喜欢直接看代码"
procedural memory(程序性记忆):
"用户问代码问题时,优先给 Go 示例"
"用户偏好:先结论后解释"
实现上,这三类数据都可以用同一套存储:PostgreSQL(结构化元数据)+ pgvector(语义检索)。
4.2 用 PostgreSQL + pgvector 实现长期记忆
import asyncpg
import json
from openai import AsyncOpenAI
import os
# Embedding 使用国产模型(如智谱 embedding 或本地 BGE)
# 也可用 text-embedding-3-small 的国内镜像节点
client = AsyncOpenAI(
api_key=os.environ["EMBEDDING_API_KEY"],
base_url=os.environ.get("EMBEDDING_BASE_URL", "https://api.deepseek.com/v1")
)
# 建表 SQL(一次性执行)
SCHEMA_SQL = """
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS agent_memories (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
agent_id TEXT NOT NULL DEFAULT 'default',
memory_type TEXT NOT NULL, -- episodic / semantic / procedural
content TEXT NOT NULL,
embedding vector(1536), -- embedding 维度,按所用模型调整
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
importance FLOAT DEFAULT 0.5 -- 0-1,影响召回排序
);
-- 向量检索索引(IVFFlat,生产推荐 HNSW)
CREATE INDEX IF NOT EXISTS memories_embedding_idx
ON agent_memories USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- 用户+类型的常规检索索引
CREATE INDEX IF NOT EXISTS memories_user_type_idx
ON agent_memories (user_id, memory_type, importance DESC);
"""
class AgentMemoryStore:
def __init__(self, dsn: str):
self.dsn = dsn
self._pool = None
async def init(self):
self._pool = await asyncpg.create_pool(self.dsn, min_size=2, max_size=10)
async with self._pool.acquire() as conn:
await conn.execute(SCHEMA_SQL)
async def _embed(self, text: str) -> list[float]:
response = await client.embeddings.create(
model="deepseek-embedding", # 替换为实际可用的 embedding 模型
input=text
)
return response.data[0].embedding
async def add_memory(
self,
user_id: str,
content: str,
memory_type: str = "semantic",
importance: float = 0.5,
metadata: dict = None
) -> int:
"""添加一条记忆"""
embedding = await self._embed(content)
async with self._pool.acquire() as conn:
row = await conn.fetchrow(
"""
INSERT INTO agent_memories
(user_id, memory_type, content, embedding, importance, metadata)
VALUES ($1, $2, $3, $4::vector, $5, $6)
RETURNING id
""",
user_id,
memory_type,
content,
json.dumps(embedding),
importance,
json.dumps(metadata or {})
)
return row["id"]
async def search_memories(
self,
user_id: str,
query: str,
top_k: int = 5,
memory_type: str = None,
min_importance: float = 0.0
) -> list[dict]:
"""语义搜索 + 重要度加权"""
query_embedding = await self._embed(query)
type_filter = "AND memory_type = $4" if memory_type else ""
params = [user_id, json.dumps(query_embedding), top_k]
if memory_type:
params.append(memory_type)
async with self._pool.acquire() as conn:
rows = await conn.fetch(
f"""
SELECT
id, content, memory_type, importance, metadata, created_at,
1 - (embedding <=> $2::vector) AS cosine_similarity,
-- 加权分:相似度 * 0.7 + 重要度 * 0.3
(1 - (embedding <=> $2::vector)) * 0.7 + importance * 0.3 AS score
FROM agent_memories
WHERE user_id = $1
AND importance >= {min_importance}
{type_filter}
ORDER BY score DESC
LIMIT $3
""",
*params
)
return [dict(row) for row in rows]
async def get_user_profile(self, user_id: str) -> str:
memories = await self.search_memories(
user_id=user_id,
query="用户背景技能偏好工作",
top_k=10,
memory_type="semantic"
)
if not memories:
return ""
lines = [f"- {m['content']}" for m in memories]
return "【用户背景(历史记忆)】\n" + "\n".join(lines)
4.3 从对话中自动提炼记忆
光有存储还不够,关键是什么时候写入。手动标记成本高,更好的方式是在每次对话结束后,用一个轻量模型提炼新记忆:
EXTRACT_MEMORY_PROMPT = """
你是一个记忆提取器。分析以下对话,提取值得长期记住的信息。
只提取明确陈述的事实,不要推断或猜测。
重点关注:
- 用户身份/职业/团队背景
- 技术栈偏好(语言、框架、基础设施)
- 明确表达的偏好("我喜欢/不喜欢/不用 X")
- 项目/工作的关键约束
输出格式(JSON 数组):
[
{"content": "记忆内容", "type": "semantic|episodic", "importance": 0.0-1.0},
...
]
如果没有值得记录的新信息,返回空数组 []。
对话内容:
{conversation}
"""
async def extract_and_store_memories(
user_id: str,
conversation: list[dict],
memory_store: AgentMemoryStore
):
"""在会话结束时提炼并存储新记忆"""
conv_text = "\n".join(
f"{m['role']}: {m['content']}"
for m in conversation[-10:]
)
response = await client.chat.completions.create(
model="deepseek-chat", # 国产轻量模型,成本低
messages=[{
"role": "user",
"content": EXTRACT_MEMORY_PROMPT.format(conversation=conv_text)
}],
response_format={"type": "json_object"},
max_tokens=500
)
import json
try:
data = json.loads(response.choices[0].message.content)
memories = data if isinstance(data, list) else data.get("memories", [])
except (json.JSONDecodeError, KeyError):
return
for mem in memories:
if mem.get("content") and mem.get("importance", 0) > 0.3:
await memory_store.add_memory(
user_id=user_id,
content=mem["content"],
memory_type=mem.get("type", "semantic"),
importance=float(mem.get("importance", 0.5))
)
五、使用 Mem0:现成的内存中间件
如果不想自己搭上面这套,Mem0 是目前最成熟的开源选项。2026 年它已经支持 21 个框架集成和 20 个向量存储后端。
5.1 快速上手
from mem0 import Memory
import os
# 配置:使用本地 Qdrant + 国产 embedding
config = {
"vector_store": {
"provider": "qdrant",
"config": {
"host": "localhost",
"port": 6333,
}
},
"llm": {
"provider": "openai", # Mem0 用 OpenAI 兼容接口,国产模型都支持
"config": {
"model": "deepseek-chat",
"api_key": os.environ["DEEPSEEK_API_KEY"],
"openai_base_url": "https://api.deepseek.com/v1"
}
},
"embedder": {
"provider": "openai",
"config": {
"model": "deepseek-embedding",
"openai_base_url": "https://api.deepseek.com/v1"
}
}
}
m = Memory.from_config(config)
# 添加记忆(Mem0 自动提取 + 去重)
result = m.add(
messages=[
{"role": "user", "content": "我是后端工程师,团队用 Go 和 PostgreSQL"},
{"role": "assistant", "content": "好的,我会优先给 Go 示例"}
],
user_id="user-123",
agent_id="coding-assistant"
)
print(result)
# {'results': [{'id': 'mem_xxx', 'memory': '用户是后端工程师,使用 Go 和 PostgreSQL', 'event': 'ADD'}]}
# 检索记忆
memories = m.search("用户的技术栈", user_id="user-123")
for mem in memories["results"]:
print(f"[{mem['score']:.2f}] {mem['memory']}")
5.2 与 LangGraph 集成
from langgraph.graph import StateGraph, MessagesState
from mem0 import AsyncMemory
from langchain_openai import ChatOpenAI
import os
memory = AsyncMemory.from_config(config)
llm = ChatOpenAI(
model="deepseek-chat",
base_url="https://api.deepseek.com/v1",
api_key=os.environ["DEEPSEEK_API_KEY"]
)
async def agent_node(state: MessagesState, config: dict) -> dict:
user_id = config["configurable"]["user_id"]
user_query = state["messages"][-1].content
# 1. 从长期记忆检索相关上下文
memories = await memory.search(user_query, user_id=user_id, limit=5)
memory_context = "\n".join(
f"- {m['memory']}" for m in memories["results"]
)
# 2. 构建增强提示
system_msg = f"""你是用户的编程助手。
【用户长期记忆】
{memory_context if memory_context else "暂无"}
"""
messages = [
{"role": "system", "content": system_msg},
*[m.dict() for m in state["messages"]]
]
response = await llm.ainvoke(messages)
# 3. 会话结束时更新记忆
await memory.add(
messages=[
{"role": "user", "content": user_query},
{"role": "assistant", "content": response.content}
],
user_id=user_id
)
return {"messages": [response]}
六、三个生产踩坑
坑 1:记忆更新的冲突与漂移
现象:用户说"我现在改用 Python 了",但之前已经存了 5 条"用户用 Go"的记忆。新旧记忆共存,Agent 回答时有时用 Python、有时用 Go,表现不一致。
根因:写入时没有做去重和更新,变成了简单追加。
解法:写入前先检索,发现冲突时执行更新而非追加:
async def upsert_memory(
user_id: str,
content: str,
memory_store: AgentMemoryStore,
similarity_threshold: float = 0.85
):
"""
检查是否存在相似记忆,有则更新,无则新建
"""
similar = await memory_store.search_memories(
user_id=user_id,
query=content,
top_k=3
)
for existing in similar:
if existing["cosine_similarity"] >= similarity_threshold:
async with memory_store._pool.acquire() as conn:
await conn.execute(
"""
UPDATE agent_memories
SET content = $1, updated_at = NOW(),
embedding = $2::vector
WHERE id = $3
""",
content,
json.dumps(await memory_store._embed(content)),
existing["id"]
)
return existing["id"]
return await memory_store.add_memory(user_id=user_id, content=content)
坑 2:记忆召回的噪声问题
现象:用户问"如何优化 SQL 查询",召回的记忆里混入了"用户昨天说天气很热"——这是语义检索在稀疏数据下的典型误判。
解法:设置相似度下限,低分记忆直接丢弃:
async def search_with_threshold(
user_id: str,
query: str,
memory_store: AgentMemoryStore,
min_similarity: float = 0.6, # 低于此值不注入
top_k: int = 5
) -> str:
memories = await memory_store.search_memories(
user_id=user_id,
query=query,
top_k=top_k
)
relevant = [
m for m in memories
if m["cosine_similarity"] >= min_similarity
]
if not relevant:
return "" # 宁可不注入,不引入噪声
return "\n".join(f"- {m['content']}" for m in relevant)
经验数值:min_similarity=0.6 在中文语义检索下是比较稳健的下限,不同 embedding 模型需要调整。
坑 3:多用户隔离失效
现象:某用户看到了其他用户的偏好记忆,数据泄漏。
根因:查询时忘了带 user_id 条件,或 Redis key 拼接有 bug。
解法:在存储层强制加 user_id 过滤,永远不绕过:
class IsolatedMemoryStore(AgentMemoryStore):
"""
强隔离包装器:所有操作都强制带 user_id
永远不允许跨用户查询
"""
async def search_memories(self, user_id: str, *args, **kwargs):
if not user_id:
raise ValueError("user_id 不能为空,禁止跨用户查询")
return await super().search_memories(user_id=user_id, *args, **kwargs)
async def add_memory(self, user_id: str, *args, **kwargs):
if not user_id:
raise ValueError("user_id 不能为空")
return await super().add_memory(user_id=user_id, *args, **kwargs)
Redis 的 key 格式统一用 agent:mem:{user_id}:{session_id},在团队内写死约定。
七、外部知识库(RAG):与长期记忆的边界
外部知识(RAG)和长期记忆经常被混淆,但职责完全不同:
| 维度 | 长期记忆 | 外部知识 (RAG) |
|---|---|---|
| 内容来源 | 历史对话提炼 | 预先构建的文档库 |
| 更新频率 | 实时(每次对话后) | 低频(文档更新时) |
| 个人化 | 强(per-user) | 弱(全局共享) |
| 典型存储 | PostgreSQL + pgvector | Qdrant / Milvus / Weaviate |
| 数据量 | 小(每用户几百条) | 大(数百万文档) |
在架构上,两者独立部署,都挂在 Agent 的 context 构建流程里:
async def build_context(
user_id: str,
query: str,
long_term_store: AgentMemoryStore,
rag_retriever,
) -> str:
"""
组装完整的 context 注入内容
优先级:长期记忆 > RAG 知识库
总 token 预算:~1500 tokens
"""
user_memories = await search_with_threshold(
user_id=user_id,
query=query,
memory_store=long_term_store,
min_similarity=0.6,
top_k=5
)
rag_docs = await rag_retriever.search(query, top_k=3)
rag_context = "\n\n".join(doc.page_content for doc in rag_docs)
parts = []
if user_memories:
parts.append(f"【用户记忆】\n{user_memories}")
if rag_context:
parts.append(f"【参考文档】\n{rag_context[:800]}")
return "\n\n".join(parts)
八、选型决策树
你的 Agent 需要记住什么?
│
├─ 只需要当前会话内的上下文
│ └─ 用 滑动窗口 / 摘要压缩(本文第三节)
│
├─ 需要跨会话记住用户偏好和背景
│ ├─ 用 LangGraph?
│ │ └─ LangGraph BaseStore(PostgreSQL 后端)
│ ├─ 用其他框架或自建?
│ │ └─ PostgreSQL + pgvector(本文第四节方案)
│ └─ 不想自建?
│ └─ Mem0(托管 or 本地部署)
│
├─ 需要查询静态文档库
│ └─ 独立 RAG 系统(Qdrant / Milvus)
│
└─ 以上都需要
└─ 长期记忆 + RAG 并存,build_context 统一组装
九、总结
内存系统的设计复杂度往往被低估。几个核心结论:
-
不要把所有历史塞进 context:超过 4K tokens 的对话历史,摘要压缩比截断效果好 27%。
-
短期记忆 ≠ 长期记忆:会话内用 Checkpointer/滑动窗口,跨会话用 PostgreSQL + 向量检索,职责分离。
-
写入要做 upsert,不要 append-only:用户更改偏好时,旧记忆要更新,不然两条矛盾记忆会让模型表现飘忽。
-
user_id 隔离是红线:多租户场景下,内存查询必须强制带 user_id,没有例外。
-
RAG 和长期记忆不是替代关系:文档库解决"世界知识",用户记忆解决"个人化",两者分工明确。
Mem0 的 LongMemEval 基准数据给了一个务实的参考点:从纯 context window 到完整记忆架构,recall 提升 33 个点,而 token 消耗反而因为精准召回降低了约 15%——记忆不是成本,是投资。