【langgraph 从入门到精通graphApi 篇】节点与边 —— 图的骨架

文章目录

    • [第 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 本章目标

学完本章你将能够:

  1. 掌握节点的多种定义方式(函数、lambda、ToolNode)
  2. 理解三种边类型:普通边、条件边、并行边
  3. 学会条件路由的分支逻辑
  4. 掌握 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 最佳实践总结

  1. 使用 Literal 标注路由函数返回值 :IDE 自动补全,且自动生成 path_map,减少手动维护
  2. 条件路由始终保留兜底分支 :即使理论上不会走到,也加一个 fallback 防止异常
  3. 并行任务用 Send,串行依赖用 add_edge :不要用 Send 处理有依赖关系的任务
  4. 循环中设置明确的退出条件:最大迭代次数 + 质量达标双重保险
  5. 先画图再写代码:复杂流程先用 Mermaid 画出图结构,思路更清晰

相关推荐
CHANG_THE_WORLD5 小时前
7.C++标准多线程实现 TCP通信
java·c++·tcp/ip
理人综艺好会5 小时前
ReAct Agent:AI智能体最基础的核心逻辑
人工智能
chen_ke_hao5 小时前
K8s 高可用集群部署
java·docker·kubernetes
RUZHUA5 小时前
2026企业媒体发稿平台选型指南:从资源效率到AI时代传播能力的全面评估
人工智能
minhuan5 小时前
LangChain+LangGraph架构拆解:基于复杂AI任务编排与状态管理的大模型应用开发21.4
人工智能·langchain·大模型应用·langgraph·复杂ai任务编排
CodeStats5 小时前
《源纹天书》第一百八十一章至第一百八十五章:跨界bug·双世界联合debug
java·源纹天书
甲维斯5 小时前
Kimi k3是真碉,但太不经用,也有拉跨点!
人工智能
小则又沐风a5 小时前
库的原理:目标文件,ELF格式,程序加载
java·linux·前端
KG_LLM图谱增强大模型5 小时前
论文浅尝 | 基于先验思考的知识图谱上大语言模型的可信推理(NeurIPS 2025)
人工智能·语言模型·知识图谱