第07章(上):LangGraph 工作流 —— 为什么需要它,以及如何入门

0 阅读13分钟

本文面向:有 Python 基础,学过 LangChain Chain/Agent 但还没接触 LangGraph 的开发者。读完本文,你将清楚地知道:LangGraph 解决什么问题、什么场景用它、它有哪些优缺点,以及如何一步步开发第一个 LangGraph 应用。

前期回顾

写给零基础开发者

先问自己这 4 个问题,如果有 2 个"是",你就需要 LangGraph:

问题你的答案
我的 AI 流程需要根据条件走不同分支吗?□ 是 □ 否
我的 AI 任务需要循环执行直到满足条件吗?□ 是 □ 否
我需要在关键步骤让人工审核/确认吗?□ 是 □ 否
我需要多个 AI 角色互相协作完成任务吗?□ 是 □ 否

答了 2 个以上"是" → 继续读。全是"否" → 用 LangChain Chain 就够了。


一、为什么需要 LangGraph?

1.1 Chain 和 Agent 的局限

在学习 LangGraph 之前,你已经学过:

  • Chain:把任务串成流水线,A → B → C,每步固定,不能分支,不能循环
  • Agent:LLM 自主决策,循环调用工具,灵活但像"黑箱",难以控制和调试

它们能覆盖大多数场景,但有些问题它们解决不了:

❌ Chain 做不到的事

场景:文章审核流程
要求:AI 审核文章 → 如果有问题则修改 → 修改后再审核 → 直到通过

用 Chain 实现:
  审核链 → 修改链 → 审核链 → 修改链 → ...(无限嵌套!怎么循环?)
  
❌ 问题:Chain 是线性的,不支持循环和条件分支

❌ Agent 做不到的事

场景:资金报销审批
要求:AI 分析报销单 → 金额超过5000元必须人工审批 → 人工批准后才能入账

用 Agent 实现:
  create_react_agent(llm, tools=[analyze_tool, approve_tool])
  
❌ 问题:Agent 自主决策,根本不会等人工!它会直接"假装"已批准后继续执行

❌ 多 Agent 协作做不到的事

场景:AI 写作系统
要求:研究员收集资料 → 写作者撰写草稿 → 审核者评审 → 不通过则发回写作者修改

用单个 Agent 实现:
  一个 Agent 需要"扮演"多个角色,提示词复杂,行为难以控制
  
❌ 问题:单 Agent 难以稳定地模拟多角色协作

1.2 LangGraph 的解决方案

LangGraph 将 AI 工作流表示为有向图(Directed Graph)

  • 节点(Node) = Python 函数,处理数据
  • 边(Edge) = 固定顺序连接
  • 条件边(Conditional Edge) = 根据结果动态选择下一步
  • 状态(State) = 所有节点共享的数据(相当于工作流的"白板")

1.3 Chain vs Agent vs LangGraph 对比

特性ChainAgentLangGraph
执行结构线性,固定顺序动态循环,LLM 决定图状态机,你控制
循环支持✅ (ReAct)✅ (显式循环)
条件分支✅ (隐式)✅ (显式,你定义)
人工介入
多 Agent 协作
可调试性
适合新手需要一定基础
灵活性最高

1.4 LangGraph 的优缺点

✅ 优点:

  1. 显式控制流:你定义图结构,行为可预测、可调试
  2. 支持复杂流程:循环、分支、并行、多 Agent 协作都原生支持
  3. Human-in-the-Loop:可以在任意节点暂停等待人工确认
  4. 持久化与恢复:通过 Checkpointer 保存状态,支持断点续传
  5. 流式输出:原生支持流式执行,用户体验好
  6. 可视化:内置 draw_mermaid()print_ascii() 查看图结构

❌ 缺点:

  1. 学习曲线:需要理解状态机概念,比 Chain/Agent 复杂
  2. 代码量更多:简单任务用 LangGraph 反而繁琐
  3. 过度工程:如果只是线性任务,用 Chain 即可

1.5 什么场景该用 LangGraph?

典型场景举例:

场景用 LangGraph 的理由
代码生成→执行→报错→修复→再执行需要循环,根据执行结果决定是否继续
文章审核→不合格→修改→再审核需要循环 + 条件分支
报销单审批(超额需人工批准)需要 Human-in-the-Loop
研究员+写作者+审核者协作写文章需要多 Agent 协作
客服:问题分类→路由到专业客服需要条件分支(路由)

二、三大核心概念

2.1 State(状态)—— 工作流的"共享白板"

State 是所有节点共享的数据字典,类似于团队协作中大家都能看到和修改的白板。

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class MyState(TypedDict):
    # ─── 普通字段:后写的值覆盖先写的值(像普通变量赋值)───
    user_name: str        # "Alice"
    task_status: str      # "pending" / "in_progress" / "done"
    result: str           # 最终结果
    
    # ─── 特殊字段:使用 Reducer 函数控制更新行为 ───
    # add_messages 是一个追加函数:新消息被追加到列表(而不是覆盖)
    # 这样可以保留完整的对话历史
    messages: Annotated[list, add_messages]

为什么要用 Annotated[list, add_messages]

# 不带 Annotated(普通字段):
# 节点A 写入 result="第一次结果"
# 节点B 写入 result="第二次结果"
# 最终 state["result"] = "第二次结果"(被覆盖)

# 带 Annotated[list, add_messages](消息字段):
# 节点A 写入 messages=[消息1]
# 节点B 写入 messages=[消息2]
# 最终 state["messages"] = [消息1, 消息2](追加合并)
# ✅ 保留了完整的对话历史

2.2 Node(节点)—— 工作流的"处理步骤"

节点是一个普通的 Python 函数,接收 State,返回要更新的字段。

def my_node(state: MyState) -> dict:
    # ─── 从状态中读取数据 ───
    user_input = state["messages"][-1].content  # 最新消息
    current_status = state.get("task_status", "pending")
    
    # ─── 执行业务逻辑(可以调用 LLM、数据库、API 等)───
    result = process_input(user_input)
    
    # ─── 返回要更新的字段(只需返回变更的部分,不用返回完整 state)───
    return {
        "result": result,
        "task_status": "done",
        # 注意:没有返回的字段保持原值不变
    }

重要规则:

  • 节点只返回需要更新的字段,不需要返回完整状态
  • 节点可以修改任何 State 字段(包括读取其他节点写入的字段)

2.3 Edge(边)—— 工作流的"流转路径"

边决定节点的执行顺序。有两种类型:

from langgraph.graph import StateGraph, START, END

builder = StateGraph(MyState)

# ── 普通边:A 执行完后,一定执行 B ──
builder.add_edge("node_a", "node_b")

# ── 条件边:根据路由函数的返回值决定下一步 ──
def my_router(state: MyState) -> str:
    """路由函数:返回下一个节点的名称。"""
    if state["task_status"] == "error":
        return "retry_node"
    elif state["result"] == "":
        return "fallback_node"
    else:
        return "next_node"

builder.add_conditional_edges(
    "node_a",          # 源节点
    my_router,         # 路由函数(返回字符串)
    {
        "retry_node":    "retry_node",    # 路由函数返回 "retry_node" → 去这里
        "fallback_node": "fallback_node", # 路由函数返回 "fallback_node" → 去这里
        "next_node":     "next_node",     # 路由函数返回 "next_node" → 去这里
    }
)

2.4 建图、编译、运行 —— 三步走

# 第1步:构建图
builder = StateGraph(MyState)
builder.add_node("step1", step1_function)
builder.add_node("step2", step2_function)
builder.add_edge(START, "step1")      # START 是内置起点
builder.add_edge("step1", "step2")
builder.add_edge("step2", END)        # END 是内置终点

# 第2步:编译(生成可执行对象)
graph = builder.compile()

# 第3步:运行(传入初始状态)
result = graph.invoke({
    "user_name": "Alice",
    "task_status": "pending",
    "result": "",
    "messages": [],
})
# result 是最终的完整状态字典
print(result["result"])

三、如何可视化验证图结构

这是 LangGraph 最重要的调试技巧之一!在运行图之前,先可视化检查一遍,确保图结构符合预期。

3.1 方式1:ASCII 图(最快,不需要额外依赖)

# 编译图后,直接打印 ASCII 图
graph = builder.compile()
graph.get_graph().print_ascii()

输出示例:

        +-----------+        
        | __start__ |        
        +-----------+        
              *               
              *               
              *               
        +---------+          
        | classify |         
        +---------+          
         *        *           
        *          *          
       *            *         
  +------+    +----------+   
  | math |    | knowledge |  
  +------+    +----------+  
       *            *        
        *          *         
         *        *          
        +---------+          
        | __end__ |          
        +---------+          

3.2 方式2:Mermaid 格式(最直观,可在线查看)

mermaid_text = graph.get_graph().draw_mermaid()
print(mermaid_text)

输出的 Mermaid 文本,复制到以下任意工具即可看到精美流程图:

  • mermaid.live (最方便,在线编辑器)
  • VS Code 插件:"Markdown Preview Mermaid Support"
  • GitHub/GitLab:直接在 Markdown 中用 ```mermaid 代码块

输出示例:

%%{init: {'flowchart': {'curve': 'linear'}}}%%
flowchart TD;
	__start__([<p>__start__</p>]):::first
	classify([classify])
	math([math])
	knowledge([knowledge])
	__end__([<p>__end__</p>]):::last
	__start__ --> classify;
	classify -.-> math;
	classify -.-> knowledge;
	math --> __end__;
	knowledge --> __end__;
	classDef default fill:#f2f0ff,line-height:1.2
	classDef first fill-opacity:0
	classDef last fill:#bfb6fc

3.3 方式3:编程方式(用于测试/断言)

# 获取图的结构信息
graph_obj = graph.get_graph()

# 查看所有节点名称
node_names = list(graph_obj.nodes.keys())
print(f"节点列表:{node_names}")

# 查看所有边
for edge in graph_obj.edges:
    print(f"边:{edge.source}{edge.target}")

# 用于写自动化测试
assert "review" in node_names, "缺少 review 节点!"
assert "publish" in node_names, "缺少 publish 节点!"

💡 开发建议:每次构建完图后,先调用 print_ascii()draw_mermaid() 检查一遍,再运行。这样可以快速发现"少连了一条边"或"边的方向反了"等常见错误。


四、5步开发工作流

从需求到可运行代码,建议按这5步走:

第1步:先画图(最重要!)

在写代码前,先用纸/白板画出工作流:

示例:文章审核流程

[开始][AI生成草稿][人工审核] → 通过 → [发布][结束]
                                    ↓
                               不通过 → [AI修改][人工审核](循环)

画完后思考:

  • 有几个节点?(对应几个函数)
  • 有几条条件边?(对应几个路由函数)
  • State 需要哪些字段?(节点间传递什么数据)

第2步:定义 State

根据第1步的分析,定义状态类:

from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages

class ArticleState(TypedDict):
    topic: str          # 写作主题(只读,不变)
    draft: str          # AI 生成的草稿(generate节点写,review节点读)
    feedback: str       # 审核意见(review节点写,revise节点读)
    approved: bool      # 是否通过审核(review节点写,路由函数读)
    final: str          # 最终内容(publish节点写)
    messages: Annotated[list, add_messages]  # 完整历史记录

第3步:实现节点(先 Mock,后接 LLM)

先写不调用 LLM 的 Mock 版本,验证逻辑正确:

# ── Mock 版本:快速验证流程 ──
def generate_node_mock(state: ArticleState) -> dict:
    print(f"  [生成] 模拟生成关于'{state['topic']}'的草稿")
    return {"draft": f"关于{state['topic']}的模拟草稿内容"}

def review_node_mock(state: ArticleState) -> dict:
    # 模拟:直接通过
    print("  [审核] 模拟审核通过")
    return {"approved": True, "feedback": ""}

def publish_node_mock(state: ArticleState) -> dict:
    print("  [发布] 模拟发布")
    return {"final": state["draft"]}

# ── 真实版本:接入 LLM ──
def generate_node(state: ArticleState) -> dict:
    response = llm.invoke([
        SystemMessage(content="你是专业写作者,请根据主题创作300字短文"),
        HumanMessage(content=f"主题:{state['topic']}")
    ])
    return {"draft": response.content}

第4步:构建图并可视化验证

builder = StateGraph(ArticleState)

# 添加节点
builder.add_node("generate", generate_node)
builder.add_node("review", review_node)
builder.add_node("publish", publish_node)

# 添加边
builder.add_edge(START, "generate")
builder.add_edge("generate", "review")
builder.add_conditional_edges(
    "review",
    lambda state: "publish" if state["approved"] else END,
    {"publish": "publish", END: END},
)
builder.add_edge("publish", END)

# ✅ 编译前先验证图结构
graph = builder.compile()
print("图结构验证:")
graph.get_graph().print_ascii()   # 先看 ASCII 验证连通性
print(graph.get_graph().draw_mermaid())  # 再看 Mermaid 确认方向

第5步:测试

# ── 先单独测试节点函数 ──
mock_state = {"topic": "AI", "draft": "", "feedback": "", "approved": False, "final": "", "messages": []}
result = generate_node(mock_state)
assert "draft" in result
assert len(result["draft"]) > 0
print("节点单元测试通过 ✅")

# ── 再做集成测试(用 Mock 节点)──
result = graph.invoke({
    "topic": "人工智能",
    "draft": "", "feedback": "", "approved": False, "final": "",
    "messages": [],
})
assert result["final"] != ""
print("集成测试通过 ✅")

五、代码讲解:基础图

代码文件:lessons/07_langgraph/01_basic_graph.py

示例1:最简单的线性图

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

# ──────────────────────────────────────────────────────
# 第1步:定义状态
# ──────────────────────────────────────────────────────
class SimpleState(TypedDict):
    input: str       # 用户输入(不变)
    processed: str   # 预处理后的文本
    result: str      # 最终结果

# ──────────────────────────────────────────────────────
# 第2步:定义节点函数
# ──────────────────────────────────────────────────────
def preprocess_node(state: SimpleState) -> dict:
    """预处理:去掉首尾空格,转小写"""
    text = state["input"]
    processed = text.strip().lower()
    return {"processed": processed}   # 只返回变更的字段

def analysis_node(state: SimpleState) -> dict:
    """分析:统计文本特征"""
    text = state["processed"]
    result = f"词数: {len(text.split())}, 字符数: {len(text)}"
    return {"result": result}

# ──────────────────────────────────────────────────────
# 第3步:构建图
# ──────────────────────────────────────────────────────
builder = StateGraph(SimpleState)

builder.add_node("preprocess", preprocess_node)   # 添加节点
builder.add_node("analysis", analysis_node)

builder.add_edge(START, "preprocess")             # 添加边
builder.add_edge("preprocess", "analysis")
builder.add_edge("analysis", END)

# ──────────────────────────────────────────────────────
# 第4步:编译并可视化验证
# ──────────────────────────────────────────────────────
graph = builder.compile()
graph.get_graph().print_ascii()   # ✅ 先验证图结构

# ──────────────────────────────────────────────────────
# 第5步:运行
# ──────────────────────────────────────────────────────
result = graph.invoke({
    "input": "  Hello LangGraph World  ",
    "processed": "",
    "result": ""
})
print(result["result"])   # 输出:词数: 3, 字符数: 21

示例2:带条件路由的图

class RoutingState(TypedDict):
    question: str    # 用户问题
    category: str    # 分类结果
    answer: str      # 答案

def classify_node(state: RoutingState) -> dict:
    """分类节点:判断问题类型"""
    question = state["question"].lower()
    if any(kw in question for kw in ["几", "多少", "计算"]):
        category = "math"
    elif any(kw in question for kw in ["是什么", "解释", "介绍"]):
        category = "knowledge"
    else:
        category = "general"
    return {"category": category}

def math_node(state: RoutingState) -> dict:
    return {"answer": f"数学处理:{state['question']}"}

def knowledge_node(state: RoutingState) -> dict:
    return {"answer": f"知识回答:{state['question']}"}

def general_node(state: RoutingState) -> dict:
    return {"answer": f"通用回答:{state['question']}"}

# ── 路由函数:返回下一个节点名 ──
def route_question(state: RoutingState) -> str:
    return state["category"]   # "math" / "knowledge" / "general"

builder = StateGraph(RoutingState)
builder.add_node("classify", classify_node)
builder.add_node("math", math_node)
builder.add_node("knowledge", knowledge_node)
builder.add_node("general", general_node)

builder.add_edge(START, "classify")

# 条件边:classify 执行后,根据 route_question 返回值路由
builder.add_conditional_edges(
    "classify",
    route_question,             # 路由函数
    {                           # 路由函数返回值 → 目标节点
        "math": "math",
        "knowledge": "knowledge",
        "general": "general",
    }
)
builder.add_edge("math", END)
builder.add_edge("knowledge", END)
builder.add_edge("general", END)

graph = builder.compile()
result = graph.invoke({"question": "什么是人工智能?", "category": "", "answer": ""})
print(result["answer"])   # 输出:知识回答:什么是人工智能?

运行方式:

python lessons/07_langgraph/01_basic_graph.py

六、代码讲解:Agent 循环图

代码文件:lessons/07_langgraph/02_agent_graph.py

这是 create_react_agent 的底层实现原理,手动构建后你才真正明白 Agent 是怎么运作的。

from langgraph.prebuilt import ToolNode  # 自动处理工具调用的内置节点

# 状态:只需要一个 messages 字段(包含完整消息历史)
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]

def agent_node(state: AgentState) -> dict:
    """
    LLM 推理节点。
    接收消息历史,调用绑定了工具的 LLM。
    LLM 决定:调用工具?还是给出最终答案?
    """
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def should_continue(state: AgentState) -> str:
    """
    路由函数:判断 LLM 是否要继续调用工具。
    - 有 tool_calls → "tools"(继续循环)
    - 无 tool_calls → END(任务完成)
    """
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"
    return END

builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)          # LLM 推理
builder.add_node("tools", ToolNode(tools))     # 工具执行

builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")   # 工具执行完后,回到 LLM(形成循环!)

graph = builder.compile()

Agent 循环图的执行流程:

运行方式:

python lessons/07_langgraph/02_agent_graph.py

七、调试技巧

7.1 打印状态变化

在每个节点开头和结尾打印状态,快速定位问题:

def debug_node(state: MyState) -> dict:
    print(f"\n[debug_node 进入] state = {state}")
    
    # ... 处理逻辑 ...
    result = {"field": "value"}
    
    print(f"[debug_node 退出] 返回 = {result}")
    return result

7.2 用 Mock 节点隔离问题

# 测试图结构时,先用不调用 LLM 的 Mock 节点
def mock_generate(state):
    return {"draft": "模拟草稿"}

def mock_review(state):
    return {"approved": True}

# 确认图流转正确后,再替换成真实的 LLM 节点
graph = builder.compile()

7.3 常见错误对照表

错误现象可能原因解决方案
KeyError: 'field_name'State 字段未初始化invoke() 时提供所有字段的初始值
图无限循环条件边路由函数永远不返回 END增加循环计数,超过上限强制结束
节点执行顺序不对边的连接方向错误print_ascii() 可视化验证
AttributeError: tool_calls消息类型检查不完整hasattr(msg, "tool_calls") 判断
路由函数返回了不在映射表里的值返回值和映射表不匹配确保路由函数的所有返回值都在映射字典的 key 中

八、小结

本章(上)涵盖了 LangGraph 入门所需的所有知识:

LangGraph 基础
├── 为什么需要?          ← Chain 无法循环,Agent 无法控制,LangGraph 解决这些
├── 什么场景用?          ← 循环/分支/多角色协作/人工介入
├── 三大概念:           
│   ├── State(共享白板)   ← TypedDict,用 Annotated[list, add_messages] 追加消息
│   ├── Node(处理函数)    ← 接收 State,返回变更字段
│   └── Edge(流转路径)    ← 普通边 or 条件边
├── 图可视化:            ← print_ascii() 快速看,draw_mermaid() 精美看
├── 5步开发工作流:        ← 先画图 → 定义State → Mock节点 → 连图可视化 → 测试
├── 代码:01_basic_graph.py  ← 线性图 + 条件路由
└── 代码:02_agent_graph.py  ← Agent 循环(LLM + ToolNode)

📌 下篇预告:第07章(下)将讲解 LangGraph 的进阶特性:

  • 检查点(Checkpointer):保存执行状态,支持断点续传
  • Human-in-the-Loop:在指定节点暂停等待人工确认
  • 多 Agent 协作:协调器 + 专业化 Agent 的完整实现

作者:阿聪谈架构
公众号:阿聪谈架构(分享后端架构 / AI / Java 技术文章)
相关代码关注公众号:【阿聪谈架构】 回复:AI专栏代码