7774 字
39 分钟
LangGraph 源码深潜:Memory、Store 与上下文工程机制解剖

LangGraph源码学习路线:第 14 篇 Memory / Store 与上下文工程源码解剖#

核心问题: LangGraph 如何把一次会话内的短期状态、跨会话长期记忆和模型上下文工程拆成不同的数据通道?

源码主线: StateGraph.compile(checkpointer=..., store=...)CompiledStateGraphRuntime contextCheckpointer / BaseStorethread state / cross-thread memory

前置文章: 第 11 篇 Checkpoint、第 13 篇 Streaming / Observability

依赖基线: langgraph==1.2.7langchain==1.3.11langchain-core==1.4.8

源码基线: https://github.com/langchain-ai/langgraph/tree/1.2.7

阅读边界: 本文覆盖 short-term memory、long-term memory、checkpointer、store、messages 裁剪和旅行规划助手的偏好记忆;不展开向量数据库内部索引、用户画像产品策略和 LangGraph Platform 托管存储实现。


0. 本篇在源码学习主线中的位置#

前面的文章已经解释了底层 Runnable、Tool、StateGraph、Checkpoint 和 Streaming。本篇把这些能力放到一个更接近生产 Agent 的问题里:Memory / Store / Context Engineering。

Checkpoint / Streaming
Memory / Store / Context Engineering
Middleware / Guardrails

本篇只解决:

  • 区分 checkpoint 保存的执行状态与 store 保存的长期记忆。
  • 解释 thread_id、namespace、key、messages state 的职责边界。
  • 把用户偏好、历史计划和检索资料放到正确的数据通道。

本篇不展开:

  • 不实现向量数据库召回算法。
  • 不讨论复杂用户画像治理。

1. 本篇问题、学习目标与能力边界#

核心问题#

LangGraph 如何把一次会话内的短期状态、跨会话长期记忆和模型上下文工程拆成不同的数据通道?

学习目标#

完成本篇后,读者必须能够:

  1. 画出Memory / Store / Context Engineering的构建期对象关系。
  2. 解释一次运行时调用如何进入主链。
  3. 说明关键分支、异常和停止条件。
  4. 区分公共 API、扩展接口和内部实现。
  5. 根据旅行规划助手场景做工程选型。

能力边界#

能力本篇是否覆盖说明
构建期对象组装解释公开参数如何变成运行时对象
运行时主链解释 invoke/stream/tool/task 等主路径
分支与异常解释失败、降级、重试、终止边界
扩展协作解释与相邻框架组件如何组合
底层供应商实现不展开模型、数据库或平台内部实现

2. 核心概念与最小心智模型#

一句话定义#

Memory / Store 是 LangGraph 中把执行状态和长期记忆分层保存的运行时能力,负责让 Agent 跨步骤、跨线程复用上下文,不负责自动决定哪些信息一定值得记住。

最小心智模型#

Checkpoint / Streaming
Memory / Store / Context Engineering
Middleware / Guardrails
工程化 Agent 能力

核心术语#

术语源码对象语义不要误解为
CheckpointBaseCheckpointSaver按 thread 保存图执行状态和恢复点用户长期偏好库
StoreBaseStore跨线程保存 namespace/key/value 记忆每一步 state 快照
Threadthread_id一次会话或任务的状态隔离维度用户 ID
Context engineeringmessages / summary / trim控制进入模型窗口的信息简单追加全部历史

与相邻抽象的边界#

对象负责什么不负责什么与本篇对象的关系
Runnable统一 invoke / stream / batch 协议不决定业务策略提供可组合执行底座
StateGraph编排状态、节点和边不实现所有外部系统承载复杂 Agent 工作流
RunnableConfig传递 config、metadata、callbacks不保存业务状态让观测和配置沿调用链传播

3. 完整执行链路#

本篇的完整链路不能只写成“用户请求进入 runtime”。真正要追踪的是四类对象如何分流:本次输入写入 messages state,短期状态由 checkpointer 按 thread_id 恢复和保存,长期记忆由 store 按 namespace/key 读取和写入,最后只有被裁剪和注入后的消息会进入模型上下文。

从一次旅行规划请求观察对象变化#

from langgraph.graph import StateGraph, MessagesState
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langchain_core.messages import HumanMessage, SystemMessage
checkpointer = InMemorySaver()
store = InMemoryStore()
def call_model(state: MessagesState, config, *, store: InMemoryStore):
user_id = config["configurable"]["user_id"]
namespace = ("travel_assistant", user_id, "preferences")
memories = store.search(namespace, query="travel preferences")
preference_text = format_preferences(memories)
visible_messages = trim_messages(state["messages"], max_tokens=3000)
model_messages = [
SystemMessage(content=f"已知长期偏好:{preference_text}"),
*visible_messages,
]
response = model.invoke(model_messages)
return {"messages": [response]}
graph = builder.compile(checkpointer=checkpointer, store=store)
result = graph.invoke(
{"messages": [HumanMessage(content="帮我规划大阪 4 天行程,少走路。")]},
config={"configurable": {"thread_id": "trip-2026-osaka", "user_id": "u_123"}},
)

这段伪代码的输入对象有两个:input 里的 HumanMessage 是本轮短期对话增量,configurable.thread_id/user_id 是运行时定位信息。thread_id 用来找同一条会话线程的 checkpoint;user_id 用来拼长期记忆 namespace。它们都不应该被混进 prompt 文本里。

输出对象也分两层:节点返回的 {"messages": [response]} 是 state update,之后由 messages reducer 合并进 thread state;store.search(...) 返回的是长期记忆条目,只是被格式化后注入本次 model_messages,不会自动变成 graph state。

对象形态发生变化的关键点有三个:input dict 被合并进 MessagesStatestoreItem 被转成 system message 里的偏好摘要,完整历史 state["messages"] 被裁剪成 visible_messages。读源码时要观察这些转换是否发生在节点代码、runtime state 合并,还是 store 协议层。

checkpointer 与 store 的分工链路#

本轮 input: {"messages": [HumanMessage(...)]}
RunnableConfig.configurable: {thread_id, user_id}
checkpointer.get_tuple(thread_id)
恢复上一轮 StateSnapshot / channel values
messages reducer 合并本轮 HumanMessage
节点通过 store.search(namespace, query) 读取长期记忆
trim_messages(state["messages"]) + memory injection
model.invoke(list[BaseMessage])
节点返回 {"messages": [AIMessage(...)]}
checkpointer.put(...) 保存新的 thread state
可选:store.put(namespace, key, value) 保存跨线程长期记忆

checkpointer 的输入核心是 thread_id 和图运行产生的 channel values,它保存的是“这条线程执行到哪里、当前 state 是什么”。它的输出通常服务于恢复:下一次同一 thread_id 调用时,runtime 可以从 checkpoint 继续合并新输入。

store 的输入核心是 namespace/key/value,它保存的是可跨线程复用的业务记忆,例如“用户偏好慢节奏旅行”“不喜欢频繁换酒店”。它的输出服务于检索和上下文注入,不代表图自动恢复执行位置。

两者最容易混淆的地方是都叫 memory。判断方法很简单:如果问题是“这条会话上次说到哪一步”,看 checkpointer;如果问题是“这个用户跨会话有什么稳定偏好”,看 store。

namespace/key 到模型上下文的对象链#

def load_travel_preferences(store, user_id: str, current_request: str) -> list[str]:
namespace = ("travel_assistant", user_id, "preferences")
items = store.search(namespace, query=current_request, limit=5)
return [item.value["text"] for item in items if item.value.get("confidence", 0) >= 0.7]
def build_model_messages(state, store, config):
user_id = config["configurable"]["user_id"]
last_user_text = state["messages"][-1].content
preferences = load_travel_preferences(store, user_id, last_user_text)
injected_memory = SystemMessage(
content="长期偏好:" + ";".join(preferences) if preferences else "暂无可用长期偏好"
)
recent_messages = trim_messages(state["messages"], max_tokens=3000)
return [injected_memory, *recent_messages]

这里的输入对象是 statestoreconfig,输出对象是 list[BaseMessage]。注意输出不是 StateSnapshot,也不是 store item,而是模型真正能消费的 messages。

对象形态改变发生在 load_travel_preferencesbuild_model_messages 两处:前者把 namespace/query 查询结果转为业务字符串列表,后者把这些字符串包成 SystemMessage。如果直接把 store item 原样塞给模型,就会泄露存储结构,也会让 prompt 难以控制。

读源码时要观察 namespace 是在哪里构造的、key 是否稳定、value 是否可序列化、检索结果是否经过置信度和权限过滤。长期记忆的质量不由 store 自动保证,必须由应用层决定哪些内容能写入和注入。

正常结束条件#

一次 Memory / Store 调用正常结束,至少要满足四件事:thread state 已按 thread_id 保存;长期记忆只在明确策略命中时写入 store;进入模型的是裁剪后的 list[BaseMessage];最终输出仍然是 graph 的 state update 或调用返回值,而不是把 checkpoint/store 内部对象暴露给业务调用方。


4. 源码地图、关键文件与阅读顺序#

核心目录#

langgraph/
├── graph/
├── pregel/
├── types.py
└── func/
langchain_core/
├── runnables/
├── tools/
├── retrievers.py
└── messages/

关键文件#

优先级文件核心对象阅读目的
1libs/langgraph/langgraph/graph/state.pyStateGraph.compile理解 checkpointer/store 如何注入 compiled graph
2libs/checkpoint/langgraph/checkpoint/base/__init__.pyBaseCheckpointSaver理解 thread state 的保存协议
3libs/checkpoint/langgraph/store/base/__init__.pyBaseStore理解 namespace/key/value 记忆协议
4libs/checkpoint/langgraph/store/memory/__init__.pyInMemoryStore理解本地 store 实现

推荐阅读顺序#

1. 先读官方概念文档,确认公共契约。
2. 再读公开 API 或装饰器入口。
3. 顺着构建期对象进入 runtime。
4. 追踪一次 invoke / stream / tool call / task call。
5. 单独检查异常、重试、恢复和扩展点。
6. 最后回到旅行规划助手做工程判断。

不建议的阅读顺序#

不建议直接从最底层 private helper 开始读。源码学习的第一目标是建立调用链,而不是收集函数名。先找公开入口,再沿参数和返回值追下去,才不会把内部实现误当成稳定 API。


5. 对象模型、继承关系与协议边界#

核心对象关系#

Public API / Decorator / Tool Protocol
Runtime wrapper
State / Config / Context
Storage / Model / Tool / Graph Runtime

对象职责#

对象生命周期输入输出核心职责
公开入口构建期或调用期用户参数runtime object提供稳定 API
runtime context单次调用config/state下游上下文传递配置、状态和观测信息
协议对象单步执行上游对象下游可消费结果维持模块边界
扩展点构建期注册、运行时触发request/response修改或观察后的结果插入业务控制逻辑

协议边界#

公共 API
负责:给业务代码稳定入口。
不负责:暴露所有内部调度细节。
运行时协议
负责:让状态、配置、工具、模型和持久化协作。
不负责:替业务判断所有策略。
扩展接口
负责:允许业务插入可维护的定制逻辑。
不负责:保证错误扩展仍然安全。

稳定接口与内部实现#

类型对象文章中的使用原则
公共 API官方文档列出的函数、类和装饰器可以用于工程示例
扩展接口middleware、store、retriever、task、Command 等协议说明契约和约束
内部实现private helper、runner、loop 细节只用于解释,不建议业务依赖

6. 源码阅读策略与证据标准#

本篇阅读策略#

先找公开入口
确认输入输出类型
沿调用方追到核心实现
记录状态与对象变化
检查分支、异常和结束条件
回到设计目的

证据等级#

标记含义写作要求
源码事实当前正式版源码可以证明附源码链接
官方契约官方文档或 API Reference 明确说明附官方链接
简化伪代码压缩真实控制流标注不是源码逐字复制
作者推断根据调用链得出的理解明确使用“从调用关系可以推断”
工程建议面向项目实践的建议说明适用条件

本篇证据清单#

结论证据类型文件或文档定位
公共入口存在稳定契约官方契约官方 docs / reference第 14 章链接
核心协议对象存在源码定义源码事实官方源码仓库第 4 章关键文件
运行时分支需要按协议处理作者推断调用链和源码结构第 9 章
工程选型依赖场景边界工程建议旅行规划助手场景第 11 章

7. 构建期源码解剖#

本章回答:compile(checkpointer=..., store=...) 如何把两个看似相似的“记忆能力”装配进可运行图,并保持短期状态与长期记忆的协议边界?

构建期职责#

输入归一化动作构建结果
StateGraph校验节点、边、state schema 和 reducer可编译图定义
checkpointer保存为运行时恢复依赖thread_id 读写 thread state
store保存为运行时跨线程依赖namespace/key 读写长期记忆
config_schema / context_schema延迟到运行时解析RunnableConfig.configurable 的契约

compile 注入 checkpointer 与 store#

def compile_travel_graph(builder, checkpointer, store):
compiled = builder.compile(
checkpointer=checkpointer,
store=store,
)
return compiled
def state_graph_compile(builder, checkpointer=None, *, store=None):
builder.validate_graph()
state_channels = builder.build_state_channels()
node_specs = builder.attach_nodes_and_edges()
compiled = CompiledStateGraph(
builder=builder,
channels=state_channels,
nodes=node_specs,
checkpointer=checkpointer,
store=store,
)
return compiled.validate()

这段伪代码的输入对象是 StateGraph 构建器、BaseCheckpointSaver | NoneBaseStore | None。输出对象是 CompiledStateGraph,它是后续 invoke/stream 的入口。

对象形态改变发生在 builder.build_state_channels()CompiledStateGraph(...):前者把用户定义的 state schema 转成 runtime channel/reducer,后者把 checkpointer 和 store 作为运行时依赖挂到 compiled graph 上。checkpointerstore 本身没有被合并成同一个对象。

读源码时要观察 compile 是否立即读写存储。通常构建期只是保存依赖和校验图结构;真正按 thread_id 读 checkpoint、按 namespace 查 store,发生在运行时调用或节点逻辑中。

namespace 与 key 设计#

def preference_namespace(config) -> tuple[str, ...]:
user_id = config["configurable"]["user_id"]
return ("travel_assistant", user_id, "preferences")
def remember_preference(store, config, fact: dict):
namespace = preference_namespace(config)
key = fact["id"]
value = {
"text": fact["text"],
"source_thread_id": config["configurable"]["thread_id"],
"confidence": fact["confidence"],
}
store.put(namespace, key, value)

输入对象是 RunnableConfig 和一个已经抽取出的业务事实 fact。输出不是模型消息,而是一次 store.put(namespace, key, value) 副作用。

对象形态改变发生在 value = {...}:自然语言偏好被转成可序列化的长期记忆记录,并附带来源 thread 和置信度。namespace 决定隔离域,key 决定同一记忆是否覆盖或更新。

读源码和工程代码时要重点看 namespace 是否包含稳定用户维度,key 是否可重复计算,value 是否避免保存敏感原文。store 只提供读写协议,不替应用判断哪些事实值得长期保存。

messages 裁剪策略准备#

def make_context_builder(max_prompt_tokens: int):
def build_context(state, store, config):
user_id = config["configurable"]["user_id"]
namespace = ("travel_assistant", user_id, "preferences")
memories = store.search(namespace, query=state["messages"][-1].content, limit=5)
memory_message = SystemMessage(
content=format_memory_for_prompt(memories)
)
recent_messages = trim_messages(
state["messages"],
max_tokens=max_prompt_tokens,
strategy="last",
include_system=False,
)
return [memory_message, *recent_messages]
return build_context

输入对象是 max_prompt_tokens 这个构建期策略参数;输出是一个运行时可调用的 build_context 函数。构建期并不知道具体 messages 内容,只固定“先取长期记忆、再裁剪短期消息、最后合成模型输入”的控制流。

对象形态改变发生在运行时闭包内部:state["messages"] 仍是 thread state 的一部分,memories 是 store item 列表,最终 return 才是要交给 ChatModel 的 list[BaseMessage]

读源码时要区分“裁剪策略被配置”和“messages 已经被裁剪”。前者发生在构建或节点定义时,后者必须等到每次调用拿到真实 state 后才能完成。

构建期产物#

产物保存的信息运行时用途
CompiledStateGraph节点、边、state channels、checkpointer、store接收 invoke/stream 调用
checkpointer 依赖checkpoint 后端和序列化协议恢复和保存 thread state
store 依赖namespace/key/value 后端读取和写入长期记忆
context builder裁剪、检索、注入策略生成模型可见 messages

8. 运行时主链源码解剖#

本章回答:构建完成后,一次调用如何恢复 thread state、读取长期记忆、裁剪 messages,并把最终上下文送进模型?

运行时总链路#

graph.invoke(input, config)
解析 configurable.thread_id / user_id
checkpointer 按 thread_id 读取上一轮 checkpoint
合并本轮 input 到 state channels
节点读取 store(namespace, query)
裁剪 state["messages"] 并注入长期记忆
model.invoke(list[BaseMessage])
节点返回 state update
checkpointer 保存新 thread state
可选 store.put(...) 写长期记忆

thread state 恢复#

def invoke_with_checkpoint(compiled, input, config):
thread_id = config["configurable"].get("thread_id")
if thread_id is None:
raise ValueError("checkpointed graph requires configurable.thread_id")
checkpoint_tuple = compiled.checkpointer.get_tuple(config)
previous_channels = checkpoint_tuple.checkpoint["channel_values"] if checkpoint_tuple else {}
next_channels = merge_input_with_channels(
previous_channels,
input,
reducers=compiled.channels,
)
return run_pregel_loop(compiled, next_channels, config)

输入对象是本轮 inputRunnableConfig。输出对象不是最终答案,而是交给 Pregel loop 的 next_channels,也就是恢复后又合并了本轮输入的 thread state。

对象形态改变发生在 merge_input_with_channels{"messages": [HumanMessage(...)]} 不再只是调用参数,而是被 reducer 合并到 checkpoint 恢复出的 channel values 中。对于 messages 字段,常见语义是追加而不是整体覆盖。

读源码时要观察 checkpoint 的定位字段来自哪里。thread_id 是恢复短期状态的关键;如果同一个用户开启多个旅行规划线程,它们应该有不同 thread_id,否则 state 会串线。

长期记忆读取与上下文注入#

def call_model_with_memory(state, config, store):
user_id = config["configurable"]["user_id"]
namespace = ("travel_assistant", user_id, "preferences")
query = state["messages"][-1].content
items = store.search(namespace, query=query, limit=5)
preferences = [item.value["text"] for item in items]
prompt_messages = [
SystemMessage(content="用户长期偏好:" + ";".join(preferences)),
*trim_messages(state["messages"], max_tokens=3000),
]
ai_message = model.invoke(prompt_messages)
return {"messages": [ai_message]}

输入对象是恢复后的 state、运行时 config 和构建期注入的 store。输出对象是 state update:{"messages": [AIMessage]}

对象形态改变发生在 items -> preferences -> SystemMessage:长期记忆先是 store item,再是业务字符串,最后才变成模型上下文的一部分。store item 不会自动进入模型,也不会自动写回 checkpoint。

读源码时要观察 store 的读取位置。LangGraph 提供 store 协议和注入方式,但“什么时候查、查哪个 namespace、查到后如何注入 prompt”通常是节点或应用代码的责任。

messages 裁剪与模型输入#

def trim_and_inject_messages(state, injected_memory):
raw_messages = state["messages"]
visible_history = trim_messages(
raw_messages,
max_tokens=3000,
strategy="last",
include_system=False,
)
return [injected_memory, *visible_history]

输入对象是完整 state["messages"] 和已经格式化好的长期记忆消息。输出对象是模型可见的 list[BaseMessage]

对象形态改变发生在 visible_history:thread state 中可能保存很多轮消息,但模型只看到裁剪后的窗口。裁剪不是删除 checkpoint 历史,而是构造本次模型调用视图。

读源码时要特别区分“state 保存了什么”和“prompt 发送了什么”。如果把完整 state 误认为完整 prompt,就会误判成本、上下文窗口和敏感信息暴露风险。

状态与记忆写回#

def finish_turn(compiled, config, state_update, extracted_facts):
new_state = apply_reducers(compiled.channels, state_update)
compiled.checkpointer.put(config, checkpoint_from(new_state))
if compiled.store is not None:
for fact in extracted_facts:
if fact["confidence"] >= 0.7 and fact["kind"] == "stable_preference":
namespace = ("travel_assistant", fact["user_id"], "preferences")
compiled.store.put(namespace, fact["id"], fact)
return new_state

输入对象包括节点返回的 state_update 和应用层抽取出的 extracted_facts。输出对象是新的 thread state;同时可能产生 checkpoint 写入和 store 写入两个副作用。

对象形态改变有两条线:state_update 经 reducer 变成新的 checkpoint 内容,extracted_facts 经筛选后变成长期记忆 value。前者服务会话恢复,后者服务跨会话复用。

读源码时要观察写入顺序和失败策略。checkpoint 写失败会影响恢复能力;store 写失败通常不应伪装成模型成功记住了偏好,至少要进入 trace 或业务日志。

运行时主链总结#

Input + config
thread_id 恢复 checkpoint
user_id 构造 namespace 并读取 store
messages 裁剪 + memory 注入
AIMessage state update
checkpoint 保存短期状态 / store 保存长期记忆

9. 关键分支、异常与边界#

本章回答:当 checkpointer、store、messages 或上下文预算不满足预期时,哪些路径可以降级,哪些必须显式失败?

分支矩阵#

分支类型触发条件行为结果
checkpoint 缺失未配置 checkpointer 或缺少 thread_id无恢复运行或抛错只能处理本轮输入
store 缺失未配置 store跳过长期记忆读取保留短期对话能力
namespace 缺失缺少 user_id拒绝长期记忆读写避免跨用户污染
记忆低置信抽取事实不稳定不写 store避免污染用户画像
上下文超限messages + memory 超预算裁剪、摘要或拒绝控制成本和窗口

store 缺失分支#

def maybe_load_memory(store, config, state):
if store is None:
return SystemMessage(content="暂无长期记忆。")
user_id = config["configurable"].get("user_id")
if user_id is None:
return SystemMessage(content="未启用用户级长期记忆。")
namespace = ("travel_assistant", user_id, "preferences")
items = store.search(namespace, query=state["messages"][-1].content, limit=5)
return SystemMessage(content=format_memory_for_prompt(items))

输入对象是可选的 storeconfig 和当前 state。输出对象始终是一个可进入 prompt 的 SystemMessage,而不是让下游处理 None 或异常 store item。

对象形态改变发生在分支出口:有 store 时由 Item 格式化为记忆消息;无 store 或无 user_id 时生成明确的空记忆消息。这样模型调用链仍然稳定,但不会假装读取到了长期记忆。

读源码时要观察降级是否显式。跳过长期记忆可以接受,悄悄跨用户使用错误 namespace 不可以接受。

记忆污染分支#

def write_memory_if_stable(store, config, extracted_fact):
if store is None:
return "skipped:no_store"
if extracted_fact["confidence"] < 0.7:
return "skipped:low_confidence"
if extracted_fact["kind"] not in {"stable_preference", "accessibility_need"}:
return "skipped:not_long_term"
namespace = ("travel_assistant", config["configurable"]["user_id"], "preferences")
store.put(namespace, extracted_fact["id"], extracted_fact)
return "written"

输入对象是抽取出的候选事实,输出对象是写入状态标记;真正的长期副作用只在最后 store.put 发生。

对象形态改变发生在候选事实通过策略筛选之后:临时聊天内容不会因为出现在 messages 中就自动变成长期记忆。只有稳定、可解释、可覆盖的事实才应该成为 store value。

读源码和业务代码时要观察“写记忆”是不是与“生成回答”解耦。模型回答成功不等于记忆写入成功;记忆写入失败也不应该伪造用户偏好已经保存。

上下文窗口超限分支#

def build_budgeted_messages(state, memory_message, token_budget):
memory_tokens = count_tokens([memory_message])
remaining = token_budget - memory_tokens
if remaining <= 0:
return [SystemMessage(content="长期记忆过长,已省略。"), state["messages"][-1]]
visible_history = trim_messages(state["messages"], max_tokens=remaining)
if not visible_history:
visible_history = [state["messages"][-1]]
return [memory_message, *visible_history]

输入对象是完整 thread messages、长期记忆消息和 token budget。输出对象是预算内的 list[BaseMessage]

对象形态改变发生在 visible_history:完整历史被压缩成本次可见历史。重要的是,裁剪只影响模型输入,不应直接删除 checkpoint 中的 state,除非业务明确执行了摘要写回策略。

读源码时要观察预算优先级:当前用户请求通常必须保留,长期记忆和旧历史可以裁剪或摘要。否则模型可能看到很多偏好,却看不到本轮真正问题。

Retry、Fallback 与恢复边界#

机制适用条件不适用条件幂等要求
Retrystore/search 临时失败、模型临时失败已经写入不可重复副作用写入 key 要稳定
Fallbackstore 不可用时退回 checkpoint-only安全或隔离要求不能降低明确记录降级原因
Repair记忆抽取格式不合法已写入错误长期记忆先修复再写入
Human review高风险偏好、敏感画像普通低风险偏好保留来源 thread 和证据

停止条件与保护上限#

正常结束:模型返回 AIMessage,state update 被 reducer 合并,checkpoint 写入完成。
可降级结束:store 缺失或检索失败,但本轮可以基于短期 state 回答。
保护性结束:上下文预算不足、namespace 不可信或记忆可能污染时拒绝写 store。
异常失败:checkpoint 恢复失败、state schema 不匹配或 reducer 无法合并时向上抛出。

能力边界#

容易误判的能力实际提供者本篇对象的真实职责
自动记住所有用户偏好应用层抽取和写入策略store 只提供 namespace/key/value 协议
自动恢复跨线程任务thread_id + checkpointercheckpoint 只恢复同线程 state
自动优化 prompt 上下文context builder / trim 策略messages state 只是保存历史
自动保证记忆安全权限、脱敏、审计策略LangGraph 提供插入位置和存储协议

10. 扩展机制与框架协作#

本章回答:Memory / Store 不应被写成孤立功能。它需要和 checkpoint、RAG、middleware、观测系统协作,但每个协作点都要保持对象边界清楚。

扩展点总览#

扩展点扩展方式执行时机可修改内容约束
checkpointer backend实现 checkpoint saver 协议thread state 读写时checkpoint 存储位置必须按 thread_id 隔离
store backend实现 BaseStore 协议长期记忆读写时namespace/key/value 存储必须处理序列化和权限
context builder应用层函数或节点逻辑模型调用前模型可见 messages不应修改原始 checkpoint
memory policy抽取、过滤、脱敏策略写 store 前长期记忆候选事实不能把临时对话全写入

自定义 store backend#

class TravelProfileStore(BaseStore):
def put(self, namespace, key, value, *, index=None):
record = serialize_memory(namespace, key, value)
database.upsert(record)
def search(self, namespace, *, query=None, filter=None, limit=10):
rows = database.search(namespace=namespace, query=query, filter=filter, limit=limit)
return [deserialize_item(row) for row in rows]

输入对象是 namespacekeyvalue。输出对象在 put 路径上是持久化副作用,在 search 路径上是 Item 列表。它们都不是模型消息。

对象形态改变发生在 serialize_memorydeserialize_item:框架层的 namespace/key/value 协议被映射到底层数据库记录,再从数据库记录还原为 store item。只要这层协议稳定,上层节点不需要知道底层是内存、SQL 还是向量索引。

读源码和实现时要观察并发、覆盖语义和序列化失败。key 相同是覆盖还是版本化,namespace 是否可被用户输入污染,都会影响长期记忆可靠性。

记忆写入策略#

def extract_and_persist_preferences(store, config, messages):
candidates = preference_extractor.invoke({"messages": messages})
written = []
for candidate in candidates:
if candidate.confidence < 0.7:
continue
if candidate.scope != "long_term":
continue
namespace = ("travel_assistant", config["configurable"]["user_id"], "preferences")
value = {
"text": redact(candidate.text),
"source": config["configurable"]["thread_id"],
"confidence": candidate.confidence,
}
store.put(namespace, candidate.id, value)
written.append(candidate.id)
return written

输入对象是本轮 messages 和运行时 config。输出对象是已写入的 memory id 列表;真正的长期副作用发生在 store.put

对象形态改变发生在 candidate -> value:模型或规则抽取出的候选偏好,经过置信度、作用域和脱敏过滤后,才成为长期记忆。这个步骤属于应用策略,不是 LangGraph 自动完成。

读源码时要观察写入策略是否可回放和可审计。生产系统通常需要记录来源 thread、置信度和写入原因,否则未来很难解释“为什么 Agent 认为用户有这个偏好”。

与 RAG 协作#

def build_grounded_context(state, store, retriever, config):
user_id = config["configurable"]["user_id"]
namespace = ("travel_assistant", user_id, "preferences")
preferences = store.search(namespace, query=state["messages"][-1].content, limit=3)
docs = retriever.invoke(state["messages"][-1].content)
memory_message = SystemMessage(content=format_preferences(preferences))
evidence_messages = format_docs_as_messages(docs)
recent_messages = trim_messages(state["messages"], max_tokens=2500)
return [memory_message, *evidence_messages, *recent_messages]

输入对象来自三条通道:thread state、long-term store、retriever。输出对象是模型可见的 list[BaseMessage]

对象形态改变发生在最终合成阶段:偏好记忆说明“用户喜欢什么”,检索文档说明“外部事实是什么”,recent messages 说明“当前任务进展到哪里”。三者进入 prompt 前要分清来源,不能混成一段不可追踪的大文本。

读源码时要观察 RAG 证据和长期偏好是否被分别标记。否则当答案出错时,很难判断是记忆污染、检索错误,还是短期 state 裁剪过度。

选择扩展还是重写流程#

条件选择扩展点选择更底层框架
只替换长期记忆后端
只调整 messages 裁剪和注入
需要改变图拓扑或人工审批节点
需要跨系统事务保证视情况
需要统一审计所有记忆写入视复杂度而定

11. 工程决策与适用场景#

适用场景#

场景是否推荐原因
旅行规划助手生产化需要状态、工具、检索、观测和恢复闭环
一次性脚本问答直接调用模型更简单
高风险工具调用可以加入 guardrail、interrupt、trace
大规模知识问答可以把 retrieval、rerank、grounding 拆清楚
简单静态 FAQ复杂运行时收益不高

工程决策表#

决策点推荐选择前提风险
是否抽象成独立模块有稳定职责时抽象边界清楚过早拆分增加复杂度
是否持久化需要恢复或复盘时持久化有 thread_id / user_id隐私和清理成本
是否加入 guardrail涉及外部工具或敏感数据时加入有明确策略误杀正常请求
是否流式观测用户等待时间长时加入前端能消费事件泄露内部状态

性能、可靠性与安全边界#

性能:额外抽象会增加序列化、检索、trace 和存储成本。
可靠性:可恢复机制要求状态 schema、幂等工具和错误分类清楚。
安全:metadata、state、tool args、retrieved docs 都可能包含敏感信息。
可观测性:至少记录 thread_id、node/tool、策略命中、错误和耗时。

12. 常见误区与源码纠正#

误区:把公共 API 当成全部源码机制#

错误原因:

公开 API 很短,看起来像全部逻辑都在这一层。

源码事实:

公开 API 通常只负责归一化和装配;真正的运行语义在 runtime、协议对象和扩展点之间流转。

工程影响:

只读公开 API 会误判能力边界,导致扩展时依赖错误位置。

误区:把所有中间结果都写进模型上下文#

错误原因:

Agent 运行时的 state、memory、documents、trace 看起来都像“上下文”。

源码事实:

state、store、Document、metadata、trace、prompt messages 是不同协议;只有经过明确注入的内容才进入模型上下文。

工程影响:

如果不区分这些协议,轻则上下文膨胀,重则泄露敏感数据或污染长期记忆。

误区:异常都应该在框架层吞掉#

错误原因:

Agent 产品希望“永远给用户一个答案”。

源码事实:

可恢复异常可以转成 retry、fallback、interrupt;不可恢复异常必须上抛并进入 trace。

工程影响:

吞掉异常会让错误状态继续进入后续节点,调试成本比显式失败高得多。


13. 最终心智模型与掌握检查#

构建期心智模型#

Public Config
Normalize
Build Runtime Object
Attach Protocol / Extension / Storage

运行时心智模型#

Input
Runtime Entry
Context / State / Config
Core Execute
State / Event / Output

分支与异常心智模型#

正常路径 → 返回协议对象并更新必要状态
分支路径 → 路由、降级、拒答、人工确认或恢复
可恢复异常 → retry / fallback / repair / resume
不可恢复异常 → trace + raise
保护上限 → 停止循环、预算或高风险动作

一句话总结#

Memory / Store / Context Engineering通过构建期协议装配形成可运行对象,运行时沿公开入口进入核心执行链,使用分支机制处理边界,并通过扩展点与 LangChain / LangGraph 的状态、工具、模型、存储和观测能力协作。

掌握检查#

  • 能说清公开入口与真实执行入口的区别。
  • 能画出构建期对象关系。
  • 能画出运行时对象流转。
  • 能解释至少一个核心函数的伪代码。
  • 能指出同步、异步、流式或批量路径的边界。
  • 能说明异常在哪里抛出、在哪里处理。
  • 能说明正常结束和保护性终止的区别。
  • 能区分公共 API、扩展接口和内部实现。
  • 能根据旅行规划助手场景判断是否应该使用该抽象。

14. 参考资料与下一篇衔接#

官方概念文档#

  1. LangGraph Memory
    https://docs.langchain.com/oss/python/langgraph/add-memory

  2. LangGraph Persistence
    https://docs.langchain.com/oss/python/langgraph/persistence

  3. LangGraph Context Engineering
    https://docs.langchain.com/oss/python/langgraph/context

官方 API Reference#

  1. BaseStore
    https://reference.langchain.com/python/langgraph/store/

  2. StateGraph.compile
    https://reference.langchain.com/python/langgraph/graphs/

官方源码#

  1. libs/checkpoint/langgraph/store/base/__init__.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/store/base/__init__.py

  2. libs/checkpoint/langgraph/store/memory/__init__.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/store/memory/__init__.py

  3. libs/checkpoint/langgraph/checkpoint/base/__init__.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/checkpoint/base/__init__.py

  4. libs/langgraph/langgraph/graph/state.py
    https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/langgraph/langgraph/graph/state.py

下一篇衔接#

下一篇进入:

第 15 篇:Middleware / Guardrails 与 Agent 执行控制源码解剖

需要继续回答:

这项能力在旅行规划助手中应该放在哪一层?
它和前一篇能力如何协作?
哪些边界需要通过源码证据确认?
LangGraph 源码深潜:Memory、Store 与上下文工程机制解剖
https://jupiter-ws.cn/posts/agent-frameworks/14_langgraph_memory_store_context_engineering_source_deep_dive/
作者
Jupiter
发布于
2026-03-18
许可协议
CC BY-NC-SA 4.0