文章目录
-
- [零基础入门 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)
- [1. `history` 用了 `Annotated[list[str], add]`](#1.
- [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. 其他字段负责存“过程变量”)
- [1. `messages` 用了 `Annotated[list[BaseMessage], add_messages]`](#1.
- [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:最后基于合格上下文输出答案)
- [1. `generate_query_or_respond`:先判断要不要查](#1.
- [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 的整体理解)
- 九、本篇总结
零基础入门 LangChain 与 LangGraph(七):真正理解 LangGraph------从工作流、状态图到三个核心案例
💬 开篇 :前面六篇已经把 LangChain 这条线基本跑通了:模型怎么接,Prompt 怎么写,输出怎么约束,RAG 怎么把外部知识接进来,到这里为止,我们已经具备了"做一个基础 LLM 应用"的能力。
但接下来真正遇到的问题,已经不是"会不会调模型",而是:怎样把一个需要多步决策、状态传递、工具调用、分支跳转、失败恢复、人工介入的系统真正组织起来?
👍 这一篇要解决的问题:
- 为什么只会链式调用还不够?
- Workflow 和 Agent 到底有什么区别?
- LangGraph 到底解决了什么痛点?
State、Node、Edge、Reducer、Compile这些核心概念分别是什么?- LangGraph 为什么天然适合做复杂 AI 工作流?
🚀 这一篇的目标:先把 LangGraph 的整体心智模型搭起来,然后通过三个典型案例真正看懂它:
- 一个纯工作流案例:智能快递配送系统
- 一个会查资料的智能代理案例:搜索增强对话
- 一个会自我修正的 Agentic RAG 案例:检索、判断、重写、再回答
这一篇我先讲解 LangGraph 的"为什么"和"怎么建图"。下一篇再单独讲它最硬核也最有工程价值的部分:持久化、记忆、人机交互和时间旅行。
一、为什么学完 LangChain 之后,需要继续学 LangGraph?
1.1 链式调用很好用,但它天然更适合"直线型任务"
如果只是做一个比较线性的任务,LangChain 其实已经很好用了。
例如下面这种流程:
bash
用户问题 -> 提示词模板 -> 模型调用 -> 输出解析
或者稍微复杂一点:
bash
用户问题 -> 检索资料 -> 把资料和问题一起交给模型 -> 输出答案
这种任务都有一个共同特点:
路径基本是提前确定的。
也就是说,我在写代码的时候,大致就知道整个流程会怎么走。
最多只是中间插几个组件,但整体仍然是一条线。
这就是链式结构最舒服的地方:
简单、直接、好理解、好上手。
但真正一做复杂应用,很快就会遇到下面这些问题:
- 模型需要先判断"要不要调用工具"
- 调完工具之后,还得回来继续思考
- 信息没收集全时,流程不能结束,要回到上一步继续追问
- 某一步失败了,不能从头重来,最好能从中间恢复
- 某些关键操作要让人类审批后才能继续
- 同一个请求里,可能会有多轮循环,而不是固定三四步
一旦问题变成这样,原来那种"从左到右一条线"的写法,就会越来越吃力。
1.2 真正复杂的 AI 应用,更像"状态机"而不是"流水线"
如果让我类比:
- LangChain 的链,很像把几个处理环节首尾相接,形成一条比较顺的流水线
- LangGraph 的图,更像一个显式建模出来的状态机 / 调度图
比如一个客服系统,它不是简单地回答一句话就结束。
它可能要经历:
- 识别用户意图
- 判断是咨询、退货还是投诉
- 缺信息就继续问
- 信息齐全后再进入处理分支
- 某些分支需要人工接管
- 最终才能结束
你会发现,这时候系统真正关心的不再只是"下一个组件是谁",而是:
- 当前状态是什么
- 接下来应该走哪条边
- 什么时候结束
- 什么时候回退
- 什么时候暂停
- 什么时候恢复
这就是 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)
这里的逻辑非常清楚:
- 先看问题能不能直接答
- 如果不能,就去检索
- 检索后先评分
- 如果评分通过,就生成最终答案
- 如果评分不过,就改写问题再检索
- 形成一个循环闭环
这里最关键的依然是 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:先规划,再把任务发给多个执行者
这个模式和单纯并行最大的区别在于:
任务数量不是提前写死的,而是运行时动态决定的。
例如我要写一份"某个主题的完整报告",最合理的做法不一定是提前写死三个章节。
更自然的做法是:
- 先让一个"协调者"生成报告大纲
- 看大纲有几个章节
- 再动态创建几个工作者分别写对应章节
- 最后汇总
LangGraph 里这类动态任务分发,通常会用到 Send。
它的本质非常像:
- 先规划任务
- 再按规划动态创建子任务
- 最后统一收敛结果
这已经有一点"多智能体协作"的味道了。
7.6 Evaluator-Optimizer:"会自我修正"的模式
这个模式特别重要,因为它很符合 LLM 应用的现实特点:
- 一次生成未必最好
- 但系统可以自己检查好不好
- 如果不好,就继续改
它的经典路径是:
bash
生成 -> 评估 -> 不合格则优化 -> 再生成
前面那个 Agentic RAG,其实本质上已经属于这一类了:
- 检索之后先评分
- 不合格就重写问题
- 再检索
- 再回答
所以你会发现,这些模式不是孤立存在的。
很多实际系统,往往就是两三种模式叠在一起。
八、对 LangGraph 的整体理解
8.1 一句话总结这一篇
LangGraph 的核心价值,不是"再封装一次模型调用",而是把复杂 AI 应用正式提升成"有状态、可分支、可循环、可恢复"的图式系统。
这句话里每个词都很重要:
- 有状态:不是一步一忘
- 可分支:不再只有一条线
- 可循环:允许反复迭代
- 可恢复:为长流程和工程可靠性打基础
8.2 怎么区分"会调模型"和"会设计智能体流程"
会调模型
通常意味着:
- 会写 Prompt
- 会接聊天模型
- 会调用工具
- 会做基础 RAG
会设计智能体流程
通常意味着:
- 能定义全局状态
- 能拆节点职责
- 能设计固定边和条件边
- 能把"判断、检索、重试、修正、人工介入"组织成闭环
- 能让系统在复杂场景下仍然保持可理解、可调试、可维护
说白了:
前者解决的是"模型能不能干活",后者解决的是"系统怎样把活干稳"。
而 LangGraph 的价值,就在后者。
九、本篇总结
一个新的系统视角:
从 LangChain 主要关注"组件怎么拼",到 LangGraph 正式开始关注"流程怎么跑"。
核心结论:
-
LangGraph 出现的根本原因,不是链式调用不够好,而是复杂 AI 应用早就不再是单条直线。
一旦出现循环、分支、状态、人工介入和失败恢复,图结构就比链结构更自然。
-
Workflow 和 Agent 的区别,核心在于流程是不是预设。
Workflow 更强调固定路径,Agent 更强调模型动态决策;而 LangGraph 两者都能表达。
-
LangGraph 最核心的抽象就是
State、Node、Edge。
State是全局上下文,Node是处理函数,Edge是调度规则;Reducer负责合并状态更新,compile()负责把图真正装配起来。 -
智能快递配送系统这个案例,本质上是在教我用图来建模工作流。
它让我第一次真正把
State、Node、Edge、条件路由和编译过程串到一起。 -
带搜索能力的智能代理,真正重点不在搜索,而在"模型和工具之间的循环闭环"。
它说明 LangGraph 不只是做固定流程,还能组织 Agent 行为。
-
Agentic RAG 的价值,在于把"评估与修正"正式纳入流程。
检索不是结束,检索质量检查、问题重写和再检索,才是更稳的工程思路。
-
五种常见工作流模式,其实就是 LangGraph 的套路库。
Prompt Chaining、Parallelization、Routing、Orchestrator-Workers、Evaluator-Optimizer,后面几乎都会反复遇到。
到这一篇为止,我终于把 LangGraph 的第一层地基搭起来了:它不是"更复杂的链",而是"把复杂 AI 流程正式建模成图"。
💬 下一篇承接方向 :下一篇我会正式进入 LangGraph 最硬核、也最能体现工程价值的部分:持久化(Persistence) 。
到那时,我会把
Thread、Checkpoint、Store、短期记忆、长期记忆、人工中断(Interrupt)、恢复执行(Command)、时间旅行(Time Travel)这些概念一口气彻底讲透。也就是说,下一篇开始,LangGraph 才真正从"会建图"进入"能跑长流程、能记住上下文、能中途暂停再继续"的阶段。