一、TypedDict:给字典定义结构
TypedDict 用来定义一种固定字段和字段类型的字典结构。
from typing import TypedDict
class ChatState(TypedDict):
user_message: str
intent: str
response: str
虽然语法使用:
class ChatState(TypedDict):
但实际数据仍然是字典:
state = {
"user_message": "我要请假",
"intent": "submit_leave",
"response": ""
}
访问方式也是:
state["intent"]
而不是:
state.intent
可以记成:
TypedDict本质上是给字典规定"里面应该有什么字段、字段是什么类型",只是定义时使用了class语法。
二、dataclass、BaseModel、TypedDict 的区别
TypedDict
主要用于:
描述字典结构。
class State(TypedDict):
intent: str
使用:
state["intent"]
dataclass
主要用于:
方便定义一个以保存数据为主的普通类。
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
使用:
user.name
它会自动生成 __init__、__repr__、比较方法等。
Pydantic BaseModel
主要用于:
数据对象 + 运行时校验 + 数据转换。
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
特别适合:
FastAPI 请求
配置文件
LLM 结构化输出
外部数据
简单记:
TypedDict → 给字典规定结构
dataclass → 快速创建数据类
BaseModel → 数据类 + 数据校验
三、Node 和节点函数不是同一个概念
之前容易误解的一句话:
Node 是 LangGraph 中的具体执行单元,通常实现为一个函数。
这句话不够严谨,容易理解成:
函数 = 节点
更准确应该是:
Node 是图中的执行单元;节点绑定一个可调用对象,通常是函数,函数负责实现这个节点的逻辑。
例如:
def recognize(state):
...
builder.add_node("intent_recognition", recognize)
这里:
"intent_recognition"
↓
节点名称
recognize
↓
节点执行的函数逻辑
甚至可以:
builder.add_node("node_a", recognize)
builder.add_node("node_b", recognize)
这时候:
node_a ──→ recognize()
node_b ──→ recognize()
图中是两个不同的节点,但是使用同一个函数逻辑。
所以记住:
节点是"点",函数是这个点执行的具体逻辑。
四、State 是节点之间通信的核心
LangGraph 的 StateGraph 本质上就是:
节点通过读取和写入共享 State 来通信。
官方对节点的典型描述就是:
State → Partial<State>
也就是:
读取当前 State
↓
执行节点逻辑
↓
返回部分 State 更新
节点之间并不是:
node_b(node_a的返回值)
而是:
node_a
↓
return {...}
↓
更新 Graph State
↓
node_b
↓
从 Graph State 读取
所以一句话:
节点之间不是直接传参数,而是通过共享 State 通信。
五、全局 State 的作用
例如:
class OverallState(TypedDict):
user_message: str
intent: str
response: str
builder = StateGraph(OverallState)
可以理解成:
OverallState定义这个 Graph 最基本、最核心的共享状态结构。
例如:
user_message
intent
response
这些就是整个图主要维护的数据。
但是需要注意:
全局 State 不应该简单理解成"整个运行时状态空间的唯一可能结构"。
如果节点声明了额外的、图能够识别的内部状态 schema,那么图内部还可以有用于节点间通信的状态字段。
所以我们后来区分出了:
全局 State
≠
脑子里所理解的所有运行时状态的唯一集合
更好的理解是:
全局 State
→ 图最核心的共享状态 schema
内部/私有 State
→ 特定节点之间需要使用的额外状态 schema
六、状态空间与全局 State 不要混为一谈
这是这两天一个非常关键的认识。
最开始容易认为:
状态空间 = OverallState
于是就会产生疑问:
OverallState 里没有
temp_result,那节点怎么可能传递temp_result?
更合适的心智模型是:
Graph 运行过程中涉及的状态 channels
│
├── 核心 State 字段
│ ├── user_message
│ ├── intent
│ └── response
│
└── 图已经认识的内部状态字段
├── temp_result
└── ...
但这里要注意一个严谨点:
不能理解成节点随便 return****一个从未在任何 schema 中声明的字段,LangGraph 就一定永久保存它。
内部字段也应该通过图所认识的状态 schema 定义。
七、私有 State
私有 State 可以理解成:
只在图内部节点之间使用、不需要作为整个 Graph 对外输入或最终输出暴露的状态。
例如:
class PrivateState(TypedDict):
temp_result: str
某个节点写入:
return {
"temp_result": "abc"
}
后续需要这个内部字段的节点可以读取:
def node_b(state: PrivateState):
print(state["temp_result"])
这里的"私有"不是 Python 的:
__xxx
这种私有属性。
而是:
这是工作流内部使用的数据,不是 Graph 对外接口最关心的数据。
八、私有 State 生命周期
私有字段不是:
节点执行完
↓
马上消失
而是在本次工作流执行过程中,作为状态 channel 供后续节点使用。
后续节点如果再次更新同一个字段:
return {
"temp_result": "new"
}
那么就会按照这个字段对应的 reducer/update 规则更新。
是否能够跨多次运行保存,则涉及:
checkpointer
thread
持久化
这是另外一层机制。
九、节点输入 State:决定这个节点看到什么
例如:
def node_a(state: OverallState):
...
这里的:
state: OverallState
可以理解成这个节点的输入状态视图/schema。
节点从 Graph 当前状态里读取自己需要的数据:
message = state["user_message"]
所以可以记:
节点输入 State 负责描述这个节点读取什么状态。
十、节点 return:返回的是 State 更新
例如:
def node_a(state):
return {
"intent": "submit_leave"
}
它不是说:
新 State 现在只有
intent。
而是在说:
我要更新当前 State 的 intent**。**
假设原来:
{
"user_message": "我要请假",
"intent": "",
"response": ""
}
节点返回:
{
"intent": "submit_leave"
}
之后可以理解成:
{
"user_message": "我要请假",
"intent": "submit_leave",
"response": ""
}
所以:
节点通常只 return 自己需要更新的那几个字段。
LangGraph 官方也把节点模型描述为读取 State、返回 Partial<State>。
十一、Reducer:决定同一字段如何更新
普通字段:
intent: str
顺序更新时,一般后一次更新会取代前一次值。
但是如果多个并行节点在同一步更新同一个普通 channel,就可能产生冲突。
如果我们希望多个结果进行合并,就定义 reducer:
from typing import Annotated
from operator import add
class State(TypedDict):
logs: Annotated[list[str], add]
那么:
{"logs": ["A"]}
和:
{"logs": ["B"]}
可以通过 add 聚合为:
["A", "B"]
官方 StateGraph 也明确说明:每个 State key 都可以配置 reducer,用来聚合多个节点对该字段的更新。
所以记:
普通字段
→ 普通状态更新
Annotated[..., reducer]
→ 按 reducer 合并
十二、invoke() 是初始化 Graph State
例如:
graph.invoke({
"user_message": "我要请假"
})
不要把它理解成:
把参数直接传给 START
也不是:
直接调用 node_a({"user_message": ...})
更好的理解:
invoke(...)
↓
为本次 Graph 运行提供初始 State
↓
START
↓
第一个节点读取当前 State
所以:
**invoke()**给整个 Graph 提供初始状态,而不是给某一个节点直接传参。
十三、START 的作用
假设:
START → A
调用:
graph.invoke({
"user_message": "我要请假"
})
流程:
初始化 Graph State
↓
START
↓
A
START 本身更像一个特殊的入口标记。
真正第一个执行你代码逻辑的是:
A
所以:
invoke()初始化 State,START指定工作流入口,START 后面的节点第一次真正读取 State。
十四、add_sequence()
例如:
builder.add_sequence([
node_a,
node_b
])
表示:
node_a
↓
node_b
LangGraph 会按照列表顺序添加并连接这些节点。
如果没有显式提供名称,节点名称会从函数/Callable 对象推断出来。官方 API 也支持:
[(name, node), ...]
这种明确指定名字的方式。
所以:
builder.add_sequence([node_a, node_b])
节点仍然有名字,只是没有手动写。
十五、add_edge:固定跳转
例如:
builder.add_edge("A", "B")
表示:
A
↓
B
A 执行结束以后,固定执行 B。
这是:
静态路由。
十六、add_conditional_edges:条件动态路由
例如:
def route(state):
if state["intent"] == "submit_leave":
return "submit_node"
else:
return "chat_node"
然后:
builder.add_conditional_edges(
"recognize",
route
)
执行:
recognize
↓
route(state)
↓
根据当前 State
↓
submit_node / chat_node
所以:
**add_conditional_edges()**是图结构层面的动态分支。
路由函数的输入也是当前 Graph State。
官方定义中,path 函数负责返回一个或多个下一节点;如果没有 path_map,返回值应该直接对应节点名。
十七、path_map
情况 1:直接返回节点名
def route(state):
return "node_a"
那么:
builder.add_conditional_edges(
"start",
route
)
不一定需要 path_map。
因为:
"node_a"
本身就是节点名。
情况 2:返回别名
例如:
def route(state):
return "a"
那么可以:
path_map = {
"a": "node_a",
"b": "node_b"
}
于是:
route 返回 "a"
↓
path_map
↓
node_a
所以:
字典形式的
path_map可以完成"路由标签 → 节点名"的映射。
情况 3:path_map 列表
例如:
path_map=["worker_node"]
这里没有:
别名 → 节点
这种映射关系。
它更多是在明确:
这个条件路径可能到达哪些节点。
如果没有 path_map 或返回类型提示,LangGraph 在图可视化等场景可能只能保守地认为它可能跳到很多节点;显式的 path_map 能帮助描述可能的目标范围。
十八、为什么多个 Send,path_map 可能只有一个值
例如:
return [
Send("worker_node", {"task": "A"}),
Send("worker_node", {"task": "B"}),
Send("worker_node", {"task": "C"})
]
虽然有 3 个 Send,但目标节点种类只有:
worker_node
只是这个节点被运行了 3 个实例:
worker_node(task=A)
worker_node(task=B)
worker_node(task=C)
所以:
path_map=["worker_node"]
完全合理。
记住:
Send 数量表示运行多少任务实例;path_map 描述可能涉及哪些节点名称。
十九、Command(goto=...)
Command 是另外一种动态控制流程的方法。
例如:
from langgraph.types import Command
def node_a(state):
if state["intent"] == "submit_leave":
return Command(goto="submit_node")
return Command(goto="chat_node")
可以理解成:
node_a
↓
节点自己决定下一步
↓
submit_node / chat_node
和:
builder.add_conditional_edges(...)
相比:
add_conditional_edges
→ 图结构外部定义路由函数
Command(goto=...)
→ 节点内部直接决定下一步
二十、Command 可以同时更新 State
例如:
return Command(
update={
"intent": "submit_leave"
},
goto="submit_node"
)
相当于同时:
1. 更新 State
2. 决定下一站
即:
intent = "submit_leave"
+
goto submit_node
官方 Command 当前支持 update 和 goto,其中 goto 可以是节点名、多个节点名、Send 或多个 Send。
二十一、add_conditional_edges 和 Command 的关系
这两者都是:
动态控制"下一步去哪"。
但是控制位置不同。
add_conditional_edges
节点执行完
↓
额外调用 route(state)
↓
route 决定下一步
Command
节点函数执行过程中
↓
自己 return Command(...)
↓
直接决定下一步
因此可以记:
add_conditional_edges**= 图层面的动态路由。**
Command(goto=...)****= 节点内部的动态跳转。
整个 Graph 当然可以同时使用这两种机制。
但同一个路由决策通常没有必要重复设计两套逻辑。
二十二、Send 是什么
Send 最核心的理解:
动态执行某个节点,并给这一次节点执行一份自定义输入。
语法:
Send(
"节点名",
这一次节点的输入
)
例如:
Send(
"worker",
{"task": "A"}
)
表示:
动态执行 worker 节点
+
这次 worker 收到:
{"task": "A"}
注意:
"worker"
是节点名,不是函数名。
假设:
def worker_func(state):
...
注册:
builder.add_node(
"worker",
worker_func
)
那么:
Send("worker", {"task": "A"})
关系是:
Send
↓
找到 "worker" 节点
↓
worker 节点绑定 worker_func
↓
用 {"task": "A"} 执行这次 worker
官方定义中,Send(node, arg) 的第一个参数就是目标节点名,第二个参数就是发送给这个节点的自定义 state/message。
二十三、Send 最大的特点:自定义目标节点输入
普通节点通常通过 Graph 当前 State 获得输入。
但:
Send("worker", {"task": "A"})
这一次 worker 的输入就是:
{"task": "A"}
官方明确说明:
Send发出的 state 可以和 Graph 的核心 State 不同。
这也是它适合 map-reduce 和动态任务分发的原因。
如果 worker 还需要其他信息:
Send(
"worker",
{
"task": task,
"user_message": state["user_message"]
}
)
就自己一起传进去。
二十四、Send 执行完成以后
Send 改变的是:
这个 worker 这一次吃什么输入。
worker 返回的数据仍然会按照 Graph State 的规则产生更新。
例如:
def worker(state):
return {
"results": [
state["task"] + "完成"
]
}
多个 worker:
worker(A)
→ ["A完成"]
worker(B)
→ ["B完成"]
worker(C)
→ ["C完成"]
为了安全聚合结果,通常会使用 reducer:
results: Annotated[list[str], add]
于是:
[
"A完成",
"B完成",
"C完成"
]
二十五、Send 的核心不是普通跳转,而是任务分发
虽然 Send 看起来像:
突然跳到 worker
这个理解有助于入门。
但是更准确的语义是:
动态发送一个执行任务到某个节点。
特别是:
[
Send("worker", {"task": "A"}),
Send("worker", {"task": "B"}),
Send("worker", {"task": "C"})
]
这就非常明显:
一个分发点
↓
┌───┼───┐
↓ ↓ ↓
A B C
它不是简单"跳转一次",而是:
动态产生多个节点执行实例。
二十六、Send 最经典的使用方式
最经典的是:
def dispatch(state):
return [
Send("worker", {"task": task})
for task in state["tasks"]
]
builder.add_conditional_edges(
"dispatch_node",
dispatch
)
流程:
dispatch_node
↓
dispatch(state)
↓
产生多个 Send
↓
worker(A)
worker(B)
worker(C)
官方 Send 文档也把 conditional edges + map-reduce 作为典型场景。
二十七、Command 也可以配合 Send
Command.goto 当前也支持 Send。
例如:
return Command(
goto=Send(
"worker",
{"task": "A"}
)
)
甚至:
return Command(
goto=[
Send("worker", {"task": "A"}),
Send("worker", {"task": "B"}),
Send("worker", {"task": "C"})
]
)
意思就是:
当前节点
↓
Command 控制下一步
↓
通过多个 Send
↓
动态派发三个任务
官方当前 Command.goto 类型明确支持一个 Send 或多个 Send。
二十八、三种机制最终怎么记
这是目前最重要的总结。
add_conditional_edges
重点:
动态选择路径。
根据 State
↓
A / B / C
Command(goto=...)
重点:
当前节点主动控制下一步。
当前节点
↓
我自己决定 goto 哪里
Send
重点:
动态分发任务,并可以给每个任务自定义输入。
任务列表
↓
worker(A)
worker(B)
worker(C)
因此可以总结成:
跳转/路由主要看 add_conditional_edges****和 Command**;** Send****更强调任务分发。
二十九、静态 Fan-out / Fan-in
Fan-out:扇出
一个节点分成多个节点:
B
↗
A
↘
C
也就是:
一个 → 多个
Fan-in:扇入
多个节点汇聚到一个节点:
B ──┐
├──→ D
C ──┘
也就是:
多个 → 一个
三十、静态扇入
如果:
builder.add_edge(
["B", "C"],
"D"
)
表示:
B 完成 ──┐
├──→ D
C 完成 ──┘
这种多起点边会等待所有指定的上游节点完成后再执行下游节点。
所以它是一种明显的:
AND 汇合
即:
B 和 C 都完成,再进入 D。
之所以叫"静态",是因为:
B
C
D
这些节点关系在建 Graph 时就已经确定。
三十一、动态扇出
使用 Send:
return [
Send("worker", {"task": task})
for task in state["tasks"]
]
如果:
tasks = ["A", "B", "C"]
就是:
dispatch
↓
worker(A)
worker(B)
worker(C)
如果 tasks 有 100 个:
worker × 100
运行之前可能不知道具体有多少个。
这就是:
动态 Fan-out。
三十二、动态扇入
多个动态 worker 返回:
{"results": [...]}
通过:
results: Annotated[list[str], add]
聚合:
worker(A) ──┐
worker(B) ──┼──→ results
worker(C) ──┘
后面的节点再读取:
state["results"]
就是典型的:
动态 fan-out → reducer 聚合 → 后续继续处理。
这也是 Send 官方文档提到的典型 map-reduce 模式。
三十三、这两天形成的最终心智模型
整个 LangGraph 可以先理解成:
Graph State
┌────────────────┐
│ user_message │
│ intent │
│ task_result │
│ results │
│ ... │
└────────────────┘
↑ ↓
写 读
↑ ↓
Node A Node B
节点:
读取 State
↓
执行逻辑
↓
返回 State 更新
边:
决定接下来执行谁
其中:
add_edge
→ 静态路径
add_conditional_edges
→ 图层面的动态路径
Command(goto=...)
→ 节点内部动态控制路径
Send
→ 动态任务分发 + 自定义目标节点输入
Reducer
→ 决定多个 State 更新如何聚合
三十四、最值得记住的 10 句话
-
节点是图里的点,函数是节点执行的逻辑。
-
LangGraph 节点之间不是直接传参数,而是通过 State 通信。
-
**invoke()**提供的是整个 Graph 的初始状态。
-
节点输入 State 决定它读取什么数据。
-
节点 return****的通常是部分 State 更新,而不是一份新的完整 State。
-
全局 State 描述图最核心的共享状态结构;不要简单把它和所有内部状态概念完全画等号。
-
Reducer 决定同一个状态字段的多个更新怎么合并。
-
add_conditional_edges****是图层面的动态路由, Command****是节点内部的动态路由。
-
**Send("节点名", 输入)**的核心是动态任务分发,并允许这次节点执行使用自定义输入。
-
Command**/** add_conditional_edges****更关注"流程往哪走", Send****更关注"任务怎么分出去"。