LangChain 09 - 上下文与记忆学习导航与实战
LangChain 09 - 上下文与记忆学习导航与实战
模块 1:你只需要记住这些
1. 为什么需要记忆
大模型本身是无状态的——每次调用 agent.invoke() 都是全新的开始,不记得之前的对话。
举例:
- 情况1:第一轮告诉 Agent"我叫小明",第二轮问"我叫什么"→ Agent 不知道
- 情况2:如果把前一轮的对话历史也传入 messages 列表 → Agent 能答出来
结论:Agent 不会自动记住上下文,需要额外的记忆模块保存历史交互信息。
2. 上下文工程
记忆(Memory):专门负责"存储历史交互信息"的组件,核心作用是「保存上下文」和「提供上下文」。
上下文工程(Context Engineering):负责"合理组织"这些记忆和任务信息,让 LLM 的响应更连贯、更贴合需求。
LangGraph 提供了三种管理上下文的方法,结合了可变性和生命周期维度:
| 上下文类型 | 描述 | 可变性 | 生命周期 | 访问方法 |
|---|---|---|---|---|
| 动态运行时上下文 | 在单次运行中会演变的可变数据 | 动态 | 单次运行 | LangGraph state 对象 |
| 动态跨会话上下文 | 在对话间共享的持久数据(用户偏好、历史洞察、知识条目) | 动态 | 跨对话 | LangGraph store 对象 |
| 静态运行时上下文 | 在启动时传入的用户元数据、工具、数据库连接 | 静态 | 单次运行 | LangGraph context 对象 |
3. 记忆的分类
LangChain 的记忆分为短期记忆和长期记忆:
短期记忆(Short-term memory / 会话级记忆 / thread-scoped memory):
- 作用范围:单个对话线程(Thread)内
- 一旦开启新对话(更换 thread_id),记忆即消失
- 对应:state 对象
长期记忆(Long-term memory / 跨会话级记忆):
- 在会话间存储用户特定或应用级数据
- 在会话线程间共享
- 记忆范围是任意自定义命名空间,不仅仅是单一线程 ID
- 对应:store 对象
版本演进:
- LangChain v0.x:通过专用的
xxxMemory类管理记忆 - LangChain v1.x:Agent 构建在 LangGraph 之上,通过 state 和 store 构建记忆系统,使用更简单、功能更统一
4. 短期记忆三要素
LangChain 1.x 的短期记忆是三者的组合:
短期记忆 = State(会话内部状态)+ Checkpointer(持久化机制)+ Thread ID(会话作用域)
| 要素 | 作用 |
|---|---|
| State | 默认存储历史消息列表 messages,通过 State 管理历史消息 |
| Checkpointer | 负责将 State 作为检查点持久化保存,检查点是某个时刻的 State 快照 |
| Thread ID | 用于唯一标识 State,LangChain 运行时按照 thread_id 读写 State 快照 |
类比:就像玩 RPG 游戏时的"自动存档"——你不需要手动保存,系统在关键节点自动记录,下次进入游戏随时可以从上次的存档点继续。
5. InMemorySaver — 基于内存的持久化器
最便捷的使用方式,适合快速测试或调试。
使用步骤:
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
from langchain.messages import HumanMessage
# 第1步:初始化记忆引擎
checkpointer = InMemorySaver()
# 第2步:绑定 Agent
agent = create_agent(
model=model,
tools=[],
checkpointer=checkpointer # 添加内存管理
)
# 第3步:设定会话 ID
config = {
"configurable": {
"thread_id": "1"
}
}
# 第1轮对话
response1 = agent.invoke(
{"messages": [HumanMessage("我叫张三")]},
config=config
)
# 第2轮对话(同一个 thread_id,记得!)
response2 = agent.invoke(
{"messages": [HumanMessage("我叫什么?")]},
config=config
)
# Agent 能回答:"你叫张三"
关键三步:
- 初始化记忆引擎:
checkpointer = InMemorySaver() - 绑定 Agent:在
create_agent时传入checkpointer - 设定会话 ID:通过
config = {"configurable": {"thread_id": "1"}}指定线程标识
6. thread_id 的作用 — 会话隔离
同一个 thread_id 共享记忆,不同 thread_id 完全隔离。
# 会话 1
config1 = {"configurable": {"thread_id": "1"}}
agent.invoke({"messages": [HumanMessage("我叫张三")]}, config=config1)
agent.invoke({"messages": [HumanMessage("我叫什么?")]}, config=config1) # 记得!
# 会话 2(完全独立)
config2 = {"configurable": {"thread_id": "2"}}
agent.invoke({"messages": [HumanMessage("你还记得我叫什么名字么?")]}, config=config2)
# Agent 不知道,因为会话 2 和会话 1 记忆空间隔离
生产场景:
- 多用户聊天:不同
thread_id= 不同用户会话 - 同一用户的不同任务:不同
thread_id= 不同任务上下文
7. 查看和更新线程状态
查看当前状态:
from rich import print as rprint
latest_state = agent.get_state(config)
rprint(latest_state)
State 快照包含完整的信息:消息列表、下一步节点(next)、配置(config)、元数据(metadata)、检查点 ID 等。
更新线程 ID:使用新的 thread_id 会重新开启对话,不会加载之前的记忆。
8. Checkpointer 工作原理
当你调用 agent.invoke() 时,checkpointer 自动执行四步:
① 读取之前的历史
② 追加新消息
③ 调用模型(传入完整历史)
④ 保存新的历史
关键:你只需要传新消息,checkpointer 会自动管理历史。
# 你只需要传新消息
agent.invoke(
{"messages": [{"role": "user", "content": "新问题"}]},
config
)
# checkpointer 自动:
# 1. 读取 thread_id 对应的历史消息
# 2. 追加新的 HumanMessage
# 3. 调用模型(传入完整历史)
# 4. 保存 AIMessage 到历史
9. 常见问题排查
问题1:为什么 Agent 不记得?
检查三个条件:
- 是否添加了
checkpointer=InMemorySaver()? - 是否传入了
config参数? - 两次调用的
thread_id是否相同?
# ❌ 错误:没有 checkpointer
agent = create_agent(model=model, tools=[])
# ❌ 错误:没有 config
agent = create_agent(model=model, tools=[], checkpointer=InMemorySaver())
agent.invoke({...}) # 不会记住
# ❌ 错误:thread_id 不同
agent.invoke({...}, config={"configurable": {"thread_id": "1"}})
agent.invoke({...}, config={"configurable": {"thread_id": "2"}}) # 不同会话
# ✅ 正确
agent = create_agent(model=model, tools=[], checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
agent.invoke({...}, config)
agent.invoke({...}, config) # 记得!
问题2:InMemorySaver 会丢失数据吗?
会!InMemorySaver 只保存在内存中:
- 同一进程内有效(不支持跨进程共享)
- 程序重启后丢失
- 不同进程无法共享
解决方案:使用持久化存储(SQLite、PostgreSQL)
问题3:内存会无限增长吗?
会!默认情况下,InMemorySaver 会保存所有消息。问题包括:
- 消息越来越多,token 消耗增加,可能超过模型限制
- 响应速度变慢、成本增加
解决方案:上下文管理(消息裁剪、消息删除、消息摘要)
问题4:如何清空某个会话的历史?
InMemorySaver 没有提供删除 API。临时方案:使用新的 thread_id 或重新创建 Agent。
10. PostgresSaver — 基于外部存储介质的持久化器
生产环境要用持久化的外部存储介质,如 PostgreSQL。
使用步骤:
from langgraph.checkpoint.postgres import PostgresSaver
DB_URL = "postgresql://langchain_user:abcd1234@118.195.128.47:5432/langchain_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 初始化 PostgreSQL 数据库(首次运行创建表)
checkpointer.setup()
agent = create_agent(
model=model,
checkpointer=checkpointer
)
config = {"configurable": {"thread_id": "1"}}
response1 = agent.invoke(
{"messages": [HumanMessage("你好,我是老王")]},
config=config
)
response2 = agent.invoke(
{"messages": [HumanMessage("你好,我是谁?")]},
config=config
)
# Agent 能回答:"你是老王"
关键点:
checkpointer.setup()首次运行创建必要的表,重复执行不会重新建表(底层是CREATE IF NOT EXISTS)- 必须在
with语句块中使用,确保连接正确管理 - 需要额外依赖:
pip install langgraph-checkpoint-postgres
PostgreSQL 表结构(setup() 创建四张表):
| 表名 | 说明 |
|---|---|
checkpoints |
主表,存每个 thread 在某个时刻的 checkpoint 快照 |
checkpoint_blobs |
存不适合直接内联进 checkpoints 表的较复杂 channel 值 |
checkpoint_writes |
存中间写入 / pending writes,不是最终完整 checkpoint |
checkpoint_migrations |
迁移版本表,非业务数据表 |
11. InMemorySaver vs PostgresSaver 对比
核心区别:进程结束后状态是否保留。
| 特性 | InMemorySaver | PostgresSaver |
|---|---|---|
| 存储介质 | 内存 | PostgreSQL 数据库 |
| 进程结束后 | 数据丢失 | 数据保留 |
| 重建 Saver() 后 | 历史状态丢失 | 历史状态可通过 thread_id 恢复 |
| 适用场景 | 测试、调试 | 生产环境 |
| 跨进程共享 | 不支持 | 支持 |
验证实验:
# 举例1:基于内存存储
# 每次运行创建新的 InMemorySaver(),历史 State 被丢弃
# → 三次 invoke 的输出完全相同(每次从头开始)
# 举例2:基于外部存储器存储
# 每次运行创建新的 PostgresSaver(),但连接同一个数据库
# → 只要 thread_id 一致,历史状态可以和当前调用串联起来(状态是累积的)
结论:即便重新创建 Saver(),只要 thread_id 一致,PostgresSaver 的历史状态就可以通过数据库加载恢复。
12. 记忆治理策略(上下文管理)
随着对话进行,历史消息不断累积,带来三大挑战:
- LLM 的上下文窗口有限,完整历史可能无法装入
- 即便窗口够大,模型会被陈旧或离题的内容"分散注意力"
- 高昂的 token 花费
需要对上下文进行管理:消息裁剪、消息删除、消息摘要。
12.1 消息裁剪(before_model)
目标:调用模型前裁剪上下文,控制 token 用量。
策略:保留系统初始消息和最近若干消息。
时机:在 before_model 钩子中执行。
from langchain_core.messages import HumanMessage
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
from langchain_core.runnables import RunnableConfig
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
first_msg = messages[0]
# 奇数取后4条,偶数取后3条,确保截取的是完整的对话对
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(),
)
关键点:
RemoveMessage(id=REMOVE_ALL_MESSAGES)是一个特殊指令,表示清空所有消息- 然后放回裁剪后的消息列表(第一条 + 最近几条)
- 裁剪发生在模型调用前,只影响模型看到的上下文
- 奇偶判断确保截取的是完整的 Human-AI 对话对
裁剪效果分析(4 次 invoke):
| 步骤 | 触发前消息数 | 是否触发裁剪 | 传给 LLM 的消息 |
|---|---|---|---|
| 1 | 1 | 不触发 | [H1] |
| 2 | 3 | 不触发 | [H1, A1, H2] |
| 3 | 5 | 触发!取后4条 | [H1(第一条), A1, H2, A2, H3(后4条)] |
| 4 | 7 | 触发!取后4条 | [H1(第一条), A2, H3, A3, H4(后4条)] |
12.2 消息删除(after_model)
目标:模型调用完成后将某些消息从列表中永久移除。
策略:保持固定数量的消息,超出阈值的最早消息被删除。
时机:在 after_model 钩子中执行。
from langchain.messages import RemoveMessage
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import after_model
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.runtime import Runtime
from langchain_core.runnables import RunnableConfig
@after_model
def delete_old_messages(state: AgentState, runtime: Runtime) -> dict | None:
messages = state["messages"]
# 保持最近的 5 条消息
if len(messages) > 5:
to_delete = len(messages) - 5
return {"messages": [RemoveMessage(id=m.id) for m in messages[:to_delete]]}
return None
agent = create_agent(
model=model,
middleware=[delete_old_messages],
checkpointer=InMemorySaver()
)
关键点:
- 消息裁剪强调"模型调用前裁剪",消息删除强调"模型调用后永久更改状态"
RemoveMessage(id=m.id)按 ID 精准删除特定消息- 删除的是最老的消息,保持最新的固定条数
消息列表数量变化分析:
第1轮:2条(不触发)→ 记住"你是老王"
第2轮:4条(不触发)→ 记住"你是老王、我是小王"
第3轮:6条(触发!删1条)→ 删除最早的"你好,我是老王"
第4轮:8条(触发!删3条)→ 保持最后5条
12.3 RemoveMessage 的底层机制
当你在中间件里返回 RemoveMessage(id="1") 时,框架不会直接从数组里删掉消息。它的底层处理是:
- 追加"墓碑"标记:框架把
RemoveMessage作为一条新记录追加到状态历史中 - 下次读取时过滤:当 LangGraph 下次从 checkpointer 读取状态时,会扫描所有墓碑标记,被标记的消息不会被加载到消息列表中
这就像 Git 的版本控制——不是物理删除,而是标记删除。历史记录仍然存在于 checkpointer 中,但逻辑上被"隐藏"了。
13. 长期记忆概述
长期记忆(Long-term memory)在会话间存储用户特定或应用级数据,基于 store 对象持久化数据。
核心概念:
- store:长期记忆对象,跨会话持久化的数据
- namespace(命名空间):用元组(tuple)表示的层级路径,组织数据
Namespace 层级结构示例:
store
├─ namespace = ("users", "Alice", "memories")
│ ├─ key = "profile"
│ │ value = {"name": "李四", "city": "上海", "preferences": ["简洁风格"]}
│ ├─ key = "travel_preference"
│ │ value = {"favorite_cities": ["北京", "杭州"], "budget_level": "medium"}
│ └─ key = "writing_style"
│ value = {"tone": "professional", "language": "zh-CN"}
│
├─ namespace = ("users", "user_2", "memories")
│ ├─ key = "profile"
│ │ value = {"name": "李四", "city": "深圳"}
│ └─ key = "product_interest"
│ value = {"topics": ["AI Agent", "RAG", "Workflow"]}
│
└─ namespace = ("users", "user_1", "thread_3", "artifacts")
├─ key = "draft_v1"
│ value = {"type": "html", "content": "..."}
└─ key = "final_scheme"
value = {"palette": ["#F5F1E8", "#1F3A5F"]}
与短期记忆的区别:
- 短期记忆:同一
thread_id内共享,不同thread_id隔离 - 长期记忆:全局共享,不受
thread_id限制,通过 namespace 组织
14. Store 基础 API
LangChain 1.2.x 的长期记忆基于 store 持久化数据,相关 API:
| API | 作用 |
|---|---|
put() |
写入数据 |
get() |
读取数据 |
search() |
检索数据 |
14.1 put() — 写入 API
def put(
self,
namespace: tuple[str, ...], # 文档所在的层级路径
key: str, # 该路径下的唯一键
value: dict[str, Any], # 要保存的 JSON-like 字典
index: Literal[False] | list[str] | None = None, # 控制语义检索索引
*,
ttl: float | None = NOT_PROVIDED, # 可选,过期时间
) -> None
参数说明:
namespace:文档所在的层级路径(元组)key:该路径下的唯一键value:要保存的 JSON-like 字典index:控制语义检索索引None(默认):使用 store 初始化时配置的索引配置False:不为该 item 建立语义索引list[str]:只对指定字段路径建索引
ttl:可选,过期时间(是否支持取决于具体 store 实现)
示例:
store.put(
("users", "alice", "memories"), # namespace
"pref_food", # key
{"category": "food", "text": "Alice likes sushi"} # value
)
14.2 get() — 读取 API
def get(
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: bool | None = None, # 是否刷新 TTL
) -> Item | None
返回值:不是单纯的 value,而是完整的 Item 对象。
item = store.get(("users", "alice", "memories"), "pref_food")
if item is not None:
print(item.value)
# {'category': 'food', 'text': 'Alice likes sushi'}
Item 对象包含字段:
namespace:命名空间key:键value:值created_at:创建时间updated_at:更新时间
14.3 InMemoryStore vs PostgresStore 的差异
InMemoryStore:
- 每次
put都会创建一个新的 Item 对象,无论 namespace 和 key 是否相同 created_at和updated_at始终一致(因为是新建而非更新)
# InMemoryStore
store.put(("users",), "user-1", {"name": "小蓝"})
# Item(namespace=['users'], key='user-1', value={'name': '小蓝'},
# created_at='2026-06-11T15:20:52...', updated_at='2026-06-11T15:20:52...')
store.put(("users",), "user-1", {"name": "小红"})
# Item(namespace=['users'], key='user-1', value={'name': '小红'},
# created_at='2026-06-11T15:23:11...', updated_at='2026-06-11T15:23:11...')
# created_at 变了!因为是新建而非更新
PostgresStore:
- 更改数据的逻辑是 update 而非覆盖
created_at固定为 Item 创建时间,updated_at为更新时间,二者可能不同
# PostgresStore
with PostgresStore.from_conn_string(DB_URL) as store:
store.setup()
store.put(("users",), "user-11", {"name": "小蓝"})
# created_at='2026-06-11T23:24:31...', updated_at='2026-06-11T23:24:31...'
store.put(("users",), "user-11", {"name": "小红"})
# created_at='2026-06-11T23:24:31...'(不变)
# updated_at='2026-06-11T23:26:11...'(变了)
14.4 search() — 检索 API
def search(
self,
namespace_prefix: tuple[str, ...], # 命名空间前缀
/,
*,
query: str | None = None, # 语义检索查询
filter: dict[str, Any] | None = None, # 结构化过滤条件
limit: int = 10, # 最大返回条数
offset: int = 0, # 跳过的条数
refresh_ttl: bool | None = None,
) -> list[SearchItem]
三种检索方式:
方式1:按 namespace 前缀搜索
# 搜索 ("users",) 下所有数据
for item in store.search(("users",)):
print(item)
# 搜索 ("users", "Alice") 下的数据(更精确的前缀)
for item in store.search(("users", "Alice")):
print(item)
方式2:按 filter 结构化过滤
filter 是 value 中的键值对组合,用于精确匹配。
# 过滤 food 为 "紫光园奶皮子酸奶" 的记录
for item in store.search(("users",), filter={"food": "紫光园奶皮子酸奶"}):
print(item)
# 过滤 sports 为 "跑步" 的记录
for item in store.search(("users",), filter={"sports": "跑步"}):
print(item)
# 过滤 course 为 "数字电路与模拟电路" 的记录
for item in store.search(("users",), filter={"course": "数字电路与模拟电路"}):
print(item)
方式3:按 query 语义检索
需要 store 初始化时配置 IndexConfig(包含嵌入函数)。
# 语义搜索:查找与 "数电模电" 最相似的数据
for item in store.search(("users",), query="数电模电"):
print(item)
# 返回结果按 score(相似度分数)降序排列
15. IndexConfig — 语义检索配置
class IndexConfig(TypedDict, total=False):
dims: int # 输出向量维度
embed: Embeddings | EmbeddingsFunc | str # 嵌入函数或模型
fields: list[str] | None # 用于计算向量的属性列表
fields 可取值:
["$"]:将 value 作为整体嵌入["field1", "field2"]:单独指定某个一级字段["parent.child"]:从嵌套 JSON 对象中获取子字段["array[*].field"]:从 JSON 数组的每个对象中获取子字段- 上述形式可同时出现,每个元素都会生成一个嵌入向量
举例1:自定义嵌入函数
from langgraph.store.memory import InMemoryStore
# 自定义嵌入函数(返回固定向量,仅用于演示)
def embed(text: list[str]) -> list[list[float]]:
return [[1.0] * 6 for _ in range(len(text))]
index_config = {
"embed": embed,
"dims": 6,
"fields": ["$", "course"] # 对整体和 course 字段分别建索引
}
store = InMemoryStore(index=index_config)
举例2:使用嵌入模型
from langgraph.store.memory import InMemoryStore
from langchain.embeddings import init_embeddings
import os
embedding_model = init_embeddings(
model="openai:text-embedding-3-large",
api_key=os.getenv("CLOSEAI_API_KEY"),
base_url=os.getenv("CLOSEAI_BASE_URL"),
)
index_config = {
"embed": embedding_model,
"dims": 3072, # text-embedding-3-large 的嵌入维度
"fields": ["$"]
}
store = InMemoryStore(index=index_config)
查看嵌入向量:
from pprint import pprint
pprint(store._vectors)
# 结构:{namespace: {key: {field: [向量]}}}
16. 在 Agent 中访问长期记忆
可以在工具或中间件中访问长期记忆。
16.1 在工具中访问
通过 ToolRuntime 的 store 属性访问长期记忆。
from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent, AgentState
from langgraph.store.memory import InMemoryStore
from typing import NotRequired
store = InMemoryStore()
# 扩展 AgentState,增加 user_id 字段
class CustomState(AgentState):
user_id: NotRequired[str]
@tool(parse_docstring=True)
def save_user_info(name: str, runtime: ToolRuntime) -> str:
"""将用户信息保存在长期记忆中"""
runtime.store.put(("users",), runtime.state["user_id"], {"name": name})
return "saved"
@tool(parse_docstring=True)
def get_user_info(runtime: ToolRuntime) -> str:
"""从长期记忆中读取用户信息"""
item = runtime.store.get(("users",), runtime.state["user_id"])
return str(item.value) if item else "unknown"
agent = create_agent(
model=model,
tools=[save_user_info, get_user_info],
store=store,
system_prompt="用户提及个人信息时及时记录,用户询问个人信息时尝试用工具检索",
state_schema=CustomState,
)
# 第一个会话(线程):写入长期记忆
response1 = agent.invoke({
"messages": [HumanMessage("你好,很高兴认识你,我是小花")],
"user_id": "user-1"
})
# 第二个会话(线程):读取长期记忆(不同 thread_id,但共享 store)
response2 = agent.invoke({
"messages": [HumanMessage("我是谁")],
"user_id": "user-1"
})
# Agent 能回答:"你是小花"
关键点:
- 两次 invoke 没有通过 config 串联,是两个独立的会话(不同 thread_id)
- 但第二个会话可以访问第一个会话写入长期记忆的内容
CustomState扩展了AgentState,增加user_id字段,让 Agent 运行时知道当前用户runtime.store是运行时自动注入的,无需手动传递
16.2 在中间件中访问
Node-style hooks(如 before_model):
# before_model 的 runtime 参数包含 store
def before_model(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
# 通过 runtime.store 访问长期记忆
item = runtime.store.get(("users",), user_id)
...
Runtime 定义(包含 store 字段):
@dataclass
class Runtime(Generic[ContextT]):
context: ContextT = field(default=None) # 静态上下文
store: BaseStore | None = field(default=None) # 长期记忆存储
stream_writer: StreamWriter = ... # 自定义流写入器
previous: Any = field(default=None) # 前次返回值
Wrap-style hooks(如 wrap_model_call、wrap_tool_call):
# wrap_model_call 通过 request.runtime.store 访问
def wrap_model_call(self, request: ModelRequest, handler):
store = request.runtime.store # 通过 request 访问 store
...
# wrap_tool_call 通过 request.runtime.store 访问
def wrap_tool_call(self, request: ToolCallRequest, handler):
store = request.runtime.store # 通过 request 访问 store
...
17. 何时写入记忆
官方介绍了两种写入记忆的方式:
方式1:在主流程里写(hot path)
用户发消息,AI 一边回答,一边决定要不要记下来。
| 优点 | 缺点 |
|---|---|
| 立即生效 | 增加延迟 |
| 下一轮马上能用 | 逻辑变复杂 |
| 用户可感知,透明 |
方式2:在后台写(background)
先回答用户,记忆整理放到后台异步做。
| 优点 | 缺点 |
|---|---|
| 主流程更快 | 不能立刻生效 |
| 记忆逻辑更独立 | 要决定多久整理一次 |
| 更适合批量整理 | 触发时机不好选 |
工程选择建议:
- 用户偏好、账号资料:可热路径写
- 对话摘要、经验沉淀、行为分析:更适合后台写
18. 静态运行时上下文(Static Runtime Context)
静态运行时上下文表示不可变的数据,如用户元数据、工具和传递给应用程序的数据库连接对象。
通常在运行开始时通过 invoke / stream 的 context 参数传入。
from dataclasses import dataclass
@dataclass
class UserContext:
username: str
agent = create_agent(
model="deepseek-chat",
tools=[get_weather, get_news],
store=store,
context_schema=UserContext, # 指定上下文类型
)
# 通过 context 参数传入静态上下文
response = agent.invoke(
{"messages": ["你好啊,今天北京天气如何?"]},
context=UserContext(username="Ada Lovelace")
)
关键点:
context_schema指定上下文对象的类型context参数是运行时静态配置,每次 invoke 互相独立- 在中间件和工具中通过
runtime.context/request.runtime.context访问
19. 综合案例:中间件 + Store + Context
19.1 工具权限过滤 + 额度校验
@dataclass
class UserContext:
username: str
class CheckCredit(AgentMiddleware):
"""在模型调用前,根据长期记忆中的用户配置过滤可用工具"""
def wrap_model_call(self, request: ModelRequest, handler):
context = request.runtime.context
store = request.runtime.store
value = store.get(namespace, context.username).value
tools = []
for tool in request.tools:
if value[tool.name] == "yes":
tools.append(tool)
else:
logger.warning(f"{context.username} 无权调用 {tool.name}")
request = request.override(tools=tools) # 仅本次生效
return handler(request)
class ToolGuard(AgentMiddleware):
"""在工具调用前,校验额度并更新长期记忆"""
def wrap_tool_call(self, request: ToolCallRequest, handler):
store = request.runtime.store
context = request.runtime.context
username = context.username
value = store.get(namespace, username).value
tool_name = request.tool.name
credits_cost = CREDIT_MAP.get(tool_name)
credits_left = value["tokens_left"]
if credits_left < credits_cost:
logger.warning(f"{username} 额度不足")
return Command(update={
"messages": [ToolMessage(
content="额度不足,无法调用工具",
tool_call_id=request.runtime.tool_call_id
)]
})
# 更新剩余额度(写入长期记忆)
credits_left -= credits_cost
value["tokens_left"] = credits_left
store.put(namespace, username, value)
return handler(request)
agent = create_agent(
model="deepseek-chat",
middleware=[CheckCredit(), ToolGuard()],
tools=[get_weather, get_news],
store=store,
context_schema=UserContext
)
19.2 dynamic_prompt — 动态系统提示词
@dynamic_prompt 是一个便捷中间件,用于动态更改系统提示词。
from langchain.agents.middleware import dynamic_prompt, ModelRequest
@dynamic_prompt
def personalized_prompt(request: ModelRequest) -> str:
username = request.runtime.context.username
store = request.runtime.store
preferences = store.get(namespace, username).value["chat_preferences"]
custom_prompt = f"# 用户偏好\n{'\n'.join(preferences)}"
return custom_prompt
agent = create_agent(
model="deepseek-chat",
middleware=[personalized_prompt],
store=store,
context_schema=UserContext
)
# 不同用户得到不同的系统提示词
response1 = agent.invoke(
{"messages": ["为什么花儿这样红?"]},
context=UserContext(username="Ada Lovelace") # 偏好:简洁
)
# → 简短回答
response2 = agent.invoke(
{"messages": ["为什么花儿这样红?"]},
context=UserContext(username="Blackwell") # 偏好:长篇大论
)
# → 详细回答
关键点:dynamic_prompt 动态更新本次调用的系统提示词,不会更改消息列表。
19.3 工具中访问长期记忆(完整案例)
结合 Pydantic 模型 + ToolRuntime + Context + Store:
from pydantic import BaseModel, Field
from typing import List, Any
from langgraph.prebuilt.tool_node import ToolRuntime
class UserInfo(BaseModel):
username: str = Field(description="用户名", default="unknown")
age: int = Field(description="年龄", default=0)
hobbies: List[str] = Field(description="兴趣爱好", default_factory=list)
@tool(parse_docstring=True)
def read_user_info(runtime: ToolRuntime[UserContext, Any]) -> UserInfo | str:
"""读取用户信息"""
user_id = runtime.context.user_id
item = runtime.store.get(namespace, user_id)
if item:
return UserInfo(**item.value)
return ''
@tool(parse_docstring=True)
def write_user_info(user_info: UserInfo, runtime: ToolRuntime[UserContext, Any]) -> bool:
"""将用户信息写入长期记忆"""
user_id = runtime.context.user_id
runtime.store.put(namespace, user_id, user_info.model_dump())
return True
agent = create_agent(
model="deepseek-chat",
tools=[read_user_info, write_user_info],
store=store,
checkpointer=InMemorySaver(),
context_schema=UserContext
)
# thread_1:第一轮对话(写入记忆)
agent.invoke(
{"messages": [SystemMessage("..."), HumanMessage("你好,我是韩立,我喜欢修仙")]},
context=UserContext(user_id='user_1'),
config=config1
)
# thread_1:第二轮对话(更新记忆,先读后写)
agent.invoke(
{"messages": ["我今年两百岁了,我喜欢跑步"]},
context=UserContext(user_id='user_1'),
config=config1
)
# thread_2:不同线程,但通过长期记忆共享信息
agent.invoke(
{"messages": ["你还记得我吗?"]},
context=UserContext(user_id='user_1'),
config=config2
)
# Agent 回答:"你是韩立,200岁,喜欢修仙和跑步"
ToolRuntime 泛型说明:
ToolRuntime[UserContext, Any]:第一个泛型是 Context 类型,第二个是 State 类型- 如果不显式指定 Context 泛型,底层认为上下文对象为 None,invoke 时传 context 会抛出警告
模块 2:带教式理解
1. 为什么大模型是"无状态"的
大模型的 API 设计是"请求-响应"模式:你发一个请求,它返回一个响应,然后这次交互就结束了。模型服务端不会为你的下一次请求保留任何上下文。
这就像打电话给一个失忆的人——每次打电话他都不记得上次聊了什么。你必须每次把前情提要重新讲一遍。
Agent 也不例外:agent.invoke() 每次都是独立调用,如果你不把历史消息放进 messages 列表,Agent 就"不记得"之前说过什么。
2. 短期记忆为什么是"三要素"的组合
单独看每一个:
- 没有 State:消息列表没地方存——历史消息无处安放
- 没有 Checkpointer:State 只在内存中存活一次调用——跨调用无法保留
- 没有 Thread ID:多个用户的对话混在一起——无法区分谁的对话
三者缺一不可:State 负责存什么,Checkpointer 负责怎么存,Thread ID 负责区分谁是谁。
3. 为什么用 InMemorySaver 而不是直接传 messages
你确实可以每次手动把历史消息拼进 messages 列表传入(情况3的做法),但这样有几个问题:
- 手动维护:你需要自己记录哪些消息是历史的,哪些是新输入的
- 无法中断恢复:如果 Agent 执行到一半被中断,你无法恢复之前的状态
- 多用户管理:你需要自己维护每个用户的对话历史
Checkpointer 自动帮你做这些事——你只需要传新消息,它自动读取历史、追加新消息、调用模型、保存结果。就像 Git 的自动 commit——你只管写代码,它帮你管理版本。
4. InMemorySaver 重建后为什么状态丢失
InMemorySaver() 把数据存在 Python 进程的内存里。当你创建一个新的 InMemorySaver() 实例时,它是一个全新的空对象——和之前那个实例没有任何关系。
这就像你换了一个新笔记本——旧笔记本上的笔记不会自动转移过来。
PostgresSaver 不同:数据存在外部数据库里,无论你创建多少个 PostgresSaver 实例,只要连接同一个数据库,用同一个 thread_id,就能读到之前的数据。
5. 消息裁剪和消息删除有什么区别
| 对比项 | 消息裁剪(before_model) | 消息删除(after_model) |
|---|---|---|
| 执行时机 | 模型调用前 | 模型调用后 |
| 影响范围 | 只影响模型看到的上下文 | 永久更改状态中的消息列表 |
| 使用工具 | RemoveMessage(id=REMOVE_ALL_MESSAGES) 清空后重建 |
RemoveMessage(id=m.id) 按 ID 精准删除 |
| 适用场景 | 控制成本,保留系统消息和最近消息 | 明确要遗忘、清理、重置某些历史 |
裁剪是"临时遮罩"——模型看不到,但消息还在。删除是"物理清除"——消息从状态中移除(通过墓碑标记)。
6. RemoveMessage 为什么用"墓碑"而不是直接删除
直接从数组里删除消息会带来一个问题:checkpointer 的快照完整性被破坏。
LangGraph 的 checkpointer 是基于快照的——每次状态变化都会记录一个 checkpoint。如果直接物理删除消息,那之前的 checkpoint 引用的消息就不存在了,回溯历史时会出现不一致。
墓碑标记的做法是:不删除原始消息,而是追加一条"删除指令"。下次读取时,框架扫描所有墓碑标记,被标记的消息不会被加载。这样既实现了逻辑删除,又保持了 checkpoint 历史的完整性。
7. Namespace 为什么用元组而不是字符串
字符串路径(如 "users/alice/memories")有歧义:如果某个 key 里包含 /,解析就会出错。
元组 ("users", "alice", "memories") 是确定性的:每个元素就是一个层级,不会出现歧义。而且元组天然支持前缀匹配——("users", "alice") 是 ("users", "alice", "memories") 的前缀,所以 search(("users", "Alice")) 能找到该前缀下所有数据。
8. InMemoryStore 为什么 created_at 和 updated_at 一样
InMemoryStore 的更新逻辑是"覆盖"而非"更新"——每次 put 都创建一个全新的 Item 对象,旧对象被丢弃。所以 created_at 和 updated_at 始终是同一个时间。
PostgresStore 的更新逻辑是 SQL 的 UPDATE——如果 namespace+key 已存在,只更新 value 和 updated_at 字段,created_at 保持不变。这更符合直觉。
9. 为什么在工具中访问 Store 需要扩展 AgentState
runtime.store 是全局共享的,但 runtime.store.get(namespace, key) 需要知道当前用户的 key(如 user_id)。
如果不扩展 AgentState,Agent 就不知道"当前是谁在说话"。通过 CustomState(AgentState) 增加 user_id 字段,调用时传入 "user_id": "user-1",工具就能通过 runtime.state["user_id"] 获取当前用户 ID,从而在 Store 中定位正确的数据。
10. ToolRuntime 的泛型为什么重要
ToolRuntime 的签名是 ToolRuntime[ContextT, StateT]。
- 第一个泛型
ContextT:静态上下文对象的类型 - 第二个泛型
StateT:State 对象的类型
如果你不显式指定 ToolRuntime[UserContext, Any],LangChain 底层默认 ContextT=None。当你在 invoke 时传了 context=UserContext(...),框架会发现类型不匹配并抛出警告。
显式指定泛型不仅是为了类型提示,更是告诉框架"我会在运行时传入一个 UserContext 类型的上下文对象"。
这一章结束后,你应该能自己回答
1、为什么 Agent 需要记忆?大模型的什么特性导致了这个问题?
大模型本身是"无状态"的——每次调用 agent.invoke() 都是全新的开始,不记得之前的对话。Agent 也是基于大模型构建的,如果不把历史消息传入 messages 列表,Agent 就不知道之前说过什么。记忆模块的作用就是保存历史交互信息,让 LLM 在每次响应时都能"看到"之前的对话内容。
2、LangGraph 提供了哪三种管理上下文的方法?分别对应什么对象?
- 动态运行时上下文(state 对象):单次运行中演变的可变数据,生命周期为单次运行
- 动态跨会话上下文(store 对象):对话间共享的持久数据,生命周期为跨对话
- 静态运行时上下文(context 对象):启动时传入的不可变元数据,生命周期为单次运行
3、短期记忆的三要素是什么?各自的作用是什么?
State(会话内部状态):默认存储历史消息列表 messages。Checkpointer(持久化机制):将 State 作为检查点持久化保存。Thread ID(会话作用域):唯一标识 State,运行时按 thread_id 读写 State 快照。三者缺一不可——State 负责存什么,Checkpointer 负责怎么存,Thread ID 负责区分谁是谁。
4、InMemorySaver 和 PostgresSaver 的核心区别是什么?
InMemorySaver 将状态保存在内存中,进程结束或重建 Saver 时历史状态丢失。PostgresSaver 将状态保存在 PostgreSQL 数据库中,即便重新创建 Saver,只要 thread_id 一致,历史状态就可以通过数据库加载恢复。InMemorySaver 适合测试调试,PostgresSaver 适合生产环境。
5、消息裁剪和消息删除的区别是什么?分别用什么 Hook 实现?
消息裁剪在模型调用前(before_model)执行,只影响模型看到的上下文,使用 RemoveMessage(id=REMOVE_ALL_MESSAGES) 清空后重建消息列表。消息删除在模型调用后(after_model)执行,永久更改状态中的消息列表,使用 RemoveMessage(id=m.id) 按 ID 精准删除特定消息。
6、Store 的 search() 支持哪三种检索方式?
- 按 namespace 前缀搜索:传入 namespace_prefix 元组,返回该前缀下所有数据
- 按 filter 结构化过滤:传入 value 中的键值对,精确匹配符合条件的记录
- 按 query 语义检索:传入自然语言查询,通过向量相似度计算返回最相似的结果(需要配置 IndexConfig)
7、在 Agent 中有哪两种方式访问长期记忆?分别通过什么属性?
- 在工具中访问:通过
runtime.store访问(ToolRuntime 对象的 store 属性) - 在中间件中访问:Node-style hooks 通过
runtime.store访问,Wrap-style hooks 通过request.runtime.store访问
小测试
1. 短期记忆的 checkpointer 工作流程是什么?
调用 agent.invoke() 时,checkpointer 自动执行四步:① 读取之前的历史(通过 thread_id 从 checkpointer 加载)② 追加新消息 ③ 调用模型(传入完整历史)④ 保存新的历史(写入 checkpointer)。你只需要传新消息,checkpointer 自动管理历史。
2. RemoveMessage 的"墓碑"机制是什么?
当返回 RemoveMessage(id=“1”) 时,框架不会从数组里物理删除消息,而是把 RemoveMessage 作为一条新记录追加到状态历史中(墓碑标记)。下次读取状态时,框架扫描所有墓碑标记,被标记的消息不会被加载到消息列表中。这保持了 checkpoint 历史的完整性。
3. IndexConfig 的 fields 参数有哪些取值方式?
["$"]:将 value 作为整体嵌入。["field1", "field2"]:单独指定一级字段。["parent.child"]:从嵌套 JSON 中获取子字段。["array[*].field"]:从 JSON 数组每个对象中获取子字段。上述形式可同时出现,每个元素生成一个独立的嵌入向量。
4. InMemoryStore 和 PostgresStore 在更新数据时有什么差异?
InMemoryStore 每次 put 都创建新的 Item 对象(覆盖逻辑),created_at 和 updated_at 始终一致。PostgresStore 的更新逻辑是 UPDATE(更新逻辑),如果 namespace+key 已存在,只更新 value 和 updated_at,created_at 保持不变。
5. 何时写入记忆的"hot path"和"background"分别是什么?如何选择?
hot path(主流程写):用户发消息时 AI 一边回答一边记录,立即生效但增加延迟。background(后台写):先回答用户,记忆整理放到后台异步做,主流程更快但不能立刻生效。用户偏好、账号资料适合热路径写,对话摘要、经验沉淀、行为分析更适合后台写。









