文章目录
-
- [第 3 章:节点与边 ------ 图的骨架](#第 3 章:节点与边 —— 图的骨架)
-
- [3.1 本章目标](#3.1 本章目标)
- [3.2 核心概念](#3.2 核心概念)
-
- [节点(Node)------ 做什么](#节点(Node)—— 做什么)
- [边(Edge)------ 控制流程](#边(Edge)—— 控制流程)
- 条件路由决策树
- [Send 并行扇出示意图](#Send 并行扇出示意图)
- [3.3 实战](#3.3 实战)
-
- [实战 1:条件路由 ------ 根据用户意图分流](#实战 1:条件路由 —— 根据用户意图分流)
- [实战 2:并行执行 ------ Send API 实现 map-reduce](#实战 2:并行执行 —— Send API 实现 map-reduce)
- [实战 3:循环控制 ------ LLM 反思循环](#实战 3:循环控制 —— LLM 反思循环)
- [3.4 API 速查](#3.4 API 速查)
- [3.5 错误与避坑指南](#3.5 错误与避坑指南)
-
- [坑 1:条件路由返回值不在 path_map 中](#坑 1:条件路由返回值不在 path_map 中)
- [坑 2:忘记从 START 连接](#坑 2:忘记从 START 连接)
- [坑 3:循环无终止条件](#坑 3:循环无终止条件)
- [坑 4:Send 并行节点共享 Key 时忘记 Reducer](#坑 4:Send 并行节点共享 Key 时忘记 Reducer)
- [3.6 最佳实践总结](#3.6 最佳实践总结)
第 3 章:节点与边 ------ 图的骨架

3.1 本章目标
学完本章你将能够:
- 掌握节点的多种定义方式(函数、lambda、ToolNode)
- 理解三种边类型:普通边、条件边、并行边
- 学会条件路由的分支逻辑
- 掌握 Send API 实现并行扇出(map-reduce 模式)
3.2 核心概念
节点(Node)------ 做什么
节点是图中执行具体工作的单元。每个节点是一个 Python 函数,接收当前 State,返回部分更新。
python
# 节点函数的三种写法:
# 1. 普通函数(最常用)
def my_node(state: State) -> dict:
return {"key": "value"}
# 2. Lambda 函数(简单逻辑)
builder.add_node("simple", lambda state: {"counter": state["counter"] + 1})
# 3. ToolNode(工具执行,第 4 章详解)
from langgraph.prebuilt import ToolNode
builder.add_node("tools", ToolNode(tools))
边(Edge)------ 控制流程
LangGraph 提供三种边类型,对应不同的流程控制需求:
#mermaid-svg-tqGUBClzaVuoMtB6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tqGUBClzaVuoMtB6 .error-icon{fill:#552222;}#mermaid-svg-tqGUBClzaVuoMtB6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tqGUBClzaVuoMtB6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tqGUBClzaVuoMtB6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tqGUBClzaVuoMtB6 .marker.cross{stroke:#333333;}#mermaid-svg-tqGUBClzaVuoMtB6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tqGUBClzaVuoMtB6 p{margin:0;}#mermaid-svg-tqGUBClzaVuoMtB6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 .cluster-label text{fill:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 .cluster-label span{color:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 .cluster-label span p{background-color:transparent;}#mermaid-svg-tqGUBClzaVuoMtB6 .label text,#mermaid-svg-tqGUBClzaVuoMtB6 span{fill:#333;color:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 .node rect,#mermaid-svg-tqGUBClzaVuoMtB6 .node circle,#mermaid-svg-tqGUBClzaVuoMtB6 .node ellipse,#mermaid-svg-tqGUBClzaVuoMtB6 .node polygon,#mermaid-svg-tqGUBClzaVuoMtB6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tqGUBClzaVuoMtB6 .rough-node .label text,#mermaid-svg-tqGUBClzaVuoMtB6 .node .label text,#mermaid-svg-tqGUBClzaVuoMtB6 .image-shape .label,#mermaid-svg-tqGUBClzaVuoMtB6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tqGUBClzaVuoMtB6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tqGUBClzaVuoMtB6 .rough-node .label,#mermaid-svg-tqGUBClzaVuoMtB6 .node .label,#mermaid-svg-tqGUBClzaVuoMtB6 .image-shape .label,#mermaid-svg-tqGUBClzaVuoMtB6 .icon-shape .label{text-align:center;}#mermaid-svg-tqGUBClzaVuoMtB6 .node.clickable{cursor:pointer;}#mermaid-svg-tqGUBClzaVuoMtB6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tqGUBClzaVuoMtB6 .arrowheadPath{fill:#333333;}#mermaid-svg-tqGUBClzaVuoMtB6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tqGUBClzaVuoMtB6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tqGUBClzaVuoMtB6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tqGUBClzaVuoMtB6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tqGUBClzaVuoMtB6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tqGUBClzaVuoMtB6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tqGUBClzaVuoMtB6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tqGUBClzaVuoMtB6 .cluster text{fill:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 .cluster span{color:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tqGUBClzaVuoMtB6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tqGUBClzaVuoMtB6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tqGUBClzaVuoMtB6 .icon-shape,#mermaid-svg-tqGUBClzaVuoMtB6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tqGUBClzaVuoMtB6 .icon-shape p,#mermaid-svg-tqGUBClzaVuoMtB6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tqGUBClzaVuoMtB6 .icon-shape .label rect,#mermaid-svg-tqGUBClzaVuoMtB6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tqGUBClzaVuoMtB6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tqGUBClzaVuoMtB6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tqGUBClzaVuoMtB6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 并行边 Send
Send
Send
Send
汇总
汇总
汇总
分发节点
任务1
任务2
任务3
汇总节点
条件边 add_conditional_edges
条件1
条件2
默认
路由节点
分支A
分支B
分支C
普通边 add_edge
固定顺序
节点A
节点B
条件路由决策树
#mermaid-svg-IV7OGzRgYU9wycGb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IV7OGzRgYU9wycGb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IV7OGzRgYU9wycGb .error-icon{fill:#552222;}#mermaid-svg-IV7OGzRgYU9wycGb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IV7OGzRgYU9wycGb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IV7OGzRgYU9wycGb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IV7OGzRgYU9wycGb .marker.cross{stroke:#333333;}#mermaid-svg-IV7OGzRgYU9wycGb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IV7OGzRgYU9wycGb p{margin:0;}#mermaid-svg-IV7OGzRgYU9wycGb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IV7OGzRgYU9wycGb .cluster-label text{fill:#333;}#mermaid-svg-IV7OGzRgYU9wycGb .cluster-label span{color:#333;}#mermaid-svg-IV7OGzRgYU9wycGb .cluster-label span p{background-color:transparent;}#mermaid-svg-IV7OGzRgYU9wycGb .label text,#mermaid-svg-IV7OGzRgYU9wycGb span{fill:#333;color:#333;}#mermaid-svg-IV7OGzRgYU9wycGb .node rect,#mermaid-svg-IV7OGzRgYU9wycGb .node circle,#mermaid-svg-IV7OGzRgYU9wycGb .node ellipse,#mermaid-svg-IV7OGzRgYU9wycGb .node polygon,#mermaid-svg-IV7OGzRgYU9wycGb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IV7OGzRgYU9wycGb .rough-node .label text,#mermaid-svg-IV7OGzRgYU9wycGb .node .label text,#mermaid-svg-IV7OGzRgYU9wycGb .image-shape .label,#mermaid-svg-IV7OGzRgYU9wycGb .icon-shape .label{text-anchor:middle;}#mermaid-svg-IV7OGzRgYU9wycGb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IV7OGzRgYU9wycGb .rough-node .label,#mermaid-svg-IV7OGzRgYU9wycGb .node .label,#mermaid-svg-IV7OGzRgYU9wycGb .image-shape .label,#mermaid-svg-IV7OGzRgYU9wycGb .icon-shape .label{text-align:center;}#mermaid-svg-IV7OGzRgYU9wycGb .node.clickable{cursor:pointer;}#mermaid-svg-IV7OGzRgYU9wycGb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IV7OGzRgYU9wycGb .arrowheadPath{fill:#333333;}#mermaid-svg-IV7OGzRgYU9wycGb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IV7OGzRgYU9wycGb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IV7OGzRgYU9wycGb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IV7OGzRgYU9wycGb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IV7OGzRgYU9wycGb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IV7OGzRgYU9wycGb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IV7OGzRgYU9wycGb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IV7OGzRgYU9wycGb .cluster text{fill:#333;}#mermaid-svg-IV7OGzRgYU9wycGb .cluster span{color:#333;}#mermaid-svg-IV7OGzRgYU9wycGb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IV7OGzRgYU9wycGb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IV7OGzRgYU9wycGb rect.text{fill:none;stroke-width:0;}#mermaid-svg-IV7OGzRgYU9wycGb .icon-shape,#mermaid-svg-IV7OGzRgYU9wycGb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IV7OGzRgYU9wycGb .icon-shape p,#mermaid-svg-IV7OGzRgYU9wycGb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IV7OGzRgYU9wycGb .icon-shape .label rect,#mermaid-svg-IV7OGzRgYU9wycGb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IV7OGzRgYU9wycGb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IV7OGzRgYU9wycGb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IV7OGzRgYU9wycGb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 意图=查询
意图=投诉
意图=闲聊
其他
START
路由节点
分析用户意图
搜索节点
投诉处理节点
闲聊节点
兜底节点
END
Send 并行扇出示意图
渲染错误: Mermaid 渲染失败: Parse error on line 2: ...OUT{分发节点
return [Send(...), Send(... -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
3.3 实战
实战 1:条件路由 ------ 根据用户意图分流
python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
# ============================================
# 定义 State
# ============================================
class State(TypedDict):
user_input: str
intent: str
response: str
# ============================================
# 定义节点
# ============================================
def classify_intent(state: State) -> dict:
"""
意图识别节点:分析用户输入,判断意图。
实际项目中这里会调用 LLM 做分类,这里用简单规则模拟。
"""
text = state["user_input"]
if "价格" in text or "多少钱" in text:
return {"intent": "query"}
elif "退款" in text or "投诉" in text:
return {"intent": "complaint"}
else:
return {"intent": "chitchat"}
def handle_query(state: State) -> dict:
"""处理产品查询"""
return {"response": f"查询结果:关于「{state['user_input']}」,产品价格是 99 元。"}
def handle_complaint(state: State) -> dict:
"""处理投诉"""
return {"response": f"已收到您的投诉,客服将在 24 小时内联系您。"}
def handle_chitchat(state: State) -> dict:
"""处理闲聊"""
return {"response": f"您好!有什么可以帮助您的吗?"}
# ============================================
# 定义路由函数(条件边的核心)
# ============================================
def route_by_intent(state: State) -> Literal["query", "complaint", "chitchat"]:
"""
路由函数:根据 intent 字段决定下一步去哪个节点。
关键规则:
1. 返回值必须是已注册的节点名(或 END)
2. 使用 Literal 类型标注,IDE 自动补全,且自动生成 path_map
"""
return state["intent"] # 返回 "query" / "complaint" / "chitchat"
# ============================================
# 构建图
# ============================================
builder = StateGraph(State)
# 注册节点
builder.add_node("classify", classify_intent)
builder.add_node("query", handle_query)
builder.add_node("complaint", handle_complaint)
builder.add_node("chitchat", handle_chitchat)
# 添加边
builder.add_edge(START, "classify")
# 添加条件边:从 classify 节点出发,根据 route_by_intent 的返回值路由
builder.add_conditional_edges(
source="classify", # 从哪个节点出发
path=route_by_intent, # 路由函数
path_map={ # 返回值 → 目标节点的映射
"query": "query",
"complaint": "complaint",
"chitchat": "chitchat",
},
)
# 所有分支最终都走到 END
builder.add_edge("query", END)
builder.add_edge("complaint", END)
builder.add_edge("chitchat", END)
graph = builder.compile()
# ============================================
# 测试
# ============================================
print("=== 测试 1:查询 ===")
result = graph.invoke({"user_input": "这个产品多少钱?"})
print(f" 意图: {result['intent']}, 回复: {result['response']}")
print("=== 测试 2:投诉 ===")
result = graph.invoke({"user_input": "我要投诉退款"})
print(f" 意图: {result['intent']}, 回复: {result['response']}")
print("=== 测试 3:闲聊 ===")
result = graph.invoke({"user_input": "你好"})
print(f" 意图: {result['intent']}, 回复: {result['response']}")
实战 2:并行执行 ------ Send API 实现 map-reduce
python
from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
# ============================================
# 定义 State
# ============================================
class State(TypedDict):
# 输入:要处理的任务列表
tasks: list[str]
# 输出:每个任务的处理结果,使用 operator.add 累加
results: Annotated[list[str], operator.add]
# ============================================
# 定义节点
# ============================================
def process_task(state: State) -> dict:
"""
处理单个任务。
注意:每个 Send 调用都会创建一个独立的 task 实例,
这里的 state["tasks"] 实际是 Send 传入的单个任务。
"""
# 因为 Send 传入的是 {"tasks": [单个任务]},取第一个即可
task = state["tasks"][0] if isinstance(state["tasks"], list) else state["tasks"]
return {"results": [f"✅ 已完成: {task}"]}
# ============================================
# 定义分发函数(返回 Send 列表)
# ============================================
def distribute_tasks(state: State) -> list[Send]:
"""
将任务列表中的每个任务分发为一个独立的并行执行单元。
Send 的签名:
- 第一个参数:目标节点名
- 第二个参数:传给目标节点的 State 数据
"""
return [Send("process_task", {"tasks": [task]}) for task in state["tasks"]]
# ============================================
# 构建图
# ============================================
builder = StateGraph(State)
builder.add_node("process_task", process_task)
# 使用 add_conditional_edges 接收 Send 列表
# 当路由函数返回 list[Send] 时,LangGraph 自动并行执行
builder.add_conditional_edges(START, distribute_tasks)
builder.add_edge("process_task", END)
graph = builder.compile()
# ============================================
# 测试
# ============================================
result = graph.invoke({
"tasks": ["写报告", "发邮件", "安排会议", "整理文件"],
"results": [],
})
print(f"处理了 {len(result['results'])} 个任务:")
for r in result["results"]:
print(f" {r}")
实战 3:循环控制 ------ LLM 反思循环
python
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
content: str # 生成的内容
quality_score: int # 质量评分
iteration: int # 当前迭代次数
max_iterations: int # 最大迭代次数
def generate_content(state: State) -> dict:
"""模拟内容生成"""
iteration = state["iteration"] + 1
return {
"content": f"第 {iteration} 版内容",
"iteration": iteration,
}
def evaluate_quality(state: State) -> dict:
"""模拟质量评估"""
# 模拟:第 3 次迭代时质量达标
score = 95 if state["iteration"] >= 3 else 60
return {"quality_score": score}
def should_continue(state: State) -> Literal["generate", "END"]:
"""决定是否继续循环"""
if state["quality_score"] >= 90:
return "END" # 质量达标,退出
if state["iteration"] >= state["max_iterations"]:
return "END" # 达到最大迭代次数,退出
return "generate" # 继续生成
builder = StateGraph(State)
builder.add_node("generate", generate_content)
builder.add_node("evaluate", evaluate_quality)
builder.add_edge(START, "generate")
builder.add_edge("generate", "evaluate")
# 条件边实现循环:evaluate → generate(如果质量不达标)
builder.add_conditional_edges(
source="evaluate",
path=should_continue,
path_map={"generate": "generate", "END": END},
)
graph = builder.compile()
result = graph.invoke({
"content": "",
"quality_score": 0,
"iteration": 0,
"max_iterations": 5,
})
print(f"最终内容: {result['content']}")
print(f"质量评分: {result['quality_score']}")
print(f"迭代次数: {result['iteration']}")
3.4 API 速查
| API | 完整签名 | 入参说明 | 返回值 | 说明 |
|---|---|---|---|---|
add_node(name, action) |
add_node(node: str, action: Callable) |
node: 节点名; action: 节点函数 |
Self |
注册节点 |
add_edge(start, end) |
add_edge(start_key: str, end_key: str) |
start_key: 源; end_key: 目标 |
Self |
普通单向边 |
add_conditional_edges(source, path, path_map) |
add_conditional_edges(source, path, path_map) |
source: 源节点; path: 路由函数; path_map: 映射表 dict[str, str] |
Self |
条件路由边 |
Send(node, arg) |
Send(node: str, arg: dict) |
node: 目标节点; arg: 传入 State 数据 |
Send 对象 |
并行分发单元 |
START |
常量 | 无 | "__start__" |
图入口标记 |
END |
常量 | 无 | "__end__" |
图出口标记 |
3.5 错误与避坑指南
坑 1:条件路由返回值不在 path_map 中
python
# ❌ 错误写法
def route(state: State) -> str:
return "unknown" # 返回值不在 path_map 中!
builder.add_conditional_edges("source", route, {
"a": "node_a",
"b": "node_b",
# 没有 "unknown" 的映射!
})
# → 运行时报错:找不到目标节点
# ✅ 正确写法:确保覆盖所有可能返回值
def route(state: State) -> Literal["a", "b", "fallback"]:
intent = state.get("intent", "fallback")
if intent in ("a", "b"):
return intent
return "fallback" # 兜底
builder.add_conditional_edges("source", route, {
"a": "node_a",
"b": "node_b",
"fallback": "node_fallback", # 覆盖所有分支
})
坑 2:忘记从 START 连接
python
# ❌ 错误写法
builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
# 忘了 START → node_a → 图没有入口,执行报错
# ✅ 正确写法
builder.add_edge(START, "node_a") # 必须指定入口
坑 3:循环无终止条件
python
# ❌ 错误写法
def always_continue(state: State) -> str:
return "loop" # 永远返回 loop → 无限循环!
# ✅ 正确写法:设置退出条件
def smart_continue(state: State) -> Literal["loop", "END"]:
if state["iteration"] >= state["max_iterations"]:
return "END"
return "loop"
坑 4:Send 并行节点共享 Key 时忘记 Reducer
python
# ❌ 错误写法
class State(TypedDict):
results: list[str] # 没有 Reducer!
# 多个并行 Send 写入 results → 可能互相覆盖,丢失数据
# ✅ 正确写法
class State(TypedDict):
results: Annotated[list[str], operator.add] # 使用 Reducer 累加
3.6 最佳实践总结
- 使用
Literal标注路由函数返回值 :IDE 自动补全,且自动生成path_map,减少手动维护 - 条件路由始终保留兜底分支 :即使理论上不会走到,也加一个
fallback防止异常 - 并行任务用
Send,串行依赖用add_edge:不要用Send处理有依赖关系的任务 - 循环中设置明确的退出条件:最大迭代次数 + 质量达标双重保险
- 先画图再写代码:复杂流程先用 Mermaid 画出图结构,思路更清晰