AI Agent 的内存工程实践:短期、长期与外部记忆的架构选型与生产落地

15 阅读11分钟

你的 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 + pgvectorQdrant / 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 统一组装

九、总结

内存系统的设计复杂度往往被低估。几个核心结论:

  1. 不要把所有历史塞进 context:超过 4K tokens 的对话历史,摘要压缩比截断效果好 27%。

  2. 短期记忆 ≠ 长期记忆:会话内用 Checkpointer/滑动窗口,跨会话用 PostgreSQL + 向量检索,职责分离。

  3. 写入要做 upsert,不要 append-only:用户更改偏好时,旧记忆要更新,不然两条矛盾记忆会让模型表现飘忽。

  4. user_id 隔离是红线:多租户场景下,内存查询必须强制带 user_id,没有例外。

  5. RAG 和长期记忆不是替代关系:文档库解决"世界知识",用户记忆解决"个人化",两者分工明确。

Mem0 的 LongMemEval 基准数据给了一个务实的参考点:从纯 context window 到完整记忆架构,recall 提升 33 个点,而 token 消耗反而因为精准召回降低了约 15%——记忆不是成本,是投资。


参考资料