一、为什么需要工作流引擎?
在构建 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, Optional
class 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 状态设计的三个原则
- 不可变性——每个节点返回新的 State 字典,而非修改传入的 State
- 最小化——State 中只放节点间需要传递的数据,不放节点的内部临时变量
- 可追溯——保留关键中间产出(如
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