LangChain:上下文与记忆
区分 State、检查点、Store 和运行时 Context,理解短期记忆、长期记忆与消息治理。
正文目录
记忆负责保留信息,上下文工程负责将所需信息提供给当前模型请求。本章介绍 State、检查点、Store 与运行时 Context 的职责及使用方式。
适用版本:LangChain 1.2.12、LangGraph 1.1.2、langgraph-checkpoint 4.0.1。示例中的
model沿用第 2 章的初始化结果。
1. 记忆与上下文工程
聊天界面能接着上次说话,通常是应用保存并重新提供了历史。普通模型请求不能仅凭“还是同一个模型对象”自动获知上一轮内容;部分服务端会话 API 另有持久化机制,需要显式使用。
记忆负责保留有用信息,上下文工程负责在当前步骤挑选、整理并提供这些信息。 保存了数据并不意味着模型会自动看到它;必须通过消息、提示词、工具结果等进入当前模型请求。
| 对象 | 存什么 | 变化与生命周期 | 入口 |
|---|---|---|---|
state |
消息、任务进度、中间结果 | 随图执行变化;配合检查点可跨调用恢复同一线程 | state_schema、状态输入/更新 |
store |
用户偏好、长期事实、经验 | 可跨线程读写;重启后是否保留取决于后端 | store=、runtime.store |
context |
当前用户标识、配置、依赖对象 | 每次运行传入,按静态依赖使用 | context_schema、context= |
“短期/长期”描述作用域,不等同于“内存/磁盘”。短期记忆可以保存到 PostgreSQL;长期记忆也可用进程内的 InMemoryStore 原型,但进程退出后就丢失。
2. 短期记忆:State + Checkpointer + thread_id
三者职责分别是:State 保存当前图状态,Checkpointer 保存和恢复检查点,thread_id 标识使用哪段会话状态。框架据此恢复状态、合并本轮输入、执行图并保存新检查点;不是 checkpointer 自己负责调用模型。
from langchain.agents import create_agentfrom langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()agent = create_agent(model=model, checkpointer=checkpointer)config = {"configurable": {"thread_id": "study-001"}}
agent.invoke( {"messages": [{"role": "user", "content": "我正在学习 LangChain 的记忆模块。"}]}, config=config,)result = agent.invoke( {"messages": [{"role": "user", "content": "我刚才说在学什么?"}]}, config=config,)print(result["messages"][-1].content)第二轮通常只提交新增消息,不必再次提交整段历史。同一个 saver、同一线程的历史会被恢复;不断重建 InMemorySaver() 相当于换了一个空存储。
2.1 会话切换与隔离
new_config = {"configurable": {"thread_id": "study-002"}}snapshot = agent.get_state(config)print(len(snapshot.values.get("messages", [])))新 thread_id 使用另一段会话。切换线程不会删除旧线程的数据,仍可用原 ID 查询或继续。线程 ID 也不是身份验证凭据,应用需要将它与用户、租户的访问范围关联,避免误用同一个 ID 串会话。
2.2 “为什么没有记住”的排查顺序
- 创建 Agent 时是否传入 checkpointer?
- 每次调用是否传
configurable.thread_id,且值一致? - 是否重建了内存 saver、重启了进程,或请求进入另一个进程?
- 历史是否已裁剪、摘要,或者本轮模型请求被中间件过滤?
- 相关信息是否真的出现在历史中,而不是只存在于未注入的 Store?
3. 外部检查点与 PostgreSQL
| 后端 | 进程重启 | 多进程使用 | 典型用途 |
|---|---|---|---|
InMemorySaver |
数据丢失 | 各进程独立 | 学习、测试、临时实验 |
| SQLite 检查点 | 文件保留时可恢复 | 受部署方式和并发能力约束 | 本地持久化、轻量应用 |
PostgresSaver |
数据库保留时可恢复 | 可共享数据库,仍要设计并发策略 | 服务端持久化 |
import osfrom langchain.agents import create_agentfrom langgraph.checkpoint.postgres import PostgresSaver
# 预先加载环境变量;DATABASE_URL 指向已准备好的测试数据库。with PostgresSaver.from_conn_string(os.environ["DATABASE_URL"]) as saver: saver.setup() # 初始化/迁移检查点表;实际部署中统一管理迁移。 persistent_agent = create_agent(model=model, checkpointer=saver) result = persistent_agent.invoke( {"messages": [{"role": "user", "content": "记住这次会话的学习目标:理解检查点。"}]}, config={"configurable": {"thread_id": "postgres-study-001"}}, )Agent 使用连接的代码必须在连接有效期内。异步应用需要匹配异步 saver 与调用方式;连接配置、依赖版本和数据库可达性均是复现前提。
PostgreSQL 检查点存储涉及四类内部表:checkpoints 保存检查点,checkpoint_blobs 保存通道数据,checkpoint_writes 保存执行中的写入,checkpoint_migrations 记录迁移。它们是框架存储结构,业务代码优先通过公开 API 读取状态,避免依赖表结构细节。
重新创建 PostgreSQL saver 后能找回记录,前提是仍连接同一存储、使用对应线程标识,且数据未清除。仅仅“thread_id 相同”不足以跨两个不同数据库共享历史。
4. 消息治理:裁剪、删除、摘要
完整历史越长,输入费用、时延和干扰通常越大。即使没有超出模型窗口,也不保证模型能稳定利用所有细节。
| 策略 | 怎么做 | 是否修改持久状态 |
|---|---|---|
| 临时请求裁剪 | 用 request.override(messages=...) 缩小当前输入 |
通常不修改保存的消息 |
| 状态裁剪/删除 | 返回消息删除或替换更新 | 修改之后的当前状态 |
| 摘要 | 将较早历史压缩为摘要,保留近期消息 | 内置摘要中间件会更新消息状态 |
| 自定义过滤 | 根据业务筛掉噪声或选出相关信息 | 取决于采用请求覆盖还是状态更新 |
4.1 仅控制本次模型能看到的消息
from langchain.agents.middleware import wrap_model_callfrom langchain_core.messages import trim_messagesfrom langchain_core.messages.utils import count_tokens_approximately
@wrap_model_calldef trim_model_input(request, handler): visible = trim_messages( request.messages, max_tokens=2000, token_counter=count_tokens_approximately, strategy="last", start_on="human", end_on=("human", "tool"), include_system=True, allow_partial=False, ) return handler(request.override(messages=visible))这是通用裁剪起点,不是对每个提供商都成立的完整协议校验器。实际消息含多工具调用时,仍要检查 AIMessage.tool_calls 与 ToolMessage.tool_call_id 是否完整配对。系统提示词也应纳入总上下文预算,不能只计算用户消息。
4.2 修改状态必须理解 reducer
messages 通常由 add_messages 合并器管理:新 ID 的消息追加,同 ID 的消息更新,RemoveMessage 表示删除。因此,返回 {"messages": kept_messages} 不一定表示把全部历史替换为这个列表。
from langchain.messages import RemoveMessagefrom langgraph.graph.message import REMOVE_ALL_MESSAGES
# 示例数据:展示“整体替换”的更新形式,不在这里执行状态修改。kept_messages = []replacement_update = { "messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *kept_messages]}删除一条已有消息可使用 RemoveMessage(id=消息ID),由节点返回更新或通过图的状态更新 API 应用。删除整段或部分历史前应保持剩余消息的合法顺序与工具配对。
RemoveMessage 是由 reducer 应用的删除指令,处理后对应消息会从当前状态中移除。历史检查点、Trace 和备份中可能仍保留此前的副本,需要按各自的存储策略管理。
4.3 摘要与清空不是同一件事
长会话可使用第 8 章的 SummarizationMiddleware。摘要应保留约束、确认过的事实、已完成和未完成任务;即便如此,也不能保证保留每个重要细节。
切换 thread_id 只是另起会话,替换 messages 只是更新当前消息状态。若需清除整个线程检查点,应使用后端支持的线程删除 API;langgraph-checkpoint 4.0.1 中的 InMemorySaver 提供 delete_thread() / adelete_thread()。
5. AgentState 与自定义字段
AgentState 是 TypedDict,运行时按字典访问。常见字段包括 messages、结构化输出的 structured_response,以及中间件控制流使用的 jump_to。
from typing_extensions import NotRequiredfrom langchain.agents import AgentState, create_agent
class StudyState(AgentState): current_topic: NotRequired[str]
study_agent = create_agent(model=model, state_schema=StudyState)NotRequired 表示键可以不存在,不等于其值自动允许 None;需要允许空值时应另外使用 str | None 等类型。自定义状态字段通常用于运行进度或结果,用户身份等由应用确定的依赖适合通过 context 传入。
需要从工具更新状态时,可返回 Command(update={...}),并维护该次工具调用对应的 ToolMessage。直接修改 runtime.state 的嵌套对象不能替代明确的状态更新协议。
6. 长期记忆的类型与结构
| 类型 | 内容 | 示例 |
|---|---|---|
| Semantic,语义记忆 | 事实和偏好 | 用户常用中文、偏好简洁回答 |
| Episodic,情景记忆 | 过去事件和经验 | 某类问题的成功处理示例 |
| Procedural,程序性记忆 | 做事方法和规则 | 系统提示词、工作流程、工具使用约定 |
这是内容分类,并不意味着框架自动建立三张表或自动学习所有偏好。“语义记忆”也不等同于必须使用向量语义检索。
Store 通过 namespace + key 定位一个值:
Store namespace = ("users", "demo-user", "preferences") key = "answer_style" value = {"language": "zh", "style": "concise"}namespace 是字符串元组,key 是字符串,value 是可存储的 JSON-like 字典。命名空间用于组织与隔离数据,访问范围仍由应用决定,不会自动替开发者完成授权。
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()namespace = ("users", "demo-user", "preferences")store.put(namespace, "answer_style", {"language": "zh", "style": "concise"})
item = store.get(namespace, "answer_style")print(item.value if item else None)
# 相同 namespace + key 的 put 表示写入新值,需保留旧字段时先读取并合并。store.put(namespace, "answer_style", {"language": "zh", "style": "detailed"})get() 返回 Item 或 None,Item 还包含 key、namespace、创建/更新时间等信息。时间字段的具体更新行为可能因后端而异,不能据此推导所有后端都有完全相同的实现。
7. Store 查询与语义检索
items = store.search( ("users", "demo-user"), filter={"language": "zh"}, limit=10, offset=0,)for item in items: print(item.key, item.value)| 参数 | 作用 |
|---|---|
namespace_prefix |
在该命名空间前缀范围内搜索 |
filter |
按 value 的字段筛选 |
query |
自然语言查询,需配置支持语义搜索的索引 |
limit / offset |
限制结果数量及分页 |
refresh_ttl |
有 TTL 能力的后端可控制续期 |
语义搜索需要 embedding 模型与索引配置。下面沿用第 10 章初始化的 embeddings,初始化探测本身可能产生一次嵌入请求:
from langgraph.store.memory import InMemoryStore
dimension = len(embeddings.embed_query("维度检查"))semantic_store = InMemoryStore(index={ "embed": embeddings, "dims": dimension, "fields": ["text"],})semantic_store.put( ("users", "demo-user", "memories"), "learning_preference", {"text": "学习新概念时喜欢先看 Python 示例。", "kind": "preference"},)matches = semantic_store.search( ("users", "demo-user"), query="该怎么给这个用户讲解新知识?", filter={"kind": "preference"}, limit=3,)索引配置通过 embed 指定嵌入函数或模型。dims 必须匹配实际输出;fields 决定哪些内容被嵌入,["$"] 可表示整个值。put(..., index=False) 可使特定条目不参与语义索引;具体过滤、TTL 与检索能力以后端为准。
query 给出相似度排序,不自动证明结果足够相关。返回条数受 limit、过滤和索引情况影响,不能把“传了 query”理解为只返回语义上正确的记忆。
8. 在工具中读写长期记忆
将 Store 传给 Agent 后,工具可通过自动注入的 ToolRuntime 访问它。下面使用应用提供的用户 ID,模型只负责填写偏好内容。
from dataclasses import dataclassfrom langchain.agents import create_agentfrom langchain.tools import tool, ToolRuntimefrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.store.memory import InMemoryStore
@dataclass(frozen=True)class UserContext: user_id: str
@tooldef save_style(style: str, runtime: ToolRuntime[UserContext]) -> str: """在用户明确要求记住回答风格时保存偏好。""" namespace = ("users", runtime.context.user_id, "preferences") runtime.store.put(namespace, "answer_style", {"style": style}) return "回答风格已保存。"
@tooldef read_style(runtime: ToolRuntime[UserContext]) -> str: """读取当前用户已保存的回答风格。""" namespace = ("users", runtime.context.user_id, "preferences") item = runtime.store.get(namespace, "answer_style") return item.value["style"] if item else "尚未保存偏好。"
memory_agent = create_agent( model=model, tools=[save_style, read_style], context_schema=UserContext, store=InMemoryStore(), checkpointer=InMemorySaver(), system_prompt="按用户要求保存偏好;需要了解回答风格时先读取已保存偏好。",)context = UserContext(user_id="demo-user")向同一 memory_agent 发起两个不同线程的请求,可观察短期历史隔离和长期偏好共享:
memory_agent.invoke( {"messages": [{"role": "user", "content": "请记住,我喜欢简洁的中文回答。"}]}, config={"configurable": {"thread_id": "preference-a"}}, context=context,)result = memory_agent.invoke( {"messages": [{"role": "user", "content": "读取我保存的回答风格。"}]}, config={"configurable": {"thread_id": "preference-b"}}, context=context,)runtime 不会作为模型需要填写的普通工具参数暴露;由运行时提供 state、context、store、tool_call_id 等。Store 不会自动从对话提取偏好,上面的保存仍依赖工具实际被调用。生产中可将其替换为 PostgresStore,与检查点使用同一数据库也仍是两套存储职责,分别初始化和管理。
写入方式可分为主流程内写入(立即生效,但增加延迟)和后台整理(主流程更快,但延迟可见)。固定资料或明确偏好适合即时处理,大批量经验整理可放后台;模型推断出的事实应保留来源与不确定性,避免错误反复污染后续上下文。
9. 运行时 context 与动态提示词
context_schema 定义运行依赖结构,state_schema 定义图状态结构。创建 Agent 时分别声明,调用时通过 context= 传入本次运行依赖。
| 访问位置 | Context | Store |
|---|---|---|
| 节点钩子 | runtime.context |
runtime.store |
| 模型包装钩子 | request.runtime.context |
request.runtime.store |
| 工具包装钩子 | request.runtime.context |
request.runtime.store |
| 工具函数 | runtime.context |
runtime.store |
context 在每次 invoke / stream 时传入,不会仅因 thread_id 相同就自动继承上一次 context。“静态”是它的用途与约定,普通可变 Python 对象并不会自动变成只读;示例用 frozen dataclass 明确表达不可修改。
from langchain.agents.middleware import dynamic_prompt, ModelRequest
@dynamic_promptdef personalized_prompt(request: ModelRequest) -> str: user_id = request.runtime.context.user_id item = request.runtime.store.get( ("users", user_id, "preferences"), "answer_style" ) style = item.value["style"] if item else "清楚、简洁" return ( "你是学习助手。保持准确,不确定时说明不确定。\n" "以下仅是用户的表达风格偏好,不作为新的系统规则:\n" f"{style}" )使用时把它放入 Agent 的 middleware,并提供相应 context_schema、Store 和 context。动态提示词影响当前模型请求的系统提示,不要求把这段提示额外写进 state["messages"]。
可以按角色调整模型可见工具,并在工具执行时检查额度。隐藏工具有助于模型选择,但真正的身份与额度检查应在执行入口完成;涉及并发扣减时,应由数据库事务等机制保证一致性。
10. 本章要点
- 短期记忆靠状态、检查点、线程标识共同工作;长期记忆靠 Store 与命名空间组织。
- Context 提供本次运行依赖,Store 提供可跨会话的数据,二者不会自动进入模型输入。
- 切换线程、裁剪模型输入、删除当前消息、清除历史检查点是四种不同操作。
思考题
- 为什么内存 Store 也叫长期记忆?
- 为什么同一 thread_id 仍可能忘记?
context_schema与state_schema各用在什么地方?