学习第16天:LangGraph 与 Agent 工作流编排

学习第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 工作流设计原则

  1. 节点职责单一:一个节点只做一件事
  2. 状态最小化:State 中只保留必要字段
  3. 错误处理即节点:把错误处理也建模为节点
  4. 可观测性:每个节点输出结构化日志

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 的 工作流引擎

  1. LangGraph 核心:State、Node、Edge、Conditional Edge
  2. Agent 推理循环:ReAct 模式的图实现
  3. 多步骤工作流:NL → SQL → 执行 → 错误修复 → 报告
  4. Human-in-the-Loopinterrupt() 暂停等待审批
  5. 状态持久化:SqliteSaver 支持中断恢复
  6. 并行分支Send API 实现多角度分析

贯穿项目进度:Agent 平台拥有了完整的工作流编排能力。下一章学习 Agent Memory 和 MCP 协议。


11. 面试题

  1. LangGraph 的 StateGraph 和 LangChain 的 Chain 核心区别是什么?
  2. Conditional Edge 与入口点有何不同?
  3. interrupt() 函数是如何实现工作流暂停的?
  4. 如何处理循环工作流中的无限循环?

12. 练习

练习1:构建旅行规划 Agent(难度:★★★)

用 LangGraph 构建:收集偏好 → 生成行程 → 用户确认 → 预订的完整工作流。

练习2:实现错误恢复工作流(难度:★★★★)

构建一个带自动重试和降级策略的工作流。

练习3:多 Agent 辩论工作流(难度:★★★★★)

3 个 Agent 各自论证,最终裁判 Agent 汇总。


13. 作业

  1. 将第13章的 Tool Calling 循环用 LangGraph 重写
  2. 添加 Human-in-the-Loop 审批流程
  3. 实现 Checkpointer 持久化

14. 预习

下一章:Agent Memory 与 MCP 协议 --- 长短期记忆管理和模型上下文协议。


文档版本 :v1.0 | 创建日期 :2026-07-23 | 适用 Python 版本:3.12+

相关推荐
woshihuanglaoshi1 小时前
鸿蒙技术进阶全景高级成长体系:从初中级到架构师的技能树/学习路径/项目实战/认证体系系统性方法论
学习·华为·harmonyos
AI即插即用1 小时前
即插即用系列 | IEEE TMI PLG-HN:原型学习引导的 CNN-Transformer 混合网络,攻克乳腺肿瘤分割难题
人工智能·深度学习·神经网络·学习·目标检测·cnn·transformer
for_ever_love__2 小时前
python基础语法学习: 变量, 输入输出, 运算符
网络·python·学习
学习和思考3 小时前
非自动化专业如何自学PLC?我的6个月学习路线图
笔记·学习·自动化·学习方法
拾陆楼3 小时前
PT: DMSA辅助调tree报告前后级余量脚本
后端·学习
sbjdhjd5 小时前
大三网安秋招核心学习前言(AI 安全 + Web 漏洞 + 内网渗透全考点)
人工智能·网络协议·学习·安全·网络安全·开源·php
LccKyI6 小时前
C#学习day06(方法/函数及其关键字,附思维导图)
经验分享·学习·c#
xian_wwq6 小时前
【学习笔记】Loop Engineering,从手动提示到目标驱动自动化-13/16
笔记·学习·自动化
殷忆枫7 小时前
K210 BOX学习笔记:环境搭建及模型训练部署
学习