LangGraph源码学习路线:第 14 篇 Memory / Store 与上下文工程源码解剖
核心问题: LangGraph 如何把一次会话内的短期状态、跨会话长期记忆和模型上下文工程拆成不同的数据通道?
源码主线:
StateGraph.compile(checkpointer=..., store=...)→CompiledStateGraph→Runtime context→Checkpointer / BaseStore→thread state / cross-thread memory前置文章: 第 11 篇
Checkpoint、第 13 篇Streaming / Observability依赖基线:
langgraph==1.2.7、langchain==1.3.11、langchain-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 如何把一次会话内的短期状态、跨会话长期记忆和模型上下文工程拆成不同的数据通道?
学习目标
完成本篇后,读者必须能够:
- 画出Memory / Store / Context Engineering的构建期对象关系。
- 解释一次运行时调用如何进入主链。
- 说明关键分支、异常和停止条件。
- 区分公共 API、扩展接口和内部实现。
- 根据旅行规划助手场景做工程选型。
能力边界
| 能力 | 本篇是否覆盖 | 说明 |
|---|---|---|
| 构建期对象组装 | 是 | 解释公开参数如何变成运行时对象 |
| 运行时主链 | 是 | 解释 invoke/stream/tool/task 等主路径 |
| 分支与异常 | 是 | 解释失败、降级、重试、终止边界 |
| 扩展协作 | 是 | 解释与相邻框架组件如何组合 |
| 底层供应商实现 | 否 | 不展开模型、数据库或平台内部实现 |
2. 核心概念与最小心智模型
一句话定义
Memory / Store 是 LangGraph 中把执行状态和长期记忆分层保存的运行时能力,负责让 Agent 跨步骤、跨线程复用上下文,不负责自动决定哪些信息一定值得记住。
最小心智模型
Checkpoint / Streaming ↓Memory / Store / Context Engineering ↓Middleware / Guardrails ↓工程化 Agent 能力核心术语
| 术语 | 源码对象 | 语义 | 不要误解为 |
|---|---|---|---|
| Checkpoint | BaseCheckpointSaver | 按 thread 保存图执行状态和恢复点 | 用户长期偏好库 |
| Store | BaseStore | 跨线程保存 namespace/key/value 记忆 | 每一步 state 快照 |
| Thread | thread_id | 一次会话或任务的状态隔离维度 | 用户 ID |
| Context engineering | messages / 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, MessagesStatefrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.store.memory import InMemoryStorefrom 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 被合并进 MessagesState,store 的 Item 被转成 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]这里的输入对象是 state、store 和 config,输出对象是 list[BaseMessage]。注意输出不是 StateSnapshot,也不是 store item,而是模型真正能消费的 messages。
对象形态改变发生在 load_travel_preferences 和 build_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/关键文件
| 优先级 | 文件 | 核心对象 | 阅读目的 |
|---|---|---|---|
| 1 | libs/langgraph/langgraph/graph/state.py | StateGraph.compile | 理解 checkpointer/store 如何注入 compiled graph |
| 2 | libs/checkpoint/langgraph/checkpoint/base/__init__.py | BaseCheckpointSaver | 理解 thread state 的保存协议 |
| 3 | libs/checkpoint/langgraph/store/base/__init__.py | BaseStore | 理解 namespace/key/value 记忆协议 |
| 4 | libs/checkpoint/langgraph/store/memory/__init__.py | InMemoryStore | 理解本地 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 compileddef 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 | None 和 BaseStore | None。输出对象是 CompiledStateGraph,它是后续 invoke/stream 的入口。
对象形态改变发生在 builder.build_state_channels() 和 CompiledStateGraph(...):前者把用户定义的 state schema 转成 runtime channel/reducer,后者把 checkpointer 和 store 作为运行时依赖挂到 compiled graph 上。checkpointer 和 store 本身没有被合并成同一个对象。
读源码时要观察 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)输入对象是本轮 input 和 RunnableConfig。输出对象不是最终答案,而是交给 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))输入对象是可选的 store、config 和当前 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 与恢复边界
| 机制 | 适用条件 | 不适用条件 | 幂等要求 |
|---|---|---|---|
| Retry | store/search 临时失败、模型临时失败 | 已经写入不可重复副作用 | 写入 key 要稳定 |
| Fallback | store 不可用时退回 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 + checkpointer | checkpoint 只恢复同线程 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]输入对象是 namespace、key 和 value。输出对象在 put 路径上是持久化副作用,在 search 路径上是 Item 列表。它们都不是模型消息。
对象形态改变发生在 serialize_memory 和 deserialize_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. 参考资料与下一篇衔接
官方概念文档
-
LangGraph Memory
https://docs.langchain.com/oss/python/langgraph/add-memory -
LangGraph Persistence
https://docs.langchain.com/oss/python/langgraph/persistence -
LangGraph Context Engineering
https://docs.langchain.com/oss/python/langgraph/context
官方 API Reference
-
BaseStore
https://reference.langchain.com/python/langgraph/store/ -
StateGraph.compile
https://reference.langchain.com/python/langgraph/graphs/
官方源码
-
libs/checkpoint/langgraph/store/base/__init__.py
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/store/base/__init__.py -
libs/checkpoint/langgraph/store/memory/__init__.py
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/store/memory/__init__.py -
libs/checkpoint/langgraph/checkpoint/base/__init__.py
https://github.com/langchain-ai/langgraph/blob/1.2.7/libs/checkpoint/langgraph/checkpoint/base/__init__.py -
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 执行控制源码解剖需要继续回答:
这项能力在旅行规划助手中应该放在哪一层?它和前一篇能力如何协作?哪些边界需要通过源码证据确认?