跳到正文

记忆负责保留信息,上下文工程负责将所需信息提供给当前模型请求。本章介绍 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_agent
from 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 “为什么没有记住”的排查顺序

  1. 创建 Agent 时是否传入 checkpointer?
  2. 每次调用是否传 configurable.thread_id,且值一致?
  3. 是否重建了内存 saver、重启了进程,或请求进入另一个进程?
  4. 历史是否已裁剪、摘要,或者本轮模型请求被中间件过滤?
  5. 相关信息是否真的出现在历史中,而不是只存在于未注入的 Store?

3. 外部检查点与 PostgreSQL

后端 进程重启 多进程使用 典型用途
InMemorySaver 数据丢失 各进程独立 学习、测试、临时实验
SQLite 检查点 文件保留时可恢复 受部署方式和并发能力约束 本地持久化、轻量应用
PostgresSaver 数据库保留时可恢复 可共享数据库,仍要设计并发策略 服务端持久化
import os
from langchain.agents import create_agent
from 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_call
from langchain_core.messages import trim_messages
from langchain_core.messages.utils import count_tokens_approximately
@wrap_model_call
def 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 RemoveMessage
from 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 NotRequired
from 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 dataclass
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
@dataclass(frozen=True)
class UserContext:
user_id: str
@tool
def save_style(style: str, runtime: ToolRuntime[UserContext]) -> str:
"""在用户明确要求记住回答风格时保存偏好。"""
namespace = ("users", runtime.context.user_id, "preferences")
runtime.store.put(namespace, "answer_style", {"style": style})
return "回答风格已保存。"
@tool
def 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_prompt
def 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 提供可跨会话的数据,二者不会自动进入模型输入。
  • 切换线程、裁剪模型输入、删除当前消息、清除历史检查点是四种不同操作。

思考题

  1. 为什么内存 Store 也叫长期记忆?
  2. 为什么同一 thread_id 仍可能忘记?
  3. context_schema 与 state_schema 各用在什么地方?

11. 参考资料

你可能还想看