学习第16天:LangGraph 与 Agent 工作流编排
贯穿项目:Enterprise AI Agent Platform --- 本章用 LangGraph 实现有状态的、可中断的多步骤 Agent 工作流
1. 学习目标
完成本章学习后,你将能够:
- 理解 LangGraph 的核心概念:State、Node、Edge、Graph
- 用 LangGraph 构建有状态的 Agent 工作流
- 实现条件路由 和循环(Agent 的"思考-行动"循环)
- 支持人工审批(Human-in-the-Loop)
- 实现持久化状态(中断、恢复、回放)
- 构建完整的 多步骤 Agent 流程
| 概念 | 含义 | 类比 |
|---|---|---|
| StateGraph | 状态图定义 | Spring State Machine |
| Node | 图中的节点(处理步骤) | BPMN 任务节点 |
| Edge | 节点间的连线(路由) | BPMN 连线 |
| Conditional Edge | 条件路由 | BPMN 排他网关 |
| Checkpointer | 状态持久化 | Saga 模式的持久化 |
| Human-in-the-Loop | 人工审批干预 | BPMN 人工节点 |
2. 知识体系图
#mermaid-svg-wDRjpepOwWEMAo05{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-wDRjpepOwWEMAo05 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wDRjpepOwWEMAo05 .error-icon{fill:#552222;}#mermaid-svg-wDRjpepOwWEMAo05 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wDRjpepOwWEMAo05 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wDRjpepOwWEMAo05 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wDRjpepOwWEMAo05 .marker.cross{stroke:#333333;}#mermaid-svg-wDRjpepOwWEMAo05 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wDRjpepOwWEMAo05 p{margin:0;}#mermaid-svg-wDRjpepOwWEMAo05 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wDRjpepOwWEMAo05 .cluster-label text{fill:#333;}#mermaid-svg-wDRjpepOwWEMAo05 .cluster-label span{color:#333;}#mermaid-svg-wDRjpepOwWEMAo05 .cluster-label span p{background-color:transparent;}#mermaid-svg-wDRjpepOwWEMAo05 .label text,#mermaid-svg-wDRjpepOwWEMAo05 span{fill:#333;color:#333;}#mermaid-svg-wDRjpepOwWEMAo05 .node rect,#mermaid-svg-wDRjpepOwWEMAo05 .node circle,#mermaid-svg-wDRjpepOwWEMAo05 .node ellipse,#mermaid-svg-wDRjpepOwWEMAo05 .node polygon,#mermaid-svg-wDRjpepOwWEMAo05 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wDRjpepOwWEMAo05 .rough-node .label text,#mermaid-svg-wDRjpepOwWEMAo05 .node .label text,#mermaid-svg-wDRjpepOwWEMAo05 .image-shape .label,#mermaid-svg-wDRjpepOwWEMAo05 .icon-shape .label{text-anchor:middle;}#mermaid-svg-wDRjpepOwWEMAo05 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wDRjpepOwWEMAo05 .rough-node .label,#mermaid-svg-wDRjpepOwWEMAo05 .node .label,#mermaid-svg-wDRjpepOwWEMAo05 .image-shape .label,#mermaid-svg-wDRjpepOwWEMAo05 .icon-shape .label{text-align:center;}#mermaid-svg-wDRjpepOwWEMAo05 .node.clickable{cursor:pointer;}#mermaid-svg-wDRjpepOwWEMAo05 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wDRjpepOwWEMAo05 .arrowheadPath{fill:#333333;}#mermaid-svg-wDRjpepOwWEMAo05 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wDRjpepOwWEMAo05 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wDRjpepOwWEMAo05 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wDRjpepOwWEMAo05 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wDRjpepOwWEMAo05 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wDRjpepOwWEMAo05 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wDRjpepOwWEMAo05 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wDRjpepOwWEMAo05 .cluster text{fill:#333;}#mermaid-svg-wDRjpepOwWEMAo05 .cluster span{color:#333;}#mermaid-svg-wDRjpepOwWEMAo05 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-wDRjpepOwWEMAo05 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wDRjpepOwWEMAo05 rect.text{fill:none;stroke-width:0;}#mermaid-svg-wDRjpepOwWEMAo05 .icon-shape,#mermaid-svg-wDRjpepOwWEMAo05 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wDRjpepOwWEMAo05 .icon-shape p,#mermaid-svg-wDRjpepOwWEMAo05 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wDRjpepOwWEMAo05 .icon-shape .label rect,#mermaid-svg-wDRjpepOwWEMAo05 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wDRjpepOwWEMAo05 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wDRjpepOwWEMAo05 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wDRjpepOwWEMAo05 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 企业级特性
工作流模式
LangGraph 核心
LangGraph
State: TypedDict 定义状态
Node: 处理函数
Edge: 路由规则
Conditional Edge: 条件分支
常见模式
顺序流程: A→B→C
条件分支: A→B 或 C
循环: Agent 推理循环
并行: A 同时到 B+C
人工审批: HITL
高级特性
Checkpointer 持久化
中断与恢复
子图嵌套
流式事件
3. 核心知识
3.1 LangGraph vs LangChain
LangChain = 线性管道(A → B → C → D)
LangGraph = 有向图(A → B → C 或 D,可以循环回 A)
LangChain 适合:单次 LLM 调用 + 后处理
LangGraph 适合:多步推理、Branching、循环、人工干预
3.2 LangGraph 的核心抽象
python
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
# 1. 定义状态(State)
class AgentState(TypedDict):
messages: list[dict] # 对话历史
next_step: str # 下一步
tool_results: list[dict] # 工具结果
# 2. 定义节点(Node)------处理函数
def agent_node(state: AgentState) -> AgentState:
# 调用 LLM
return {"messages": [...], "next_step": "tools"}
def tool_node(state: AgentState) -> AgentState:
# 执行工具
return {"tool_results": [...]}
# 3. 定义路由(Edge)
def should_continue(state: AgentState) -> str:
if state.get("tool_calls"):
return "tools"
return "end"
# 4. 构建图(Graph)
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.set_entry_point("agent")
builder.add_conditional_edges("agent", should_continue)
builder.add_edge("tools", "agent") # 工具执行后回到 agent
graph = builder.compile()
4. 详细讲解
4.1 构建 Agent 推理循环
这是 Agent 最经典的工作流模式------ReAct 循环的 LangGraph 实现:
python
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
import operator
class AgentState(TypedDict):
"""Agent 状态定义"""
messages: Annotated[Sequence[BaseMessage], add_messages]
# add_messages 表示追加而非替换
def build_react_agent(llm, tools):
"""构建 ReAct Agent 图"""
# 将工具绑定到 LLM
llm_with_tools = llm.bind_tools(tools)
# 节点1:Agent 决策
def call_model(state: AgentState):
messages = state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
# 节点2:工具执行
def call_tools(state: AgentState):
messages = state["messages"]
last_message = messages[-1]
tool_messages = []
# LangGraph 内置的 ToolNode 可简化这一步
for tool_call in last_message.tool_calls:
tool = tools_by_name[tool_call["name"]]
result = tool.invoke(tool_call["args"])
tool_messages.append(
ToolMessage(content=str(result), tool_call_id=tool_call["id"])
)
return {"messages": tool_messages}
# 路由函数
def should_continue(state: AgentState) -> str:
messages = state["messages"]
last_message = messages[-1]
# 如果 LLM 调用了工具 → 去执行工具
if last_message.tool_calls:
return "tools"
# 否则 → 结束
return END
# 构建图
workflow = StateGraph(AgentState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", call_tools)
workflow.set_entry_point("agent")
workflow.add_conditional_edges(
"agent",
should_continue,
{
"tools": "tools",
END: END,
}
)
workflow.add_edge("tools", "agent") # 工具执行后回到 agent
return workflow.compile()
4.2 多步骤工作流 --- 数据分析 Agent
python
class AnalysisState(TypedDict):
query: str
sql: str
query_result: str
visualization_spec: dict
report: str
error: str
def build_analysis_workflow(llm, db):
"""构建数据分析工作流:NL → SQL → 查询 → 可视化 → 报告"""
# 步骤1:自然语言 → SQL
def nl_to_sql(state: AnalysisState):
prompt = f"将以下问题转为 SQL:{state['query']}"
response = llm.invoke(prompt)
return {"sql": response.content}
# 步骤2:执行查询
def execute_query(state: AnalysisState):
try:
result = db.execute(state["sql"])
return {"query_result": str(result)}
except Exception as e:
return {"error": str(e), "query_result": ""}
# 步骤3:错误处理
def check_error(state: AnalysisState) -> str:
if state.get("error"):
return "fix_sql"
return "generate_report"
# 步骤4:修复 SQL
def fix_sql(state: AnalysisState):
prompt = f"以下 SQL 报错:{state['error']}\n原始 SQL:{state['sql']}\n请修复。"
response = llm.invoke(prompt)
return {"sql": response.content, "error": ""}
# 步骤5:生成报告
def generate_report(state: AnalysisState):
prompt = f"基于以下数据生成分析报告:{state['query_result']}"
response = llm.invoke(prompt)
return {"report": response.content}
# 构建图
workflow = StateGraph(AnalysisState)
workflow.add_node("nl_to_sql", nl_to_sql)
workflow.add_node("execute_query", execute_query)
workflow.add_node("fix_sql", fix_sql)
workflow.add_node("generate_report", generate_report)
workflow.set_entry_point("nl_to_sql")
workflow.add_edge("nl_to_sql", "execute_query")
workflow.add_conditional_edges("execute_query", check_error, {
"fix_sql": "fix_sql",
"generate_report": "generate_report",
})
workflow.add_edge("fix_sql", "execute_query") # 修复后重新执行
workflow.add_edge("generate_report", END)
return workflow.compile()
4.3 Human-in-the-Loop(人工审批)
python
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
class ApprovalState(TypedDict):
request: dict
approved: bool
result: str
def build_approval_workflow(llm):
"""构建带人工审批的工作流"""
def create_request(state: ApprovalState):
"""创建请求"""
return {"request": {"action": "delete_agent", "agent_id": 42}}
def ask_approval(state: ApprovalState):
"""请求人工审批------工作流在此中断"""
# interrupt() 暂停执行,等待外部输入
approval = interrupt(f"请审批:{state['request']}")
return {"approved": approval}
def execute_action(state: ApprovalState):
"""执行批准后的操作"""
if state["approved"]:
return {"result": "操作已执行"}
return {"result": "操作已取消"}
# 构建图
workflow = StateGraph(ApprovalState)
workflow.add_node("create_request", create_request)
workflow.add_node("ask_approval", ask_approval)
workflow.add_node("execute", execute_action)
workflow.set_entry_point("create_request")
workflow.add_edge("create_request", "ask_approval")
workflow.add_edge("ask_approval", "execute")
workflow.add_edge("execute", END)
# 使用 Checkpointer 支持中断
memory = MemorySaver()
return workflow.compile(checkpointer=memory)
# 使用示例
async def run_with_approval():
graph = build_approval_workflow(llm)
config = {"configurable": {"thread_id": "request-001"}}
# 第一次运行------会在 ask_approval 节点暂停
result = graph.invoke({"request": {}}, config)
# result 中包含中断信息
# 人工审批后恢复
result = graph.invoke(
Command(resume=True), # 恢复执行,审批结果通过 Command 传入
config,
)
4.4 状态持久化与恢复
python
from langgraph.checkpoint.sqlite import SqliteSaver
import sqlite3
# 使用 SQLite 持久化状态
conn = sqlite3.connect("agent_checkpoints.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)
graph = builder.compile(checkpointer=checkpointer)
# 线程隔离------不同对话的状态互不影响
config = {"configurable": {"thread_id": "user-123-session-456"}}
# 中断后可以恢复
graph.invoke({"messages": [...]}, config)
# ... 系统重启 ...
graph.invoke({"messages": [...]}, config) # 自动恢复之前的状态
5. 代码示例
5.1 贯穿项目:企业 Agent 工作流引擎
python
# app/workflow/engine.py
from typing import TypedDict, Annotated, Sequence, Literal
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from app.core.llm.router import ModelRouter
from app.core.tools.registry import ToolRegistry
from app.rag.rag_pipeline import RAGPipeline
import structlog
logger = structlog.get_logger(__name__)
class WorkflowState(TypedDict):
"""统一工作流状态"""
messages: Annotated[Sequence[BaseMessage], add_messages]
agent_type: str # agent 类型
reasoning_steps: list[dict] # 推理步骤记录
tool_results: list[dict] # 工具执行结果
retrieved_docs: list[dict] # RAG 检索结果
final_answer: str # 最终答案
needs_human_approval: bool # 是否需要人工审批
class WorkflowEngine:
"""企业 Agent 工作流引擎
支持流程:
1. Simple: 直接对话
2. RAG: 检索增强
3. Tool: 工具调用
4. Approval: 需人工审批
"""
def __init__(
self,
router: ModelRouter,
tool_registry: ToolRegistry,
rag_pipeline: RAGPipeline = None,
db_path: str = "workflow_checkpoints.db",
):
self.router = router
self.tools = tool_registry
self.rag = rag_pipeline
# 持久化
import sqlite3
conn = sqlite3.connect(db_path, check_same_thread=False)
self.checkpointer = SqliteSaver(conn)
# 预编译工作流图
self._graph = self._build_graph()
def _build_graph(self):
"""构建完整工作流图"""
workflow = StateGraph(WorkflowState)
# 节点定义
workflow.add_node("router", self._route_agent)
workflow.add_node("simple_agent", self._simple_agent)
workflow.add_node("rag_search", self._rag_search)
workflow.add_node("tool_executor", self._tool_executor)
workflow.add_node("human_approval", self._human_approval)
workflow.add_node("generate_final", self._generate_final)
workflow.set_entry_point("router")
# 路由逻辑
workflow.add_conditional_edges(
"router",
self._decide_path,
{
"simple": "simple_agent",
"rag": "rag_search",
"tool": "tool_executor",
}
)
workflow.add_edge("simple_agent", "generate_final")
workflow.add_edge("rag_search", "generate_final")
# Tool Executor 可能循环
workflow.add_conditional_edges(
"tool_executor",
self._tool_should_continue,
{
"continue": "tool_executor",
"approval": "human_approval",
"final": "generate_final",
}
)
workflow.add_edge("human_approval", "generate_final")
workflow.add_edge("generate_final", END)
return workflow.compile(checkpointer=self.checkpointer)
async def _route_agent(self, state: WorkflowState) -> dict:
"""路由到对应的 Agent 类型"""
logger.info("routing_agent", type=state.get("agent_type", "simple"))
return {} # 状态不变,路由由 _decide_path 决定
def _decide_path(self, state: WorkflowState) -> str:
agent_type = state.get("agent_type", "simple")
if agent_type == "rag" and self.rag:
return "rag"
elif agent_type == "tool":
return "tool"
return "simple"
async def _simple_agent(self, state: WorkflowState):
"""简单对话 Agent"""
# 将 LangGraph messages 转换为我们统一的 Message
response = await self.router.chat_with_fallback(
messages=state["messages"]
)
return {
"messages": [AIMessage(content=response.content)],
"final_answer": response.content,
}
async def _rag_search(self, state: WorkflowState):
"""RAG 检索"""
last_message = state["messages"][-1].content
result = await self.rag.query(last_message)
return {
"retrieved_docs": result["sources"],
"final_answer": result["answer"],
}
async def _tool_executor(self, state: WorkflowState):
"""工具执行(在循环中)"""
# 用 LLM 判断是否需要工具
response = await self.router.chat_with_fallback(
messages=state["messages"],
tools=self.tools.to_openai_tools(),
)
# 如果有 Tool Call,执行
if response.tool_calls:
results = []
for tc in response.tool_calls:
result = await self.tools.execute(
tc["function"]["name"],
tc["function"]["arguments"],
)
results.append({
"tool": tc["function"]["name"],
"result": result,
})
return {
"tool_results": results,
"needs_human_approval": any(
"危险" in str(r) for r in results
),
}
return {"final_answer": response.content}
def _tool_should_continue(self, state: WorkflowState) -> str:
if state.get("needs_human_approval"):
return "approval"
if state.get("final_answer"):
return "final"
return "final"
async def _human_approval(self, state: WorkflowState):
"""人工审批节点"""
from langgraph.types import interrupt
approval = interrupt({
"message": "需要审批以下操作",
"operations": state["tool_results"],
})
if approval:
return {"final_answer": "操作已批准并执行"}
return {"final_answer": "操作被拒绝"}
async def _generate_final(self, state: WorkflowState):
return {"final_answer": state.get("final_answer", "无法处理")}
async def run(
self, user_input: str, thread_id: str, agent_type: str = "simple"
) -> dict:
"""运行工作流"""
config = {"configurable": {"thread_id": thread_id}}
initial_state = {
"messages": [HumanMessage(content=user_input)],
"agent_type": agent_type,
"reasoning_steps": [],
"tool_results": [],
"retrieved_docs": [],
"final_answer": "",
"needs_human_approval": False,
}
result = await self._graph.ainvoke(initial_state, config)
return result
5.2 并行分支 --- 多 Agent 协作
python
from langgraph.graph import StateGraph, END
from langgraph.constants import Send
from typing import TypedDict, Annotated
import operator
class ParallelState(TypedDict):
question: str
analyses: Annotated[list, operator.add] # 累积结果
final_summary: str
def build_parallel_analysis(llm):
"""构建并行分析工作流:一个问题 → 3 个专业角度 → 汇总"""
# 分发节点:生成 3 个不同角度的分析
def dispatcher(state: ParallelState):
perspectives = [
"技术角度",
"商业角度",
"用户体验角度",
]
# 用 Send 发送到并行分支
return [
Send("analyze", {"perspective": p, "question": state["question"]})
for p in perspectives
]
# 分析节点:每个角度独立分析
def analyze(state: dict):
prompt = f"从{state['perspective']}分析:{state['question']}"
response = llm.invoke(prompt)
return {"analyses": [f"[{state['perspective']}]\n{response.content}"]}
# 汇总节点
def summarize(state: ParallelState):
combined = "\n\n".join(state["analyses"])
prompt = f"汇总以下分析:\n{combined}"
response = llm.invoke(prompt)
return {"final_summary": response.content}
workflow = StateGraph(ParallelState)
workflow.add_node("dispatcher", dispatcher)
workflow.add_node("analyze", analyze)
workflow.add_node("summarize", summarize)
workflow.set_entry_point("dispatcher")
workflow.add_edge("dispatcher", "analyze") # 并行
workflow.add_edge("analyze", "summarize")
workflow.add_edge("summarize", END)
return workflow.compile()
6. 实战案例
案例1:代码 Review Agent 工作流
python
class CodeReviewState(TypedDict):
pr_diff: str
analysis: dict
review_comments: list
needs_changes: bool
final_review: str
def build_code_review_workflow(llm):
"""代码 Review 工作流:分析 → 检查 → 评论 → 汇总"""
def analyze_changes(state):
"""分析变更范围"""
response = llm.invoke(f"分析以下代码变更的范围和影响:{state['pr_diff']}")
return {"analysis": response}
def check_quality(state):
"""检查代码质量"""
response = llm.invoke(f"Review 以下代码,列出问题:{state['pr_diff']}")
return {"review_comments": response}
def needs_human(state) -> str:
return "approve" if state.get("needs_changes") else "done"
# ... 构建图
案例2:NL2SQL 完整工作流(端到端)
这是贯穿项目中的真实场景------用户用自然语言查询数据库,Agent 自动生成 SQL、执行、纠错、格式化结果:
python
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, END
import operator
class NL2SQLState(TypedDict):
"""NL2SQL 工作流状态"""
user_question: str
table_schema: str
sql_query: str
sql_error: str
query_result: list
formatted_answer: str
retry_count: int
messages: Annotated[list, operator.add] # 累积日志
async def build_nl2sql_workflow(llm, db_conn):
"""
构建完整的 NL2SQL 工作流:
用户问题 → 理解 Schema → 生成 SQL → 安全校验 → 执行 → 结果格式化
"""
# ======== 节点函数 ========
async def understand_schema(state: NL2SQLState) -> dict:
"""节点 1:让 LLM 理解数据库 Schema"""
prompt = f"""你是一个 SQL 专家。以下是数据库的 Schema:
{state["table_schema"]}
请简要总结:这些表之间的关系是什么,主键和外键的关联关系。"""
response = llm.invoke(prompt)
return {
"messages": [f"[Schema理解] {response.content[:200]}"]
}
async def generate_sql(state: NL2SQLState) -> dict:
"""节点 2:根据用户问题生成 SQL"""
retry_hint = ""
if state.get("sql_error"):
retry_hint = f"\n上次 SQL 执行出错:{state['sql_error']}\n请修正后重新生成。"
prompt = f"""你是一个 SQL 专家。根据以下 Schema 生成 SQL 查询。
数据库 Schema:
{state["table_schema"]}
用户问题:{state["user_question"]}
{retry_hint}
要求:
1. 只返回 SQL 语句,不要解释
2. 使用参数化查询或安全的字符串拼接
3. 对于 SELECT 查询,添加合适的 LIMIT(不超过 100)
4. 绝对不要执行 DELETE、DROP、TRUNCATE 操作"""
response = llm.invoke(prompt)
sql = response.content.strip()
# 清理常见的 markdown 包装
sql = sql.replace("```sql", "").replace("```", "").strip("`").strip()
return {
"sql_query": sql,
"messages": [f"[SQL生成] {sql}"],
}
async def validate_sql(state: NL2SQLState) -> dict:
"""节点 3:安全校验------确保 SQL 不包含危险操作"""
sql = state["sql_query"].upper()
dangerous_keywords = ["DROP ", "DELETE ", "TRUNCATE ", "ALTER ", "CREATE "]
for keyword in dangerous_keywords:
if keyword in sql:
return {
"sql_error": f"SQL 包含危险操作:{keyword}。已拒绝执行。",
"formatted_answer": "抱歉,无法执行包含修改数据库的操作。",
}
# 确保有 LIMIT
if "SELECT" in sql and "LIMIT" not in sql:
return {
"sql_query": state["sql_query"].rstrip(";") + " LIMIT 100",
"messages": ["[SQL校验] 自动添加 LIMIT 100"],
}
return {"messages": ["[SQL校验] ✅ 通过安全检查"]}
async def execute_sql(state: NL2SQLState) -> dict:
"""节点 4:执行 SQL"""
try:
# 模拟执行(实际项目中使用数据库连接)
# result = await db_conn.execute(state["sql_query"])
import sqlite3
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute(state["sql_query"])
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description] if cursor.description else []
result = [dict(zip(columns, row)) for row in rows]
return {
"query_result": result,
"messages": [f"[SQL执行] ✅ 返回 {len(result)} 行数据"],
}
except Exception as e:
retry = state.get("retry_count", 0)
if retry < 2:
return {
"sql_error": str(e),
"retry_count": retry + 1,
"messages": [f"[SQL错误] {e} → 将重试({retry + 1}/2)"],
}
else:
return {
"formatted_answer": f"SQL 执行失败(已重试 {retry} 次):{e}",
"messages": [f"[SQL错误] 重试耗尽:{e}"],
}
async def format_result(state: NL2SQLState) -> dict:
"""节点 5:将查询结果格式化为自然语言"""
if state.get("formatted_answer"):
return {} # 已经有错误消息,不需要格式化
result_str = str(state["query_result"])[:500] # 截断太长
prompt = f"""将以下数据库查询结果用自然、友好的语言回复用户。
用户问题:{state["user_question"]}
查询结果(JSON):{result_str}
要求:
1. 用中文回复
2. 如果结果为空,告知用户没有找到相关数据
3. 用简洁清晰的语言,不要罗列原始数据"""
response = llm.invoke(prompt)
return {
"formatted_answer": response.content,
"messages": [f"[结果格式化] 完成"],
}
# ======== 路由函数 ========
def should_retry(state: NL2SQLState) -> Literal["generate_sql", "format_result", END]:
"""决定流程走向"""
if state.get("formatted_answer"):
return END # 有最终答案,结束
if state.get("sql_error"):
return "generate_sql" # 有错误,重试
if state.get("query_result") is not None:
return "format_result" # 有结果,格式化
return END
# ======== 构建图 ========
workflow = StateGraph(NL2SQLState)
# 添加节点
workflow.add_node("understand_schema", understand_schema)
workflow.add_node("generate_sql", generate_sql)
workflow.add_node("validate_sql", validate_sql)
workflow.add_node("execute_sql", execute_sql)
workflow.add_node("format_result", format_result)
# 连线
workflow.set_entry_point("understand_schema")
workflow.add_edge("understand_schema", "generate_sql")
workflow.add_edge("generate_sql", "validate_sql")
workflow.add_edge("validate_sql", "execute_sql")
workflow.add_edge("format_result", END)
# 条件路由
workflow.add_conditional_edges(
"execute_sql",
should_retry,
{
"generate_sql": "generate_sql",
"format_result": "format_result",
END: END,
},
)
return workflow.compile()
# ===== 使用示例 =====
async def demo_nl2sql():
schema = """
Table: employees
- id (INTEGER, PK)
- name (TEXT)
- department (TEXT)
- salary (REAL)
- hire_date (DATE)
Table: departments
- id (INTEGER, PK)
- name (TEXT)
- manager_id (INTEGER, FK → employees.id)
"""
workflow = await build_nl2sql_workflow(llm, db_conn=None)
result = await workflow.ainvoke({
"user_question": "哪个部门的平均工资最高?",
"table_schema": schema,
})
print(result["formatted_answer"])
print(f"SQL: {result.get('sql_query')}")
for msg in result.get("messages", []):
print(f" {msg}")
案例3:Subgraph 模式------多 Agent 协作
Subgraph 是将一个完整的 Agent 图作为另一个图的节点使用。这是 LangGraph 中实现多 Agent 协作的标准模式:
python
from langgraph.graph import StateGraph, END
from typing import TypedDict
# ===== 子图 1:研究 Agent =====
class ResearchState(TypedDict):
topic: str
findings: str
def build_research_agent(llm):
"""研究 Agent:搜索并总结某个主题"""
workflow = StateGraph(ResearchState)
async def research(state):
response = llm.invoke(f"研究以下主题并总结:{state['topic']}")
return {"findings": response.content}
workflow.add_node("research", research)
workflow.set_entry_point("research")
workflow.add_edge("research", END)
return workflow.compile()
# ===== 子图 2:写作 Agent =====
class WritingState(TypedDict):
topic: str
research_findings: str
draft: str
def build_writing_agent(llm):
"""写作 Agent:基于研究结果撰写文章"""
workflow = StateGraph(WritingState)
async def write(state):
prompt = f"""基于以下研究结果,撰写一篇关于「{state['topic']}」的文章。
研究结果:{state['research_findings']}"""
response = llm.invoke(prompt)
return {"draft": response.content}
workflow.add_node("write", write)
workflow.set_entry_point("write")
workflow.add_edge("write", END)
return workflow.compile()
# ===== 父图:编排研究 → 写作 =====
class OrchestratorState(TypedDict):
topic: str
research_findings: str
draft: str
final_output: str
def build_orchestrator(llm):
"""父图:将研究和写作子图串联"""
workflow = StateGraph(OrchestratorState)
# 将子图作为节点添加
research_agent = build_research_agent(llm)
writing_agent = build_writing_agent(llm)
workflow.add_node("research_agent", research_agent)
workflow.add_node("writing_agent", writing_agent)
# 状态映射:子图的字段需要和父图匹配或转换
workflow.set_entry_point("research_agent")
workflow.add_edge("research_agent", "writing_agent")
workflow.add_edge("writing_agent", END)
return workflow.compile()
# ===== 使用 =====
# orchestrator = build_orchestrator(llm)
# result = orchestrator.invoke({"topic": "量子计算在 AI 中的应用"})
# print(result["draft"])
Java 对照:Subgraph 模式 ≈ Camunda 中的 Call Activity(子流程)或 BPMN 的嵌入式子流程。但在 LangGraph 中,子图是完全独立的 Agent------有自己的状态、工具、LLM 配置。
7. Java 对比
| 维度 | Java | Python LangGraph |
|---|---|---|
| 工作流引擎 | Camunda / Flowable | LangGraph |
| 状态定义 | 流程变量 Map | TypedDict(类型安全) |
| 条件路由 | BPMN 排他网关 | Conditional Edge |
| 持久化 | 数据库表 | Checkpointer |
| 人工任务 | UserTask | interrupt() |
| 并行分支 | 并行网关 | Send API |
8. 企业最佳实践
8.1 工作流设计原则
- 节点职责单一:一个节点只做一件事
- 状态最小化:State 中只保留必要字段
- 错误处理即节点:把错误处理也建模为节点
- 可观测性:每个节点输出结构化日志
8.2 Checkpointer 选择
| 方案 | 适用 | 持久化 | 并发安全 |
|---|---|---|---|
| MemorySaver | 开发/测试 | ❌ | ❌ |
| SqliteSaver | 单机部署 | ✅ | 有限 |
| PostgresSaver | 生产环境 | ✅ | ✅ |
9. 常见错误与解决方案
| 错误 | 原因 | 解决 |
|---|---|---|
| 循环不终止 | 没有 max_iterations 限制 | 用 graph.with_config(recursion_limit=N) |
| 状态被覆盖 | 两个节点同时写同一字段 | 用 Annotated + operator.add 累积 |
| 中断后无法恢复 | Checkpointer 配置不一致 | 确保 thread_id 和 checkpointer 配置不变 |
| 子图状态隔离 | 子图状态不会自动同步父图 | 显式传递状态字段 |
10. 本章总结
本章完成了 Enterprise AI Agent Platform 的 工作流引擎:
- LangGraph 核心:State、Node、Edge、Conditional Edge
- Agent 推理循环:ReAct 模式的图实现
- 多步骤工作流:NL → SQL → 执行 → 错误修复 → 报告
- Human-in-the-Loop :
interrupt()暂停等待审批 - 状态持久化:SqliteSaver 支持中断恢复
- 并行分支 :
SendAPI 实现多角度分析
贯穿项目进度:Agent 平台拥有了完整的工作流编排能力。下一章学习 Agent Memory 和 MCP 协议。
11. 面试题
- LangGraph 的 StateGraph 和 LangChain 的 Chain 核心区别是什么?
- Conditional Edge 与入口点有何不同?
interrupt()函数是如何实现工作流暂停的?- 如何处理循环工作流中的无限循环?
12. 练习
练习1:构建旅行规划 Agent(难度:★★★)
用 LangGraph 构建:收集偏好 → 生成行程 → 用户确认 → 预订的完整工作流。
练习2:实现错误恢复工作流(难度:★★★★)
构建一个带自动重试和降级策略的工作流。
练习3:多 Agent 辩论工作流(难度:★★★★★)
3 个 Agent 各自论证,最终裁判 Agent 汇总。
13. 作业
- 将第13章的 Tool Calling 循环用 LangGraph 重写
- 添加 Human-in-the-Loop 审批流程
- 实现 Checkpointer 持久化
14. 预习
下一章:Agent Memory 与 MCP 协议 --- 长短期记忆管理和模型上下文协议。
文档版本 :v1.0 | 创建日期 :2026-07-23 | 适用 Python 版本:3.12+