LangGraph 入门指南
一、LangGraph 是什么
1.1 命名由来
Lang:
- 生态纽带:延续 LangChain 命名体系,是 LangChain 生态的官方核心组件
- 领域聚焦:明确指向 LLM 驱动的智能体(Agent)开发
- 用户认知:降低学习门槛,让 LangChain 开发者快速理解其定位
Graph:
- 计算图:将 AI 工作流建模为有向图,节点表示执行单元,边表示执行路径
- 状态图:内置强状态管理,图中流动的是包含上下文、历史、执行状态的完整"状态对象"
- 突破线性限制:天然支持循环迭代、并行执行、动态分支、断点续跑
核心公式:LangGraph = LLM 驱动 + 图结构编排 + 状态管理
1.2 与 LangChain 的关系
| 框架 | 核心结构 | 适用场景 |
|---|---|---|
| LangChain(Chain 链) | 线性流程 | 简单问答、RAG、单步骤任务 |
| LangGraph(Graph 图) | 有向图 + 状态 | 复杂 Agent、长会话、多轮反思 |
链是图的特例,图是链的超集,二者共同构成 LangChain 生态的"基础层 + 高级层"架构。
1.3 回顾 LangChain 六大核心组件
| 组件 | 作用 | 示例 |
|---|---|---|
| 模型集成(Models) | 统一封装各类 LLM、Embedding 模型 | 一行代码切换 GPT-4/Claude/Gemini |
| 提示模板(Prompts) | 标准化提示词,提升输出稳定性 | 问答模板、摘要模板 |
| 记忆(Memory) | 保存对话历史,支持多轮交互 | 会话缓冲区、摘要记忆 |
| 检索(Retrievers) | 连接外部知识库,实现 RAG | 向量数据库检索、文档搜索 |
| 工具(Tools) | 让 LLM 调用外部能力 | 计算器、API 调用、数据库查询 |
| 链(Chains) | 串联组件,实现复杂逻辑 | LCEL 线性链、RAG 链 |
二、LangGraph核心概念:四大基石
| 组件 | 作用 | 通俗理解 |
|---|---|---|
| State(状态) | 全局共享数据载体,所有节点读写 | 工作流的"全局数据库" |
| Node(节点) | 执行单元,封装业务逻辑 | 图中的"站点",每个站点做一件具体事 |
| Edge(边) | 定义节点间执行顺序 | 站点间的"道路",分普通边和条件边 |
| Checkpointer(持久化) | 保存/恢复状态,支持断点续跑 | 工作流的"存档点" |
三、快速上手
3.1 需求
先问个问题,根据问题分类成闲聊和技术类,然后闲聊类走闲聊分支,技术类走技术分支。
lua
+-----------+
| __start__ |
+-----------+
*
*
*
+-------------------+
| classify_question |
+-------------------+
... ...
. .
+------------+ +------------+
| chat_reply | | tech_reply |
+------------+ +------------+
*** ***
* *
** **
+---------+
| __end__ |
+---------+
3.2 完整代码
python
from langchain_core.output_parsers import StrOutputParser
from langgraph.graph import START, END, StateGraph
from llm import llm
from typing import TypedDict
# ==================== 定义全局状态 State ====================
class ChatState(TypedDict):
question: str # 用户输入的问题
answer: str # 最终生成的回答
q_type: str # 问题分类结果:chat(闲聊)或 tech(技术)
# ==================== 定义节点函数 ====================
def classify_question(state: ChatState) -> ChatState:
chain = llm | StrOutputParser()
res = chain.invoke(f"判断问题类型,返回 chat 或 tech:{state['question']}")
return {"q_type": res}
def chat_reply(state: ChatState) -> ChatState:
chain = llm | StrOutputParser()
res = chain.invoke(f"友好闲聊回复:{state['question']}")
return {"answer": res}
def tech_reply(state: ChatState) -> ChatState:
chain = llm | StrOutputParser()
res = chain.invoke(f"专业简洁解答技术问题:{state['question']}")
return {"answer": res}
# ==================== 定义条件路由 ====================
NODE_CLASSIFY_QUESTION = "classify_question"
NODE_CHAT_REPLY = "chat_reply"
NODE_TECH_REPLY = "tech_reply"
nodes = [NODE_CHAT_REPLY, NODE_TECH_REPLY]
path_map = {node: node for node in nodes}
def route_by_type(state: ChatState) -> str:
if state["q_type"] == "chat":
return NODE_CHAT_REPLY
else:
return NODE_TECH_REPLY
# ==================== 组装并编译图 ====================
graph = (
StateGraph(ChatState)
.add_node(NODE_CLASSIFY_QUESTION, classify_question)
.add_node(NODE_CHAT_REPLY, chat_reply)
.add_node(NODE_TECH_REPLY, tech_reply)
.add_edge(START, NODE_CLASSIFY_QUESTION)
.add_conditional_edges(
NODE_CLASSIFY_QUESTION,
route_by_type,
path_map
)
.add_edge(NODE_CHAT_REPLY, END)
.add_edge(NODE_TECH_REPLY, END)
.compile()
)
# ==================== 图结构可视化 ====================
mindmap = graph.get_graph()
mindmap.print_ascii()
print(mindmap.draw_mermaid())
png_bytes = mindmap.draw_mermaid_png()
with open("mindmap.png", "wb") as f:
f.write(png_bytes)
# ==================== 运行测试 ====================
res1 = graph.invoke({"question": "今天天气怎么样?"})
print("【闲聊问题回复】\n", res1["answer"], "\n")
res2 = graph.invoke({"question": "Python 列表和元组的区别?"})
print("【技术问题回复】\n", res2["answer"])
3.3 构建图的固定流程
scss
定义 State → 定义节点函数 → 定义路由函数(可选) → 组装图(add_node/add_edge) → compile() → invoke()
四、State 详解
4.1 两种定义方式
方式一:TypedDict(简单覆盖)
默认行为:节点返回的 dict 会整体覆盖 state 中对应字段的值。
python
from typing import TypedDict
class MyState(TypedDict):
question: str
answer: str
方式二:Annotated + Reducer(追加/合并)
当某个字段需要累加 而非覆盖时(如消息列表),使用 Annotated[类型, reducer函数]:
python
from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
messages: Annotated[list, add_messages] # 追加消息而非覆盖
question: str # 普通覆盖
Reducer 的工作原理:
add_messages(left, right):将新消息追加到已有消息列表,相同 ID 的消息会被更新- 也可以自定义 reducer:
Annotated[int, lambda old, new: old + new]实现累加
4.2 内置 State:MessagesState
LangGraph 提供了内置的 MessagesState,等价于:
python
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
直接使用:
python
from langgraph.graph import MessagesState, StateGraph
graph = StateGraph(MessagesState)
4.3 StateGraph 构造函数参数
python
StateGraph(
state_schema, # 必填:State 类型(TypedDict 类)
context_schema=None, # 可选:上下文 schema,用于子图间共享只读上下文
*,
input_schema=None, # 可选:图的输入 schema,不填则用 state_schema
output_schema=None, # 可选:图的输出 schema,不填则用 state_schema
)
| 参数 | 说明 | 使用场景 |
|---|---|---|
state_schema |
State 类型定义 | 必填,定义全局共享状态 |
input_schema |
限制图的输入字段 | 只暴露部分字段给调用者,隐藏内部中间状态 |
output_schema |
限制图的输出字段 | 只返回最终结果,不暴露中间处理数据 |
context_schema |
只读上下文 | 子图间共享配置信息,节点只读不写 |
input_schema / output_schema 示例:
python
class InternalState(TypedDict):
question: str
answer: str
q_type: str # 内部中间状态,不想暴露给外部
class InputState(TypedDict):
question: str # 只接收 question
class OutputState(TypedDict):
answer: str # 只返回 answer
graph = StateGraph(
InternalState,
input_schema=InputState,
output_schema=OutputState,
)
# 调用者只需传 {"question": "..."},只能拿到 {"answer": "..."}
# q_type 是内部实现细节,对外不可见
五、Node 详解
5.1 节点函数的签名
节点函数接收两个参数(第二个可选):
python
def my_node(state: MyState, config: RunnableConfig) -> dict:
# ↑ 必填 ↑ 可选
return {"field": new_value}
state:当前全局状态config:LangChain 的RunnableConfig,可获取config["configurable"]中的运行时参数
5.2 节点返回值规则
| 返回类型 | 行为 |
|---|---|
dict |
部分更新 state,只修改返回的 key |
None / 不返回 |
state 不变 |
Command |
高级用法,可同时更新 state + 控制路由(见第八章) |
5.3 add_node 完整参数
python
graph.add_node(
node, # 节点名称(str) 或 直接传函数(自动取函数名)
action=None, # 当 node 是 str 时,此参数为处理函数
*,
defer=False, # 是否延迟执行(等到运行即将结束时才执行)
metadata=None, # 节点元数据 dict,用于调试/追踪
input_schema=None, # 节点专属输入 schema(默认用图的 state_schema)
retry_policy=None, # 重试策略
cache_policy=None, # 缓存策略
error_handler=None, # 节点级错误处理函数
destinations=None, # 声明节点可能跳转的目标(仅用于图渲染,不影响执行)
timeout=None, # 超时设置(秒 / timedelta / TimeoutPolicy)
)
两种注册方式:
python
# 方式1:名称 + 函数分开传
graph.add_node("my_node", my_function)
# 方式2:直接传函数,自动用函数名作为节点名
graph.add_node(my_function) # 节点名 = "my_function"
常用参数详解:
| 参数 | 类型 | 说明 |
|---|---|---|
defer |
bool |
延迟执行,适用于"收尾"节点,等所有非延迟节点完成后再执行 |
retry_policy |
RetryPolicy |
节点执行失败时自动重试,可设置最大次数、退避策略等 |
timeout |
float / timedelta / TimeoutPolicy |
节点执行超时限制,超时抛出 NodeTimeoutError(仅异步节点支持) |
error_handler |
Callable |
节点出错时的降级处理函数,接收错误信息,返回备用结果 |
input_schema |
TypedDict |
让节点只接收 state 的子集,而非整个 state |
5.4 重试策略 RetryPolicy
当节点函数抛出异常时,LangGraph 可以按策略自动重试,避免因临时网络抖动、API 限流等瞬态错误导致整个图执行失败。
参数详解
python
from langgraph.types import RetryPolicy
retry = RetryPolicy(
max_attempts=3, # 最大尝试次数(含首次执行),默认 3
initial_interval=0.5, # 首次重试前的等待秒数,默认 0.5
backoff_factor=2.0, # 退避倍数,默认 2.0
max_interval=128.0, # 最大等待秒数上限,默认 128.0
jitter=True, # 是否添加随机抖动,默认 True
retry_on=Exception, # 触发重试的异常条件,默认所有 Exception
)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_attempts |
int |
3 | 最大尝试次数(含首次执行),例如 3 = 1 次正常 + 2 次重试 |
initial_interval |
float |
0.5 | 首次重试前的等待秒数 |
backoff_factor |
float |
2.0 | 退避倍数,每次重试等待 = 上次 × 此值 |
max_interval |
float |
128.0 | 最大等待秒数上限,退避时间不会超过此值 |
jitter |
bool |
True | 是否添加随机抖动,避免多个请求同时重试(惊群效应) |
retry_on |
type / Sequence / Callable |
所有 Exception | 触发重试的异常条件 |
退避时间计算
以 initial_interval=1.0, backoff_factor=2.0 为例:
erlang
第 1 次执行失败 → 等待 1s → 第 2 次重试
第 2 次重试失败 → 等待 2s → 第 3 次重试
第 3 次重试失败 → 等待 4s → 第 4 次重试(如果 max_attempts 允许)
...
等待时间不会超过 max_interval(默认 128s)
retry_on 的三种用法
python
# 方式一:指定异常类型(只有该类型异常才重试)
retry = RetryPolicy(retry_on=ConnectionError)
# 方式二:指定多个异常类型
retry = RetryPolicy(retry_on=(ConnectionError, TimeoutError))
# 方式三:自定义判断函数(最灵活)
retry = RetryPolicy(retry_on=lambda e: "rate limit" in str(e).lower())
实际开发中,建议对调用外部 API 的节点设置
retry_on=ConnectionError,避免业务逻辑错误也被无意义地重试。
使用示例
python
retry = RetryPolicy(max_attempts=3, initial_interval=1.0, backoff_factor=2.0)
graph = (
StateGraph(State)
.add_node("call_llm", call_llm, retry_policy=retry)
.add_node("process", process) # 不需要重试的节点不传 retry_policy
...
)
提示 :
retry_policy是节点级别的,可以给不同节点配置不同的重试策略。也可以在compile()时设置全局默认重试策略,节点级别的会覆盖全局的。
5.5 缓存策略 CachePolicy
当相同输入状态再次进入节点时,CachePolicy 可以让节点直接返回缓存结果,跳过函数体执行。这对 LLM 调用等耗时操作特别有价值。
两个条件缺一不可
节点缓存生效需要同时满足两个条件:
| 条件 | 代码 | 作用 |
|---|---|---|
| ① 定义缓存策略 | add_node(cache_policy=CachePolicy(ttl=60)) |
定义"怎么缓存"(key 算法、TTL) |
| ② 提供存储后端 | compile(cache=GraphCache()) |
提供"缓存在哪存"(内存/Redis) |
⚠️ 只设 cache_policy 不传 cache,缓存不会生效! 这是初学者最常犯的错误。
python
from langgraph.cache.memory import InMemoryCache as GraphCache
from langgraph.types import CachePolicy
# ✅ 正确:两个条件都满足
graph = (
StateGraph(State)
.add_node("answer", answer_qs, cache_policy=CachePolicy(ttl=60))
...
.compile(cache=GraphCache()) # 必须传入!
)
# ❌ 错误:只设了 cache_policy,没有存储后端,缓存不生效
graph = (
StateGraph(State)
.add_node("answer", answer_qs, cache_policy=CachePolicy(ttl=60))
...
.compile() # 缺少 cache=...
)
CachePolicy 参数详解
python
from langgraph.types import CachePolicy
cache_policy = CachePolicy(
ttl=60, # 缓存过期时间(秒),None 表示永不过期
key_func=... # 缓存 key 生成函数,默认用 pickle 序列化输入后哈希
)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ttl |
int / None |
None | 缓存过期时间(秒),None 表示永不过期 |
key_func |
Callable |
pickle 哈希 | 缓存 key 生成函数,一般不需要自定义 |
存储后端选择
| 存储后端 | 来源 | 适用场景 |
|---|---|---|
langgraph.cache.memory.InMemoryCache |
内存 | 开发调试、单进程场景 |
langgraph.cache.redis.RedisCache |
Redis | 生产环境、多进程/分布式场景 |
python
# 内存缓存(开发调试)
from langgraph.cache.memory import InMemoryCache as GraphCache
graph = ... .compile(cache=GraphCache())
# Redis 缓存(生产环境)
from langgraph.cache.redis import RedisCache
graph = ... .compile(cache=RedisCache(redis_url="redis://localhost:6379"))
缓存 key 的工作原理
- 节点执行前,用
key_func对节点的输入状态计算缓存 key - 在
cache存储后端中查找该 key - 如果命中且未过期 → 直接返回缓存结果,函数体不执行
- 如果未命中 → 正常执行函数,将输出结果写入缓存
注意 :缓存 key 基于节点的输入状态 计算。如果状态中包含
add_messages等 reducer,每次 invoke 后状态会累加变化,导致 key 不同,缓存可能不会命中。这是设计如此------状态变了就应该重新执行。
5.6 三层机制对比
LLM 缓存、节点缓存、检查点持久化容易混淆,下表帮你彻底理清:
| LLM 缓存 | 节点缓存 | 检查点持久化 | |
|---|---|---|---|
| 来源 | langchain_core.caches |
langgraph.cache |
langgraph.checkpoint |
| 缓存什么 | LLM API 响应 | 节点函数的完整输出 | 图的执行状态 |
| 粒度 | LLM 调用级别 | 节点函数级别 | 整个图级别 |
| 效果 | 相同 prompt 不再调 API | 相同输入不再执行函数 | 能记住"执行到哪了" |
| 设置方式 | set_llm_cache() 全局 |
cache_policy + cache |
checkpointer + config |
| 跳过执行? | ✅ 跳过 API 调用 | ✅ 跳过整个函数体 | ❌ 不跳过,只保存状态 |
| 典型场景 | 开发调试时避免重复调 API | 耗时计算的幂等节点 | 断点续跑、人工审批 |
arduino
需要"相同问题不重复调 LLM" → LLM 缓存(第一层)
需要"相同输入不重复执行节点函数" → 节点缓存(第二层)
需要"记住执行进度,支持暂停/继续" → 检查点持久化(第三层)
5.7 缓存与重试完整代码示例
python
import operator
import time
from typing import Annotated, TypedDict
# 第一层:LLM 缓存(全局设置,对所有 LLM 调用生效)
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
# 第二层:节点缓存所需的存储后端
from langgraph.cache.memory import InMemoryCache as GraphCache
from langgraph.types import CachePolicy
# 第三层:检查点持久化
from langgraph.checkpoint.memory import MemorySaver
# 重试策略
from langgraph.types import RetryPolicy
# 其他导入
from langchain_core.output_parsers import StrOutputParser
from langgraph.graph import START, END, StateGraph, add_messages
from llm import llm
# 启用第一层:LLM 缓存
set_llm_cache(InMemoryCache())
class StateMessage(TypedDict):
messages: Annotated[list, add_messages]
listNumber: Annotated[float, operator.add]
question: str
def answer_qs(state):
chain = llm | StrOutputParser()
return {"messages": [chain.invoke(state["question"])]}
def smile1(state):
return {"listNumber": 1}
def smile2(state):
return {"listNumber": 2}
NODE_QUESTION_QS = "answer_qs"
NODE_SMILE1 = "smile1"
NODE_SMILE2 = "smile2"
# 重试策略:最多 3 次,首次等 1s,退避倍数 2(1s → 2s → 4s)
retry = RetryPolicy(max_attempts=3, initial_interval=1.0, backoff_factor=2.0)
# 缓存策略:缓存 60 秒
cache_policy = CachePolicy(ttl=60)
graph = (
StateGraph(StateMessage)
.add_node(NODE_QUESTION_QS, answer_qs, retry_policy=retry, cache_policy=cache_policy)
.add_node(NODE_SMILE1, smile1)
.add_node(NODE_SMILE2, smile2)
.add_edge(START, NODE_QUESTION_QS)
.add_edge(NODE_QUESTION_QS, NODE_SMILE1)
.add_edge(NODE_SMILE1, NODE_SMILE2)
.add_edge(NODE_SMILE2, END)
# checkpointer=MemorySaver() → 第三层:检查点持久化
# cache=GraphCache() → 第二层:节点缓存的存储后端
.compile(checkpointer=MemorySaver(), cache=GraphCache())
)
config = {"configurable": {"thread_id": "1"}}
t1 = time.time()
print("第一次执行:")
print(graph.invoke({'question': '1+2=? 直接返回答案就行'}, config))
print(f"耗时: {time.time()-t1:.2f}s")
print("\n1111111111111111111111111111")
t2 = time.time()
print("第二次运行(利用缓存,瞬间返回):")
print(graph.invoke({'question': '1+2=? 直接返回答案就行'}, config))
print(f"耗时: {time.time()-t2:.2f}s")
六、Edge 详解
6.1 普通边 add_edge
python
graph.add_edge(
start_key, # 起始节点名(str)或 节点名列表(list[str])
end_key, # 目标节点名(str)
)
多节点汇聚 :当 start_key 是列表时,表示等待所有起始节点都完成后,才执行目标节点:
python
# research 和 analysis 都完成后,才进入 summary
graph.add_edge(["research", "analysis"], "summary")
6.2 条件边 add_conditional_edges
当需要条件分支时,定义路由函数来决定下一步走哪个节点。
路由函数的返回值
路由函数返回下一个节点的名称 (字符串),或返回 'END' 直接结束:
python
def route_fn(state: MyState) -> str:
if state["q_type"] == "chat":
return "chat_reply"
elif state["q_type"] == "tech":
return "tech_reply"
else:
return END # 直接结束,不进入任何节点
add_conditional_edges 完整参数
python
graph.add_conditional_edges(
source, # 源节点名称(str)
path, # 路由函数(Callable)或 Runnable
path_map=None # 路径映射(dict 或 list),可选
)
path_map 的四种写法:
python
# 写法1:dict 映射(路由返回值 → 目标节点名)
path_map = {"chat": "chat_reply", "tech": "tech_reply"}
# 写法2:dict 映射(返回值和节点名相同时的简写)
path_map = {"chat_reply": "chat_reply", "tech_reply": "tech_reply"}
# 写法3:list(每个元素既是返回值又是节点名)
path_map = ["chat_reply", "tech_reply"]
# 写法4:不传 path_map
# 此时路由函数的返回值直接作为节点名
# ⚠️ 缺点:图可视化时无法确定可能的分支,会假设可以跳到任何节点
path 也可以是 Runnable
python
from langchain_core.runnables import RunnableLambda
route_runnable = llm | StrOutputParser() # 用 LLM 来决定路由
graph.add_conditional_edges(
"classify",
route_runnable,
path_map={"chat": "chat_reply", "tech": "tech_reply"}
)
七、Compile 详解
python
graph.compile(
checkpointer=None, # 持久化检查点,支持断点续跑
*,
cache=None, # 缓存后端
store=None, # 跨线程的长期记忆存储
interrupt_before=None, # 在指定节点执行前中断(Human-in-the-loop)
interrupt_after=None, # 在指定节点执行后中断
debug=False, # 调试模式
name=None, # 编译后的图名称
transformers=None, # 流式输出转换器
)
| 参数 | 说明 | 典型用法 |
|---|---|---|
checkpointer |
持久化存储,保存每步的 state 快照 | MemorySaver(内存)/ SqliteSaver(SQLite),配合 thread_id 实现多轮对话 |
interrupt_before |
在某节点执行前暂停 | Human-in-the-loop:在执行敏感操作前暂停,等待人工确认 |
interrupt_after |
在某节点执行后暂停 | 先让 LLM 生成草稿,暂停后人工审核再继续 |
store |
跨线程的长期记忆 | InMemoryStore,不同 thread 间共享的记忆 |
debug |
打印详细执行日志 | 开发调试时设为 True |
checkpointer 示例:
python
from langgraph.checkpoint.memory import MemorySaver
graph = StateGraph(MyState).add_node(...).add_edge(...).compile(
checkpointer=MemorySaver()
)
# 必须传 thread_id
config = {"configurable": {"thread_id": "thread-1"}}
result = graph.invoke({"question": "你好"}, config)
# 同一 thread_id 下,下次调用会保留上次的 state
result2 = graph.invoke({"question": "继续"}, config)
interrupt_before 示例:
python
graph = builder.compile(
checkpointer=MemorySaver(),
interrupt_before=["send_email"] # 发邮件前暂停
)
# 第一次调用会在 send_email 前暂停
result = graph.invoke(inputs, config)
# 人工确认后,传 None 继续
result = graph.invoke(None, config)
八、高级控制原语
前面我们掌握了 LangGraph 的基础构建块:State、Node、Edge、Checkpointer。这些组件覆盖了大多数场景,但当图变得更复杂时,你会遇到三个基础组件无法优雅解决的问题:
- 边是静态的 ------ 编译前必须确定"A → B",但运行时才知道要分发到几个节点怎么办?
- 节点只能返回状态更新或走静态边 ------ 想同时更新状态 + 动态跳转怎么办?
- 节点只能拿到 state 和 config ------ 需要访问用户身份、持久化存储、流式输出等运行时能力怎么办?
LangGraph 提供了三个高级原语来解决这些问题:
| 原语 | 解决的问题 | 属于哪层的扩展 |
|---|---|---|
| Send | 动态扇出(1→N),运行时决定分发几条边 | 边/路由层的扩展 |
| Command | 合并"更新状态 + 控制跳转"的一站式指令 | 节点返回值层的扩展 |
| Runtime Context | 节点的随身工具箱(用户身份、存储、流式输出等) | 节点函数签名层的扩展 |
它们和已有概念的关系:
rust
┌─────────────────────────────────────────────────────────────┐
│ LangGraph 构建块体系 │
├──────────────┬──────────────────┬───────────────────────────┤
│ 基础组件 │ 增强配置 │ 高级控制原语 │
├──────────────┼──────────────────┼───────────────────────────┤
│ State │ RetryPolicy │ Send(动态扇出) │
│ Node │ CachePolicy │ Command(状态+路由一体化) │
│ Edge │ Checkpointer │ Runtime Context(随身工具箱)│
│ 条件边 │ Store │ │
└──────────────┴──────────────────┴───────────────────────────┘
基础组件:构建图的最小单元,必须掌握
增强配置:让图更健壮(重试、缓存、持久化),按需添加
高级原语:解决基础组件无法优雅处理的复杂场景,进阶使用
8.1 Send ------ 动态扇出(Map-Reduce 模式)
解决什么问题?
普通的边(Edge)是静态的:你在编译前就必须确定"A → B"。但有些场景,你不知道要执行几次下游节点。
经典例子:Map-Reduce
用户输入 3 个主题 → 每个主题各生成一个笑话 → 汇总所有笑话
问题在于:3 个主题是运行时才知道的,你没法提前写 3 条边。Send 就是解决这个问题的。
适用场景:
- 文档批量分片摘要:一篇长文拆成 10 段,并行调用摘要节点 10 次
- 多工具并行查询:同时查天气、数据库、搜索引擎
- 多子问题拆解:把用户大问题拆成 N 个子问题,并行求解再汇总
怎么理解?
把 Send 想象成一个动态分发器:
css
普通边: A ──→ B (1 对 1,写死的)
Send: A ──→ B ──→ B ──→ B (1 对 N,运行时决定 N 是几,且并行执行)
代码示例
python
from langgraph.types import Send
# 状态中有 subjects: ["猫", "狗", "程序员"]
def continue_to_jokes(state: OverallState):
# 为每个 subject 动态创建一条"边",发送到 generate_joke 节点
# 每个 Send 携带不同的输入状态
return [
Send("generate_joke", {"subject": "猫"}),
Send("generate_joke", {"subject": "狗"}),
Send("generate_joke", {"subject": "程序员"}),
]
# 用 add_conditional_edges 注册,和普通条件边一样
graph.add_conditional_edges("node_a", continue_to_jokes)
关键点:
Send("节点名", 状态)= 向指定节点发送一份独立的状态- 返回列表中有几个
Send,下游节点就执行几次 - 每次执行拿到的状态互不相同(各自只收到自己的那份)
- 并行执行:多个 Send 会并发运行,不是串行
Send 与 add_conditional_edges 的配合机制
很多初学者的困惑是:Send 明明是"动态分发",为什么要用 add_conditional_edges 来注册?
核心原因:Send 本质上是一种特殊的条件边返回值 。add_conditional_edges 是 LangGraph 中"从节点出发,运行时决定下一步去哪"的唯一机制,Send 只是让这个"决定"从"去一个节点"变成了"去 N 个节点"。
三种条件边返回值对比
python
# add_conditional_edges 的 path 函数可以返回三种东西:
# ① 返回字符串 → 走 1 条分支(最普通用法)
def route(state):
if state["type"] == "A":
return "node_a" # 去节点 A
return "node_b" # 去节点 B
# ② 返回 Send → 走 1 条分支,但携带自定义状态
def route(state):
return Send("node_a", {"custom": "data"}) # 去节点 A,且注入自定义状态
# ③ 返回 Send 列表 → 走 N 条分支(动态扇出)
def route(state):
return [
Send("node_a", {"subject": "猫"}),
Send("node_a", {"subject": "狗"}),
Send("node_a", {"subject": "程序员"}),
] # 节点 A 执行 3 次,每次拿到不同的状态
逐步拆解:从普通条件边到 Send
第一步:普通条件边(1 → 1)
python
def route(state):
return "node_b" # 返回节点名,走一条路
graph.add_conditional_edges("node_a", route, ["node_b", "node_c"])
第二步:条件边返回 Send(1 → 1,但携带自定义状态)
python
def route(state):
return Send("node_b", {"subject": "猫"})
graph.add_conditional_edges("node_a", route, ["node_b"])
第三步:条件边返回 Send 列表(1 → N,动态扇出)
python
def route(state):
return [
Send("node_b", {"subject": "猫"}),
Send("node_b", {"subject": "狗"}),
]
graph.add_conditional_edges("node_a", route, ["node_b"])
add_conditional_edges 的第三个参数
python
graph.add_conditional_edges("source_node", path_func, path_map)
# ↑ ↑ ↑
# 从哪个节点出发 路由函数 可能的目标节点列表
path_map是声明式的:告诉 LangGraph "路由函数可能返回的目标节点有哪些"- 当路由函数返回
Send时,path_map中必须包含Send指向的节点名 - 如果省略
path_map,LangGraph 会尝试自动推断,但显式声明更安全
Superstep 机制:并行结果如何"等齐"再汇合?
一个常见的疑问:add_edge("generate_joke", "collect_jokes") 只是一条普通边,collect_jokes 怎么知道要等 3 个并行结果全部完成?
答案是 LangGraph 的 Superstep(超步)机制------框架自动保证,不需要手动处理。
LangGraph 的执行按超步 推进,一个超步内的所有并行任务必须全部完成,才会进入下一个超步:
css
Superstep 1: START → 条件边返回 3 个 Send
↓
Superstep 2: generate_joke × 3(并行执行)
┌─ Send1: {"subject":"狗"} → 返回 {"jokes": ["狗笑话"]}
├─ Send2: {"subject":"猫"} → 返回 {"jokes": ["猫笑话"]}
└─ Send3: {"subject":"程序员"} → 返回 {"jokes": ["程序员笑话"]}
↓
⚡ 3 个全部完成后,add reducer 累加结果
jokes = ["狗笑话", "猫笑话", "程序员笑话"]
↓
Superstep 3: collect_jokes(此时 jokes 已包含全部 3 个结果)
两个关键保证:
| 保证 | 由谁负责 | 作用 |
|---|---|---|
| 等齐 | Superstep 机制 | 同一批 Send 触发的并行任务全部完成后,才推进到下游节点 |
| 攒齐 | add reducer |
多个并行节点的返回值自动累加,而不是互相覆盖 |
如果缺少 add reducer:
python
# ❌ 不用 add reducer,jokes 只是 list[str]
class OverallState(TypedDict):
jokes: list[str] # 默认 last writer wins(覆盖)
# 3 个并行结果只保留最后一个,前两个被覆盖!
# collect_jokes 只能看到 1 个笑话,而不是 3 个
简单记忆:Superstep 保证"等齐",add reducer 保证"攒齐",两者缺一不可。
Superstep 等的是"批次"而非"节点"
如果 Send 发到不同的节点 ,Superstep 也会等所有任务完成吗?答案是会。
python
def route(state):
return [
Send("node_a", {"data": 1}),
Send("node_a", {"data": 2}),
Send("node_b", {"data": 3}),
]
ini
Superstep 1: 条件边返回 3 个 Send
↓
Superstep 2: node_a × 2(并行) + node_b × 1(并行)
┌─ node_a(data=1) ──→ 下游 node_c
├─ node_a(data=2) ──→ 下游 node_c
└─ node_b(data=3) ──→ 下游 node_d
↓
⚡ 3 个任务全部完成,才进入下一个 Superstep
(node_c 不会因为 node_a 先完成就提前执行,必须等 node_b 也完成)
↓
Superstep 3: node_c 和 node_d 同时开始
Superstep 是按"批次"隔离的,不是按"节点"隔离的。 同一批 Send 出去的所有任务(不管目标节点是否相同),必须全部完成才能推进到下一批。
和普通条件边的区别
| 普通条件边 | Send | |
|---|---|---|
| 下游执行次数 | 每次只走 1 条分支 | 可以同时走 N 条 |
| 每次的状态 | 共享同一个状态 | 每条分支有独立状态 |
| 边的数量 | 编译时确定 | 运行时动态决定 |
| 典型场景 | 根据类型走不同分支 | Map-Reduce、批量处理 |
常见错误
python
# ❌ 错误 1:在节点函数中返回 Send(节点必须返回 dict)
def my_node(state):
return [Send("target", {"data": 1})] # TypeError!
# ✅ 正确:在条件边函数中返回 Send
def my_router(state):
return [Send("target", {"data": 1})]
graph.add_conditional_edges("my_node", my_router, ["target"])
# ❌ 错误 2:path_map 中漏掉了 Send 指向的节点
graph.add_conditional_edges("node_a", router, ["other_node"])
# 如果 router 返回 Send("target", ...),但 "target" 不在 path_map 中 → 报错
# ✅ 正确:path_map 包含所有可能的目标
graph.add_conditional_edges("node_a", router, ["target"])
完整代码示例
python
"""
Send 最小示例:Map-Reduce 模式
图结构:
START ──(条件边+Send)──→ generate_joke × 3(并行)
│
▼
collect_jokes
│
▼
END
"""
from typing import Annotated, TypedDict
from operator import add
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from llm import llm
from langchain_core.output_parsers import StrOutputParser
# OverallState:图的全局状态,所有节点共享
# jokes 用 Annotated[list[str], add] 声明,多个并行节点返回的 jokes 列表会自动累加
class OverallState(TypedDict):
subjects: list[str]
jokes: Annotated[list[str], add]
# JokeState:Send 传给 generate_joke 的私有状态
# 每次并行执行时,节点只收到 Send 携带的那份数据
class JokeState(TypedDict):
subject: str
def generate_joke(state: JokeState) -> dict:
subject = state["subject"]
chain = llm | StrOutputParser()
joke = chain.invoke(f"请讲一个关于{subject}的笑话,只需返回笑话内容")
return {"jokes": [joke]}
def collect_jokes(state: OverallState) -> dict:
print(f"\n汇总 {len(state['jokes'])} 个笑话:")
for i, joke in enumerate(state["jokes"], 1):
print(f" {i}. {joke}")
return {}
graph = (
StateGraph(OverallState)
.add_node("generate_joke", generate_joke)
.add_node("collect_jokes", collect_jokes)
.add_conditional_edges(
START,
lambda state: [
Send("generate_joke", {"subject": subject})
for subject in state["subjects"]
],
["generate_joke"],
)
.add_edge("generate_joke", "collect_jokes")
.add_edge("collect_jokes", END)
.compile()
)
if __name__ == "__main__":
print("Send 示例:Map-Reduce 模式")
result = graph.invoke({"subjects": ["猫", "狗", "程序员"]})
8.2 Command ------ 状态更新 + 路由一体化
解决什么问题?
传统 LangGraph 分离逻辑:
- 节点只能返回状态字典,不能控制跳转;
- 跳转必须单独写 add_conditional_edges + 独立路由函数,逻辑拆分两处。
Command 让你一步到位:既更新状态,又控制跳转。
但更重要的是,Command 解决了两个条件边完全做不到的场景:
| 场景 | 不可替代性 | 说明 |
|---|---|---|
| interrupt + resume | ⭐⭐⭐ 唯一方案 | 暂停图等外部输入,条件边完全做不到 |
| 工具返回 Command | ⭐⭐⭐ 唯一方案 | 工具内部控制流程,条件边够不到工具内部 |
| 节点内更新状态+路由 | ⭐ 可替代 | 能用 add_conditional_edges 替代,只是写法更紧凑 |
Command 的价值不在"省一行条件边",而在打通了条件边够不到的两个地方:
- 图的边界 ------
interrupt让图能暂停等外部世界,resume让外部世界能注入数据继续 - 工具的内部 ------ 工具不再只是"干活的",还能"指路的"
简单记忆:Command 的杀手级场景 = 暂停恢复 + 工具指路,其他场景用条件边就好。
四个参数
| 参数 | 作用 | 使用场景 |
|---|---|---|
update |
更新状态(等同于节点返回 dict) |
从节点返回时 |
goto |
跳转到指定节点(等同于条件边路由) | 从节点返回时 |
resume |
中断后恢复执行时传入的值 | 配合 interrupt 使用 |
graph |
跨子图跳转 | 从子图跳回父图时使用 |
使用场景一:中断后恢复(resume)
python
"""
Command 杀手级场景一:interrupt + resume(中断恢复)
图结构:
START ──→ submit ──→ review(中断等审批) ──Command──→ execute(执行) ──→ END
└──→ reject(拒绝) ──→ END
执行时序:
第1次 invoke: {"request": "申请休假"} → 执行到 review → interrupt 暂停
第2次 invoke: Command(resume="批准") → review 恢复 → 走 execute 分支
或
第2次 invoke: Command(resume="拒绝") → review 恢复 → 走 reject 分支
"""
from typing import Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt
from langgraph.checkpoint.memory import MemorySaver
class State(TypedDict):
request: str # 用户提交的请求
approval: str # 审批结果:"批准" 或 "拒绝"
result: str # 最终处理结果
def submit(state: State) -> dict:
"""提交请求(只是打印,不做实际处理)"""
print(f" 📝 提交请求: {state['request']}")
return {}
def review(state: State) -> Command[Literal["execute", "reject"]]:
"""
人工审批节点 ------ 整个 demo 的核心
执行流程:
1. interrupt("请审批此请求") 暂停整个图
- 图的状态被 checkpointer 保存
- invoke() 返回,包含 __interrupt__ 信息
- 此时图"冻结"在 review 节点
2. 外部决定审批结果后,再次 invoke(Command(resume="批准/拒绝"))
- interrupt() 的返回值就是 resume 传入的值
- 图从冻结点恢复,继续执行 review 节点的剩余代码
3. 根据 approval 值返回 Command,同时更新状态 + 控制跳转
- "批准" → Command(update={"approval": "批准"}, goto="execute")
- 其他 → Command(update={"approval": "拒绝"}, goto="reject")
"""
print(" ⏸️ 等待人工审批...")
# interrupt() 暂停执行,返回值由外部 Command(resume=...) 传入
# 第一次执行到这里时,图暂停,approval 还没有值
# 第二次用 Command(resume="批准") 恢复时,approval = "批准"
approval = interrupt("请审批此请求")
if approval == "批准":
return Command(
update={"approval": "批准"}, # 更新状态:记录审批结果
goto="execute" # 跳转:去执行请求
)
else:
return Command(
update={"approval": "拒绝"}, # 更新状态:记录审批结果
goto="reject" # 跳转:去拒绝请求
)
def execute(state: State) -> dict:
"""
执行请求(审批通过后走这里)
注意:execute 不是"同意"的意思,而是"执行"
- review 节点负责审批(同意/拒绝)
- execute 节点负责把同意的请求落地执行
- reject 节点负责处理被拒绝的请求
"""
print(f" ✅ 执行请求: {state['request']}(已{state['approval']})")
return {"result": f"已执行: {state['request']}"}
def reject(state: State) -> dict:
"""拒绝请求(审批不通过走这里)"""
print(f" ❌ 拒绝请求: {state['request']}(已{state['approval']})")
return {"result": f"已拒绝: {state['request']}"}
graph = (
StateGraph(State)
.add_node("submit", submit)
.add_node("review", review)
.add_node("execute", execute)
.add_node("reject", reject)
.add_edge(START, "submit")
.add_edge("submit", "review")
# review 返回 Command,自带路由信息,不需要 add_conditional_edges
.add_edge("execute", END)
.add_edge("reject", END)
# ⚠️ 必须加 checkpointer!
# interrupt 依赖 checkpointer 保存中断时的状态
# 没有 checkpointer,interrupt 无法工作
.compile(checkpointer=MemorySaver())
)
graph.get_graph().print_ascii()
if __name__ == "__main__":
# ── 第一次调用:执行到 review 节点时被 interrupt 暂停 ──
print("=" * 50)
print("第一次调用:提交请求,遇到 interrupt 暂停")
print("=" * 50)
# config 中的 thread_id 标识一个执行线程
# 同一个 thread_id 的多次 invoke 共享状态(由 checkpointer 管理)
config = {"configurable": {"thread_id": "1"}}
result1 = graph.invoke({"request": "申请休假3天"}, config)
# 此时图在 review 节点暂停了
# result1 包含 __interrupt__ 信息,表示图被中断了
print(f" 暂停后的状态: {result1}")
# ── 第二次调用:用 Command(resume=...) 传入审批结果,恢复执行 ──
print("\n" + "=" * 50)
print("第二次调用:传入审批结果,恢复执行")
print("=" * 50)
# Command(resume="批准") 做了两件事:
# 1. 把 "批准" 作为 interrupt() 的返回值注入
# 2. 从 checkpointer 恢复之前保存的状态,继续执行 review 节点
# 试试改成 Command(resume="拒绝") 看不同结果
result2 = graph.invoke(Command(resume="批准"), config)
print(f"\n最终结果: {result2['result']}")
# ── 再演示一次拒绝的场景 ──
print("\n" + "=" * 50)
print("再试一次:拒绝场景")
print("=" * 50)
# 用不同的 thread_id,开启一个新的执行线程
config2 = {"configurable": {"thread_id": "2"}}
graph.invoke({"request": "申请加薪50%"}, config2)
result3 = graph.invoke(Command(resume="拒绝"), config2)
print(f"\n最终结果: {result3['result']}")
⚠️ 重要注意事项 :返回 Command 时必须 加类型注解 Command[Literal["node_b", "node_c"]]
interrupt() 的本质机制
interrupt() 不是普通函数,它是一个特殊的控制流指令 ,效果类似 Python 的 yield:
ini
第一次 invoke 执行到 interrupt() 时:
approval = interrupt("请审批此请求")
# ↑
# 到这里就"断电"了!
# 下面的 if/else 根本不会执行
# 图的状态被 checkpointer 保存
# invoke() 立即返回
第二次 invoke(Command(resume="批准")) 时:
approval = interrupt("请审批此请求")
# ↑
# 这里"来电"了!resume 的值 "批准" 成为 interrupt() 的返回值
# approval = "批准"
# 继续执行下面的 if/else
interrupt() 做了三件事:
- 保存现场 ------ 把当前状态写入 checkpointer(所以必须加
MemorySaver) - 抛出中断信号 ------ 图的执行引擎捕获这个信号,停止推进
- 等待恢复 ------ 下次
invoke(Command(resume=...))时,resume的值作为interrupt()的返回值注入,从断点继续执行
简单记忆:interrupt() = 图的"暂停键",Command(resume=...) = 图的"播放键",checkpointer = 保存暂停位置的"存档卡"。
使用场景二:从工具返回
python
from langgraph.types import Command
@tool
def my_tool(some_input: str) -> Command[Literal["next_node"]]:
result = do_something(some_input)
return Command(
update={"tool_result": result}, # 更新状态
goto="next_node" # 工具执行完跳到指定节点
)
什么时候用 Command,什么时候用条件边?
| 场景 | 选择 |
|---|---|
| 只需要路由,不需要更新状态 | 条件边 add_conditional_edges |
| 需要同时更新状态 + 路由 | Command |
| 需要中断后恢复 | Command(resume=...) |
| 从工具中控制流程 | Command |
8.3 Runtime Context ------ 节点的随身工具箱
解决什么问题?
之前节点函数只能拿到 state 和 config。但有些信息不属于业务状态,却又是节点执行时需要的:
| 信息 | 例子 | 放 State? | 放 config? |
|---|---|---|---|
| 当前用户是谁 | user_id="alice" |
❌ 不是业务数据 | ⚠️ 能放但不类型安全 |
| 数据库连接 | db_conn=... |
❌ 不是业务数据 | ⚠️ 能放但不类型安全 |
| 持久化存储 | store=InMemoryStore() |
❌ 不是业务数据 | ⚠️ 太底层 |
| 流式输出 | stream_writer(...) |
❌ 不是业务数据 | ⚠️ 不该暴露 |
| 执行元信息 | checkpoint_id, task_id |
❌ 不是业务数据 | ⚠️ 太底层 |
Runtime Context 就是把这些"不属于 State 但节点又需要的东西"统一打包,以类型安全的方式注入节点。
怎么理解?
State 是节点间传递的"货物",Runtime Context 是节点执行时的"随身工具箱"。
货物(State)会随着图的执行流动、变化;工具箱(Runtime Context)是每次执行时注入的辅助信息,节点只读不改。
Runtime 包含什么?
python
from langgraph.runtime import Runtime, get_runtime
# 在节点函数中,runtime 包含以下内容:
runtime.context # 🧩 自定义上下文(如 user_id, db_conn)
runtime.store # 📦 持久化存储(BaseStore)
runtime.stream_writer # 📡 自定义流式输出
runtime.previous # ⏮️ 上一次该线程的返回值
runtime.execution_info # ℹ️ 执行元信息(只读)
runtime.heartbeat # 💓 心跳(防止长任务超时)
runtime.control # 🎛️ 运行控制(如请求优雅停止)
最常用:自定义上下文 context
这是你自己定义的只读上下文,类似依赖注入:
python
from dataclasses import dataclass
from langgraph.graph import StateGraph
from langgraph.runtime import Runtime
# 第一步:定义上下文结构
@dataclass
class Context:
user_id: str
db_conn: str
class State(TypedDict):
response: str
# 第二步:告诉 StateGraph 上下文的结构
graph = StateGraph(state_schema=State, context_schema=Context)
# 第三步:节点函数第二个参数接收 runtime
def my_node(state: State, runtime: Runtime[Context]) -> dict:
user_id = runtime.context.user_id # 类型安全!IDE 有提示
db_conn = runtime.context.db_conn
return {"response": f"Hello, {user_id}!"}
graph.add_node("my_node", my_node)
# ...
# 第四步:调用时传入上下文
result = graph.invoke({}, context=Context(user_id="alice", db_conn="postgres://..."))
和 State 的区别:
- State:节点可读可写,在节点间流动
- Context:节点只读,整个运行期间不变
和 config 的区别:
- config:无类型提示,
config["configurable"]["user_id"]容易拼错 - context:有类型提示,
runtime.context.user_idIDE 自动补全
持久化存储 store
跨线程/跨会话的长期记忆,不同于 State(单次运行)和 Checkpointer(单线程状态):
python
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
store.put(("users",), "alice", {"name": "Alice"})
def my_node(state: State, runtime: Runtime) -> dict:
if memory := runtime.store.get(("users",), "alice"):
name = memory.value["name"]
return {"response": f"Hello, {name}!"}
graph = ... .compile(store=store)
自定义流式输出 stream_writer
在节点执行过程中,向客户端推送自定义数据(不等节点执行完):
python
def my_node(state: State, runtime: Runtime) -> dict:
runtime.stream_writer({"progress": "50%"}) # 中途推送进度
# ... 做一些耗时操作 ...
runtime.stream_writer({"progress": "100%"})
return {"result": "done"}
# 调用端用 stream_mode="custom" 接收
for chunk in graph.stream({"input": "..."}, stream_mode="custom"):
print(chunk) # {"progress": "50%"} → {"progress": "100%"}
执行元信息 execution_info
当前执行的元数据,调试和日志时很有用:
python
def my_node(state: State, runtime: Runtime) -> dict:
info = runtime.execution_info
print(f"线程: {info.thread_id}")
print(f"检查点: {info.checkpoint_id}")
print(f"任务: {info.task_id}")
print(f"第几次重试: {info.node_attempt}") # 配合 RetryPolicy 使用
return {}
两种访问方式
python
# 方式一:作为节点函数参数(推荐,类型安全)
def my_node(state: State, runtime: Runtime[Context]) -> dict:
user_id = runtime.context.user_id
# 方式二:在函数内部调用 get_runtime()(适用于无法改签名的场景)
from langgraph.runtime import get_runtime
def my_node(state: State) -> dict:
runtime = get_runtime(Context)
user_id = runtime.context.user_id
三种"数据载体"对比
| State | config | Runtime Context | |
|---|---|---|---|
| 是什么 | 业务数据 | 运行配置 | 随身工具箱 |
| 可读? | ✅ | ✅ | ✅ |
| 可写? | ✅ | ❌ | ❌ |
| 类型安全? | ✅ TypedDict | ❌ 纯 dict | ✅ 泛型 RuntimeT |
| 跨节点流动? | ✅ | ❌ | ❌(每次执行注入) |
| 典型内容 | question, answer | thread_id, callbacks | user_id, store, stream_writer |
简单记忆:State 是"货物",Runtime Context 是"工具箱"。货物在站点间流转,工具箱是每个工人随身携带的。
8.4 三个高级原语的选用指南
rust
需要"运行时动态分发到 N 个节点" → Send
需要"同时更新状态 + 控制跳转" → Command
需要"在节点中访问运行时能力" → Runtime Context
| 场景 | 选择 | 替代方案(不推荐) |
|---|---|---|
| Map-Reduce、批量并行处理 | Send | 写 N 条条件边(N 未知时无法实现) |
| 节点内根据条件更新状态+跳转 | Command | 条件边 + 中间节点过渡(啰嗦) |
| 中断后恢复执行 | Command(resume=...) | 手动管理状态标志位 |
| 节点需要用户身份等上下文 | Runtime.context | config"configurable"(无类型安全) |
| 节点需要跨会话持久化 | Runtime.store | 外部全局变量(不可靠) |
| 节点需要中途推送进度 | Runtime.stream_writer | 写入 State 再轮询(低效) |
九、速查表
9.1 核心方法参数速查
| 方法 | 必填参数 | 常用可选参数 |
|---|---|---|
StateGraph() |
state_schema |
input_schema, output_schema |
add_node() |
node 或 node + action |
defer, retry_policy, timeout, error_handler |
add_edge() |
start_key, end_key |
--- |
add_conditional_edges() |
source, path |
path_map |
compile() |
--- | checkpointer, interrupt_before/after, store |
invoke() |
input |
config(含 thread_id) |
9.2 三层机制速查
| LLM 缓存 | 节点缓存 | 检查点持久化 | |
|---|---|---|---|
| 来源 | langchain_core.caches |
langgraph.cache |
langgraph.checkpoint |
| 缓存什么 | LLM API 响应 | 节点函数的完整输出 | 图的执行状态 |
| 粒度 | LLM 调用级别 | 节点函数级别 | 整个图级别 |
| 跳过执行? | ✅ | ✅ | ❌ |
| 设置方式 | set_llm_cache() |
cache_policy + cache |
checkpointer + config |
9.3 高级原语速查
| 原语 | 核心能力 | 不可替代场景 |
|---|---|---|
| Send | 动态扇出 1→N | Map-Reduce、批量并行 |
| Command | 状态更新 + 路由一体化 | interrupt/resume、工具控制流程 |
| Runtime Context | 节点随身工具箱 | 类型安全的上下文注入、跨会话存储、流式输出 |