零基础入门 LangChain 与 LangGraph(七):真正理解 LangGraph——从工作流、状态图到三个核心案例

文章目录

    • [零基础入门 LangChain 与 LangGraph(七):真正理解 LangGraph------从工作流、状态图到三个核心案例](#零基础入门 LangChain 与 LangGraph(七):真正理解 LangGraph——从工作流、状态图到三个核心案例)
    • [一、为什么学完 LangChain 之后,需要继续学 LangGraph?](#一、为什么学完 LangChain 之后,需要继续学 LangGraph?)
      • [1.1 链式调用很好用,但它天然更适合"直线型任务"](#1.1 链式调用很好用,但它天然更适合“直线型任务”)
      • [1.2 真正复杂的 AI 应用,更像"状态机"而不是"流水线"](#1.2 真正复杂的 AI 应用,更像“状态机”而不是“流水线”)
      • [1.3 LangGraph 解决的,不是"再多几个 API",而是流程组织问题](#1.3 LangGraph 解决的,不是“再多几个 API”,而是流程组织问题)
    • 二、Agent、Workflow、LangGraph
      • [2.1 什么是 Agent?](#2.1 什么是 Agent?)
      • [2.2 什么是 Workflow?](#2.2 什么是 Workflow?)
      • [2.3 Workflow 和 Agent 到底差在哪?](#2.3 Workflow 和 Agent 到底差在哪?)
      • [2.4 LangGraph 和 LangChain 到底是什么关系?](#2.4 LangGraph 和 LangChain 到底是什么关系?)
    • [三、LangGraph 的核心抽象:State、Node、Edge、Reducer、Compile](#三、LangGraph 的核心抽象:State、Node、Edge、Reducer、Compile)
      • [3.1 一个"有状态的图"](#3.1 一个“有状态的图”)
      • [3.2 State:本质上就是贯穿全流程的上下文结构体](#3.2 State:本质上就是贯穿全流程的上下文结构体)
      • [3.3 Node:本质上就是一个处理函数](#3.3 Node:本质上就是一个处理函数)
      • [3.4 Edge:它本质上就是调度规则](#3.4 Edge:它本质上就是调度规则)
        • [1. 固定边](#1. 固定边)
        • [2. 条件边](#2. 条件边)
        • [3. 特殊入口与出口](#3. 特殊入口与出口)
      • [3.5 Reducer:它决定"新状态"如何和"旧状态"合并](#3.5 Reducer:它决定“新状态”如何和“旧状态”合并)
      • [3.6 Compile:把图装配并检查好](#3.6 Compile:把图装配并检查好)
    • 四、案例一:智能快递配送系统
      • [4.1 这个案例为什么特别适合入门?](#4.1 这个案例为什么特别适合入门?)
      • [4.2 第一步:定义 State](#4.2 第一步:定义 State)
        • [1. `history` 用了 `Annotated[list[str], add]`](#1. history 用了 Annotated[list[str], add])
        • [2. `total_distance` 也用了 `add`](#2. total_distance 也用了 add)
      • [4.3 第二步:定义各个节点](#4.3 第二步:定义各个节点)
      • [4.4 第三步:把节点和边组织成图](#4.4 第三步:把节点和边组织成图)
      • [4.5 第四步:编译并执行](#4.5 第四步:编译并执行)
    • 五、案例二:带搜索能力的智能代理,为什么它已经不再是单纯工作流?
      • [5.1 这个案例的重点,不在"搜索",而在"循环"](#5.1 这个案例的重点,不在“搜索”,而在“循环”)
      • [5.2 为什么这里的 State 要换成"消息列表"?](#5.2 为什么这里的 State 要换成“消息列表”?)
      • [5.3 `llm_call` 节点干的事情:让模型先决定要不要用工具](#5.3 llm_call 节点干的事情:让模型先决定要不要用工具)
      • [5.4 `tool_node` 为什么一定要构造 `ToolMessage`?](#5.4 tool_node 为什么一定要构造 ToolMessage?)
      • [5.5 条件边的作用:决定继续循环还是结束](#5.5 条件边的作用:决定继续循环还是结束)
    • [六、案例三:Agentic RAG------为什么检索之后还要"判断对不对"?](#六、案例三:Agentic RAG——为什么检索之后还要“判断对不对”?)
      • [6.1 这个案例为什么特别重要?](#6.1 这个案例为什么特别重要?)
      • [6.2 第一步:定义 State](#6.2 第一步:定义 State)
        • [1. `messages` 用了 `Annotated[list[BaseMessage], add_messages]`](#1. messages 用了 Annotated[list[BaseMessage], add_messages])
        • [2. 其他字段负责存"过程变量"](#2. 其他字段负责存“过程变量”)
      • [6.3 第二步:定义四个核心节点](#6.3 第二步:定义四个核心节点)
        • [1. `generate_query_or_respond`:先判断要不要查](#1. generate_query_or_respond:先判断要不要查)
        • [2. `retrieve`:真正去查资料](#2. retrieve:真正去查资料)
        • [3. `rewrite_question`:当检索质量不好时重写问题](#3. rewrite_question:当检索质量不好时重写问题)
        • [4. `generate_answer`:最后基于合格上下文输出答案](#4. generate_answer:最后基于合格上下文输出答案)
      • [6.4 第三步:加入评分节点,形成质量闭环](#6.4 第三步:加入评分节点,形成质量闭环)
      • [6.5 第四步:把节点和边组织成图](#6.5 第四步:把节点和边组织成图)
      • [6.6 第五步:编译并执行](#6.6 第五步:编译并执行)
      • [6.7 这个案例真正说明了什么?](#6.7 这个案例真正说明了什么?)
    • [七、LangGraph 里最常见的五种工作流模式,我现在怎么理解](#七、LangGraph 里最常见的五种工作流模式,我现在怎么理解)
      • [7.1 五种模式](#7.1 五种模式)
      • [7.2 Prompt Chaining:最像传统流水线的一种模式](#7.2 Prompt Chaining:最像传统流水线的一种模式)
      • [7.3 Parallelization:多个分析同时做,最后再合并](#7.3 Parallelization:多个分析同时做,最后再合并)
      • [7.4 Routing:先分类,再走专门分支](#7.4 Routing:先分类,再走专门分支)
      • [7.5 Orchestrator-Workers:先规划,再把任务发给多个执行者](#7.5 Orchestrator-Workers:先规划,再把任务发给多个执行者)
      • [7.6 Evaluator-Optimizer:"会自我修正"的模式](#7.6 Evaluator-Optimizer:“会自我修正”的模式)
    • [八、对 LangGraph 的整体理解](#八、对 LangGraph 的整体理解)
      • [8.1 一句话总结这一篇](#8.1 一句话总结这一篇)
      • [8.2 怎么区分"会调模型"和"会设计智能体流程"](#8.2 怎么区分“会调模型”和“会设计智能体流程”)
    • 九、本篇总结

零基础入门 LangChain 与 LangGraph(七):真正理解 LangGraph------从工作流、状态图到三个核心案例

💬 开篇 :前面六篇已经把 LangChain 这条线基本跑通了:模型怎么接,Prompt 怎么写,输出怎么约束,RAG 怎么把外部知识接进来,到这里为止,我们已经具备了"做一个基础 LLM 应用"的能力。

但接下来真正遇到的问题,已经不是"会不会调模型",而是:怎样把一个需要多步决策、状态传递、工具调用、分支跳转、失败恢复、人工介入的系统真正组织起来?

👍 这一篇要解决的问题

  1. 为什么只会链式调用还不够?
  2. Workflow 和 Agent 到底有什么区别?
  3. LangGraph 到底解决了什么痛点?
  4. StateNodeEdgeReducerCompile 这些核心概念分别是什么?
  5. LangGraph 为什么天然适合做复杂 AI 工作流?

🚀 这一篇的目标:先把 LangGraph 的整体心智模型搭起来,然后通过三个典型案例真正看懂它:

  • 一个纯工作流案例:智能快递配送系统
  • 一个会查资料的智能代理案例:搜索增强对话
  • 一个会自我修正的 Agentic RAG 案例:检索、判断、重写、再回答

这一篇我先讲解 LangGraph 的"为什么"和"怎么建图"。下一篇再单独讲它最硬核也最有工程价值的部分:持久化、记忆、人机交互和时间旅行


一、为什么学完 LangChain 之后,需要继续学 LangGraph?

1.1 链式调用很好用,但它天然更适合"直线型任务"

如果只是做一个比较线性的任务,LangChain 其实已经很好用了。

例如下面这种流程:

bash 复制代码
用户问题 -> 提示词模板 -> 模型调用 -> 输出解析

或者稍微复杂一点:

bash 复制代码
用户问题 -> 检索资料 -> 把资料和问题一起交给模型 -> 输出答案

这种任务都有一个共同特点:

路径基本是提前确定的。

也就是说,我在写代码的时候,大致就知道整个流程会怎么走。

最多只是中间插几个组件,但整体仍然是一条线。

这就是链式结构最舒服的地方:

简单、直接、好理解、好上手。

但真正一做复杂应用,很快就会遇到下面这些问题:

  • 模型需要先判断"要不要调用工具"
  • 调完工具之后,还得回来继续思考
  • 信息没收集全时,流程不能结束,要回到上一步继续追问
  • 某一步失败了,不能从头重来,最好能从中间恢复
  • 某些关键操作要让人类审批后才能继续
  • 同一个请求里,可能会有多轮循环,而不是固定三四步

一旦问题变成这样,原来那种"从左到右一条线"的写法,就会越来越吃力。


1.2 真正复杂的 AI 应用,更像"状态机"而不是"流水线"

如果让我类比:

  • LangChain 的链,很像把几个处理环节首尾相接,形成一条比较顺的流水线
  • LangGraph 的图,更像一个显式建模出来的状态机 / 调度图

比如一个客服系统,它不是简单地回答一句话就结束。

它可能要经历:

  1. 识别用户意图
  2. 判断是咨询、退货还是投诉
  3. 缺信息就继续问
  4. 信息齐全后再进入处理分支
  5. 某些分支需要人工接管
  6. 最终才能结束

你会发现,这时候系统真正关心的不再只是"下一个组件是谁",而是:

  • 当前状态是什么
  • 接下来应该走哪条边
  • 什么时候结束
  • 什么时候回退
  • 什么时候暂停
  • 什么时候恢复

这就是 LangGraph 真正要接手的问题。


1.3 LangGraph 解决的,不是"再多几个 API",而是流程组织问题

它不是一个"让模型更聪明"的框架,而是一个"让复杂 AI 流程更可控"的框架。

如果把一个复杂 Agent 应用比作一家公司,那么:

  • 模型 = 干活的人
  • 工具 = 可以调动的外部资源
  • Prompt = 给员工的任务说明
  • LangGraph = 流程系统 + 调度系统 + 状态管理系统

所以 LangGraph 的重点从来不在"替代模型能力",而在:

  • 让流程有状态
  • 让状态可以传递
  • 让路径可以分支
  • 让执行可以循环
  • 让系统可以恢复
  • 让人类可以介入

这才是它和前面 LangChain 最大的层级差异。


二、Agent、Workflow、LangGraph

2.1 什么是 Agent?

用一句非常朴素的话来定义它:

Agent,就是一个能围绕目标自主决定下一步做什么的 AI 程序。

重点不在"它会不会说话",而在"它会不会自己决定流程"。

例如我告诉它:

bash 复制代码
帮我规划下周去上海的差旅

如果它只是回你一句"好的,我来帮你规划",那还不算真正意义上的 Agent。

但如果它开始自己做下面这些事情:

  • 先查天气
  • 再比较高铁和航班
  • 再给出酒店建议
  • 再生成行程安排
  • 最后提醒你带哪些东西

而且中间步骤不需要你一条条手动发命令,那它就已经有明显的 Agent 行为了。

也就是说,Agent 的核心不是"长得像聊天窗口",而是:

  • 能理解目标
  • 能规划步骤
  • 能调用工具
  • 能根据结果调整后续行为

2.2 什么是 Workflow?

Workflow 就容易理解得多了。

Workflow,就是预先定义好的执行路径。

它不强调"自主决策",而强调:

  • 先做什么
  • 后做什么
  • 什么条件下跳到哪里
  • 最终什么时候结束

这特别像我们熟悉的工程流程:

bash 复制代码
提交工单 -> 分类 -> 分配 -> 处理 -> 回访 -> 结束

或者:

bash 复制代码
读取文档 -> 切分 -> 向量化 -> 检索 -> 生成答案

它们的共性是:

流程路径是提前设计好的。

所以 Workflow 更适合那些:

  • 任务边界明确
  • 路径可预设
  • 逻辑相对稳定
  • 需要高一致性的场景

2.3 Workflow 和 Agent 到底差在哪?

这个地方我专门整理成一个表,看一眼就清楚。

类型 核心驱动 路径是否预设 灵活度 更适合什么场景
Workflow 代码逻辑/规则 明确流程任务
Agent 模型动态决策 不一定 开放式复杂任务

再说得直白一点:

  • Workflow 更像按图纸施工
  • Agent 更像带判断能力的执行者

但这里要注意一个非常关键的点:

LangGraph 不是只用来做 Agent。

它既能做:

  • 固定路径的 Workflow

也能做:

  • 带动态决策的 Agent

甚至还能做:

  • Workflow + Agent 混合系统

比如一个大流程是固定的,但其中某个节点内部交给模型自主决策。

这在实际项目里非常常见。


2.4 LangGraph 和 LangChain 到底是什么关系?

LangChain 更像高层组件库,LangGraph 更像底层流程编排器。

前面学 LangChain,我更多是在解决这些问题:

  • 模型怎么接
  • Prompt 怎么组织
  • 输出怎么约束
  • Document 怎么读
  • 检索怎么做
  • RAG 怎么搭

而到了 LangGraph,我开始关心这些问题:

  • 状态怎么传
  • 节点怎么拆
  • 边怎么连
  • 条件路由怎么写
  • 失败后怎么恢复
  • 长流程怎么保存
  • 人怎么在中间插进来

所以在我现在的理解是,两者不是替代关系,而是分层关系:
模型
Prompt
工具
检索/RAG
LangChain 组件组织
LangGraph 流程编排

如果只做简单应用,到 LangChain 很多时候就够了。

但只要你开始做复杂、有状态、可恢复的智能体系统,LangGraph 基本就绕不开。


三、LangGraph 的核心抽象:State、Node、Edge、Reducer、Compile

3.1 一个"有状态的图"

LangGraph 最核心的思想,其实非常简单:

把整个应用建模成一张图。

这张图里有三类最重要的东西:

  • 节点(Node):表示某一步要执行什么逻辑
  • 边(Edge):表示执行完之后往哪里走
  • 状态(State):表示整个流程当前携带的数据

如果你以前写过编译器、状态机、事件驱动系统,或者哪怕只是写过一个稍微复杂的业务流程代码,你都会发现这个抽象特别自然。

因为现实里的复杂系统,本来就不是"一条线"。


3.2 State:本质上就是贯穿全流程的上下文结构体

对我来说,State 最容易理解的方式就是:

把它看成一个贯穿整个流程的 struct

只是 Python 里常用 TypedDict 来表达。

例如:

python 复制代码
from typing import TypedDict, Annotated
from operator import add

class PackageState(TypedDict):
    package_id: str
    origin: str
    destination: str
    priority: str
    status: str
    history: Annotated[list[str], add]
    total_distance: Annotated[int, add]

区别在于,LangGraph 不只是"把它当数据结构",还会把它当成:

  • 节点输入
  • 节点输出更新的归宿
  • 条件判断的依据
  • 整个流程的共享上下文

所以 State 不是某个局部变量,而是整个图的"公共上下文"。


3.3 Node:本质上就是一个处理函数

LangGraph 里的节点,说白了就是函数。

一个节点最常见的形态就是:

  • 接收当前状态
  • 做一件事
  • 返回状态更新

例如:

python 复制代码
def receive_package(state: PackageState):
    return {
        "status": "已揽收",
        "history": [f"在{state['origin']}揽收"]
    }

节点不一定返回整个状态,只需要返回"这一步改了什么"。

这一点很重要,因为它意味着:

  • 节点职责更单一
  • 状态更新更清晰
  • 后续合并更灵活

3.4 Edge:它本质上就是调度规则

边决定的是:

一个节点执行完之后,下一步去哪里。

边大致有三种最常见形态:

1. 固定边
bash 复制代码
A 一定走到 B
2. 条件边
bash 复制代码
如果条件1成立,走到 B
如果条件2成立,走到 C
3. 特殊入口与出口
  • START:开始节点
  • END:结束节点

如果类比状态机,Edge 就是状态转移规则。

如果类比事件循环,它像调度器里的下一跳逻辑。


3.5 Reducer:它决定"新状态"如何和"旧状态"合并

这个概念是很多人第一次看 LangGraph 会忽略的,但它其实非常关键。

比如一个字段是:

python 复制代码
status: str

那通常新值会直接覆盖旧值。

但如果一个字段是历史列表:

python 复制代码
history: Annotated[list[str], add]

这里的意思就是:

新的历史记录不是替换旧记录,而是追加到后面。

同理:

python 复制代码
total_distance: Annotated[int, add]

表示新里程会和旧里程累加。

这和普通赋值完全不一样。

所以 Reducer 的本质,就是:

定义字段级别的状态合并策略。

你可以把它理解成:

同一个全局状态里,不同字段可以有不同的"更新规则"。


3.6 Compile:把图装配并检查好

这不是传统的编译

LangGraph 里的 compile()是在做下面这些事:

  • 把你定义的节点和边真正组装成可执行图
  • 检查图结构是否合理
  • 检查有没有孤立节点
  • 检查从 START 到各节点、各节点到 END 是否可达
  • 最终生成一个可运行对象

所以它更像:

运行前的图结构装配 + 合法性校验

你可以把它看成初始化阶段对状态机的一次"建图和验图"。


四、案例一:智能快递配送系统

4.1 这个案例为什么特别适合入门?

因为它几乎把 LangGraph 的几个核心抽象一次性全串起来了:

  • 包裹信息 = State
  • 各个站点 = Node
  • 运输路线 = Edge
  • 历史记录累加 = Reducer
  • 最终执行 = compile() + invoke()

整个图的结构可以先画成这样:
普通件
加急件
START
揽收站
分拣中心
标准配送
加急配送
派送站
END

这个图一看就很直观:

  • 所有包裹都先揽收
  • 然后进入分拣
  • 分拣之后根据优先级分流
  • 最后统一进入派送并结束

这就是一个标准的工作流图。


4.2 第一步:定义 State

先定义包裹状态:

python 复制代码
from typing import TypedDict, Annotated
from operator import add

class PackageState(TypedDict):
    package_id: str
    origin: str
    destination: str
    priority: str
    status: str
    history: Annotated[list[str], add]
    total_distance: Annotated[int, add]
1. history 用了 Annotated[list[str], add]

表示每个节点新增的历史记录,最终要追加到整个 history 列表里。

2. total_distance 也用了 add

表示不同运输环节产生的里程,会不断累加。

这两个字段特别适合用来理解 Reducer。

因为它们不是"覆盖",而是"合并"。


4.3 第二步:定义各个节点

python 复制代码
def receive_package(state: PackageState):
    return {
        "status": "已揽收",
        "history": [f"在{state['origin']}揽收"]
    }

def sort_package(state: PackageState):
    destination = state["destination"]
    if "北京" in destination:
        next_station = "北京分拣中心"
    elif "上海" in destination:
        next_station = "上海分拣中心"
    else:
        next_station = "其他地区分拣中心"

    return {
        "status": "已分拣",
        "history": [f"分拣至{next_station}"]
    }

def standard_delivery(state: PackageState):
    return {
        "status": "运输中",
        "history": ["标准陆运"],
        "total_distance": 500
    }

def express_delivery(state: PackageState):
    return {
        "status": "加急运输",
        "history": ["空运加急"],
        "total_distance": 800
    }

def final_delivery(state: PackageState):
    return {
        "status": "已签收",
        "history": [f"已送达{state['destination']}"]
    }

这段代码最重要的是体现了节点设计原则:

一个节点只做一件事。减少耦合


4.4 第三步:把节点和边组织成图

python 复制代码
from langgraph.graph import StateGraph, START, END

delivery = StateGraph(PackageState)

delivery.add_node("揽收站", receive_package)
delivery.add_node("分拣中心", sort_package)
delivery.add_node("标准配送", standard_delivery)
delivery.add_node("加急配送", express_delivery)
delivery.add_node("派送站", final_delivery)

这一步其实就是把"函数"正式注册成"图里的节点"。

接着定义固定边和条件边:

python 复制代码
delivery.add_edge(START, "揽收站")
delivery.add_edge("揽收站", "分拣中心")

def select_delivery(state: PackageState):
    if state["priority"] == "加急":
        return "加急配送"
    return "标准配送"

delivery.add_conditional_edges(
    "分拣中心",
    select_delivery,
    ["加急配送", "标准配送"]
)

delivery.add_edge("标准配送", "派送站")
delivery.add_edge("加急配送", "派送站")
delivery.add_edge("派送站", END)

这里最关键的是 add_conditional_edges()

它的作用就是:

让图在运行时根据当前状态选择下一条路径。

这一步非常像根据状态值做路由分发。


4.5 第四步:编译并执行

python 复制代码
delivery_system = delivery.compile()

package = {
    "package_id": "P001",
    "origin": "北京",
    "destination": "上海",
    "priority": "普通",
    "status": "待处理",
    "history": [],
    "total_distance": 0
}

result = delivery_system.invoke(package)
print(result)

执行后你最终拿到的是完整状态。

例如:

python 复制代码
{
    "package_id": "P001",
    "origin": "北京",
    "destination": "上海",
    "priority": "普通",
    "status": "已签收",
    "history": ["在北京揽收", "分拣至上海分拣中心", "标准陆运", "已送达上海"],
    "total_distance": 500
}

这个结果非常能说明 LangGraph 的风格:

  • 每一步只返回局部更新
  • 最终框架帮你把状态合并完整
  • 条件边控制流程路径
  • 执行结束后你拿到的是一份"最终状态快照"

这和传统"函数调用链返回一个最终字符串"完全不是一个层次。


五、案例二:带搜索能力的智能代理,为什么它已经不再是单纯工作流?

5.1 这个案例的重点,不在"搜索",而在"循环"

如果要做一个会搜索的问答系统,最直接的想法可能是:

bash 复制代码
用户提问 -> 搜索 -> 把搜索结果喂给模型 -> 输出答案

这当然能做。

但真实情况往往是:

  • 有些问题根本不需要搜索
  • 有些问题需要搜索一次
  • 有些问题可能需要多轮搜索
  • 模型必须先判断"是否需要工具"

你会发现,这个流程已经不再是固定直线了。

它更像这样:
需要工具
不需要工具
用户问题
llm_call
tool_node
END

这张图的重点非常关键:

模型和工具之间是会来回循环的。

这就是一个典型的 Agent 行为。


5.2 为什么这里的 State 要换成"消息列表"?

在对话系统里,最核心的状态往往不是一个简单字符串,而是整段消息历史。

所以这里通常会这样定义:

python 复制代码
from typing_extensions import TypedDict, Annotated
from langchain_core.messages import AnyMessage
import operator

class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], operator.add]
    llm_calls: int

这个定义里最重要的是:

python 复制代码
messages: Annotated[list[AnyMessage], operator.add]

它的含义非常明确:

每次新消息不是替换旧消息,而是追加到历史记录后面。

就像一个不断增长的对话上下文。

这就是多轮对话系统的基础。


5.3 llm_call 节点干的事情:让模型先决定要不要用工具

python 复制代码
from langchain_core.messages import SystemMessage

def llm_call(state: MessagesState):
    response = model_with_tools.invoke(
        [SystemMessage(content="你是一个支持搜索工具的助手")] + state["messages"]
    )
    return {
        "messages": [response],
        "llm_calls": state.get("llm_calls", 0) + 1
    }

这个节点做的事情:"主决策器":

  • 把当前消息历史交给模型
  • 让模型自己判断要不要调用搜索工具
  • 如果模型想调工具,它会在返回结果里带上 tool_calls
  • 如果不需要工具,就可以直接结束

也就是说,模型在这里不仅输出"答案",还输出"行动意图"。

这就是 Agent 和普通问答最大的不同之一。


5.4 tool_node 为什么一定要构造 ToolMessage

因为对模型来说:

工具执行结果也必须作为消息回到上下文里。

所以通常会写成这样:

python 复制代码
from langchain_core.messages import ToolMessage

tools_by_name = {tool.name: tool for tool in tools}

def tool_node(state: MessagesState):
    result = []
    for tool_call in state["messages"][-1].tool_calls:
        tool = tools_by_name[tool_call["name"]]
        observation = tool.invoke(tool_call["args"])
        result.append(
            ToolMessage(
                content=str(observation),
                tool_call_id=tool_call["id"]
            )
        )
    return {"messages": result}

这一步非常关键。

因为它相当于告诉模型:

  • 你刚刚要求调用的工具,我已经替你执行了
  • 执行结果在这里
  • 你现在可以基于这个结果继续推理了

如果没有 ToolMessage 这一步,模型只知道自己"想调用工具",却拿不到工具结果,自然也就没法继续完成后续回答。


5.5 条件边的作用:决定继续循环还是结束

python 复制代码
def should_continue(state: MessagesState):
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tool_node"
    return END
  • 如果模型返回里带了 tool_calls

    • 说明它决定调用工具
    • 那就走到 tool_node
  • 如果没有

    • 说明它已经准备好直接回答
    • 那就结束

然后把它接进图里:

python 复制代码
agent_builder.add_conditional_edges(
    "llm_call",
    should_continue,
    ["tool_node", END]
)
agent_builder.add_edge("tool_node", "llm_call")

一旦 tool_node -> llm_call 这条边加上之后,整个闭环就形成了。

Agent 本质上不是"多几个 API",而是"模型、工具、状态"之间反复迭代的闭环。


六、案例三:Agentic RAG------为什么检索之后还要"判断对不对"?

6.1 这个案例为什么特别重要?

如果说前两个案例帮助我们理解了 LangGraph 里的 状态管理、节点执行、边路由,那这个案例真正把 LangGraph 拉进了"实际 AI 应用"的范畴。

因为现实里的 RAG,很少像教程里那么理想。

普通 RAG 的流程通常是:

text 复制代码
用户问题 -> 检索文档 -> 把文档交给模型 -> 输出答案

这个流程看上去很顺,但它默认了一个很强的前提:

检索出来的内容一定足够相关,而且足够回答问题。

但真实场景里,这个前提经常不成立。

比如:

  • 用户问题本身表述得很模糊
  • 向量召回没有命中真正关键的片段
  • 文档切分得太碎,检索到的上下文不完整
  • top-k 太小,导致真正有用的信息没被取回来
  • 看起来相关,但其实无法支撑最终回答

于是就会出现一种很典型的问题:

  • 检索到了,但没有检准
  • 资料有点沾边,但不够用
  • 模型拿着半相关上下文开始"硬答"
  • 最终答案表面流畅,实际上并不可靠

所以更强的做法不是:

检索完就直接生成

而是:

检索完先评估:这些资料到底够不够回答问题?

如果不够,就继续优化问题,再检索一次。

这就是 Agentic RAG 最核心的思想:

让系统具备"检索后自我检查、自我纠偏"的能力。

整个图的结构可以先画成这样:
直接回答
需要检索
相关
不相关
START
generate_query_or_respond
END
retrieve
grade_documents
generate_answer
rewrite_question

这个图一看就能发现,它和普通线性 RAG 最大的区别就在于:

  • 它不是"查一次就答"
  • 而是"查完先判断资料行不行"
  • 如果不行,就重写问题再查
  • 直到拿到足够可靠的上下文,才进入最终回答

这就是一个非常典型的:

评估 -> 优化 -> 再尝试

闭环。


6.2 第一步:定义 State

先定义这个 Agentic RAG 流程里要共享的状态。

python 复制代码
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage

class AgenticRAGState(TypedDict, total=False):
    messages: Annotated[list[BaseMessage], add_messages]
    question: str
    retrieved_context: str
    rewritten_question: str
    need_retrieval: bool
    grade_result: str

这个状态比前面的快递案例更像"真实应用状态"。

因为它不再只是存几个业务字段,而是在存:

  • 当前对话消息
  • 原始问题
  • 检索到的上下文
  • 重写后的问题
  • 是否需要检索
  • 检索评分结果
1. messages 用了 Annotated[list[BaseMessage], add_messages]

这表示图里的每个节点都可以往消息流里追加内容,最后由框架自动合并。

这很适合对话型系统,因为:

  • 模型输出是一条消息
  • 工具返回也是一条消息
  • 最终回答还是一条消息

这些都天然适合被组织进 messages 这个状态字段里。

2. 其他字段负责存"过程变量"

比如:

  • question:原始用户问题
  • retrieved_context:本轮检索出的上下文
  • rewritten_question:改写后的检索问题
  • need_retrieval:当前是否需要先查资料
  • grade_result:这次检索是否合格

这些字段的共同特点是:

它们不是最终答案,而是控制流程继续往哪里走的中间状态。

这正是 LangGraph 特别适合做 Agent 的原因之一:

它允许你把"思考过程"显式放进状态里。


6.3 第二步:定义四个核心节点

这个案例里最关键的,就是四个节点各司其职,每个节点只做一件事。

python 复制代码
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

def generate_query_or_respond(state: AgenticRAGState):
    question = state["question"]

    prompt = f"""
你是一个路由助手。
请判断下面这个问题是否必须依赖知识库检索。

问题:
{question}

如果不需要检索,直接回答;
如果需要检索,请只返回:NEED_RETRIEVAL
"""

    response = model.invoke([HumanMessage(content=prompt)])

    if "NEED_RETRIEVAL" in response.content:
        return {
            "need_retrieval": True,
            "messages": [AIMessage(content="我需要先检索相关资料。")]
        }

    return {
        "need_retrieval": False,
        "messages": [response]
    }


def retrieve(state: AgenticRAGState):
    query = state.get("rewritten_question", state["question"])
    docs = retriever.invoke(query)
    context = "\n\n".join(doc.page_content for doc in docs)

    return {
        "retrieved_context": context,
        "messages": [AIMessage(content=f"检索到的上下文如下:\n\n{context}")]
    }


def rewrite_question(state: AgenticRAGState):
    question = state["question"]
    context = state.get("retrieved_context", "")

    prompt = f"""
你是一个检索优化助手。
原问题如下:

{question}

当前检索结果如下:

{context}

请把用户问题改写成一个更适合检索的版本。
只返回改写后的问题。
"""

    response = model.invoke([HumanMessage(content=prompt)])

    return {
        "rewritten_question": response.content,
        "messages": [AIMessage(content=f"重写后的问题:{response.content}")]
    }


def generate_answer(state: AgenticRAGState):
    question = state["question"]
    context = state["retrieved_context"]

    prompt = f"""
你是一个问答助手。
请严格基于给定上下文回答问题。
如果上下文不足以支持答案,就明确说不知道,不要编造。

问题:
{question}

上下文:
{context}

答案:
"""

    response = model.invoke([HumanMessage(content=prompt)])

    return {
        "messages": [response]
    }

这四个节点看上去不算复杂,但职责分工非常清楚。


1. generate_query_or_respond:先判断要不要查

这个节点负责做第一层路由判断:

  • 这个问题能不能直接答?
  • 还是必须去知识库里找资料?

它不是在"回答业务问题",而是在回答一个更上层的系统问题:

这个问题现在该进入哪个处理流程?


2. retrieve:真正去查资料

这个节点只负责检索。

它不负责判断检索质量,也不负责最终回答,只做一件事:

把和问题最相关的上下文从知识库里取回来。


3. rewrite_question:当检索质量不好时重写问题

这是 Agentic RAG 和普通 RAG 最明显的区别之一。

普通 RAG 通常默认用户问题就是最好的检索 query。

但现实里往往不是这样。

所以这个节点负责把原问题变成一个更适合检索系统理解的版本,比如:

  • 补足关键词
  • 让表达更具体
  • 减少歧义
  • 强化知识库里更容易命中的概念

4. generate_answer:最后基于合格上下文输出答案

只有在资料被认为足够相关时,才进入这一步。

这一步才是真正的"RAG 回答"。

也就是说,在这个案例里,模型其实扮演了很多种不同角色:

  • 路由器
  • 检索器配合者
  • 问题重写器
  • 最终回答器

这也是这个案例特别能体现 LangGraph 价值的地方:

同一个模型,在不同节点里可以承担完全不同的系统职责。


6.4 第三步:加入评分节点,形成质量闭环

这个案例真正的核心,不在检索,而在"检索之后要不要相信这次检索"。

也就是这个评分节点:

python 复制代码
def grade_documents(state: AgenticRAGState):
    question = state["question"]
    context = state["retrieved_context"]

    prompt = f"""
你是一个评分员,请判断下面检索结果是否与问题相关,并且是否足以支持回答。

问题:
{question}

检索结果:
{context}

如果相关且足够回答,返回 yes;
否则返回 no。
"""

    response = model.invoke([HumanMessage(content=prompt)])
    score = response.content.strip().lower()

    return {
        "grade_result": score,
        "messages": [AIMessage(content=f"检索评分结果:{score}")]
    }

这个节点从工程角度看,作用不是"生成答案",而是:

回答系统问题:当前检索质量合格吗?

这一步之所以特别重要,是因为它打破了普通 RAG 一个很危险的默认前提:

只要检索到了,就说明能回答。

但在真实系统里,检索结果可能只是"沾边",根本不够支撑最终输出。

所以这个评分节点本质上就是在做一件很像"健康检查"的事:

  • 资料够不够
  • 相关不相关
  • 能不能继续往下走
  • 还是应该先纠偏

这一层判断一加进去,整个系统就不再是"线性流水线",而变成了一个带反馈能力的闭环系统。

这一步特别能体现 LangGraph 的优势:

它不是只会把流程走完,而是允许系统在流程中途做自我评估。


6.5 第四步:把节点和边组织成图

python 复制代码
from langgraph.graph import StateGraph, START, END

rag = StateGraph(AgenticRAGState)

rag.add_node("generate_query_or_respond", generate_query_or_respond)
rag.add_node("retrieve", retrieve)
rag.add_node("grade_documents", grade_documents)
rag.add_node("rewrite_question", rewrite_question)
rag.add_node("generate_answer", generate_answer)

这一步仍然是在做同一件事:

把各个函数正式注册成图节点。

接着定义固定边和条件边:

python 复制代码
rag.add_edge(START, "generate_query_or_respond")

def route_after_generate(state: AgenticRAGState):
    if state["need_retrieval"]:
        return "retrieve"
    return END

rag.add_conditional_edges(
    "generate_query_or_respond",
    route_after_generate,
    ["retrieve", END]
)

rag.add_edge("retrieve", "grade_documents")

def route_after_grade(state: AgenticRAGState):
    if state["grade_result"] == "yes":
        return "generate_answer"
    return "rewrite_question"

rag.add_conditional_edges(
    "grade_documents",
    route_after_grade,
    ["generate_answer", "rewrite_question"]
)

rag.add_edge("rewrite_question", "retrieve")
rag.add_edge("generate_answer", END)

这里的逻辑非常清楚:

  1. 先看问题能不能直接答
  2. 如果不能,就去检索
  3. 检索后先评分
  4. 如果评分通过,就生成最终答案
  5. 如果评分不过,就改写问题再检索
  6. 形成一个循环闭环

这里最关键的依然是 add_conditional_edges()


6.6 第五步:编译并执行

python 复制代码
rag_system = rag.compile()

query = "LangGraph 为什么比传统线性 RAG 更稳?"

result = rag_system.invoke({
    "question": query,
    "messages": [HumanMessage(content=query)]
})

print(result["messages"][-1].content)

执行时,你并不需要一开始就把所有状态字段补满。

只要把当前最基本的信息传进去就可以,比如:

  • 原始问题 question
  • 对话入口消息 messages

剩下的字段会在后续节点执行过程中逐步被补充进状态里。

这也是 LangGraph 非常重要的一种风格:

状态不是一次性填满的,而是在流程执行中逐步长出来的。

最终你拿到的结果,可能会包含这样的状态信息:

python 复制代码
{
    "question": "LangGraph 为什么比传统线性 RAG 更稳?",
    "retrieved_context": "...",
    "rewritten_question": "...",
    "need_retrieval": True,
    "grade_result": "yes",
    "messages": [
        HumanMessage(content="LangGraph 为什么比传统线性 RAG 更稳?"),
        AIMessage(content="我需要先检索相关资料。"),
        AIMessage(content="检索到的上下文如下:..."),
        AIMessage(content="检索评分结果:yes"),
        AIMessage(content="LangGraph 比传统线性 RAG 更稳,是因为它把检索后的评估与纠偏显式设计进流程...")
    ]
}

这个结果非常能体现 Agentic RAG 的风格:

  • 不是检索完立刻答
  • 而是先判断有没有必要检索
  • 检索后再判断资料是否合格
  • 不合格就自动重写问题再来一轮
  • 最终输出的不是一次"赌对"的结果,而是一轮"纠偏后"的结果

这和传统线性 RAG 最大的区别就在于:

它不再赌第一次检索一定对,而是把"失败后如何修正"显式设计进系统里。


6.7 这个案例真正说明了什么?

如果说前面的案例让我们理解了 LangGraph 是"带状态的流程图",那这个案例真正说明了:

LangGraph 最擅长的,不只是把流程串起来,而是把不稳定流程做得更稳。

在真实 AI 应用里,最难的通常不是"正确流程怎么走",而是:

  • 检索错了怎么办
  • 工具没拿到足够信息怎么办
  • 第一次尝试失败了怎么办
  • 系统怎么自己发现问题并纠偏

而 Agentic RAG 恰恰就是把这些"失败场景"正面设计进流程里。

所以这个案例很能代表 LangGraph 的价值:

  • 它不只是工作流框架

  • 它也不只是一个节点调度器

  • 它真正厉害的地方在于:

    允许你把"判断、评估、修正、再尝试"这些智能行为,显式写进系统结构里。


七、LangGraph 里最常见的五种工作流模式,我现在怎么理解

7.1 五种模式

模式 核心特点 最典型用途
Prompt Chaining 前一步输出喂给后一步 内容生成、分步处理
Parallelization 多路并行后汇总 多维度分析
Routing 根据输入走不同分支 智能分类、客服分流
Orchestrator-Workers 动态分配任务给多个工作者 报告生成、长文拆解
Evaluator-Optimizer 先生成,再评估,再修正 RAG 优化、代码改进

这一节先把模式思维立起来。

因为你后面一看到流程图,大概率就是这些模式的组合。


7.2 Prompt Chaining:最像传统流水线的一种模式

这是最容易理解的。

例如写文章时:

bash 复制代码
主题 -> 大纲 -> 初稿 -> 润色 -> 终稿

它的特点就是:

  • 每一步都依赖上一步结果
  • 路径固定
  • 很适合把复杂任务拆小

这类模式本质上还是一条线,只不过每个节点都更单一、更可复用。

如果你从 LangChain 过来,这种模式几乎是最自然的过渡。


7.3 Parallelization:多个分析同时做,最后再合并

例如要分析一个产品方案时,可以并行做:

  • 市场分析
  • 竞品分析
  • 技术分析

最后统一汇总报告。

这种模式特别适合那些:

  • 彼此相对独立
  • 可以并发执行
  • 最后需要统一整合的任务

它和 Prompt Chaining 的最大区别是:

不是一条线,而是多条支线同时跑。


7.4 Routing:先分类,再走专门分支

例如客服系统收到问题后,先做意图识别:

  • 售前问题
  • 售后问题
  • 技术问题

然后分别进入不同节点。

它的核心思路就是:

先做路由决策,再进入专业处理链路。

这特别像网络系统里的路由分发,也像后端系统里的策略分派。


7.5 Orchestrator-Workers:先规划,再把任务发给多个执行者

这个模式和单纯并行最大的区别在于:

任务数量不是提前写死的,而是运行时动态决定的。

例如我要写一份"某个主题的完整报告",最合理的做法不一定是提前写死三个章节。

更自然的做法是:

  1. 先让一个"协调者"生成报告大纲
  2. 看大纲有几个章节
  3. 再动态创建几个工作者分别写对应章节
  4. 最后汇总

LangGraph 里这类动态任务分发,通常会用到 Send

它的本质非常像:

  • 先规划任务
  • 再按规划动态创建子任务
  • 最后统一收敛结果

这已经有一点"多智能体协作"的味道了。


7.6 Evaluator-Optimizer:"会自我修正"的模式

这个模式特别重要,因为它很符合 LLM 应用的现实特点:

  • 一次生成未必最好
  • 但系统可以自己检查好不好
  • 如果不好,就继续改

它的经典路径是:

bash 复制代码
生成 -> 评估 -> 不合格则优化 -> 再生成

前面那个 Agentic RAG,其实本质上已经属于这一类了:

  • 检索之后先评分
  • 不合格就重写问题
  • 再检索
  • 再回答

所以你会发现,这些模式不是孤立存在的。

很多实际系统,往往就是两三种模式叠在一起。


八、对 LangGraph 的整体理解

8.1 一句话总结这一篇

LangGraph 的核心价值,不是"再封装一次模型调用",而是把复杂 AI 应用正式提升成"有状态、可分支、可循环、可恢复"的图式系统。

这句话里每个词都很重要:

  • 有状态:不是一步一忘
  • 可分支:不再只有一条线
  • 可循环:允许反复迭代
  • 可恢复:为长流程和工程可靠性打基础

8.2 怎么区分"会调模型"和"会设计智能体流程"

会调模型

通常意味着:

  • 会写 Prompt
  • 会接聊天模型
  • 会调用工具
  • 会做基础 RAG
会设计智能体流程

通常意味着:

  • 能定义全局状态
  • 能拆节点职责
  • 能设计固定边和条件边
  • 能把"判断、检索、重试、修正、人工介入"组织成闭环
  • 能让系统在复杂场景下仍然保持可理解、可调试、可维护

说白了:

前者解决的是"模型能不能干活",后者解决的是"系统怎样把活干稳"。

而 LangGraph 的价值,就在后者。


九、本篇总结

一个新的系统视角:

从 LangChain 主要关注"组件怎么拼",到 LangGraph 正式开始关注"流程怎么跑"。

核心结论:

  1. LangGraph 出现的根本原因,不是链式调用不够好,而是复杂 AI 应用早就不再是单条直线。

    一旦出现循环、分支、状态、人工介入和失败恢复,图结构就比链结构更自然。

  2. Workflow 和 Agent 的区别,核心在于流程是不是预设。

    Workflow 更强调固定路径,Agent 更强调模型动态决策;而 LangGraph 两者都能表达。

  3. LangGraph 最核心的抽象就是 StateNodeEdge
    State 是全局上下文,Node 是处理函数,Edge 是调度规则;Reducer 负责合并状态更新,compile() 负责把图真正装配起来。

  4. 智能快递配送系统这个案例,本质上是在教我用图来建模工作流。

    它让我第一次真正把 StateNodeEdge、条件路由和编译过程串到一起。

  5. 带搜索能力的智能代理,真正重点不在搜索,而在"模型和工具之间的循环闭环"。

    它说明 LangGraph 不只是做固定流程,还能组织 Agent 行为。

  6. Agentic RAG 的价值,在于把"评估与修正"正式纳入流程。

    检索不是结束,检索质量检查、问题重写和再检索,才是更稳的工程思路。

  7. 五种常见工作流模式,其实就是 LangGraph 的套路库。

    Prompt Chaining、Parallelization、Routing、Orchestrator-Workers、Evaluator-Optimizer,后面几乎都会反复遇到。

到这一篇为止,我终于把 LangGraph 的第一层地基搭起来了:它不是"更复杂的链",而是"把复杂 AI 流程正式建模成图"。


💬 下一篇承接方向 :下一篇我会正式进入 LangGraph 最硬核、也最能体现工程价值的部分:持久化(Persistence)

到那时,我会把 ThreadCheckpointStore、短期记忆、长期记忆、人工中断(Interrupt)、恢复执行(Command)、时间旅行(Time Travel)这些概念一口气彻底讲透。

也就是说,下一篇开始,LangGraph 才真正从"会建图"进入"能跑长流程、能记住上下文、能中途暂停再继续"的阶段。

相关推荐
噜噜噜阿鲁~1 小时前
python学习笔记 | 7.2、高级特性-迭代
笔记·python·学习
WL_Aurora1 小时前
2026天梯赛题解
python·算法
无风听海2 小时前
深入理解 Python 生成器
python
knight_9___2 小时前
RAG面试篇7
java·面试·agent·rag·智能体
郝学胜-神的一滴2 小时前
系统设计:新鲜事系统扩展与优化
java·python·职场和发展·php·软件工程·软件构建
思绪无限2 小时前
YOLOv5至YOLOv12升级:零售柜商品检测软件的设计与实现(完整代码+界面+数据集项目)
人工智能·python·深度学习·目标检测·计算机视觉·零售柜商品检测·yolov12
zl_dfq2 小时前
Python学习6 之 【Lambda表达式、列表与元组、推导式】
python
AI算法沐枫2 小时前
从客服转行AI Agent:半年学习与求职复盘
人工智能·深度学习·学习·大模型·agent·智能体·ai应用开发
kishu_iOS&AI2 小时前
深度学习 —— 正则化&批量归一化BN
人工智能·pytorch·python·深度学习