LangGraph高级特性
流式处理(Streaming)、状态持久化(Persistence)、时间回溯(Time-Travel)、子图(Subgraphs)
流式处理(Streaming)
LangGraph 里,流式处理的范围更大。它不仅能输出模型生成过程,还能把图执行过程中的状态变化、节点进度、子图过程、自定义消息一边执行一边暴露出来
这也是 LangGraph 流式处理和普通模型流式输出最大的区别:
- 普通 LLM 流式:更关注“模型文字怎么一点点吐出来”
- LangGraph 流式:更关注“整张图现在跑到哪一步了,状态发生了什么变化”
可以先把 LangGraph Streaming 理解成:把图执行过程拆开给你看,而不是等整张图完全跑完才给最终结果。
LangGraph 图本身实现了 Runnable 接口,所以自然拥有这些流式能力。这也让它和 LangChain Runnable / LCEL 体系能够衔接起来
invoke():等整张图跑完,再返回最终结果stream()/astream():图在运行过程中,就把中间结果分批往外送
多补一个关键点:stream() 不是只有一种固定输出格式。你在调用它时,通常还会配合一个很重要的参数:stream_mode。这个参数决定了“图在执行过程中,到底往外流什么”,比如是流完整状态、流增量更新、流模型消息片段,还是流自定义进度信息
stream_model参数:
| 模式 | 含义 |
|---|---|
values |
每一步结束后输出当前完整状态快照 |
updates |
每一步结束后只输出本步的增量更新 |
messages |
输出 LLM 生成过程中的消息片段 / token |
custom |
输出节点内部主动写出的自定义消息 |
debug |
输出更完整、更底层的调试信息 |
如果你想同时拿到多种流,可以直接传列表,例如:
stream_mode=["updates", "custom"]
values和update的区别:
values:每一步都给你“当前完整 State 长什么样”updates:每一步只给你“这一小步到底改了哪些字段”.
从调试体验上说:
values更像“每一步的全量快照”updates更像“每一步的增量日志”
案例:流图状态
# 流式传输图状态:对比 stream_mode 为 updates 与 values 时,每一步向调用方推送的内容差异
# stream(..., stream_mode="updates")`:每步只推送“本节点本次改了什么”,更像增量日志
# `stream(..., stream_mode="values")`:每步推送“当前完整状态长什么样”,更像全量快照
#
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 声明约束
class DiliState(TypedDict):
topic: str
joke: str
# 节点1
def refine_topic(state: DiliState):
return {"topic": state["topic"] + " and cats"}
# 节点2
def generate_joke(state: DiliState):
return {"joke": f"This is a joke about {state['topic']}"}
def main():
graph = (
StateGraph(DiliState)
.add_node(refine_topic)
.add_node(generate_joke)
.add_edge(START, "refine_topic")
.add_edge("refine_topic", "generate_joke")
.add_edge("generate_joke", END)
.compile()
)
# updates:每步结束后只流出「本步对状态的更新」
for chunk in graph.stream({"topic": "ice cream"}, stream_mode="updates"):
print(chunk)
print()
# values:每步结束后流出「当前完整 state」(未写字段可能仍为空字符串等初始形态)
for chunk in graph.stream({"topic": "ice cream"}, stream_mode="values"):
print(chunk)
if __name__ == "__main__":
main()
"""
【输出示例】
{'refine_topic': {'topic': 'ice cream and cats'}}
{'generate_joke': {'joke': 'This is a joke about ice cream and cats'}}
{'topic': 'ice cream'}
{'topic': 'ice cream and cats'}
{'topic': 'ice cream and cats', 'joke': 'This is a joke about ice cream and cats'}
"""
多模式流与Debug
当你把 stream_mode 设成列表时,一次运行里就能同时拿到多种类型的流。这个案例最值得观察的是:
- 多种模式一起开时,输出长什么样
debug模式为什么更适合调试,而不是直接拿去做业务 UI
# :同一图依次演示 values、updates、列表组合 [values, updates]、以及 debug 模式
# stream_mode 为列表时,每次迭代得到 (mode, chunk) 元组,便于前端按类型分别处理
# `values` 看“全貌”,`updates` 看“增量”;`debug` 输出更细,适合调试,不适合直接当业务输出
# 这个案例的核心价值是帮你建立“同一张图可以同时暴露多种观察视角”,而不是背住某个模式名
# 节点函数返回的字典仍按 State 的 Reducer 合并;本例字段未显式 Annotated,默认就是覆盖更新
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class DiliState(TypedDict):
question: str
answer: str
confidence: float # 置信度分数
steps: list
def think(state: DiliState) -> DiliState:
"""思考节点:模拟多步推理,写入 steps。"""
question = state["question"]
steps = [f"分析问题: {question}", "检索相关知识", "形成初步答案"]
return {"steps": steps}
def respond(state: DiliState) -> DiliState:
"""回应节点:根据关键词生成答案与置信度。"""
question = state["question"]
if "天气" in question:
answer = "今天天气晴朗"
confidence = 0.9
elif "时间" in question:
answer = "现在是上午10点"
confidence = 0.8
else:
answer = "这是一个很好的问题"
confidence = 0.7
return {
"answer": answer,
"confidence": confidence,
}
def reflect(state: DiliState) -> DiliState:
"""反思节点:在 steps 上追加校验与结论。"""
answer = state["answer"]
confidence = state["confidence"]
steps = state.get("steps", [])
steps.append(f"验证答案: {answer}")
steps.append(f"置信度评估: {confidence}")
if confidence > 0.8:
conclusion = "高置信度答案"
elif confidence > 0.5:
conclusion = "中等置信度答案"
else:
conclusion = "低置信度答案"
steps.append(f"结论: {conclusion}")
return {"steps": steps}
def main():
builder = StateGraph(DiliState)
builder.add_node("think", think)
builder.add_node("respond", respond)
builder.add_node("reflect", reflect)
builder.add_edge(START, "think")
builder.add_edge("think", "respond")
builder.add_edge("respond", "reflect")
builder.add_edge("reflect", END)
graph = builder.compile()
print("=== LangGraph 多模式流式传输演示 ===\n")
input_state = {
"question": "今天天气怎么样?",
"answer": "",
"confidence": 0.0,
"steps": [],
}
print("--- 1. 使用 stream_mode='values' 模式 ---")
print("显示每一步执行后的完整状态:")
for chunk in graph.stream(input_state, stream_mode="values"):
print(f" {chunk}")
print("\n" + "=" * 60 + "\n")
print("--- 2. 使用 stream_mode='updates' 模式 ---")
print("只显示每一步的状态更新:")
for chunk in graph.stream(input_state, stream_mode="updates"):
print(f" {chunk}")
print("\n" + "=" * 60 + "\n")
print("--- 3. 同时使用 stream_mode=[values, updates] 多种流模式 ---")
print("同时显示完整状态和状态更新:")
for mode, chunk in graph.stream(input_state, stream_mode=["values", "updates"]):
print(f" [{mode}]: {chunk}")
print("\n" + "=" * 60 + "\n")
print("--- 4. 使用 debug 模式 ---")
print("显示详细的调试信息:")
try:
for chunk in graph.stream(input_state, stream_mode="debug"):
print(f" {chunk}")
except Exception as e:
print(f" Debug模式可能需要特殊配置: {e}")
if __name__ == "__main__":
main()
"""
【输出示例】
=== LangGraph 多模式流式传输演示 ===
--- 1. 使用 stream_mode='values' 模式 ---
显示每一步执行后的完整状态:
{'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': []}
{'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
{'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
{'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}
============================================================
--- 2. 使用 stream_mode='updates' 模式 ---
只显示每一步的状态更新:
{'think': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}}
{'respond': {'answer': '今天天气晴朗', 'confidence': 0.9}}
{'reflect': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}}
============================================================
--- 3. 同时使用 stream_mode=[values, updates] 多种流模式 ---
同时显示完整状态和状态更新:
[values]: {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': []}
[updates]: {'think': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}}
[values]: {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
[updates]: {'respond': {'answer': '今天天气晴朗', 'confidence': 0.9}}
[values]: {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}
[updates]: {'reflect': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}}
[values]: {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}
============================================================
--- 4. 使用 debug 模式 ---
显示详细的调试信息:
{'step': 1, 'timestamp': '2026-03-23T10:18:42.693927+00:00', 'type': 'task', 'payload': {'id': '11d771f2-98ac-b00c-931e-2b5bcb28f2ec', 'name': 'think', 'input': {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': []}, 'triggers': ('branch:to:think',)}}
{'step': 1, 'timestamp': '2026-03-23T10:18:42.693983+00:00', 'type': 'task_result', 'payload': {'id': '11d771f2-98ac-b00c-931e-2b5bcb28f2ec', 'name': 'think', 'error': None, 'result': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}, 'interrupts': []}}
{'step': 2, 'timestamp': '2026-03-23T10:18:42.694026+00:00', 'type': 'task', 'payload': {'id': 'a3af09a2-2f1a-d7f5-7957-55d931a52ed7', 'name': 'respond', 'input': {'question': '今天天气怎么样?', 'answer': '', 'confidence': 0.0, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}, 'triggers': ('branch:to:respond',)}}
{'step': 2, 'timestamp': '2026-03-23T10:18:42.694074+00:00', 'type': 'task_result', 'payload': {'id': 'a3af09a2-2f1a-d7f5-7957-55d931a52ed7', 'name': 'respond', 'error': None, 'result': {'answer': '今天天气晴朗', 'confidence': 0.9}, 'interrupts': []}}
{'step': 3, 'timestamp': '2026-03-23T10:18:42.694113+00:00', 'type': 'task', 'payload': {'id': 'c2627962-4fda-05bb-b1f5-5c40e36195a7', 'name': 'reflect', 'input': {'question': '今天天气怎么样?', 'answer': '今天天气晴朗', 'confidence': 0.9, 'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案']}, 'triggers': ('branch:to:reflect',)}}
{'step': 3, 'timestamp': '2026-03-23T10:18:42.694234+00:00', 'type': 'task_result', 'payload': {'id': 'c2627962-4fda-05bb-b1f5-5c40e36195a7', 'name': 'reflect', 'error': None, 'result': {'steps': ['分析问题: 今天天气怎么样?', '检索相关知识', '形成初步答案', '验证答案: 今天天气晴朗', '置信度评估: 0.9', '结论: 高置信度答案']}, 'interrupts': []}}
"""
LLM逐token流式输出:
如果某个节点里调用了大模型,messages 模式就特别有价值。它可以帮助你在图运行过程中,直接拿到模型生成的消息片段,而不用等节点完全执行完。这也是为什么 LangGraph Streaming 不只是“图状态流”,它还能把模型输出也纳入统一流式体系。
# messages 流模式:从图中调用 LLM 的节点逐 token(或片段)推送输出,便于打字机效果
# stream_mode="messages" 时,每次迭代一般为 (message_chunk, metadata):chunk 为模型输出片段,metadata 标明节点等上下文
# 流式消费侧通常关心 `chunk.content` 和 `metadata`;前者是输出片段,后者帮助你知道这些片段来自哪个节点。
# 需配置环境变量(如 aliQwen-api)与网络;模型、base_url 按你本地教程为准
import os
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, START
from dotenv import load_dotenv
load_dotenv(encoding="utf-8")
class State(TypedDict):
query: str
answer: str
def node(state: State):
print("开始调用 node 节点")
model = init_chat_model(
model="qwen-plus",
model_provider="openai",
api_key=os.getenv("aliQwen-api"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
llm_result = model.invoke([("user", state["query"])])
print("llm invoke 结束", end="\n\n")
return {"answer": llm_result}
def main():
graph = (
StateGraph(state_schema=State).add_node(node).add_edge(START, "node").compile()
)
inputs = {"query": "帮我生成一个200字的小学生作文,主题为我的一天"}
# messages:从图内触发的大模型调用处流式输出;(chunk, metadata) 见官方文档
for chunk, _metadata in graph.stream(inputs, stream_mode="messages"):
# print(f"type of chunk:{type(chunk)}") # 调试时可打开
print(chunk.content, end="")
# print(chunk, end="")
if __name__ == "__main__":
main()
自定义数据流:
有时候你想流的不是状态,也不是 token,而是业务自定义进度。例如:
- “正在检索知识库”
- “正在生成回答”
- “当前进度 60%”
这时就可以在节点内部通过流写入器主动写出自定义数据,然后用 custom 模式接收
这两个案例的关系可以这样理解:
StreamCustomDataSimple.py:先看最小可运行版本StreamCustomData.py:再看更贴近真实项目的进度与组合模式写法
# 自定义流(custom)最简版:在节点内通过 get_stream_writer() 写入任意可序列化数据,stream 侧用 custom 接收。
# 这是 `custom` 模式的最小案例,重点不是业务逻辑,而是先看懂“节点内部怎么主动写出一段流式消息”
# `get_stream_writer()` 仅在图执行(stream/astream)过程中有效;调用 `graph.stream` 时,`stream_mode` 里必须包含 `custom`
# 自定义块与状态更新是分开的:前者更适合 UI/日志/进度提示,后者仍然通过 State 和 Reducer 管理。
from typing import TypedDict
from langgraph.config import get_stream_writer
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
query: str
answer: str
def nodeStart(state: State):
writer = get_stream_writer()
writer({"startMsg", "嘿,是我,你要进我的米奇妙妙屋吗?"})
return{"answer:", "嗨嗨嗨,来了哈"}
def node(state: State):
writer = get_stream_writer()
writer({"custom_key": "欢迎来到线上Agent班级学习,O(∩_∩)O"})
return {"answer": "some data"}
def main():
graph = (
StateGraph(State)
.add_node(node)
.add_edge(START, "node")
.add_edge("node", END)
.compile()
)
# 仅 custom:for chunk in graph.stream({"query": "example"}, stream_mode=["custom"]): print(chunk)
# custom + updates:for mode, chunk in graph.stream(..., stream_mode=["updates", "custom"]): ...
for chunk in graph.stream({"query": "example"}, stream_mode=["values", "custom"]):
print(chunk)
if __name__ == "__main__":
main()
进阶
# `get_stream_writer()` 负责把“图运行过程中的自定义消息”主动往外推;它和节点返回的状态更新是两条并行通道。
# `stream_mode=["custom", "updates"]` 时,迭代得到 `(mode, chunk)`,非常适合前端一边看业务进度,一边看状态更新。
# 本例最值得观察的是:`writer(...)` 写出的 `custom` 数据不会自动进 State;节点真正写回图状态的,仍然是最后 return 的那份 dict。
from typing import TypedDict
from langgraph.config import get_stream_writer
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
query: str
answer: str
progress: list
def node_with_custom_streaming(state: State) -> State:
"""带自定义流式传输的节点:边写自定义流边更新状态。"""
writer = get_stream_writer()
writer({"custom_key": "开始处理查询"})
writer({"progress": "步骤1: 分析查询内容", "status": "running"})
query = state["query"]
writer({"progress": "步骤2: 生成结果", "status": "running"})
writer({"progress": "步骤3: 完成处理", "status": "completed"})
writer({"custom_key": "查询处理完成"})
result = f"处理结果: {query.upper()}"
return {
"answer": result,
"progress": state.get("progress", []) + ["处理完成"],
}
def main():
print("=== LangGraph 自定义数据流式传输演示 ===\n")
graph = (
StateGraph(State)
.add_node("node_with_custom_streaming", node_with_custom_streaming)
.add_edge(START, "node_with_custom_streaming")
.add_edge("node_with_custom_streaming", END)
.compile()
)
inputs = {"query": "hello world", "answer": "", "progress": []}
print("--- 1. 单独使用 custom 流模式 ---")
try:
for chunk in graph.stream(inputs, stream_mode="custom"):
print(f"自定义数据块: {chunk}")
except Exception as e:
print(f"错误: {e}")
print(
"说明: 在 Graph API 中,自定义流数据需在节点中通过 get_stream_writer 发送"
)
print("\n" + "=" * 50 + "\n")
print("--- 2. 单独使用 updates 流模式 ---")
for chunk in graph.stream(inputs, stream_mode="updates"):
print(f"状态更新: {chunk}")
print("\n" + "=" * 50 + "\n")
print("--- 3. 同时使用 custom 和 updates 流模式 ---")
try:
for mode, chunk in graph.stream(inputs, stream_mode=["custom", "updates"]):
print(f"[{mode}]: {chunk}")
except Exception as e:
print(f"错误: {e}")
print("说明: 请确认 LangGraph 版本支持多模式流")
if __name__ == "__main__":
main()
"""
【输出示例】
=== LangGraph 自定义数据流式传输演示 ===
--- 1. 单独使用 custom 流模式 ---
自定义数据块: {'custom_key': '开始处理查询'}
自定义数据块: {'progress': '步骤1: 分析查询内容', 'status': 'running'}
自定义数据块: {'progress': '步骤2: 生成结果', 'status': 'running'}
自定义数据块: {'progress': '步骤3: 完成处理', 'status': 'completed'}
自定义数据块: {'custom_key': '查询处理完成'}
==================================================
--- 2. 单独使用 updates 流模式 ---
状态更新: {'node_with_custom_streaming': {'answer': '处理结果: HELLO WORLD', 'progress': ['处理完成']}}
==================================================
--- 3. 同时使用 custom 和 updates 流模式 ---
[custom]: {'custom_key': '开始处理查询'}
[custom]: {'progress': '步骤1: 分析查询内容', 'status': 'running'}
[custom]: {'progress': '步骤2: 生成结果', 'status': 'running'}
[custom]: {'progress': '步骤3: 完成处理', 'status': 'completed'}
[custom]: {'custom_key': '查询处理完成'}
[updates]: {'node_with_custom_streaming': {'answer': '处理结果: HELLO WORLD', 'progress': ['处理完成']}}
"""
什么时候用什么流
| 需求 | 更适合的模式 |
|---|---|
| 想看整张图当前完整状态 | values |
| 想看每一步改了什么 | updates |
| 想看 LLM token | messages |
| 想推送业务自定义进度 | custom |
| 想详细调试图内部执行 | debug |
状态持久化
流式处理解决的是“图在运行过程中怎么被看见”,那状态持久化解决的就是另一件很重要的事:图跑到一半、跑完之后,状态能不能被记住,并在下次继续使用。
LangGraph 的持久化核心围绕 checkpoint(检查点) 展开。官方文档的说法可以概括成一句话:当你给图配置了 checkpointer,图在执行的每一步都可以把当前状态保存成一个检查点
这些检查点会被组织到某个 thread 下面。入门阶段,可以先把 thread 理解成“同一条会话或同一条工作流链路”的 ID 容器,而最常见的就是通过:
{"configurable": {"thread_id": "..."}}
短期记忆- CheckPointer:
这里说的 Checkpointer,本质上是:
- 按
thread_id保存图的执行状态 - 让同一个线程下的多次调用可以继续沿用之前的状态
所以 Checkpointer 更像是:线程内、会话内、工作流运行期的短期记忆
长期记忆- Store/BaseStore
Checkpointer 保存线程内状态,Store 用来保存跨线程共享的长期信息, 所以两者最核心的区别可以这样记:
- Checkpointer:保存图在某条 thread 里的运行状态
- Store / BaseStore:保存跨 thread、跨会话仍然要长期保留的数据
例如:
- 用户偏好
- 长期业务事实
- 跨会话共享的知识片段
这些都更适合放 Store,而不是硬塞进单条 thread 的 checkpoint 链里。
持久化后端怎么选:
- 内存:最适合学习和临时验证
- SQLite:适合本地开发、小型项目、单机轻量部署
- Postgres / Redis / 其他数据库后端:更适合生产环境
案例-内存检查点
# 内存检查点 InMemorySaver:编译图时传入 checkpointer,用 thread_id 区分会话,演示 get_state / get_state_history / 二次 invoke。
# compile(checkpointer=...) 后,每次 invoke 会在检查点中留下快照;config["configurable"]["thread_id"] 标识一条「对话线程」
# get_state(config) 取当前线程最新状态;get_state_history(config) 取历史快照序列(用于调试或时间回溯)
# `InMemorySaver` 数据仅在进程内存中,进程结束即丢失;它最适合先帮助你理解“checkpoint 到底是什么”
# Persistence 不只是“把结果存起来”,而是把图每一步的状态历史都保留下来,为后面的 Time-Travel 打基础
from typing import Annotated
import operator
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict
class PersistenceDemoState(TypedDict):
# operator.add:列表/数值等按「相加」语义合并(列表相当于拼接)
messages: Annotated[list, operator.add]
step_count: Annotated[int, operator.add]
def step_one(state: PersistenceDemoState) -> dict:
print("执行步骤 1")
return {
"messages": ["执行了步骤 1"],
"step_count": 1,
}
def step_two(state: PersistenceDemoState) -> dict:
print("执行步骤 2")
return {
"messages": ["执行了步骤 2"],
"step_count": 1,
}
def step_three(state: PersistenceDemoState) -> dict:
print("执行步骤 3")
return {
"messages": ["执行了步骤 3"],
"step_count": 1,
}
def create_graph():
builder = StateGraph(PersistenceDemoState)
builder.add_node("step_one", step_one)
builder.add_node("step_two", step_two)
builder.add_node("step_three", step_three)
builder.add_edge(START, "step_one")
builder.add_edge("step_one", "step_two")
builder.add_edge("step_two", "step_three")
builder.add_edge("step_three", END)
return builder
def main():
print("=== LangGraph 1.0 内存持久化存储演示 ===\n")
graph = create_graph()
app = graph.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "user_13811112222"}}
print("1. 首次执行工作流:")
result = app.invoke(
{
"messages": ["开始执行"],
"step_count": 0,
},
config,
)
print(f"执行结果 result: {result}\n")
print("2. 检查存储的状态:")
saved_state = app.get_state(config)
print(f"保存的状态: {saved_state.values}")
print(f"下一个节点: {saved_state.next}\n")
# 正序遍历:从最早到最晚的检查点快照
history = app.get_state_history(config)
for checkpoint in history:
print("=" * 50)
print(f"当前状态: {checkpoint.values}")
print("=" * 80)
print("3. 恢复执行工作流:")
# 工作流若已结束,再次 invoke(None, config) 通常直接返回已落盘的结果
result2 = app.invoke(None, config)
print(f"恢复执行结果: {result2}\n")
print("=== 演示结束 ===")
if __name__ == "__main__":
main()
SQLite检查点
# 依赖包:项目根目录 `requirements.txt` 已包含 `langgraph-checkpoint-sqlite`;全量安装用 `pip install -r requirements.txt`,或单独 `pip install langgraph-checkpoint-sqlite`。生产环境更常用 Postgres(`langgraph-checkpoint-postgres`)等实现。
# SqliteSaver(conn=...) 与 sqlite3.connect 配合;数据库文件路径需本机可写,目录需事先存在。
# 与 InMemorySaver 用法相同:`compile(checkpointer=...)`、`invoke(..., config)`、`get_state(config)`,区别主要在于存储介质
#
import sqlite3
import operator
from pathlib import Path
from typing import Annotated, TypedDict
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, START, END
class MyState(TypedDict):
messages: Annotated[list, operator.add]
def node_1(state: MyState):
return {"messages": ["abc", "def"]}
def main():
# 默认写在项目旁,避免硬编码 Windows 盘符
db_dir = Path(__file__).resolve().parent / "sqlite_checkpoints"
db_dir.mkdir(parents=True, exist_ok=True)
db_path = db_dir / "sqlite_data.db"
conn = sqlite3.connect(database=str(db_path), check_same_thread=False)
sqlite_db = SqliteSaver(conn=conn)
builder = StateGraph(MyState)
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", END)
graph = builder.compile(checkpointer=sqlite_db)
# 同一 thread_id 表示同一会话;多次执行会累积检查点,调试时可删 .db 或换 thread_id
config = {"configurable": {"thread_id": "user-001"}}
initial_state = graph.get_state(config)
print(f"Initial state: {initial_state}")
result = graph.invoke({"messages": []}, config)
print(f"Result: {result}")
print()
print("====================查看执行后的状态====================")
final_state = graph.get_state(config)
print()
print(f"Final state: {final_state}")
conn.close()
if __name__ == "__main__":
main()
"""
【输出示例】
Initial state: StateSnapshot(values={}, next=(), config={'configurable': {'thread_id': 'user-001'}}, metadata=None, created_at=None, parent_config=None, tasks=(), interrupts=())
Result: {'messages': ['abc', 'def']}
====================查看执行后的状态====================
Final state: StateSnapshot(values={'messages': ['abc', 'def']}, next=(), config={'configurable': {'thread_id': 'user-001', 'checkpoint_ns': '', 'checkpoint_id': '1f1272f0-d724-675e-8001-bb885d01bb16'}}, metadata={'source': 'loop', 'step': 1, 'parents': {}}, created_at='2026-03-24T03:10:46.773535+00:00', parent_config={'configurable': {'thread_id': 'user-001', 'checkpoint_ns': '', 'checkpoint_id': '1f1272f0-d723-6a48-8000-d3aac2954c9d'}}, tasks=(), interrupts=())
"""
预建Agent与持久化
# create_agent 搭配 InMemorySaver,实现同一 thread_id 下的多轮对话与上下文延续
# `create_agent(..., checkpointer=...)` 说明高层 Agent 接口底层仍然可以吃到 LangGraph 的持久化能力
# 同一 `thread_id` 下的多次 invoke 会连续使用同一条线程状态,这也是“多轮对话为什么能续上”的关键
#
import os
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver
from dotenv import load_dotenv
load_dotenv(encoding="utf-8")
def main():
llm = init_chat_model(
model="qwen-plus",
model_provider="openai",
api_key=os.getenv("aliQwen-api"),
temperature=0.0,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
checkPointer = InMemmorySaver()
agent = creat_agnet(model=llm, checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-001"}}
msg1 = agent.invoke(
{"messages": [("user", "你好,我叫张三,喜欢足球,60字内简洁回复")]},
config,
)
msg1["messages"][-1].pretty_print()
msg2 = agent.invoke(
{"messages": [("user", "我叫什么?我喜欢做什么?")]},
config,
)
msg2["messages"][-1].pretty_print()
if __name__ == "__main__":
main()
"""
【输出示例】
================================== Ai Message ==================================
你好张三!很高兴认识一位足球爱好者,祝你绿茵场上挥洒汗水、享受快乐!
================================== Ai Message ==================================
你叫张三,喜欢足球!⚽
"""
时间回溯:
它解决的问题不是“怎么正常跑一张图”,而是:图已经跑过了,我能不能回到历史中的某一步,从那里重新继续跑,甚至改一改状态再跑。在 LangGraph 里,因为前面已经有了 checkpoint 链,所以时间回溯就变得自然了。
回溯价值:
真实项目里很常见的问题包括:这次为什么回答对了,我想回看中间过程;这次为什么跑偏了,我想定位到底在哪一步开始出问题;如果在某一步换一个状态、换一条分支,后面会发生什么。
所以时间回溯特别适合:调试、复盘、分支探索、人工修正后重跑。
回溯怎么做
- 先跑一遍图,生成历史 checkpoint
- 用
get_state_history(...)找到你想回到的那个历史点 - 视情况决定是原样恢复,还是先
update_state(...)改状态 - 再从那个历史点继续
invoke(...)或stream(...)
如果你把这四步理解成:
- 先有“历史录像”
- 再选“从哪一帧切回去”
- 可选“改一下剧本”
- 然后“从那里继续往后演”
就会非常好记。
# 在带 checkpointer 的图上先跑完全程,用 get_state_history 选历史快照,update_state 改写状态后 invoke(None, new_config) 从分叉点重跑
# 时间回溯(Time-Travel)
# 基本步骤:(1)invoke/stream 跑图;
# (2)get_state_history 找 checkpoint_id;
# (3)可选 update_state 改 values;
# (4)invoke(None, config) 从指定检查点继续
# update_state 返回的新 config 含新 checkpoint,后续应使用该 config 作为「时间旅行起点」
# NotRequired 表示该键在开始时可不出现,适合分步填满的故事状态
#
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict, NotRequired
class StoryState(TypedDict):
"""故事状态:字段均可逐步写入。"""
character: NotRequired[str]
setting: NotRequired[str]
plot: NotRequired[str]
ending: NotRequired[str]
def create_character(state: StoryState):
"""创建故事角色(模拟 LLM 节点)。"""
print("执行节点: create_character")
mock_character = "一只会说话的猫"
print(f"创建的角色: {mock_character}")
return {"character": mock_character}
def set_setting(state: StoryState):
"""设置故事背景。"""
print("执行节点: set_setting")
mock_setting = "在一个神秘的图书馆里"
print(f"设置的背景: {mock_setting}")
return {"setting": mock_setting}
def develop_plot(state: StoryState):
"""发展故事情节。"""
print("执行节点: develop_plot")
character = state.get("character", "未知角色")
setting = state.get("setting", "未知背景")
mock_plot = f"{character}在{setting}发现了一本会发光的书"
print(f"发展的剧情: {mock_plot}")
return {"plot": mock_plot}
def write_ending(state: StoryState):
"""编写故事结局。"""
print("执行节点: write_ending")
plot = state.get("plot", "未知剧情")
mock_ending = f"当{plot}时,整个图书馆都被魔法光芒照亮了"
print(f"编写的结局: {mock_ending}")
return {"ending": mock_ending}
def main():
print("=== LangGraph 高级时间旅行演示 ===\n")
workflow = StateGraph(StoryState)
workflow.add_node("create_character", create_character)
workflow.add_node("set_setting", set_setting)
workflow.add_node("develop_plot", develop_plot)
workflow.add_node("write_ending", write_ending)
workflow.add_edge(START, "create_character")
workflow.add_edge("create_character", "set_setting")
workflow.add_edge("set_setting", "develop_plot")
workflow.add_edge("develop_plot", "write_ending")
workflow.add_edge("write_ending", END)
graph = workflow.compile(checkpointer = InMemorySver())
print("1. 生成第一个故事...")
config1 = {
"configurable":{
"thread_id":str(uuid.uuid4())
}
}
story1 = graph.invoke({}, config1)
print(f"角色: {story1['character']}")
print(f"背景: {story1['setting']}")
print("2. 查看第一个故事的历史状态...")
states1 = list(graph..get_state_history(config1))
print("历史状态:")
for i, state in enumerate(states1):
print(f" {i}. 下一步节点: {state.next}")
print(f" 检查点ID: {state.config['configurable']['checkpoint_id']}")
if state.values:
print(f" 状态值: {state.values}")
print()
print("3. 从中间状态恢复执行,创建第二个故事...")
# 索引需与 get_state_history 顺序一致;states1[2] 对应 create_character 执行后的快照(请以本地打印为准调整)
character_state = states1[2]
print(f"选中的状态值: {character_state.values}")
new_config = graph.update_state(
character_state.config,
values={"character": "一只会飞的龙"},
)
print(f"新配置: {new_config}")
print("4. 从新检查点恢复执行,生成第二个故事...")
story2 = graph.invoke(None, new_config)
print(f"新角色: {story2['character']}")
print(f"背景: {story2['setting']}")
print("5. 比较两个故事:")
print(" 故事1:")
print(f" 角色: {story1['character']}")
print(f" 背景: {story1['setting']}")
print(" 故事2:")
print(f" 角色: {story2['character']}")
print(f" 背景: {story2['setting']}")
if __name__ == "__main__":
main()
子图
当图开始变复杂时,最自然的问题就是:能不能把一整张图,当成另一张图里的一个节点来复用?LangGraph 里的子图,可以理解成:把一个已经编译好的图,嵌入到另一张更大的父图里。所以子图的价值,本质上在于:复杂流程拆分、模块化复用、父子流程解耦

三种理解方式:
- 最简单模式:把编译后的子图直接当成父图里的节点
- 共享字段模式:父图和子图共享部分状态字段
- 状态转换模式:父图状态和子图状态结构不同,需要代理节点做转换
1.子图作为节点
# 子图作为节点:将 compile 后的子图直接 add_node 进父图;父子共用同一 State 类型时,由 Reducer 合并 messages。
from operator import add
from typing import Annotated, TypedDict
from langgraph.constants import END
from langgraph.graph import StateGraph, START
class DiliState(TypedDict):
"""
状态:messages 使用 operator.add 合并策略——新返回的列表与原有列表拼接(非覆盖)。
"""
messages: Annotated[list[str], add]
def sub_node(state: DiliState) -> DiliState:
return {"messages": ["response from subgraph"]}
# --- 子图 ---
subgraph_builder = StateGraph(DiliState)
subgraph_builder.add_node("sub_node", sub_node)
subgraph_builder.add_edge(START, "sub_node")
subgraph_builder.add_edge("sub_node", END)
subgraph = subgraph_builder.compile()
# --- 父图:节点即子图 ---
builder = StateGraph(DiliState)
builder.add_node("subgraph_node", subgraph)
builder.add_edge(START, "subgraph_node")
builder.add_edge("subgraph_node", END)
graph = builder.compile()
"""
子图调用的状态传递逻辑当主图调用子图节点时,整个过程会触发两次状态合并:
第一步:主图把初始状态 {"messages": ["main-graph"]} 传递给子图
第二步:子图内部执行 sub_node,返回 {"messages": ["response from subgraph"]},
由于 add 策略,子图会把传入的 ["main-graph"] 和返回的 ["response from subgraph"] 拼接,
得到 ["main-graph", "response from subgraph"]
第三步:子图执行完成后,主图会再次应用 add 策略,
把主图原有的 ["main-graph"]
和子图返回的 ["main-graph", "response from subgraph"] 拼接,
最终得到 ["main-graph", "main-graph", "response from subgraph"]
"""
print(graph.invoke({"messages": ["main-graph"]}))
print()
父子图共享状态字段:
当父图和子图共享部分字段时,理解重点就变成了:
- 哪些字段是共享的
- 哪些字段是子图内部私有的
- 父图最终能看到哪些结果
它帮助读者意识到:子图不是完全孤立的小黑盒,它可以和父图共享部分状态空间。
# 父图 State 与子图 State 均含 parent_messages;子图内可改共享列表;子图私有字段不会出现在父图最终 state(父 schema 未声明)
# 子图 compile 后作为父图的一个 node;父图 invoke 的初始状态会传入子图(字段对齐时)
# 子图 TypedDict 多出的键(如 sub_message)仅在子图内部可见,父图输出按 ParentState 过滤
#
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class ParentState(TypedDict):
parent_messages: list
class SubgraphState(TypedDict):
parent_messages: list
sub_message: str
def subgraph_node(state: SubgraphState) -> SubgraphState:
"""子图节点:更新共享列表 + 写入子图私有字段。"""
state["parent_messages"].append("message from subgraph updateO(∩_∩)O")
state["sub_message"] = "subgraph private message"
return state
def parent_node(state: ParentState) -> ParentState:
"""父图首节点:保证 parent_messages 为列表并追加父侧消息。"""
if not state.get("parent_messages"):
state["parent_messages"] = []
state["parent_messages"].append("message from 父亲 node")
return state
def build_subgraph():
"""构建并返回编译后的子图。"""
sub_builder = StateGraph(SubgraphState)
sub_builder.add_node("sub_node", subgraph_node)
sub_builder.add_edge(START, "sub_node")
sub_builder.add_edge("sub_node", END)
return sub_builder.compile()
def build_parent_graph(compiled_subgraph):
"""构建并返回编译后的父图。"""
builder = StateGraph(ParentState)
builder.add_node("parent_node", parent_node)
builder.add_node("subgraph_node", compiled_subgraph)
builder.add_edge(START, "parent_node")
builder.add_edge("parent_node", "subgraph_node")
builder.add_edge("subgraph_node", END)
return builder.compile()
def main():
# 构建子图
compiled_subgraph = build_subgraph()
# 构建父图
parent_graph = build_parent_graph(compiled_subgraph)
initial_state = {"parent_messages": ["我是父消息"]}
print("初始状态:", initial_state)
# 父图执行时会进入子图;sub_message 不会出现在父图最终 dict(ParentState 无此键)
final_state = parent_graph.invoke(initial_state)
print("\n执行后最终状态:", final_state)
if __name__ == "__main__":
main()
"""
初始状态: {'parent_messages': ['我是父消息']}
执行后最终状态: {'parent_messages': ['我是父消息', 'message from 父亲 node', 'message from subgraph updateO(∩_∩)O']}
"""
代理节点与状态转换
很多时候,父图和子图的状态结构根本不是一套:
- 父图关心用户请求和最终答案
- 子图关心分析输入、中间步骤、分析结果
这时就不能直接把子图粗暴塞进去,而更推荐用一个父图代理节点完成三件事:
- 父状态 → 子图输入
- 调用子图
- 子图输出 → 父状态
这个模式非常重要,因为它是“跨图状态解耦”的关键
# 父子 State 字段完全不同,不能直接把子图挂成节点;在父图节点里手动构造子图输入、invoke 子图、再把结果写回父 State
# 父状态 ParentState 专注业务(user_query / final_answer),子状态 SubgraphState 专注分析过程,二者无交集字段时必须「代理节点」做映射
# 代理节点签名仍为 (父 state) -> 父 state 的增量/全量;内部调用 compiled_subgraph.invoke(subgraph_input)
# 该模式可扩展任意形状的状态转换,是多智能体、流水线拆图时的常用技巧
# 父子图状态不一致时,不要硬凑,直接用“代理节点”做父→子、子→父的状态转换
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 定义不同结构的父子图状态
# 父图状态:仅包含用户查询和最终答案(与子图状态完全不同)
class ParentState(TypedDict):
user_query: str
final_answer: str | None
# 子图状态:专注于分析逻辑(与父图状态无重叠字段)
class SubgraphState(TypedDict):
analysis_input: str
analysis_result: str
intermediate_steps: list
# 定义子图核心逻辑
def subgraph_analysis_node(state: SubgraphState) -> SubgraphState:
"""子图核心节点:模拟分析流水线。"""
query = state["analysis_input"]
state["intermediate_steps"] = [f"解析查询:{query}", "执行分析逻辑", "生成结果"]
state["analysis_result"] = f"针对「{query}」的分析结果:这是子图处理后的内容"
return state
# 创建子图节点实例
def build_subgraph() -> StateGraph:
sub_builder = StateGraph(SubgraphState)
sub_builder.add_node("subgraph_analysis_node", subgraph_analysis_node)
sub_builder.add_edge(START, "subgraph_analysis_node")
sub_builder.add_edge("subgraph_analysis_node", END)
return sub_builder.compile()
compiled_subgraph = build_subgraph()
# 定义父图代理节点(核心:状态转换+调用子图)从节点调用图
def call_subgraph_proxy(state: ParentState) -> ParentState:
"""
父图代理节点:
1) 父 -> 子:拼子图输入;
2) 调用子图 invoke;
3) 子 -> 父:把 analysis_result 写入 final_answer。
"""
subgraph_input = {
"analysis_input": state["user_query"],
"intermediate_steps": [],
"analysis_result": "",
}
subgraph_response = compiled_subgraph.invoke(subgraph_input)
return {
"user_query": state["user_query"],
"final_answer": subgraph_response["analysis_result"],
}
def build_parent_graph():
parent_builder = StateGraph(ParentState)
# 添加代理节点(核心:手动处理状态转换+调用子图)
parent_builder.add_node("call_subgraph_proxy", call_subgraph_proxy)
# 父图执行链路:START → 代理节点 → END
parent_builder.add_edge(START, "call_subgraph_proxy")
parent_builder.add_edge("call_subgraph_proxy", END)
return parent_builder.compile()
def main():
# 1. 构建父图
parent_graph = build_parent_graph()
# 2. 定义父图初始状态(仅包含user_query,符合父图状态结构)
initial_state = {
"user_query": "请分析Python中StateGraph的使用场景",
"final_answer": None,
}
print("父图初始状态:", initial_state)
# 3. 执行父图,实际而言父图调用了call_subgraph_proxy
final_state = parent_graph.invoke(initial_state)
# 4. 输出结果
print("\n父图最终状态:", final_state)
print("\n子图处理后的最终答案:", final_state["final_answer"])
if __name__ == "__main__":
main()
"""
【输出示例】
父图初始状态: {'user_query': '请分析Python中StateGraph的使用场景', 'final_answer': None}
父图最终状态: {'user_query': '请分析Python中StateGraph的使用场景', 'final_answer': '针对「请分析Python中StateGraph的使用场景」的分析结果:这是子图处理后的内容'}
子图处理后的最终答案: 针对「请分析Python中StateGraph的使用场景」的分析结果:这是子图处理后的内容
"""

浙公网安备 33010602011771号