LangGraph 工作流设计:状态图驱动的多 Agent 编排实战

22 阅读7分钟

一、为什么需要工作流引擎?

在构建 LLM 应用时,开发者很快会面临一个共同挑战:单个 LLM 调用不够用

真实场景往往需要:

  • 先分析问题,再检索文档,然后生成回答,最后验证答案
  • 根据意图类型,走不同的处理分支
  • 多个 Agent 并行处理,然后融合结果
  • 失败时重试,超时时降级

直接在业务代码中用 if-else 和循环来编排这些逻辑,很快就会变成难以维护的"意大利面条代码"。LangGraph 正是为应对这一挑战而生的——它用有向图来建模工作流,每个节点是一个处理单元,边定义了状态如何流转。

本文基于两个实际项目——LexRAG(法律 RAG 系统)HeritageMind(非遗多智能体系统) ——深入解析 LangGraph 的两种核心工作流模式。


二、LangGraph 核心概念速览

2.1 三要素

┌────────────────────────────────────────┐
│              StateGraph                │
│                                       │
│  ┌──────┐    edge    ┌──────┐         │
│  │ Node │ ────────> │ Node │         │
│  │  A   │           │  B   │         │
│  └──────┘           └──────┘         │
│       │                               │
│       │  conditional edge              │
│       ▼                               │
│  ┌──────┐    ┌──────┐                 │
│  │ Node │    │ Node │                 │
│  │  C   │    │  D   │                 │
│  └──────┘    └──────┘                 │
│                                       │
│  State: 在节点间流转的共享数据结构      │
└────────────────────────────────────────┘
要素说明类比
State在节点间传递的共享数据流水线上的工件
Node接收 State,返回更新后的 State流水线上的工位
Edge连接节点,定义流转方向传送带

2.2 边的三种类型

from langgraph.graph import StateGraph, END
​
# 1. 普通边 — 固定流向
graph.add_edge("node_a", "node_b")  # A 完成后必定到 B# 2. 条件边 — 根据 State 动态路由
graph.add_conditional_edges(
    "node_a",
    router_function,        # (state) -> str  返回目标节点名
    {"path_a": "node_b", "path_b": "node_c"}
)
​
# 3. 入口点 — 工作流的起点
graph.set_entry_point("node_a")

三、模式一:ReAct Agent 工作流(LexRAG)

3.1 ReAct 模式原理

ReAct(Reasoning + Acting)是 LLM Agent 的经典范式:

Thought → Action → Observation → Thought → Action → ... → Final Answer

LangGraph 提供了 create_react_agent 预置实现,将 ReAct 循环封装为现成的图结构。

3.2 LexRAG 的 ReAct 实现

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
​
# 1. 定义工具集
tools = [
    search_statute,      # 检索法条
    search_case,         # 检索案例
    lookup_article,      # 查阅具体条款
    analyze_legal_relation,  # 法律关系分析
]
​
# 2. 创建 ReAct Agent
llm = ChatOpenAI(model="deepseek-chat", temperature=0.1)
agent = create_react_agent(llm, tools, state_modifier=system_prompt)
​
# 3. 执行
result = agent.invoke({"messages": [HumanMessage(content=user_query)]})

3.3 工作流内部状态机

                    ┌─────────────┐
    用户输入 ─────>│   Agent     │
                    │   Node      │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │ 需要工具?  │             │
              ▼            │             ▼
        ┌──────────┐       │      ┌──────────┐
        │ 调用工具  │       │      │ 生成最终  │
        │ Tool Node│       │      │  回答    │
        └────┬─────┘       │      └────┬─────┘
             │             │           │
             └─────────────┘           ▼
                  (循环)              END

Agent 在每个回合自主决定:

需要调用哪个工具? —— 法律检索?案例查找?条款查阅?

用什么参数调用? —— 从用户问题中提取关键信息

何时停止? —— 信息足够回答时,生成最终答案

3.4 ReAct Agent 的优势与局限

优势

  • 零样本工具使用——不需要为每个工具编写调用逻辑
  • 自适应推理——Agent 自主决定调用顺序和次数
  • 代码简洁——一个 create_react_agent 调用就完成了编排

局限

  • 可控性弱——你无法精确控制 Agent 的推理路径
  • 调试困难——Agent 为什么调用某个工具、为什么做出了某个决策,都是"黑盒"
  • 不适合确定性流程——如果业务逻辑要求"A 必须在 B 之前执行",ReAct 无法保证

四、模式二:StateGraph 自定义工作流(HeritageMind)

当业务流程有明确步骤时,自定义 StateGraph 是更好的选择。HeritageMind 的多 Agent 协作就采用了这种模式。

4.1 状态定义

from typing import TypedDict, List, Dict, Optionalclass WorkflowState(TypedDict):
    # ── 输入 ──
    question: str
    user_profile: str
​
    # ── 分析结果 ──
    question_analysis: dict        # {required_experts, key_entities, complexity}
    required_experts: List[str]    # ["craft_expert", "history_expert"]
    key_entities: List[str]        # ["景泰蓝", "掐丝珐琅"]
    complexity: str                # "simple" | "medium" | "complex"
​
    # ── Agent 响应 ──
    expert_responses: Dict[str, dict]  # {agent_name: AgentResponse}
​
    # ── 融合结果 ──
    fused_content: str
    use_debate: bool
    debate_session: Optional[dict]
    debate_mode: Optional[str]     # "progressive" | "parallel" | "multi_perspective"
​
    # ── 质量保障 ──
    gap_detection: dict
    has_gaps: bool
    gap_report: str
​
    # ── 输出 ──
    adapted_content: str
    final_response: str
    source_agents: List[str]
​
    # ── 异常 ──
    errors: List[str]
    metadata: dict

设计要点

单一数据源——所有节点读写同一个 State 对象,消除了跨节点的数据传递问题

显式错误字段——errors: List[str] 让错误信息随状态传播,而非通过异常中断流程

可追溯性——每个阶段的产出都有对应字段,可以完整还原推理链路

4.2 节点实现

每个节点是一个纯函数:(WorkflowState) -> WorkflowState

def analyze_question_node(state: WorkflowState) -> WorkflowState:
    """节点1: 问题分析"""
    dispatcher = DispatcherAgent()
    analysis = dispatcher.analyze_question(state["question"])
​
    return {
        **state,
        "question_analysis": analysis,
        "required_experts": analysis["required_experts"],
        "key_entities": analysis["key_entities"],
        "complexity": analysis.get("complexity", "medium"),
    }
​
​
def dispatch_to_experts_node(state: WorkflowState) -> WorkflowState:
    """节点2: 分发到领域专家 Agent"""
    dispatcher = DispatcherAgent()
    responses = {}
​
    # 为每个所需的专家初始化 Agent 并调用 process()
    for expert_name in state["required_experts"]:
        agent_class = AGENT_REGISTRY[expert_name]  # 从注册表获取
        agent = agent_class()
        response = agent.process(
            question=state["question"],
            context={"key_entities": state["key_entities"]}
        )
        responses[expert_name] = response
​
    return {**state, "expert_responses": responses}

关键设计:Agent 注册表模式

AGENT_REGISTRY = {
    "craft_expert": CraftExpert,
    "history_expert": HistoryExpert,
    "heritage_expert": HeritageExpert,
}

这种注册表模式带来两个好处:

可扩展——新增 Agent 只需在注册表中加一行

可配置——调度器通过字符串名称引用 Agent,无需硬编码

4.3 条件路由

工作流中最精妙的部分是条件边——它们根据 State 内容做出路由决策:

def has_expert_responses(state: WorkflowState) -> str:
    """检查专家是否返回了有效响应"""
    if state.get("expert_responses") and len(state["expert_responses"]) > 0:
        return "fuse"
    return "error"
​
​
def should_detect_gaps(state: WorkflowState) -> str:
    """根据复杂度决定是否执行缺口检测"""
    if state.get("complexity") in ("medium", "complex"):
        return "detect_gaps"
    return "skip_gaps"
​
​
def should_include_narrative(state: WorkflowState) -> str:
    """是否启用传承人叙事模式"""
    if state.get("include_narrative", False):
        return "narrative"
    return "standard"

4.4 图的组装

from langgraph.graph import StateGraph, END
​
def build_workflow() -> StateGraph:
    workflow = StateGraph(WorkflowState)
​
    # ── 注册节点 ──
    workflow.add_node("analyze_question", analyze_question_node)
    workflow.add_node("dispatch_to_experts", dispatch_to_experts_node)
    workflow.add_node("collect_responses", collect_expert_responses_node)
    workflow.add_node("fuse_knowledge", fuse_knowledge_node)
    workflow.add_node("detect_gaps", detect_gaps_node)
    workflow.add_node("generate_response_standard", generate_response_node)
    workflow.add_node("generate_response_narrative", generate_narrative_response_node)
    workflow.add_node("handle_error", error_handler_node)
​
    # ── 设置入口 ──
    workflow.set_entry_point("analyze_question")
​
    # ── 固定边 ──
    workflow.add_edge("analyze_question", "dispatch_to_experts")
    workflow.add_edge("dispatch_to_experts", "collect_responses")
​
    # ── 条件边 ──
    workflow.add_conditional_edges(
        "collect_responses",
        has_expert_responses,
        {"fuse": "fuse_knowledge", "error": "handle_error"}
    )
​
    workflow.add_edge("fuse_knowledge", "detect_gaps")  # 总是先到检测节点
    workflow.add_conditional_edges(
        "detect_gaps",
        should_detect_gaps,  # 但检测节点可以根据条件跳过
        {"detect_gaps": "generate_response_standard",  # 注意:实际执行检测
         "skip_gaps": "generate_response_standard"}      # 或跳过(都到同一节点)
    )
​
    workflow.add_conditional_edges(
        "generate_response_standard",
        should_include_narrative,
        {"narrative": "generate_response_narrative",
         "standard": END}
    )
​
    workflow.add_edge("generate_response_narrative", END)
    workflow.add_edge("handle_error", END)
​
    return workflow.compile()

4.5 完整工作流可视化

                        ┌──────────────┐
                        │ analyze_     │
                        │ question     │
                        └──────┬───────┘
                               │
                        ┌──────▼───────┐
                        │ dispatch_to_ │
                        │ experts      │
                        └──────┬───────┘
                               │
                        ┌──────▼───────┐
                        │ collect_     │
                        │ responses    │
                        └──────┬───────┘
                               │
                    ┌──────────┼──────────┐
                    │ 有响应?  │          │ 无响应
                    ▼          │          ▼
              ┌──────────┐    │    ┌──────────┐
              │ fuse_    │    │    │ handle_  │
              │ knowledge│    │    │ error    │──> END
              └────┬─────┘    │    └──────────┘
                   │          │
              ┌────▼─────┐    │
              │ detect_  │    │
              │ gaps     │    │
              └────┬─────┘    │
                   │          │
         ┌─────────┼──────────┘
         │ 复杂?  │
         ▼         ▼
    ┌─────────┐ ┌──────────┐
    │ (执行   │ │ (跳过    │
    │  检测)  │ │  检测)   │
    └────┬────┘ └────┬─────┘
         │           │
         └─────┬─────┘
               │
        ┌──────▼───────┐
        │ generate_    │
        │ response_    │
        │ standard     │
        └──────┬───────┘
               │
      ┌────────┼────────┐
      │ 叙事?  │        │ 标准
      ▼        │        ▼
  ┌────────┐  │      END
  │narrative│  │
  │ gen     │  │
  └───┬────┘  │
      │       │
      ▼       │
     END     │

五、两种模式的对比与选择

维度ReAct Agent自定义 StateGraph
灵活性Agent 自主决策预设路径
可控性黑盒推理每步可预期
可调试性需解析 Agent 日志State 快照可审计
开发成本几行代码需定义节点和边
适用场景工具灵活组合确定性业务流程
错误处理依赖 Agent 自身显式错误节点和路由

选择决策树

需要 Agent 自主选择工具和推理路径?
  ├── 是 → 用 ReAct Agent(如:法律问答,不同问题需要不同的检索策略)
  └── 否 → 流程步骤是否可预先确定?
            ├── 是 → 用自定义 StateGraph(如:非遗问答,分析→分发→融合→输出)
            └── 否 → 回到 ReAct Agent

混合使用

两个模式并不互斥。LexRAG 项目就展示了混合使用的最佳实践:

class LegalRAGWorkflow:
    def query(self, question: str) -> QueryResult:
        # 前置处理:StateGraph 式的确定性流程
        intent = self.intent_router.route(question)      # 步骤1: 意图分析
        rewritten = self.query_rewriter.rewrite(question) # 步骤2: 查询重写
​
        # 核心推理:ReAct Agent 的灵活工具使用
        agent_result = self.react_agent.invoke({          # 步骤3: Agent 推理
            "messages": [HumanMessage(content=rewritten)]
        })
​
        # 后置处理:StateGraph 式的验证流程
        verified = self.verifier.verify(agent_result)     # 步骤4: 自我验证
        if not verified.passed:
            verified = self.verifier.correct(verified)    # 步骤5: 纠错
​
        return verified

核心原则用 StateGraph 定义"骨架",用 ReAct Agent 填充"肌肉"。


六、实战要点

6.1 状态设计的三个原则

  1. 不可变性——每个节点返回新的 State 字典,而非修改传入的 State
  2. 最小化——State 中只放节点间需要传递的数据,不放节点的内部临时变量
  3. 可追溯——保留关键中间产出(如 question_analysis),便于调试和审计

6.2 错误处理的三种策略

# 策略1: 错误随状态传播
def safe_node(state: WorkflowState) -> WorkflowState:
    try:
        result = risky_operation(state)
        return {**state, "result": result}
    except Exception as e:
        return {**state, "errors": [*state.get("errors", []), str(e)]}
​
# 策略2: 专用错误节点
workflow.add_conditional_edges(
    "risky_node",
    lambda s: "error" if s.get("errors") else "continue",
    {"error": "error_handler", "continue": "next_node"}
)
​
# 策略3: 降级路径
def router_with_fallback(state: WorkflowState) -> str:
    if state.get("primary_result"):
        return "use_primary"
    elif state.get("fallback_result"):
        return "use_fallback"
    return "error"

6.3 测试工作流

def test_workflow_analyze_question():
    """测试问题分析节点"""
    state = create_initial_state("景泰蓝的制作工艺是什么?")
    result = analyze_question_node(state)
​
    assert "craft_expert" in result["required_experts"]
    assert "景泰蓝" in result["key_entities"]
​
​
def test_workflow_error_path():
    """测试错误处理路径"""
    state = create_initial_state("")
    result = workflow.invoke(state)
​
    assert len(result["errors"]) > 0
    assert result.get("final_response") is None