让你的 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。
三、记忆治理:别让上下文撑爆了
大模型的上下文窗口是有限的,无限制地累积历史消息会导致:
- 信息过载:模型注意力分散,回答质量下降
- Token 费用激增:每次请求都要传输全部历史
- 可能超出上下文窗口:导致请求失败
因此,我们需要对记忆进行"治理"。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 长期记忆设计要点
- 命名空间设计 :建议使用
("users", user_id, "memories")的层级结构 - 语义索引字段 :选择
"$"(全文)和关键字段(如"course"、"preferences") - TTL 管理:为临时数据设置过期时间,避免存储无限膨胀
7.3 Embedding 模型选择建议
| 模型 | 维度 | 适用场景 |
|---|---|---|
text-embedding-3-small |
1536 | 性价比高,适合大多数场景 |
text-embedding-3-large |
3072 | 精度更高,适合对检索质量要求高的场景 |
voyage-4-lite |
1024 | 轻量级,适合资源受限场景 |
7.4 常见问题
Q: InMemorySaver 和 InMemoryStore 的区别?
InMemorySaver:存储会话状态(短期记忆),进程结束即丢失InMemoryStore:存储长期记忆,进程结束即丢失(仅用于测试)
生产环境请分别使用 PostgresSaver 和 PostgresStore。
Q: 摘要会丢失信息吗?
会丢失一些细节,但重要信息(姓名、关键事实)会保留,最近的消息完整保留,对于大部分场景足够。
Q: 如何监控摘要触发频率?
如果频繁触发,说明阈值设得太低,应提高阈值;如果从不触发,说明阈值设得太高,应降低阈值以控制成本。
Q: 语义搜索的相似度分数阈值设多少合适?
建议根据业务场景测试确定。通常 0.3 以上可以认为是相关结果,低于 0.2 可能不相关。可以在测试集上计算最优阈值。
结语
记忆系统是构建高质量 AI Agent 的基石。本文从短期记忆的三要素(State + Checkpointer + Thread ID),到记忆治理的三种策略(裁剪、删除、摘要),再到跨会话的长期记忆(Store + Namespace + Key + Value),以及基于 Embedding 的语义搜索,完整覆盖了 Agent 记忆系统的方方面面。
在实际项目中,你需要根据业务场景:
- 选择合适的存储后端:内存用于测试,数据库(PostgreSQL)用于生产
- 配置合理的记忆治理策略:平衡上下文长度与信息完整性
- 设计良好的长期记忆数据结构:命名空间 + 语义索引
- 选配合适的 Embedding 模型:平衡检索质量与成本
有了完整的记忆能力,你的 Agent 才能真正做到"认识用户",提供连贯、个性化的智能体验。
📌 代码仓库:所有示例代码均可在 LangChain/LangGraph 官方文档中找到完整版本。
📖 扩展阅读 :LangGraph 记忆文档 | OpenAI Embeddings 文档