Agent 记忆系统:从短期记忆到长期记忆

让你的 AI Agent 拥有"记性",从会话记忆到跨会话记忆的完整实现,附 Embedding 语义搜索实战


引言

想象一下,你正在和一个 AI 助手聊天。你告诉它你的名字叫"小王",然后关闭了页面。第二天你重新打开对话,问它"你还记得我叫什么吗?"------如果它一脸茫然地回答"我不知道",你是不是会觉得有点失望?

在构建 AI Agent 应用时,记忆能力是决定用户体验的关键因素之一。一个没有记忆的 Agent,每次对话都像"初次见面",无法提供连贯、个性化的服务。

本文将带你深入 LangChain/LangGraph 的记忆系统,从短期记忆(会话级)到长期记忆(跨会话),从理论到实践,手把手构建一个拥有完整记忆能力的 Agent。


一、记忆的分类

在开始编码之前,我们先理清概念。根据 CoALA(Cognitive Architectures for Language Agents)论文的分类,记忆可以从不同维度划分:

维度 说明 典型场景
短期记忆 会话级别的记忆,在同一会话内有效 多轮对话的上下文
长期记忆 跨会话持久化存储 用户偏好、历史经验
静态上下文 系统级固定信息 系统提示词、知识库

而我们常说的"记忆",主要分为两大类:

  • 短期记忆(Short-term Memory) :同一会话(Thread)内的上下文,会话结束即消失
  • 长期记忆(Long-term Memory) :跨会话持久化存储,随时可检索

二、短期记忆:让 Agent 记住当前会话

2.1 核心三要素

在 LangChain 1.x 中,短期记忆是三者的组合:

text 复制代码
短期记忆 = State + Checkpointer + Thread ID
组件 作用
State 会话内部状态,默认存储 messages 历史消息列表
Checkpointer 将 State 作为检查点持久化保存(内存或数据库)
Thread ID 唯一标识会话,LangChain 按 thread_id 读写 State

2.2 基础示例:没有记忆 vs 有记忆

先看没有记忆的情况:

python 复制代码
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
import os
from dotenv import load_dotenv

load_dotenv(override=True)

model = init_chat_model(
    model="deepseek-v4-flash",
    base_url=os.getenv("OPENROUTER_BASE_URL"),
    api_key=os.getenv("OPENROUTER_API_KEY"),
    model_provider="openai"
)

agent = create_agent(model=model, tools=[])

# 第一轮:告诉 Agent 名字
response1 = agent.invoke({
    "messages": [HumanMessage("你好,我是 Jack")]
})
print(f"Agent: {response1['messages'][-1].content}")
# 输出:你好,Jack!很高兴认识你!

# 第二轮:询问名字
response2 = agent.invoke({
    "messages": [HumanMessage("我是谁?")]
})
print(f"Agent: {response2['messages'][-1].content}")
# 输出:根据我们的对话记录,我目前没有关于您身份的具体信息...

看到了吗?Agent 完全不记得上一轮说过什么!

加上记忆:

python 复制代码
from langgraph.checkpoint.memory import InMemorySaver

# 1. 创建检查点存储(内存版)
checkpointer = InMemorySaver()

# 2. 创建 Agent 时传入 checkpointer
agent = create_agent(
    model=model,
    tools=[],
    checkpointer=checkpointer
)

# 3. 指定 thread_id
config = {
    "configurable": {
        "thread_id": "1"
    }
}

# 第一轮对话
response1 = agent.invoke(
    {"messages": [HumanMessage("你好,我是 Jack")]},
    config=config
)
print(f"Agent: {response1['messages'][-1].content}")
# 输出:你好,Jack!很高兴认识你!

# 第二轮对话(相同 thread_id)
response2 = agent.invoke(
    {"messages": [HumanMessage("我的名字是什么?")]},
    config=config
)
print(f"Agent: {response2['messages'][-1].content}")
# 输出:你的名字是 Jack!你刚刚在对话开始时自己介绍过哦~

关键点: 同一个 thread_id 共享记忆,不同 thread_id 完全隔离。

python 复制代码
# 使用不同的 thread_id
config2 = {"configurable": {"thread_id": "2"}}
response3 = agent.invoke(
    {"messages": [HumanMessage("你还记得我叫什么名字么?")]},
    config=config2
)
print(response3['messages'][-1].content)
# 输出:由于我无法存储个人数据或跨会话记录信息,每次对话都是全新的开始...

2.3 查看存储的状态

python 复制代码
from rich import print as rprint

thread_state = agent.get_state(config)
rprint(thread_state)

你会看到完整的消息历史,包括每次对话的 HumanMessage 和 AIMessage。


三、记忆治理:别让上下文撑爆了

大模型的上下文窗口是有限的,无限制地累积历史消息会导致:

  1. 信息过载:模型注意力分散,回答质量下降
  2. Token 费用激增:每次请求都要传输全部历史
  3. 可能超出上下文窗口:导致请求失败

因此,我们需要对记忆进行"治理"。LangChain 提供了三种策略:

3.1 消息裁剪(Trimming)

在模型调用之前,对消息列表进行裁剪,只保留最近 N 条消息。

python 复制代码
from langchain.agents.middleware import before_model
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.runtime import Runtime
from typing import Any

@before_model
def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    messages = state["messages"]
    if len(messages) <= 3:
        return None
    
    # 保留第一条 + 最近 3-4 条
    first_msg = messages[0]
    recent_messages = messages[-3:] if len(messages) % 2 == 0 else messages[-4:]
    new_messages = [first_msg] + recent_messages
    
    return {
        "messages": [
            RemoveMessage(id=REMOVE_ALL_MESSAGES),  # 移除所有旧消息
            *new_messages
        ]
    }

agent = create_agent(
    model=model,
    middleware=[trim_messages],
    checkpointer=InMemorySaver(),
)

3.2 消息删除(Deletion)

在模型调用之后,删除最老的消息,保持列表不超过阈值。

python 复制代码
from langchain.agents.middleware import after_model

@after_model
def delete_old_messages(state: AgentState, runtime: Runtime) -> dict | None:
    messages = state["messages"]
    if len(messages) > 5:
        to_delete = len(messages) - 5
        return {
            "messages": [
                RemoveMessage(id=m.id) for m in messages[:to_delete]
            ]
        }
    return None

💡 原理说明RemoveMessage 并不是真正从内存中删除消息,而是追加一个"墓碑"标记。下次读取时,框架的 Reducer 会过滤掉被标记的消息。这种方式保留了完整的历史追踪能力。

3.3 摘要(Summarization)

当消息超过阈值时,使用 LLM 将历史对话压缩成摘要。

python 复制代码
from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(
    model=model,
    tools=[],
    checkpointer=InMemorySaver(),
    middleware=[
        SummarizationMiddleware(
            model=model,  # 用于生成摘要的模型
            trigger=[("tokens", 100)],  # 超过 100 tokens 触发摘要
            keep=("messages", 2),  # 保留最近 2 条消息完整
            summary_prompt="对历史消息摘要,消息列表如下\n{messages}",
        )
    ]
)

摘要策略的建议:

模型上下文窗口 建议触发阈值
4K 3000 tokens
8K 6000 tokens
16K 12000 tokens

注意:阈值应留有余量,给工具调用和系统提示预留空间。


四、长期记忆:跨会话持久化

短期记忆虽然好用,但 thread_id 不同就无法共享。对于需要跨会话记住用户信息的场景(如用户偏好、身份信息),我们需要长期记忆

4.1 长期记忆的存储模型

LangChain 的长期记忆采用四层结构:

text 复制代码
Store → Namespace → Key → Value
层级 类型 说明
Store BaseStore 子类 存储引擎(内存/PostgreSQL)
Namespace tuple[str, ...] 命名空间,用于分组隔离,如 ("users", "alice")
Key str 唯一键,如 "preferences"
Value dict[str, Any] 存储的实际数据
python 复制代码
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()

# 存储数据
namespace = ("users", "alice")
store.put(namespace, "preferences", {
    "name": "Alice",
    "city": "Beijing",
    "hobby": "reading"
})

# 读取数据
item = store.get(namespace, "preferences")
print(item.value)  # {'name': 'Alice', 'city': 'Beijing', 'hobby': 'reading'}

4.2 使用 PostgreSQL 持久化

生产环境建议使用数据库存储:

python 复制代码
from langgraph.store.postgres import PostgresStore

DB_URL = "postgresql://user:pass@localhost:5432/db?sslmode=disable"

with PostgresStore.from_conn_string(DB_URL) as store:
    store.setup()  # 初始化表结构
    store.put(("users",), "user_123", {"name": "大刘"})
    print(store.get(("users",), "user_123"))

4.3 语义搜索:使用 Embedding 实现智能检索

长期记忆不仅支持精确的 get,还支持语义检索。这里我们详细展示如何使用 Embedding 模型实现这一功能。

方案一:直接使用 OpenAI Embedding API

如果你需要精细控制 API 调用,可以直接使用 OpenAI 的 Embedding 接口:

python 复制代码
from openai import OpenAI
from tenacity import retry, wait_random_exponential, stop_after_attempt

client = OpenAI()

# 加入重试机制,优雅地处理速率限制
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
def get_embedding(text: str, model: str = "text-embedding-3-small") -> list[float]:
    """获取文本的向量表示"""
    return client.embeddings.create(
        input=[text],
        model=model
    ).data[0].embedding

# 使用示例
embedding = get_embedding("人工智能的发展历程")
print(f"向量维度: {len(embedding)}")  # text-embedding-3-small 输出 1536 维

方案二:使用 LangChain 的 OpenAIEmbeddings

如果你已经在使用 LangChain 框架,使用 OpenAIEmbeddings 会更方便:

python 复制代码
from langchain_openai import OpenAIEmbeddings

# 初始化嵌入模型
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

# 批量生成向量
docs = [
    "Thrilling Finale Awaits: The Countdown to the Cricket World Cup Championship",
    "Global Giants Clash: Football World Cup Semi-Finals Set the Stage for Epic Showdowns",
]

embed_docs = embeddings.embed_documents(docs)

print(len(embed_docs))       # 输出: 2 (与输入文档数量一致)
print(len(embed_docs[0]))    # 输出: 3072 (text-embedding-3-large 的向量维度)

# 单个查询向量
query_embedding = embeddings.embed_query("世界杯决赛")
print(len(query_embedding))  # 输出: 3072

方案三:集成到 InMemoryStore 实现语义搜索

这是最完整的方案,将 Embedding 能力整合到长期记忆中:

python 复制代码
from langgraph.store.memory import InMemoryStore
from langchain_openai import OpenAIEmbeddings

# 1. 初始化嵌入模型
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# 2. 配置 InMemoryStore 的语义索引
index_config = {
    "embed": embedding_model,  # 传入嵌入模型
    "dims": 1536,              # text-embedding-3-small 的向量维度
    "fields": ["$"]            # 对完整的 value 建立索引("$" 表示全部字段)
}

store = InMemoryStore(index=index_config)

# 3. 存入数据
namespace1 = ("users", "Alice", "memories")
store.put(namespace1, "preferences", {
    "course": "计算机组成原理",
    "sports": "跑步",
    "food": "紫光园奶皮子酸奶"
})

namespace2 = ("users", "Bob", "memories")
store.put(namespace2, "preferences", {
    "course": "数字电路与模拟电路",
    "sports": "跑步",
    "food": "奶皮子糖葫芦"
})

namespace3 = ("users", "Black", "memories")
store.put(namespace3, "preferences", {
    "course": "数字电路与模拟电路",
    "sports": "羽毛球",
    "food": "紫光园奶皮子酸奶"
})

# 4. 执行语义搜索
print("=" * 30, "语义搜索: '数电模电'", "=" * 30)
results = store.search(
    ("users",),
    query="数电模电"  # 语义搜索
)

for item in results:
    print(f"用户: {item.namespace[1]}, 课程: {item.value['course']}, 相似度: {item.score:.4f}")

输出示例:

text 复制代码
============================== 语义搜索: '数电模电' ==============================
用户: Bob, 课程: 数字电路与模拟电路, 相似度: 0.3620
用户: Black, 课程: 数字电路与模拟电路, 相似度: 0.3268
用户: Alice, 课程: 计算机组成原理, 相似度: 0.1803

自定义 Embedding 类(适配非 OpenAI 接口)

如果你使用的是其他 Embedding 服务(如 Voyage、Cohere 等),可以自定义 Embedding 类:

python 复制代码
import openai
from langchain_core.embeddings import Embeddings

class CustomEmbedding(Embeddings):
    def __init__(self, model: str, api_key: str, base_url: str):
        self.client = openai.OpenAI(
            api_key=api_key,
            base_url=base_url
        )
        self.model = model

    def embed_documents(self, texts: list[str]) -> list[list[float]]:
        resp = self.client.embeddings.create(
            input=texts,
            model=self.model
        )
        return [d.embedding for d in resp.data]

    def embed_query(self, text: str) -> list[float]:
        return self.embed_documents([text])[0]

# 使用示例(以 Voyage 为例)
embedding_model = CustomEmbedding(
    model="voyage-4-lite",
    api_key=os.getenv("OPENROUTER_API_KEY"),
    base_url=os.getenv("OPENROUTER_BASE_URL")
)

4.4 search() 方法的三种检索方式

方式 参数 说明
前缀搜索 namespace_prefix 按命名空间前缀匹配,如 ("users",) 匹配所有用户
结构化过滤 filter 按 value 中的字段精确匹配,如 {"sports": "羽毛球"}
语义检索 query 使用向量相似度搜索,如 query="数电模电"

组合使用示例:

python 复制代码
# 在特定命名空间下,按条件过滤 + 语义排序
results = store.search(
    namespace_prefix=("users",),
    query="羽毛球",           # 语义检索
    filter={"food": "紫光园奶皮子酸奶"},  # 精确过滤
    limit=5
)

五、在 Agent 中集成长期记忆

5.1 通过工具访问长期记忆

最常用的方式是将记忆操作封装为工具,让 Agent 自主决定何时读写。

python 复制代码
from langchain_core.tools import tool
from langgraph.prebuilt import ToolRuntime
from langchain.agents import AgentState
from typing import NotRequired

# 自定义 State,加入 user_id
class CustomState(AgentState):
    user_id: NotRequired[str]

# 保存记忆的工具
@tool(parse_docstring=True)
def save_user_info(name: str, runtime: ToolRuntime) -> str:
    """将用户名保存到长期记忆
    
    Args:
        name: 用户名
        runtime: 工具运行时
    """
    namespace = ("users",)
    key = runtime.state["user_id"]
    runtime.store.put(namespace, key, {"name": name})
    return "已保存"

# 读取记忆的工具
@tool(parse_docstring=True)
def get_user_info(runtime: ToolRuntime) -> str:
    """从长期记忆中获取用户信息"""
    namespace = ("users",)
    key = runtime.state["user_id"]
    item = runtime.store.get(namespace, key)
    return str(item.value) if item else "未知用户"

# 创建 Agent
agent = create_agent(
    model=model,
    tools=[save_user_info, get_user_info],
    store=store,  # 传入长期记忆存储
    state_schema=CustomState,
    system_prompt="用户提及个人信息时及时记录,用户询问个人信息时尝试用工具检索"
)

# 第一个会话:保存信息
response1 = agent.invoke({
    "messages": [HumanMessage("你好,我是小花")],
    "user_id": "user-1"
})

# 第二个会话(不同 thread_id):检索信息
response2 = agent.invoke({
    "messages": [HumanMessage("我是谁")],
    "user_id": "user-1"  # 相同的 user_id
})
print(response2['messages'][-1].content)
# 输出:你是小花。

5.2 使用 PostgreSQL 持久化长期记忆

python 复制代码
from langgraph.store.postgres import PostgresStore

DB_URL = "postgresql://langchain_user:abcd1234@192.168.196.128:5432/langchain_db?sslmode=disable"

with PostgresStore.from_conn_string(DB_URL) as store:
    store.setup()
    
    agent = create_agent(
        model=model,
        tools=[save_user_info, get_user_info],
        store=store,
        state_schema=CustomState,
        system_prompt="用户提及个人信息时及时记录,用户询问个人信息时尝试用工具检索"
    )
    
    # 使用方式与 InMemoryStore 完全相同
    response = agent.invoke({
        "messages": [HumanMessage("你好,我是小花")],
        "user_id": "user-1"
    })

5.3 写入时机的选择

策略 说明 适用场景
热路径写入 在对话流程中同步写入 用户偏好、账号资料、即时信息
后台写入 异步处理,不阻塞主流程 对话摘要、经验沉淀、行为分析

热路径写入的优点是立即生效,但会增加延迟。后台写入不会影响用户体验,但记忆不会立即生效,需要权衡。


六、完整架构图

text

vbnet 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                         Agent 应用                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐      │
│  │  短期记忆     │    │  记忆治理    │    │  长期记忆    │      │
│  │              │    │              │    │              │      │
│  │ • State      │    │ • 裁剪       │    │ • Store      │      │
│  │ • Checkpointer│   │ • 删除       │    │ • Namespace  │      │
│  │ • Thread ID  │    │ • 摘要       │    │ • Key/Value  │      │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘      │
│         │                   │                   │              │
│         ▼                   ▼                   ▼              │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐      │
│  │ InMemorySaver│    │  上下文窗口   │    │ InMemoryStore│      │
│  │ PostgresSaver│    │  Token 限制  │    │ PostgresStore│      │
│  └──────────────┘    └──────────────┘    └──────────────┘      │
│                                              │                  │
│                                              ▼                  │
│                                     ┌──────────────┐           │
│                                     │ 语义搜索     │           │
│                                     │ + Embedding  │           │
│                                     └──────────────┘           │
└─────────────────────────────────────────────────────────────────┘

七、最佳实践总结

7.1 短期记忆配置建议

python 复制代码
# 生产环境推荐配置
from langgraph.checkpoint.postgres import PostgresSaver

checkpointer = PostgresSaver.from_conn_string(DB_URL)
checkpointer.setup()

agent = create_agent(
    model=model,
    checkpointer=checkpointer,  # 使用数据库持久化
    # 根据模型窗口配置记忆治理策略
    middleware=[
        SummarizationMiddleware(
            model=summary_model,  # 可以用更便宜的模型做摘要
            trigger=[("tokens", 6000)],
            keep=("messages", 10),
        )
    ]
)

7.2 长期记忆设计要点

  1. 命名空间设计 :建议使用 ("users", user_id, "memories") 的层级结构
  2. 语义索引字段 :选择 "$"(全文)和关键字段(如 "course""preferences"
  3. TTL 管理:为临时数据设置过期时间,避免存储无限膨胀

7.3 Embedding 模型选择建议

模型 维度 适用场景
text-embedding-3-small 1536 性价比高,适合大多数场景
text-embedding-3-large 3072 精度更高,适合对检索质量要求高的场景
voyage-4-lite 1024 轻量级,适合资源受限场景

7.4 常见问题

Q: InMemorySaverInMemoryStore 的区别?

  • InMemorySaver:存储会话状态(短期记忆),进程结束即丢失
  • InMemoryStore:存储长期记忆,进程结束即丢失(仅用于测试)

生产环境请分别使用 PostgresSaverPostgresStore

Q: 摘要会丢失信息吗?

会丢失一些细节,但重要信息(姓名、关键事实)会保留,最近的消息完整保留,对于大部分场景足够。

Q: 如何监控摘要触发频率?

如果频繁触发,说明阈值设得太低,应提高阈值;如果从不触发,说明阈值设得太高,应降低阈值以控制成本。

Q: 语义搜索的相似度分数阈值设多少合适?

建议根据业务场景测试确定。通常 0.3 以上可以认为是相关结果,低于 0.2 可能不相关。可以在测试集上计算最优阈值。


结语

记忆系统是构建高质量 AI Agent 的基石。本文从短期记忆的三要素(State + Checkpointer + Thread ID),到记忆治理的三种策略(裁剪、删除、摘要),再到跨会话的长期记忆(Store + Namespace + Key + Value),以及基于 Embedding 的语义搜索,完整覆盖了 Agent 记忆系统的方方面面。

在实际项目中,你需要根据业务场景:

  1. 选择合适的存储后端:内存用于测试,数据库(PostgreSQL)用于生产
  2. 配置合理的记忆治理策略:平衡上下文长度与信息完整性
  3. 设计良好的长期记忆数据结构:命名空间 + 语义索引
  4. 选配合适的 Embedding 模型:平衡检索质量与成本

有了完整的记忆能力,你的 Agent 才能真正做到"认识用户",提供连贯、个性化的智能体验。


📌 代码仓库:所有示例代码均可在 LangChain/LangGraph 官方文档中找到完整版本。

📖 扩展阅读LangGraph 记忆文档 | OpenAI Embeddings 文档

相关推荐
大鱼>1 小时前
DSPy:LLM程序自动编译与提示词优化
开发语言·人工智能·python·深度学习
初学AI的小高1 小时前
RAG 效果差先别调 Prompt:检索评测方法论 + 94 条实测
llm·agent
2401_843253701 小时前
金融智能:AI如何重构银行业未来
人工智能·python·金融
uncle_ll1 小时前
服务器选型、微调范式、训练优化与环境搭建
服务器·python·gpt·llm·nlp
久久学姐2 小时前
Python开发爬虫的常用技术架构
爬虫·python·http·框架·数据存储
circuitsosk2 小时前
大规模离线数据管道构建:样本获取、清洗、加工与合成
人工智能·python·机器学习·搜索引擎
tang777892 小时前
分布式爬虫优化指南:如何用代理IP把采集效率提升300%
分布式·爬虫·python·tcp/ip·分布式爬虫·爬虫代理·代理ip
海兰2 小时前
【思考】银行落地业务Agent的思考
人工智能·机器学习·agent
gwf2162 小时前
Soft-RoCE与Soft-iWARP深度解析:无硬件RDMA学习环境搭建(零基础必知必会)
人工智能·python·tcp/ip·tcp·tcpdump