前言
上一篇讲了工具------Agent 与外部世界交互的接口。这篇讲记忆。
记忆是让 Agent 从"一次性问答机器"变成"能持续协作的伙伴"的关键。没有记忆的 Agent 每次都从零开始:它不记得你叫什么、不喜欢什么、上次聊到哪了。这种 Agent 用两次就会让人觉得"聪明但健忘",信任感很难建立。
但记忆管理远没有"把所有对话存下来"这么简单。LangChain 1.0 把记忆拆成了两个层次:短期记忆 和长期记忆 。这两者的分界线不是"存在内存里"还是"存在数据库里"------这是一个极其常见的误解。分界线在于数据的生命周期跟谁绑定。
官方文档在短期记忆页面写得很清楚:
Short-term memory in LangGraph is part of the agent state, persisted by the checkpointer, and organized by thread_id.
(LangGraph 中的短期记忆是 Agent 状态的一部分,由检查点机制持久化,按 thread_id 组织。)
而长期记忆,官方在另一篇文档里说:
Long-term memory is built on the LangGraph store, organized as JSON documents by namespace and key, persisted across sessions, and commonly recalled using user_id.
(长期记忆建在 LangGraph Store 之上,以 JSON 文档形式按 namespace 加 key 组织,跨会话可召回,常用 user_id 作为 namespace。)
一个是 thread_id,一个是 user_id。这就是分界线。
这篇文章会把这件事从头到尾讲透:先厘清概念,再逐个拆解 Checkpointer、上下文裁剪、自定义 State、向量数据库、BaseStore 的 API 细节,最后给出生产环境的组合架构。
一、短期 vs 长期记忆:分界线到底在哪
1.1 一个普遍的误解
"内存 = 短期记忆,数据库 = 长期记忆"
这句话错在把存储介质等同于记忆类型 。事实上,PostgresSaver 把数据存在 PostgreSQL 数据库里,但它管理的仍然是短期记忆;InMemoryStore 把数据存在 Python 进程内存里,但它管理的是长期记忆。
存储介质(内存 / 数据库 / 向量库)解决的是"数据放在哪"的问题;记忆类型解决的是"数据活多久"的问题。两件事不该混在一起。
1.2 正确的分界标准
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 生命周期 | 绑定到会话(thread)。会话结束,数据语义上失效 | 绑定到用户 / 业务实体。跨会话持久保留 |
| 组织方式 | 按 thread_id 隔离。不同 thread 互不可见 |
按 namespace(通常是 user_id)+ key 组织 |
| 检索模式 | 自动加载:传入 thread_id 时,框架自动恢复完整历史 | 主动检索:Agent 在运行时通过工具主动查询 |
| 数据形态 | 完整的消息列表(messages) | 结构化档案(KV)、语义向量(Embedding)、用户偏好 |
| 典型载体 | InMemorySaver、PostgresSaver |
BaseStore(KV)、向量数据库(Chroma / Pinecone) |
| 清理时机 | 会话结束后无意义(但持久化载体不会自动删) | 需要主动管理:TTL 过期、手动清理、压缩归档 |
一句话总结:短期记忆跟着对话走,长期记忆跟着用户走。
1.3 为什么需要两层
只用短期记忆,Agent 每次新对话都是失忆状态。用户的偏好、习惯、历史决策全部丢失。
只用长期记忆,每次对话都得重新理解上下文。"我刚才说的那个""接着上面的"这类指代全靠用户重新解释。
两层配合,短期记忆维持当前对话的连贯,长期记忆积累跨对话的知识和偏好。一个让 Agent 会说人话,一个让 Agent 越用越懂你。
二、短期记忆管理:Checkpointer 机制
2.1 Checkpointer 是短期记忆的灵魂
不加 checkpointer 参数:Agent 是无状态的。每次 invoke 都是全新的开始,不记得上一轮说了什么。
加一行 checkpointer=memory:LangGraph 会在每一步执行后,把完整的 state(包括所有消息历史)序列化并存入 Checkpointer。当你再次 invoke 并传入同一个 thread_id 时,LangGraph 先去查"这个 ID 上次停在哪里",加载状态,把新消息 append 进去,然后继续运行。
用户第一次发消息 → Agent 处理 → 状态存到 Checkpointer(thread_id 作为 key)
用户第二次发消息 → 加载旧状态 → 追加新消息 → Agent 带着完整上下文处理 → 再次存储
2.2 Thread ID:短期记忆的钥匙
thread_id 就是 Web 开发里的 Session ID。你需要为每个用户或每次对话生成一个唯一标识。不同 thread_id 之间的记忆完全隔离------用户 A 和用户 B 的对话互不可见。
python
config_user_a = {"configurable": {"thread_id": "session_user_A"}}
config_user_b = {"configurable": {"thread_id": "session_user_B"}}
# 两者的记忆完全隔离
2.3 InMemorySaver:开发环境首选
最简单的 Checkpointer 实现,数据存在 Python 进程的内存字典里。进程重启后数据丢失。
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
load_dotenv(override=True)
# ==================== 定义工具 ====================
@tool
def get_user_info(name: str) -> str:
"""查询用户信息,返回姓名、年龄和爱好。
Args:
name: 用户姓名,如"陈明"、"张三"
"""
user_db = {
"陈明": {"age": 28, "hobby": "旅游、滑雪、喝茶"},
"张三": {"age": 32, "hobby": "编程、阅读、电影"},
}
info = user_db.get(name)
if not info:
return f"未找到用户 {name} 的信息"
return f"姓名: {name}, 年龄: {info['age']}岁, 爱好: {info['hobby']}"
# ==================== 加载模型 ====================
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
)
# ==================== 创建带短期记忆的 Agent ====================
memory = InMemorySaver() # 内存级检查点
agent = create_agent(
model=model,
tools=[get_user_info],
system_prompt="你是一个友好的助手,记住用户提到的个人信息。",
checkpointer=memory, # ← 这一行启用短期记忆
)
# ==================== 测试多轮对话 ====================
config = {"configurable": {"thread_id": "user_123"}}
# 第一轮:用户自我介绍
r1 = agent.invoke(
{"messages": [{"role": "user", "content": "你好,我叫陈明,很高兴认识你!"}]},
config=config
)
print(f"AI: {r1['messages'][-1].content}")
# 第二轮:测试记忆
r2 = agent.invoke(
{"messages": [{"role": "user", "content": "你还记得我叫什么名字吗?"}]},
config=config # 同一个 thread_id → 自动携带上下文
)
print(f"AI: {r2['messages'][-1].content}")
# 预期输出:当然记得,你叫陈明!
# 第三轮:查看当前记忆状态
state = agent.get_state(config)
print(f"当前记忆中共有 {len(state.values['messages'])} 条消息")
# 第四轮:新会话(不同 thread_id)→ 无记忆
config_new = {"configurable": {"thread_id": "user_456"}}
r3 = agent.invoke(
{"messages": [{"role": "user", "content": "我们之前聊过吗?"}]},
config=config_new
)
print(f"新会话 AI: {r3['messages'][-1].content}")
# 预期输出:我们之前没有聊过,这是我们的第一次对话。
2.4 PostgresSaver:生产环境持久化
InMemorySaver 开发够用,但生产环境需要数据持久化。PostgresSaver 把检查点存到 PostgreSQL,进程重启不丢失,支持多实例共享。
但请注意:即使数据存到了数据库,它仍然是短期记忆。 原因:
- 作用域限制 :只检索和加载当前
thread_id的数据 - 生命周期语义:数据属于"本次会话",即使物理上没删
- 无跨会话检索:新会话中无法自动访问旧会话数据(除非手动指定旧 thread_id)
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg_pool import ConnectionPool
load_dotenv(override=True)
# ==================== 数据库配置 ====================
DB_URI = os.getenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/agent_db")
# ==================== 定义工具 ====================
@tool
def get_user_info(name: str) -> str:
"""查询用户信息,返回姓名、年龄和爱好。
Args:
name: 用户姓名
"""
user_db = {
"陈明": {"age": 28, "hobby": "旅游、滑雪、喝茶"},
"张三": {"age": 32, "hobby": "编程、阅读、电影"},
}
info = user_db.get(name)
if not info:
return f"未找到用户 {name} 的信息"
return f"姓名: {name}, 年龄: {info['age']}岁, 爱好: {info['hobby']}"
# ==================== 模型 ====================
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ==================== 使用 Postgres 持久化 ====================
# 使用连接池管理数据库连接
with ConnectionPool(
conninfo=DB_URI,
max_size=20, # 最大连接数
kwargs={"autocommit": True}
) as pool:
checkpointer = PostgresSaver(pool)
checkpointer.setup() # 幂等操作:首次运行自动创建表结构
agent = create_agent(
model=model,
tools=[get_user_info],
system_prompt="你是一个友好的助手,记住用户提到的个人信息。",
checkpointer=checkpointer,
)
# 使用方式与 InMemorySaver 完全一致
config = {"configurable": {"thread_id": "prod_user_001"}}
agent.invoke(
{"messages": [{"role": "user", "content": "我是张三,请记住我的信息"}]},
config=config
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "我是谁?"}]},
config=config
)
print(f"AI: {result['messages'][-1].content}")
# 即使重启程序,只要 thread_id 不变,记忆依然存在
2.5 两种 Checkpointer 对比
| 维度 | InMemorySaver |
PostgresSaver |
|---|---|---|
| 存储位置 | Python 进程内存 | PostgreSQL 数据库 |
| 持久化 | 进程重启后丢失 | 持久保存 |
| 性能 | 极高(纳秒级) | 较高(毫秒级) |
| 扩展性 | 单进程限制 | 支持多实例、高并发 |
| 适用环境 | 开发、测试 | 生产、分布式部署 |
| 核心定位 | 短期记忆 | 短期记忆(持久化版) |
关键认知: 两者都是短期记忆。区别只在于"数据能不能扛住重启",不在于"是不是长期记忆"。
三、上下文裁剪:防止记忆撑爆 Token
3.1 问题
短期记忆会累积所有消息。随着对话进行,state["messages"] 可能包含几千条消息。直接全部传给 LLM 会:
- 烧钱------每轮都按全量 token 计费
- 超限报错------超过模型的上下文窗口(如 128K / 8K)直接报错
- 性能下降------上下文越长,模型的注意力越分散
3.2 解决思路
State 中保存 100% 的完整历史(用于审计和回溯),但传给 LLM 时只传最近的 N 条消息或 N 个 token。
这就是 trim_messages 的作用。
State 中:[msg1, msg2, msg3, ..., msg100] ← 完整保存
传给 LLM:[msg95, msg96, msg97, ..., msg100] ← 只传最近的
3.3 trim_messages 核心参数
| 参数 | 类型 | 说明 |
|---|---|---|
messages |
list | 待裁剪的消息列表 |
max_tokens |
int | 允许的最大 token 数,超过则触发裁剪 |
token_counter |
Callable / str | token 计数函数,可传 len(按条数)或自定义函数(精确计算 token) |
strategy |
str | "last" 保留最新消息,"first" 保留最早消息 |
include_system |
bool | 是否保留 System 消息(通常必须保留) |
allow_partial |
bool | 是否允许部分消息(False 则保持消息完整性) |
start_on |
str | 裁剪后第一条消息的角色,"human" 确保以用户消息开始 |
3.4 精确 Token 计数
不同模型使用不同的 token 编码。tiktoken 是 OpenAI 官方的 token 编码库,支持精确计数。
python
import tiktoken
def get_token_encoder(model_name: str = "gpt-4o-mini"):
"""获取 tiktoken 编码器实例"""
try:
encoding = tiktoken.encoding_for_model(model_name)
return encoding
except KeyError:
# 不在映射表中的模型,使用默认编码
return tiktoken.get_encoding("o200k_base")
# 模型与编码器对应关系:
# - gpt-4o, gpt-4o-mini: o200k_base
# - gpt-4-turbo: cl100k_base
# - text-davinci-003: p50k_base
TOKEN_ENCODER = get_token_encoder("gpt-4o-mini")
def count_tokens(messages):
"""精确计算消息列表的 token 总数"""
total = 0
for msg in messages:
role_tokens = len(TOKEN_ENCODER.encode(msg.type))
content_tokens = len(TOKEN_ENCODER.encode(msg.content))
format_overhead = 4 # 每条约 4 个特殊 token(消息边界标记)
total += role_tokens + content_tokens + format_overhead
return total
3.5 实战:多轮对话 + 自动裁剪
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, trim_messages
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
import tiktoken
load_dotenv(override=True)
# ==================== 配置 ====================
MAX_TOKENS = 2000 # 裁剪阈值(根据模型上下文窗口调整)
MODEL_NAME = "gpt-4o-mini"
TOKEN_ENCODER = tiktoken.encoding_for_model(MODEL_NAME)
def count_tokens(messages):
"""精确 token 计数"""
total = 0
for msg in messages:
total += len(TOKEN_ENCODER.encode(msg.type))
total += len(TOKEN_ENCODER.encode(msg.content))
total += 4 # 格式开销
return total
# ==================== 工具 ====================
@tool
def search_weather(city: str) -> str:
"""查询指定城市的天气信息。
Args:
city: 城市名称,如"北京"、"上海"
"""
data = {"北京": "晴朗 25°C", "上海": "多云 28°C", "广州": "小雨 30°C"}
return f"{city}:{data.get(city, '暂无数据')}"
# ==================== 模型 ====================
model = ChatOpenAI(model=MODEL_NAME, temperature=0)
# ==================== 创建带记忆的 Agent ====================
memory = InMemorySaver()
agent = create_agent(
model=model,
tools=[search_weather],
system_prompt="你是一个简洁的助手,记住用户提到的信息。",
checkpointer=memory,
)
# ==================== 裁剪并调用的封装函数 ====================
def invoke_with_trim(agent, user_input: str, config: dict):
"""在调用 Agent 前自动裁剪上下文"""
# 1. 获取当前历史
state = agent.get_state(config)
history = state.values.get("messages", []) if state else []
if history:
current_tokens = count_tokens(history)
print(f" [裁剪前] {len(history)} 条消息, {current_tokens} tokens")
# 2. 裁剪
trimmed = trim_messages(
history,
max_tokens=MAX_TOKENS,
token_counter=count_tokens,
strategy="last", # 保留最新消息
include_system=True, # 保留系统消息
allow_partial=False, # 保持消息完整性
start_on="human", # 裁剪后以用户消息开始
)
new_tokens = count_tokens(trimmed)
print(f" [裁剪后] {len(trimmed)} 条消息, {new_tokens} tokens")
else:
trimmed = []
# 3. 追加新消息
new_messages = trimmed + [HumanMessage(content=user_input)]
# 4. 调用 Agent
return agent.invoke({"messages": new_messages}, config=config)
# ==================== 测试多轮对话 ====================
config = {"configurable": {"thread_id": "trim_demo"}}
conversations = [
"你好,我叫陈明",
"帮我查一下北京天气",
"上海呢?",
"明天北京天气如何?",
"我是谁来着?", # 测试裁剪后是否还记得
]
for i, query in enumerate(conversations, 1):
print(f"\n--- 第 {i} 轮 ---")
print(f"用户: {query}")
result = invoke_with_trim(agent, query, config)
print(f"AI: {result['messages'][-1].content}")
start_on="human" 为什么重要? 如果裁剪后第一条消息是 AI 的回复(没有对应的用户问题),某些模型会感到困惑,可能输出一段莫名其妙的回答。这个参数确保裁剪后的对话总是以用户消息开始。
四、自定义 State:扩展短期记忆的维度
4.1 为什么需要扩展
默认的 AgentState 只有 messages 字段。但实际业务中,你需要在 Agent 执行过程中持久化更多上下文:
- 用户身份 :
user_id用于权限控制和个性化 - 用户偏好:主题、语言、通知方式
- 执行状态:重试次数、当前步骤、错误计数
- 审计信息:原始查询、创建时间
这些字段需要在 Agent 的所有步骤间共享(LLM 调用 → 工具调用 → 结果解析),不能只放在工具内部变量里。
4.2 用 TypedDict 扩展 AgentState
LangChain 1.0 推荐使用 TypedDict 而非 Pydantic。原因:
- 性能更高(无序列化开销,在高频 API 调用场景优势明显)
- 与 LangGraph 的状态管理系统原生兼容
- 从 LangChain 1.0 开始,
state_schema必须是 TypedDict 类型,不再支持 Pydantic 和 dataclass
python
from typing import TypedDict
from langchain.agents import AgentState
class CustomAgentState(AgentState):
"""扩展的 Agent 状态"""
user_id: str # 用户唯一标识
preferences: dict # 用户偏好
visit_count: int # 访问次数
4.3 实战:带用户偏好的 Agent
python
import os
from typing import TypedDict
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import ToolMessage
from langchain.agents import AgentState, create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
load_dotenv(override=True)
# ==================== 自定义 State ====================
class CustomAgentState(AgentState):
user_id: str
preferences: dict
visit_count: int
# ==================== 模型 ====================
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ==================== 工具:读写自定义 State ====================
@tool
def update_preference(theme: str, runtime: "ToolRuntime") -> Command:
"""更新用户的主题偏好设置。
Args:
theme: 主题名称,如"暗黑模式"、"浅色模式"
"""
current_prefs = runtime.state.get("preferences", {})
current_prefs["theme"] = theme
return Command(update={
"preferences": current_prefs,
"messages": [
ToolMessage(
content=f"已成功将主题设置为:{theme}",
tool_call_id=runtime.tool_call_id,
)
],
})
@tool
def get_greeting(runtime: "ToolRuntime") -> str:
"""根据用户偏好生成个性化问候。"""
user_id = runtime.state.get("user_id", "访客")
prefs = runtime.state.get("preferences", {})
theme = prefs.get("theme", "默认")
visit_count = runtime.state.get("visit_count", 0)
return f"欢迎回来,{user_id}!当前主题:{theme},这是你的第 {visit_count} 次访问。"
# ==================== 创建 Agent ====================
memory = InMemorySaver()
agent = create_agent(
model=model,
tools=[update_preference, get_greeting],
state_schema=CustomAgentState, # ← 关键:传入自定义状态类型
system_prompt="你是一个个性化助手,帮助用户管理偏好设置。",
checkpointer=memory,
)
# ==================== 测试 ====================
config = {"configurable": {"thread_id": "custom_state_demo"}}
# 第一轮:传入自定义字段
r1 = agent.invoke({
"messages": [{"role": "user", "content": "把主题设成暗黑模式"}],
"user_id": "user_789",
"preferences": {"language": "zh-CN"},
"visit_count": 1,
}, config=config)
print(f"AI: {r1['messages'][-1].content}")
# 第二轮:读取状态
r2 = agent.invoke(
{"messages": [{"role": "user", "content": "打个招呼"}]},
config=config
)
print(f"AI: {r2['messages'][-1].content}")
# 查看完整状态
state = agent.get_state(config)
print(f"\n当前状态:")
print(f" 用户ID:{state.values.get('user_id')}")
print(f" 偏好:{state.values.get('preferences')}")
print(f" 消息数:{len(state.values['messages'])}")
4.4 TypedDict vs Pydantic:什么时候用哪个
| 场景 | TypedDict | Pydantic |
|---|---|---|
| Agent state_schema | ✅ 最佳选择(LangChain 1.0 强制要求) | ❌ 不再支持 |
| context_schema | ✅ 推荐 | ✅ 可用 |
| response_format | ❌ 不支持 | ✅ 必须用 BaseModel |
| API 请求体 | ⚠️ 需手动校验 | ✅ 原生验证 |
| 内部函数参数 | ✅ 轻量有效 | ⚠️ 略重 |
记住:持久化状态用 TypedDict,对外输出契约用 Pydantic。
五、长期记忆:向量数据库方案
长期记忆的第一种实现路径是向量数据库。适合非结构化的语义记忆------用户偏好、历史对话摘要、知识片段。
5.1 核心原理
用户说"我喜欢草莓" → 文本向量化 → 存入向量数据库
用户问"我有什么忌口" → 查询向量化 → 语义相似度搜索 → 找到"草莓""花生过敏"
向量数据库的优势在于模糊匹配------不需要精确的关键词,语义相近就能找到。
5.2 主流向量数据库对比
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Chroma | 轻量级,支持本地持久化 | 开发、小规模应用 |
| Pinecone | 云原生,全托管 | 生产环境、大规模 |
| Milvus | 开源,高性能,支持大规模检索 | 企业级部署 |
| Qdrant | 高性能向量搜索引擎 | 需要丰富过滤条件的场景 |
5.3 实战:基于 Chroma 的长期记忆
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langchain_core.documents import Document
from langchain_chroma import Chroma
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
load_dotenv(override=True)
# ==================== 初始化向量数据库 ====================
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = Chroma(
collection_name="agent_long_term_memory",
embedding_function=embeddings,
# persist_directory="./chroma_db" # 取消注释启用本地持久化
)
# ==================== 定义记忆工具 ====================
@tool
def save_memory(content: str) -> str:
"""将重要信息保存到长期记忆中。
当获知用户的喜好、职业、计划或其他长期有效的事实时调用。
Args:
content: 要保存的记忆内容,用一句话概括关键信息
"""
doc = Document(
page_content=content,
metadata={"source": "user_interaction"}
)
vector_store.add_documents([doc])
print(f"[记忆] 已保存:{content}")
return "记忆已保存。"
@tool
def search_memory(query: str) -> str:
"""从长期记忆中搜索相关信息。
当被问及关于用户过去的问题,或不确定答案时使用。
Args:
query: 搜索查询,描述你想查找的信息
"""
results = vector_store.similarity_search(query, k=3)
if not results:
return "没有找到相关的记忆。"
memory_content = "\n".join([f"- {doc.page_content}" for doc in results])
return f"找到以下相关记忆:\n{memory_content}"
# ==================== 创建 Agent ====================
model = ChatOpenAI(model="gpt-4o", temperature=0)
SYSTEM_PROMPT = """你是一个拥有长期记忆的私人助手。
核心规则:
1. 用户告诉你关于他们自己的事实(名字、喜好、职业、忌口等)时,必须调用 save_memory 保存
2. 用户问你可能在记忆中存在的问题时,先调用 search_memory 查找
3. 普通闲聊不需要调用记忆工具
记忆格式要求:每条记忆用一句话概括,包含关键信息。
例如:"用户最喜欢的水果是草莓" 而不是 "用户说他喜欢吃草莓"
"""
checkpointer = InMemorySaver()
agent = create_agent(
model,
tools=[save_memory, search_memory],
system_prompt=SYSTEM_PROMPT,
checkpointer=checkpointer,
)
# ==================== 场景演示 ====================
# 场景 A:存入记忆(今天的对话)
print("--- 场景 A:用户告诉 Agent 喜好 ---")
config_a = {"configurable": {"thread_id": "session_today"}}
for chunk in agent.stream(
{"messages": [HumanMessage(content="你好,记住我最喜欢草莓,而且对花生过敏。")]},
config=config_a,
stream_mode="values"
):
pass
print(f"Agent: {chunk['messages'][-1].content}")
# 场景 B:新会话,短期记忆已清空,但长期记忆可跨会话访问
print("\n--- 场景 B:第二天(新 Session,短期记忆已清空)---")
config_b = {"configurable": {"thread_id": "session_tomorrow"}}
for chunk in agent.stream(
{"messages": [HumanMessage(content="我想吃点零食,但我忘了有什么忌口,帮我查查?")]},
config=config_b,
stream_mode="values"
):
pass
print(f"Agent: {chunk['messages'][-1].content}")
# 预期输出:Agent 调用 search_memory,找到"花生过敏"的记忆并提醒用户
六、长期记忆:BaseStore 结构化方案
向量数据库擅长语义检索,但不擅长精确的结构化存储。长期记忆的第二种路径是 BaseStore------LangGraph 提供的通用键值存储,专为结构化数据设计。
6.1 核心特性
-
命名空间(Namespace) :层次化元组路径,类似文件系统目录
pythonnamespace = ("users", "user_123", "preferences") # 对应逻辑路径:users/user_123/preferences -
核心操作 :
put()存储、get()精确检索、search()搜索、delete()删除 -
支持 TTL:自动过期清理
-
跨线程访问 :不受
thread_id限制
6.2 BaseStore vs 向量数据库
| 维度 | BaseStore | 向量数据库 |
|---|---|---|
| 存储内容 | 结构化字典(JSON 序列化) | 非结构化文本(自动 Embedding) |
| 检索方式 | get() 精确匹配 + search() 搜索 |
similarity_search() 语义相似 |
| 查询灵活性 | 必须精确 key 或有限搜索 | 自然语言模糊查询 |
| 写入速度 | < 1ms(内存版) | 50-200ms(含 Embedding 计算) |
| 更新成本 | O(1) 直接覆盖 | O(n) 需重新计算向量 |
| 适用场景 | 用户档案、偏好设置、配置信息 | 对话摘要、知识片段、模糊回忆 |
生产环境最佳实践是两者组合使用------BaseStore 存结构化档案,向量数据库存语义记忆。
6.3 实战:基于 PostgresStore 的跨线程记忆
这是最复杂的场景:用户 Alice 今天跟 Agent 聊天,关掉浏览器,第二天重新打开(新的 thread_id),Agent 依然记得她是谁。
python
import os
import time
from typing import Annotated
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
from langchain.agents import AgentState, create_agent
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.store.postgres import PostgresStore
from langgraph.store.base import BaseStore
from langgraph.prebuilt import InjectedStore, InjectedState
from langgraph.types import Command
from psycopg_pool import ConnectionPool
from pydantic import BaseModel, Field
load_dotenv(override=True)
# ==================== 数据库配置 ====================
DB_URI = os.getenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/agent_db")
# ==================== 模型 ====================
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# ==================== 自定义 State ====================
class CrossThreadState(AgentState):
user_id: str # 跨线程记忆的关键标识
# ==================== 结构化输出模型 ====================
class UserInfo(BaseModel):
"""从文本中提取的用户信息"""
user_name: str = Field(description="用户的名字")
additional_info: str = Field(description="关于用户的其他信息")
# ==================== 记忆工具 ====================
@tool
def remember_info(
info: str,
state: Annotated[dict, InjectedState()],
store: Annotated[BaseStore, InjectedStore()],
) -> str:
"""将用户信息存入跨线程长期记忆。
当用户告诉你他的名字、职业、偏好等信息时调用。
Args:
info: 要记忆的信息
"""
# 用 LLM 提取结构化信息
structured_llm = model.with_structured_output(UserInfo)
try:
extracted = structured_llm.invoke(f"从以下文本提取用户名和信息:{info}")
user_id = extracted.user_name.lower() if extracted.user_name else state.get("user_id", "unknown")
full_info = f"{extracted.user_name}: {extracted.additional_info}"
except Exception:
user_id = state.get("user_id", "unknown")
full_info = info
# 存入 BaseStore(namespace 设计:按用户 ID 隔离)
namespace = (user_id, "profile")
import uuid
memory_id = str(uuid.uuid4())
store.put(namespace, memory_id, {
"info": full_info,
"timestamp": "2025-01-01", # 实际项目中用 datetime.now().isoformat()
"source": "user_input",
})
return Command(update={
"messages": [ToolMessage(content=f"已记住:{full_info}", tool_call_id=info)],
})
@tool
def recall_info(
query: str,
state: Annotated[dict, InjectedState()],
store: Annotated[BaseStore, InjectedStore()],
) -> str:
"""从跨线程记忆中检索用户信息。
当用户问"你还记得我吗"或类似问题时调用。
Args:
query: 查询关键词
"""
user_id = state.get("user_id")
if not user_id:
return "无法确定用户身份。"
# 搜索该用户的所有记忆
namespace_prefix = (user_id,)
memories = store.search(namespace_prefix, limit=20)
if not memories:
return "没有找到相关记忆。"
results = []
for item in memories:
info = item.value.get("info", "未知")
results.append(f"- {info}")
return f"找到 {len(results)} 条记忆:\n" + "\n".join(results)
# ==================== 创建 Agent ====================
with ConnectionPool(
conninfo=DB_URI,
max_size=20,
kwargs={"autocommit": True}
) as pool:
checkpointer = PostgresSaver(pool)
store = PostgresStore(pool)
checkpointer.setup()
store.setup()
agent = create_agent(
model=model,
tools=[remember_info, recall_info],
state_schema=CrossThreadState,
system_prompt="""你是一个具备跨线程记忆的助手。
规则:
1. 用户告诉你关于他们的信息时,调用 remember_info 保存
2. 用户问你还记得什么时,调用 recall_info 检索
3. 记忆是跨会话持久的,即使换了新对话也能记住""",
store=store, # ← 注入 BaseStore
checkpointer=checkpointer,
)
# ==================== 测试跨线程记忆 ====================
# Alice 第一次对话
print("--- Alice 首次对话 ---")
config_a1 = {"configurable": {"thread_id": "alice_session_001"}}
for chunk in agent.stream(
{"messages": [HumanMessage(content="我是 Alice,一名 Python 工程师,喜欢深度学习。")],
"user_id": "alice"},
config=config_a1,
stream_mode="values"
):
pass
print(f"Agent: {chunk['messages'][-1].content}")
# Alice 第二天重新打开(新 thread_id)
print("\n--- Alice 第二天(新 Session)---")
time.sleep(1)
config_a2 = {"configurable": {"thread_id": "alice_session_002"}}
for chunk in agent.stream(
{"messages": [HumanMessage(content="你还记得我是谁吗?")],
"user_id": "alice"},
config=config_a2,
stream_mode="values"
):
pass
print(f"Agent: {chunk['messages'][-1].content}")
# 预期:Agent 调用 recall_info,从 PostgresStore 中找到 Alice 的信息
# Bob 的对话(验证记忆隔离)
print("\n--- Bob 的对话 ---")
config_b = {"configurable": {"thread_id": "bob_session_001"}}
for chunk in agent.stream(
{"messages": [HumanMessage(content("我是 Bob,产品经理。")],
"user_id": "bob"},
config=config_b,
stream_mode="values"
):
pass
print(f"Agent: {chunk['messages'][-1].content}")
关键设计: InjectedStore() 和 InjectedState() 这两个注解让 Pydantic 在生成工具 Schema 时跳过这些参数------模型看不到 store 和 state,只知道 info 和 query。但运行时 LangGraph 会自动注入真实对象。这是长期记忆工具的标准写法。
七、生产环境最佳实践
7.1 企业级组合架构
┌────────────────────────────────────────────────────┐
│ Agent 运行时 │
├────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ 短期记忆层 │ │ 长期记忆层 │ │
│ │ │ │ │ │
│ │ PostgresSaver │ │ BaseStore(KV) │ │
│ │ ├─ thread_id 隔离│ │ ├─ user_id 命名空间 │ │
│ │ └─ 完整对话历史 │ │ └─ 用户档案、偏好 │ │
│ │ │ │ │ │
│ │ │ │ 向量数据库(Semantic) │ │
│ │ │ │ ├─ 对话摘要 │ │
│ │ │ │ └─ 语义模糊检索 │ │
│ └─────────────────┘ └──────────────────────┘ │
│ │
└────────────────────────────────────────────────────┘
身份标识 :除了 thread_id,必须传入 user_id。前者管短期记忆,后者管长期记忆。
7.2 记忆生命周期管理
| 策略 | 说明 | 实现方式 |
|---|---|---|
| TTL 过期 | 自动清理过期记忆 | BaseStore put() 时设置 TTL |
| 手动清理 | 提供管理接口删除无用记忆 | store.delete(namespace, key) |
| 压缩归档 | 对历史记忆进行摘要压缩 | 定期用 LLM 把多条记忆压成一条 |
| 容量限制 | 防止记忆无限增长 | 每个 namespace 限制最大条目数 |
7.3 性能优化
- 索引优化:为常用 namespace 建立索引
- 缓存策略:热数据(最近活跃用户)放内存,冷数据放数据库
- 批量操作 :批量
put()减少 I/O 开销 - 异步加载:长期记忆检索使用异步调用,不阻塞主循环
7.4 安全注意事项
- 不同用户的记忆必须通过 namespace 严格隔离
- 长期记忆中的敏感信息(PII)需要加密或脱敏
- 记忆写入要有审计日志
- 设置合理的记忆容量上限,防止滥用
核心要点总结
- 分界线是生命周期,不是存储介质。 短期记忆跟着
thread_id走,长期记忆跟着user_id走。PostgresSaver存在数据库里也是短期记忆。 - Checkpointer 是短期记忆的灵魂。 不加它,Agent 无状态;加了它,同一
thread_id自动携带上下文。 - 上下文裁剪不能省。
trim_messages配合精确的tiktoken计数,在 State 中保留完整历史、传给 LLM 时只传最近的。start_on="human"防止裁剪后对话结构异常。 - 自定义 State 用 TypedDict。 LangChain 1.0 强制要求,不再支持 Pydantic。通过
ToolRuntime和InjectedState/InjectedStore让工具读写状态。 - 长期记忆两条路。 向量数据库做语义检索(模糊、灵活),BaseStore 做结构化 KV 存储(精确、高效)。生产环境建议组合使用。
- 安全隔离是底线。 namespace 隔离、PII 脱敏、审计日志、容量限制,缺一不可。
参考资料
| 主题 | 官方文档 |
|---|---|
| 短期记忆、checkpointer、上下文压缩 | Short-term memory |
| 长期记忆、store、namespace/key 结构 | Long-term memory |
| Agent 定义、create_agent 参数 | Agents |
| 工具定义、ToolRuntime | Runtime |
| 预置中间件(摘要、上下文裁剪) | Prebuilt middleware |