langgraph_01
00. 环境配置
从零开始,一步一步完成本课程所需的所有环境配置。
1. 安装 Miniconda
Conda 是一个包管理和环境管理工具。Miniconda 是 Conda 的精简版,只包含最基本的组件,体积小、安装快,推荐使用。
1.1 下载 Miniconda
打开 Miniconda 官方下载页面:
https://docs.anaconda.com/miniconda/
根据操作系统选择对应版本:
| 操作系统 | 推荐安装包 |
|---|---|
| Windows | Miniconda3 Windows 64-bit(.exe 安装包) |
| macOS | Miniconda3 macOS Apple M1 64-bit(Apple 芯片)或 macOS Intel x86 64-bit(Intel 芯片) |
| Linux | Miniconda3 Linux 64-bit(.sh 脚本) |
注意:Miniconda 自带 Python 3.13,本课程也使用 Python 3.13,无需额外调整。
1.2 Windows 安装步骤
- 双击下载的
Miniconda3-windows-*.exe,启动安装向导 - 点击 Next → 选择 Just Me (recommended) → Next
- 安装路径建议保持默认(如
C:\Users\你的用户名\miniconda3),点击 Next - 关键步骤:勾选以下两项:
- ✅ Add Miniconda3 to my PATH environment variable(将 Conda 加入系统 PATH)
- ✅ Register Miniconda3 as my default Python 3.x(设为默认 Python)
- 点击 Install,等待安装完成
- 安装完成后,打开 Anaconda Prompt (miniconda3)(从开始菜单搜索)
验证安装是否成功:
conda --version
# 输出类似:conda 26.x.x
1.3 macOS 安装步骤
# 在终端中执行(以 Apple M1 芯片为例)
bash ~/Downloads/Miniconda3-latest-MacOSX-arm64.sh
# 按提示操作:
# - 按 Enter 阅读协议
# - 输入 yes 接受协议
# - 确认安装路径(默认 ~/miniconda3 即可)
# - 输入 yes 执行 conda init
安装完成后,关闭并重新打开终端,验证:
conda --version
1.4 Linux 安装步骤
# 在终端中执行
bash ~/Downloads/Miniconda3-latest-Linux-x86_64.sh
# 按提示操作,步骤同 macOS
2. Conda 基础操作
以下是本课程会用到的 Conda 命令,有个印象即可:
| 命令 | 作用 |
|---|---|
conda --version |
查看 Conda 版本 |
conda info --envs |
列出所有虚拟环境 |
conda create -n 环境名 python=3.10 |
创建新环境 |
conda activate 环境名 |
激活(进入)某个环境 |
conda deactivate |
退出当前环境 |
conda remove -n 环境名 --all |
删除某个环境 |
conda list |
列出当前环境已安装的包 |
核心概念:虚拟环境就是一个隔离的 Python 运行空间,不同环境之间互不影响。比如你可以同时拥有一个用 Python 3.13 的
langgraph环境和一个用 Python 3.12 的other_project环境。
3. 创建课程环境
3.1 创建并激活环境
# 创建名为 langgraph 的环境,指定 Python 3.13
conda create -n langgraph python=3.13
# 出现 Proceed ([y]/n)? 时输入 y 回车确认
# 等待下载和安装完成(约 1-2 分钟)
# 激活环境
conda activate langgraph
激活成功后,终端提示符前面会出现 (langgraph),表示当前已进入该环境:
(langgraph) C:\Users\你的用户名>
验证 Python 版本:
python --version
# 输出:Python 3.13.x
3.2 配置 pip 镜像源(中国大陆用户建议)
由于课程依赖包较多,配置国内镜像可以大幅提升下载速度:
# 设置清华大学 pip 镜像源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
验证配置:
pip config list
# 应看到:global.index-url='https://pypi.tuna.tsinghua.edu.cn/simple'
如果后续遇到镜像源不稳定,可随时恢复默认:
pip config unset global.index-url
4. 安装 PyCharm 并创建项目
PyCharm 是 JetBrains 推出的 Python IDE,Community 版免费且功能齐全,本课程全程使用 PyCharm 进行开发和调试。
4.1 下载 PyCharm
打开 PyCharm 官方下载页面:
https://www.jetbrains.com/pycharm/download/
选择 PyCharm Community Edition(免费版),根据操作系统下载对应安装包:
| 操作系统 | 安装包格式 |
|---|---|
| Windows | .exe 安装包 |
| macOS | .dmg 磁盘映像 |
| Linux | .tar.gz 压缩包 |
4.2 安装步骤
Windows:
- 双击下载的
.exe安装包,启动安装向导 - 安装路径建议保持默认(如
C:\Program Files\JetBrains\PyCharm Community Edition) - 关键步骤:在安装选项中勾选:
- ✅ Add "Open Folder as Project"(右键菜单快捷打开项目)
- ✅ Add launchers dir to the PATH(方便命令行启动)
- 其余选项默认即可,点击 Install,等待安装完成
- 安装完成后,勾选 Run PyCharm → Finish,启动 PyCharm
macOS:
- 双击下载的
.dmg文件 - 将 PyCharm 图标拖入 Applications 文件夹
- 从启动台打开 PyCharm
4.3 创建课程项目
-
PyCharm 启动后,点击 New Project(新建项目)
-
在新建项目窗口中配置:
设置项 值 Location 选择一个合适的路径,末尾项目名填写 langgraph,如E:\code\langgraphCreate Git repo ❌ 不勾选(课程不涉及 Git) Interpreter type 选择 Conda(或 Custom environment → Conda) -
Python 版本选 Python 3.13(Conda 会自动创建对应环境)
-
点击 Create,等待 PyCharm 完成项目初始化
创建完成后,PyCharm 生成的空项目结构如下:
langgraph/ ← 项目根目录(PyCharm 自动创建)
├── .idea/ ← PyCharm 配置文件(无需手动修改)
└── main.py ← 空入口文件(可删除或保留练手)
💡 这就是本课程的项目根目录。后续所有
pip install、.env配置、代码文件都在这个目录下进行。.idea/是 PyCharm 自动生成的配置目录,不要手动修改或提交到 Git。
4.4 PyCharm 终端
PyCharm 底部的 Terminal 面板(快捷键:Alt + F12)会自动激活当前项目配置的 Conda 环境,终端的提示符前面会显示 (langgraph):
(langgraph) PS E:\code\langgraph>
👉 后续所有命令(
pip install、python、jupyter等)都建议在 PyCharm 的 Terminal 中执行,无需手动conda activate。
5. 安装课程依赖
课程提供了两个依赖文件,均位于 2.资料/ 目录下:
| 文件 | 说明 | 适用阶段 |
|---|---|---|
requirements.txt |
核心依赖,约 180 个包 | 全部课程 |
requirements_full.txt |
完整依赖,额外包含 Jupyter Notebook 和 langgraph-cli 部署工具 |
Ch 9 及之后 |
5.1 安装核心依赖
# 确保已激活 langgraph 环境
conda activate langgraph
# 进入项目根目录
cd 课程目录/langgraph
# 安装核心依赖
pip install -r 2.资料/requirements.txt
安装过程约 5-10 分钟,会依次下载和安装约 180 个 Python 包。
5.2 安装完整依赖(推荐)
pip install -r 2.资料/requirements_full.txt
相比核心依赖,额外安装:
| 额外包 | 用途 |
|---|---|
jupyter |
在浏览器中运行 .ipynb 笔记本 |
langgraph-cli[inmem] |
LangGraph 本地开发服务器,用于 Ch 9 项目部署 |
5.3 核心包说明
安装完成后,环境中包含以下关键包(无需手动逐个安装):
LangGraph 全家桶:
| 包名 | 用途 |
|---|---|
langgraph |
LangGraph 核心库,构建状态图 |
langgraph-prebuilt |
预置的 Agent 组件 |
langgraph-checkpoint |
检查点(Checkpoint)机制 |
langgraph-checkpoint-postgres |
PostgreSQL 持久化检查点 |
langgraph-sdk |
LangGraph Python SDK |
LangChain 全家桶:
| 包名 | 用途 |
|---|---|
langchain |
LangChain 核心 |
langchain-core |
LangChain 基础抽象 |
langchain-community |
社区集成 |
langchain-openai |
OpenAI 集成 |
langchain-deepseek |
DeepSeek 集成 |
langchain-anthropic |
Anthropic Claude 集成 |
模型服务商 SDK:
| 包名 | 用途 |
|---|---|
openai |
OpenAI 官方 SDK |
anthropic |
Anthropic 官方 SDK |
dashscope |
阿里百炼(Qwen 等模型) |
tencentcloud-sdk-python |
腾讯云 SDK |
其他重要依赖:
| 包名 | 用途 |
|---|---|
fastapi / uvicorn |
Web 服务框架,部署时使用 |
pydantic |
数据校验,状态定义时大量使用 |
httpx / httpx-sse |
HTTP 客户端 + SSE 流式支持 |
loguru |
日志库 |
python-dotenv |
读取 .env 环境变量文件 |
tiktoken |
OpenAI 的 token 计数工具 |
SQLAlchemy / psycopg |
数据库操作,持久化时使用 |
pymilvus |
Milvus 向量数据库客户端 |
6. 配置 API Key
课程示例默认使用 DeepSeek 大模型,至少需要配置 DeepSeek 的 API Key。按实际需要也可以配置其他模型服务商。
6.1 获取 DeepSeek API Key
- 打开 DeepSeek 开放平台:https://platform.deepseek.com/
- 注册 / 登录账号
- 进入 API Keys 页面,点击 创建 API Key
- 复制生成的 key(形如
sk-xxxxxxxx),注意 key 只显示一次,请立即保存
DeepSeek 是新用户注册即送 500 万 tokens 额度,足够学完整套课程。
6.2 创建 .env 文件
在项目根目录(langgraph/)下新建 .env 文件,填入以下内容:
# ============================================
# 必填:课程示例默认使用 DeepSeek 模型
# ============================================
DEEPSEEK_API_KEY=sk-你的DeepSeek_API_Key
# ============================================
# 可选:其他模型服务商(如需切换模型时使用)
# ============================================
# OPENAI_API_KEY=sk-你的OpenAI_API_Key
# ANTHROPIC_API_KEY=sk-ant-你的Anthropic_API_Key
# ============================================
# 部署时需要(Ch 9 项目部署章节)
# ============================================
# LANGSMITH_API_KEY=lsv2-pt-你的LangSmith_API_Key
# LANGSMITH_TRACING=true
# LANGSMITH_ENDPOINT=https://api.smith.langchain.com
# LANGSMITH_PROJECT="langgraph-tutorial"
.env文件已在.gitignore中排除,不会被提交到 Git,可以放心保存密钥。
6.3 代码中如何读取 .env
课程每个 notebook 或示例代码开头都会包含以下两行,自动加载 .env 中的密钥:
from dotenv import load_dotenv
load_dotenv(override=True)
之后创建模型客户端时,SDK 会自动从环境变量中读取对应的 API Key,无需在代码中硬编码:
from langchain_deepseek import ChatDeepSeek
# API Key 自动从 DEEPSEEK_API_KEY 环境变量读取
model = ChatDeepSeek(model="deepseek-v4-flash")
7. Jupyter Notebook 配置
课程中的练习代码以 .ipynb(Jupyter Notebook)格式提供,位于 3.代码/ 目录下。
7.1 启动 Jupyter
# 确保已激活 langgraph 环境
conda activate langgraph
# 进入项目根目录
cd 课程目录/langgraph
# 启动 Jupyter
jupyter notebook
执行后会:
- 终端显示一个带 token 的本地 URL(如
http://localhost:8888/?token=xxx) - 自动打开默认浏览器,进入 Jupyter 文件管理界面
7.2 验证 Kernel
在 Jupyter 界面右上角或菜单栏中确认当前 Kernel 为 Python (langgraph)。如果不是:
- 点击菜单 Kernel → Change kernel → 选择
Python (langgraph)
如果列表中没有
Python (langgraph),说明没有创建对应的名称。在langgraph环境中重新执行:conda activate langgraph pip install ipykernel python -m ipykernel install --user --name langgraph --display-name "Python (langgraph)"
8. 环境验证
全部配置完成后,执行以下验证脚本,确保一切就绪。
8.1 基础验证
在终端中:
# 1. 确认 Conda 版本
conda --version
# 2. 确认 Python 版本和当前环境
python --version
# 输出:Python 3.13.x
# 3. 确认当前环境
conda info --envs
# langgraph 前面应有 * 号表示当前激活
# 4. 确认关键包已安装
pip show langgraph
pip show langchain-core
pip show langchain-deepseek
pip show jupyter
8.2 代码验证
在 Jupyter 中新建一个 Notebook,或创建一个测试脚本 test_env.py:
# test_env.py
print("环境验证开始")
print("=" * 40)
# 1. Python 版本
import sys
print(f"Python 版本: {sys.version}")
# 2. 关键包
from importlib.metadata import version as pkg_version
print(f"LangGraph 版本: {pkg_version('langgraph')}")
import langchain_core
print(f"LangChain Core 版本: {langchain_core.__version__}")
import pydantic
print(f"Pydantic 版本: {pydantic.__version__}")
# 3. .env 加载
from dotenv import load_dotenv
import os
load_dotenv(override=True)
deepseek_key = os.getenv("DEEPSEEK_API_KEY")
if deepseek_key:
print(f"DeepSeek API Key 已配置: {deepseek_key[:8]}...{deepseek_key[-4:]}")
else:
print("⚠️ 未检测到 DEEPSEEK_API_KEY,请在 .env 文件中配置")
# 4. 模型调用测试
if deepseek_key:
from langchain_deepseek import ChatDeepSeek
model = ChatDeepSeek(model="deepseek-v4-flash")
response = model.invoke("回复'一切正常'四个字,不要其他内容")
print(f"模型调用测试: {response.content}")
else:
print("⚠️ 跳过模型调用测试(缺少 API Key)")
print("=" * 40)
print("环境验证完成")
print("=" * 40)
运行脚本:
python test_env.py
如果一切正常,输出类似:
========================================
环境验证开始
========================================
Python 版本: 3.13.14
LangGraph 版本: 1.1.2
LangChain Core 版本: 1.2.18
Pydantic 版本: 2.12.5
DeepSeek API Key 已配置: sk-xxxx...xxxx
模型调用测试: 一切正常
========================================
环境验证完成
========================================
9. 常见问题
Q1: conda 命令找不到?
Windows:确保安装时勾选了"Add Miniconda3 to my PATH"。如果还是找不到,从开始菜单打开 Anaconda Prompt (miniconda3) 即可。
macOS / Linux:执行 ~/miniconda3/bin/conda init 后重启终端。
Q2: PyCharm 中找不到 Conda 环境?
- 打开 File → Settings → Project: langgraph → Python Interpreter
- 点击齿轮 → Add Interpreter → Add Local Interpreter
- 选择 Conda Environment → Use existing environment
- 点击文件夹图标,浏览到 Conda 环境目录:
- Windows:
C:\Users\你的用户名\miniconda3\envs\langgraph - macOS/Linux:
~/miniconda3/envs/langgraph
- Windows:
- 选择
python.exe(或python),点击 OK
如果列表中没有 langgraph 环境,先在 Terminal 中手动创建:
conda create -n langgraph python=3.13
创建后再回到上述步骤手动添加。
Q3: pip install 速度很慢?
配置国内镜像源(见 3.2 节):
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
Q4: 安装依赖时报错 Microsoft Visual C++ 14.0 is required?
部分包(如 unstructured)需要 C++ 编译环境。解决方案:
- 下载 Visual Studio Build Tools:https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022
- 安装时勾选 "Desktop development with C++" 工作负载
- 重新执行
pip install
如果不想装 C++ 工具链,也可以跳过该包的学习章节,不影响核心课程。
Q5: Jupyter 中找不到 Python (langgraph) Kernel?
conda activate langgraph
pip install ipykernel
python -m ipykernel install --user --name langgraph --display-name "Python (langgraph)"
重启 Jupyter 即可看到。
Q6: import langgraph 报 ModuleNotFoundError?
说明包没有正确安装到当前环境:
# 确认当前在 langgraph 环境
conda activate langgraph
# 确认包已安装
pip show langgraph
# 如果未安装,重新安装
pip install -r 2.资料/requirements.txt
Q7: 模型调用时报 AuthenticationError?
检查以下三点:
.env文件中是否配置了正确的 API Key.env文件是否在项目根目录(langgraph/下)- 代码开头是否执行了
load_dotenv(override=True)
Q8: 想把环境恢复到初始状态?
# 退出当前环境
conda deactivate
# 删除并重建
conda remove -n langgraph --all
conda create -n langgraph python=3.13
conda activate langgraph
pip install -r 2.资料/requirements_full.txt
10. 环境配置检查清单
在开始第一章之前,逐项确认:
全部打勾后,就可以进入 01-LangGraph基础入门 开始学习了。
01. LangGraph总览
LangGraph 运行时底层基于自研的 Pregel 运行时,其核心思想借鉴了 Google Pregel 计算模型,用于组织和执行复杂的图计算流程。
1.0. LangGraph和LangChain的版本迭代及定位
LangChain 1.x 的定位发生了明显变化:旧式 Chain、Retriever、Memory 等组件被迁移至 langchain-classic;LCEL 保留为 langchain-core 的底层组合机制,但不再是主要开发入口。
LangChain 1.x 现在聚焦于 Agent 开发,核心入口是 create_agent,围绕 Agent 提供模型、工具、消息、结构化输出、Middleware` 和记忆管理等高层抽象。
LangGraph 是更底层的编排框架与 Agent Runtime,负责复杂工作流和有状态 Agent 的执行,提供持久化、流式输出、Durable Execution、Human-in-the-loop 等运行时能力。create_agent 底层基于 LangGraph` 实现。
二者的定位对比如下:
| 对比维度 | LangChain | LangGraph |
|---|---|---|
| 定位 | Agent 高层开发框架 | 底层编排框架 & Agent Runtime |
| 核心入口 | create_agent |
StateGraph / @entrypoint |
| 适用场景 | 结构直接的 Agent 应用 | 复杂工作流、持久化状态、长时间运行、人工介入 |
| 流程控制 | Agent 循环自动管理 | 精细控制节点、边、条件分支 |
| 学习成本 | 较低 | 较高 |
LangChain提供易于使用的 Agent 高层抽象,LangGraph提供可靠、可持久化且可精细控制的底层执行能力。
对于大多数 Agent 项目,从 LangChain 的 create_agent 开始即可;需要复杂工作流编排、确定性步骤与 Agent 步骤混合、长时间运行或底层状态控制时,再引入 LangGraph。

1.1. 构成图的基本要素
LangGraph 运行时主要由三个基本要素构成:State(状态)、Node(节点) 和 Edge(边)
State(状态):LangGraph 运行过程中的共享数据结构,用于表示应用在某一时刻的状态快照。它承载了图运行所需的上下文信息、中间结果和后续节点需要读取的数据,是节点之间传递信息的核心载体。它与我们在学习 LangChain Agent 时使用的 State 是同一概念。
Node(节点):LangGraph 中的具体执行单元,通常实现为一个函数。节点会读取当前 State,执行相应的业务逻辑,并返回对 State 的局部更新。节点本身并不直接修改全局状态,状态的合并与提交由运行时统一完成。
Edges(边):用于定义节点之间的流转关系,决定一个节点执行完成后下一步应该进入哪个节点。Edge 可以是固定流转,也可以根据当前 State 进行条件判断,从而实现分支、循环等复杂控制流程。
下图展示了一个非常简单的运行图,只有两个计算节点,其拓扑结构为
START --> node_1 --> node_2 --> END
如下图所示

1.2. 图运行过程
LangGraph 的图运行过程基于 Superstep(超步) 来组织和推进。Superstep 可以理解为图运行过程中的一次”单步循环”。一次图运行过程从开始到结束,就是由一系列连续的 Superstep 串联而成。
下文以 node_1执行完毕后的Superstep 为例说明。
每个 Superstep 通常可以分为三个阶段:
- 计划/路由阶段(Plan / Routing):根据当前的 State(状态) 和 Edge(边) 的逻辑,确定本轮超步中应该被执行的节点。

- 执行阶段(Execution):运行本轮被选中的节点。如果本轮有多个节点同时被触发,它们会并行执行。每个节点都会基于本轮开始时的状态快照进行计算,并输出各自对状态的局部更新。在本阶段中,一个节点产生的更新不会立即被其他节点读取到。

- 状态更新/提交阶段(Update / Commit):当本轮所有节点都执行完成后,LangGraph 会将它们的输出统一合并到 State 中,生成新的状态快照。这个新状态会作为下一轮 Superstep 的输入。

完整的运行流程查看 index.html ,双击在浏览器打开即可。
其中和Checkpoint相关的部分,学习相关内容后再看。
1.3. Graph API vs Functional API
LangGraph 提供了两种不同的 API 来构建运行图:Graph API(图式 API) 和 Functional API(函数式 API)。这两种 API 共享相同的底层运行时,可以在同一应用程序中协同使用,但它们针对不同的使用场景和开发偏好而设计。
1.3.1. Graph API
Graph API 采用声明式方式构建工作流。开发者需要显式定义 State、Node 和 Edge,将业务流程组织成一个可视化的图结构。
当流程中存在较复杂的分支、多个节点之间共享状态、并行执行、结果汇聚,或者需要通过图结构帮助调试和团队协作时,更适合使用 Graph API。官方文档也明确建议,在需要复杂流程可视化、显式状态管理、多条件分支、并行路径以及团队协作时,优先选择 Graph API。
总之:
Graph API 更适合构建结构清晰、节点关系复杂、需要长期维护的工作流。
典型场景包括:
| 场景 | 说明 |
|---|---|
| 多节点复杂流程 | 流程中存在多个处理节点,需要清晰表达节点之间的关系 |
| 条件分支较多 | 根据 State 中的不同字段决定后续执行路径 |
| 并行执行与结果汇聚 | 多个节点并行运行,之后汇总结果 |
| 多组件共享状态 | 多个节点都需要读写同一个全局 State |
| 需要图结构展示 | 便于调试、讲解、文档化和团队协作 |
1.3.2. Functional API
Functional API 采用命令式方式构建工作流,更接近普通 Python 函数调用。开发者可以使用 @entrypoint 定义工作流入口,使用 @task 定义可被检查点记录的任务,然后在函数内部使用普通的 if/else、循环和函数调用来组织流程。官方文档指出,当已有过程式代码需要最小改造、流程主要是线性的、分支逻辑较简单、希望快速原型验证时,更适合使用 Functional API。
可以这样理解:
Functional API 更适合在普通 Python 函数流程中,以较低成本接入 LangGraph 的持久化、中断恢复和任务记录能力。
典型场景包括:
| 场景 | 说明 |
|---|---|
| 现有代码改造 | 原本已有函数式或过程式代码,不希望重构成完整图结构 |
| 线性流程 | 主要是 A → B → C 的顺序执行 |
| 简单分支 | 只有少量 if/else 判断 |
| 快速原型验证 | 希望减少样板代码,快速验证业务逻辑 |
| 局部任务持久化 | 希望某些函数作为独立 task 被检查点记录 |
1.3.3. 二者的核心区别
| 对比项 | Graph API | Functional API |
|---|---|---|
| 编程风格 | 声明式图结构 | 命令式函数流程 |
| 核心抽象 | State、Node、Edge | entrypoint、task |
| 状态管理 | 显式定义全局 State | 更多依赖函数参数和返回值 |
| 流程表达 | 通过节点和边表达 | 通过普通 Python 控制流表达 |
| 可视化能力 | 强,天然适合画图和调试 | 弱,更像普通代码流程 |
| 适合场景 | 复杂工作流、多分支、多节点协作 | 简单流程、快速原型、已有代码改造 |
| 学习成本 | 相对更高 | 相对更低 |
1.3.4. 选型建议
从零构建或流程结构复杂,用 Graph API;现有代码改造、快速原型验证或流程逻辑简单,用 Functional API。
学习 LangGraph 建议优先掌握 Graph API。因为后者更能体现 LangGraph 的核心思想:通过 State、Node、Edge 显式描述一个可执行的计算图。
本文介绍 Graph API,对 Functional API 感兴趣的同学自行查阅
https://docs.langchain.com/oss/python/langgraph/functional-api
02. 图的基础构建与运行
本节将从一个基础案例入手,掌握图的基本创建、调用和可视化方法。
图的构建与运行分为三个阶段:定义状态图、编译状态图、调用状态图
2.1. 定义状态图
状态图的定义可以分为以下几个步骤:
1. 定义全局状态 定义整个运行图共享的全局状态(实际上,除了全局状态,LangGraph还支持其它类型的状态,下文详述)
2. 创建状态图 创建 StateGraph实例,将状态和状态图绑定
3. 定义图结构 定义节点和边并添加到状态图中
2.1.1. 定义全局状态
全局状态是 LangGraph 运行图每个节点都可以访问的公共对象。
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
cur_id: str
2.1.2. 创建状态图
builder = StateGraph(state_schema=OverAllState)
2.1.3. 定义图结构
2.1.3.1. 定义图节点
def node_1(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
2.1.3.2. 添加节点
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
2.1.3.3. 添加边
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
2.2. 编译状态图
将上一步得到的状态图编译为可执行的运行图
graph = builder.compile()
2.3. 调用状态图
可以通过invoke调用计算图
print(graph.invoke({"cur_id": "start"}))
2.4. 完整代码
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from operator import add
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
cur_id: str
def node_1(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))
运行结果如下
{'logs': ['node_1 运行完毕', 'node_2 运行完毕'], 'cur_id': 'start, node_1, node_2'}
2.5. 图结构可视化
2.5.1. 绘制mermaid并获取源码
mermaid是一种生成图表和流程图的文本标记语言,它可以把简单的文本描述直接渲染成可视化图形。
通过以下代码可以用mermaid语法表达图结构,并打印mermaid源码
raw_mermaid = graph.get_graph().draw_mermaid()
print(raw_mermaid)
输出如下
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
node_1(node_1)
node_2(node_2)
__end__([<p>__end__</p>]):::last
__start__ --> node_1;
node_1 --> node_2;
node_2 --> __end__;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fcmermaidmermain
输出文本从graph TD;开始的部分为mermaid源码。
2.5.2. 绘制mermaid并转换为png
Langgraph也支持将mermaid转换为图片字节流
png_bytes = graph.get_graph().draw_mermaid_png()
draw_mermaid_png()底层会先调用draw_mermaid()得到mermaid语法的图表代码,然后调用在线服务渲染为mermaid图片。
默认的mermaid在线渲染服务链接为: https://mermaid.ink ,由于网络问题可能存在渲染失败的情况,通常重试即可成功。
可以通过draw_mermaid_png()的base_url参数替换可用的mermaid在线渲染服务,
2.5.2.1. jupyter环境下直接展示图片
在jupyter环境下,可以通过IPython提供的功能将图片字节流直接展示为图片。
from IPython.display import display, Image
png_bytes = graph.get_graph().draw_mermaid_png()
png = Image(png_bytes)
display(png)
运行结果如下所示

2.5.2.2. 保存为图片文件
也可以将图片字节流转换为PNG文件落盘
png_bytes = graph.get_graph().draw_mermaid_png()
png_filename = 'first_demo_graph.png'
with open(png_filename, "wb") as f:
f.write(png_bytes)
运行结束后,代码文件同级目录下将会出现生成好的图片文件,如下图所示。

如果看不到文件,可以刷新上级目录

图片内容如下

2.5.2.3. 快捷用法
在 Jupyter 环境下,可以直接使用 display(graph) 快速展示编译后的图结构:
from IPython.display import display
display(graph)
效果如下

03. 图的状态(State)管理
3.1. 状态定义
状态的定义实际上是在声明状态的Schema,后者是状态字段的完整描述。
官方推荐了三种定义Schema的方式:TypedDict、dataclass、Pydantic
3.1.1. TypedDict
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from operator import add
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
cur_id: str
def node_1(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))
输出
{'logs': ['node_1 运行完毕', 'node_2 运行完毕'], 'cur_id': 'start, node_1, node_2'}
3.1.2. dataclass
属性调用方式由['字段名']变为.字段名。
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from dataclasses import dataclass
from operator import add
@dataclass
class OverAllState:
logs: Annotated[list[str], add]
cur_id: str
def node_1(state: OverAllState) -> OverAllState:
pre_id = state.cur_id
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state.cur_id
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))
输出
{'logs': ['node_1 运行完毕', 'node_2 运行完毕'], 'cur_id': 'start, node_1, node_2'}
3.1.3. Pydantic
Pydantic模型的字段访问方式和dataclass相同。
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from pydantic import BaseModel
from operator import add
class OverAllState(BaseModel):
logs: Annotated[list[str], add]
cur_id: str
def node_1(state: OverAllState) -> OverAllState:
pre_id = state.cur_id
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state.cur_id
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))
输出
{'logs': ['node_1 运行完毕', 'node_2 运行完毕'], 'cur_id': 'start, node_1, node_2'}
3.1.4. 校验行为
学习LangChain的结构化输出时我们提到:Pydantic对格式要求最严格,如果模型返回的内容不符合结构化Schema的要求,则抛出ValidationError。而其余方式都不会对模型的返回结果进行校验,即便模型返回的内容不符合结构化要求,也会原样返回给用户。
而作为LangGraph计算图的状态时,这三种方式都要求字段名称完全一致。只是处理方式不同。具体规则如下
3.1.4.1. 输入字段不匹配
1. TypedDict
TypedDict将输入字段视为字典的Key,不匹配时抛出KeyError异常。
2. dataclass
dataclass将输入字段视为类的属性,不匹配时抛出TypeError(类型错误)异常。
3. Pydantic
Pydantic对输入字段进行校验,不匹配时抛出ValidationError异常。
3.1.4.2. 节点返回字段不匹配
图节点返回的是对于状态的更新,如果返回字段和状态字段不匹配,上述三种Schema定义方式的行为是统一的:状态更新会被忽略。
3.1.5. 推荐用法
在实际使用中,推荐优先使用 TypedDict 定义 LangGraph 状态图的 State Schema。
大多数官方案例也采用 TypedDict 方式定义状态 Schema。这种方式写法简洁、结构清晰,能够直接描述状态中包含哪些字段,以及每个字段对应的数据类型,非常适合用于定义图运行过程中的共享状态。
相比普通 dict,TypedDict 可以提供更明确的字段约束和类型提示;相比 dataclass,TypedDict 更贴近 LangGraph 中状态的更新方式,因为节点通常返回的是表示“部分状态更新”的字典,而不是完整对象;相比 Pydantic BaseModel,它又更加轻量,不会引入额外的数据校验开销。因此,在没有复杂校验需求的情况下,TypedDict 是定义 LangGraph State Schema 的首选方式。
3.2. State Reducer
3.2.1. 什么是State Reducer
State Reducer 是 LangGraph 中用于合并状态更新的核心机制。在 LangGraph 的 StateGraph 中,每个节点可以读取和写入共享状态,而 Reducer 定义了如何将多个节点对同一状态键的更新合并
Reducer 的核心特征:
- 函数签名:
(Value, Value) -> Value,接收当前值和更新值,返回合并后的新值 - 注解定义:通过
Annotated[Type, reducer_function]为状态键指定 Reducer - 默认行为:未指定 Reducer 的状态键使用覆盖策略(Last-Write-Wins)
3.2.2. 如何定义Reducer
3.2.2.1. 定义Reducer函数
Reducer 本质上是一个二元合并函数,用于定义当同一个字段产生多个更新值时,LangGraph 应该如何将这些值合并为一个最终结果。
函数签名:(Value, Value) -> Value
示例代码如下:
def my_reducer(left: list[str], right: list[str]) -> list[str]:
return left + right
left = ['a', 'b']
right = ['c']
print(my_reducer(left, right))
其中,my_reducer 用于处理 list[str] 类型的数据。它接收两个列表参数:
left:当前已累计的状态值;right:本次待合并的新值。
函数内部通过 left + right 将两个列表合并,并返回合并后的结果。
因此,该 Reducer 的作用是:当某个状态字段存在多次列表更新时,将这些列表内容追加合并,而不是直接覆盖原值。
运行结果如下
['a', 'b', 'c']
3.2.2.2. 将Reducer和状态字段关联
在 LangGraph 中,Reducer 通常通过 Python 的 typing.Annotated 与状态字段进行关联。
Annotated[] 是 Python 提供的一种类型注解扩展机制,用于在原始类型之外附加额外的元数据信息。需要注意的是,Annotated[] 本身并不规定这些元数据的具体含义,它只负责在类型注解中保留这些信息。
严格来说,Annotated 的第一个参数是被注解的原始类型,后续参数是附加的元数据。至于这些元数据表示什么、如何解析,则由使用它的框架或工具自行决定。
在 LangGraph 中,框架利用这一机制,将状态字段的类型和 Reducer 规则同时声明在字段定义中。其基本形式如下:
Annotated[Type, reducer_function]
其中:
Type:表示状态字段的数据类型;reducer_function:表示该字段对应的 Reducer 函数。
示例代码如下:
from typing import TypedDict, Annotated
class OverAllState(TypedDict):
logs: Annotated[list[str], my_reducer]
cur_id: str
在上述代码中:
logs字段的类型是list[str];my_reducer是与logs字段关联的 Reducer 函数;- 当多个节点同时更新
logs字段时,LangGraph 会使用my_reducer将多个列表合并; cur_id字段没有指定 Reducer,因此采用默认更新规则。
3.2.2.3. 常用内置Reducer函数
1. operator.add
operator.add 是 Python 内置的加法操作函数,底层由 C 实现
它接收两个参数,等价于 a(第一个参数)+b(第二个参数)
代码如下
from operator import add
print(f"{add(1,2) = }")
print(f"{add([1,2], [3,4]) = }")
print(f"{add(['a','b'], ['c']) = }")
输出如下
add(1,2) = 3
add([1,2], [3,4]) = [1, 2, 3, 4]
add(['a','b'], ['c']) = ['a', 'b', 'c']
2. langgraph.graph.message.add_messages
add_messages 是 LangGraph 中专用于合并消息列表的 Reducer 函数,常用于维护对话历史类的状态字段。其函数签名如下:
def add_messages(
left: Messages,
right: Messages,
*,
format: Literal["langchain-openai"] | None = None,
) -> Messages:
...
return merged
参数说明:
left:状态中已有的消息列表;right:当前节点返回的消息更新值;format:可选参数,用于指定返回消息的格式,通常无需手动设置。
left 与 right 的类型均为 Messages。Messages 可以理解为 LangChain 消息对象的列表,其中每个元素都是 BaseMessage 或其子类的实例,常见子类包括:
HumanMessage:用户的输入消息;AIMessage:AI 的回复消息;SystemMessage:系统提示消息;ToolMessage:工具调用的结果消息。
add_messages 处理的是对话消息序列,而非普通的字符串列表。
BaseMessage 包含一个可选的 id 属性,用于唯一标识一条消息。add_messages 在合并 left 与 right 时,不是简单地执行列表拼接,而是依据消息的 id 进行合并:
- 若
right中的某条消息的id在left中不存在,则将该消息追加到结果列表末尾; - 若
right中的某条消息的id与left中已有消息的id相同,则使用right中的新消息替换left中的旧消息。
因此,add_messages 的作用可以概括为:在保留历史消息的基础上追加新消息,并允许通过相同的消息 id 覆盖已有消息。
需要特别说明,add_messages 并非简单地对 left 与 right 求“并集”。更准确地说,它是一个基于消息 id 的消息列表合并函数:既支持追加新消息,也支持更新已有消息。
可以理解为:
merged = left + right
但若 right 中存在与 left 相同 id 的消息,则最终结果中不会出现重复消息,而是用 right 中的消息覆盖 left 中对应的旧消息。
示例代码如下
from langgraph.graph.message import add_messages
from langchain.messages import HumanMessage, AIMessage, SystemMessage
left = [
SystemMessage(content="你是个善解人意的助手", id='1'),
HumanMessage(content="你好", id='2'),
AIMessage(content="你好~", id='3'),
]
right = [
HumanMessage(content="我是老王,你是小王", id='2'),
AIMessage(content="好的,我记住啦", id='3'),
HumanMessage(content="你是谁?", id='4'),
AIMessage(content="我是小王", id='5'),
]
merged = add_messages(left, right)
for msg in merged:
print(msg)
输出如下
content='你是个善解人意的助手' additional_kwargs={} response_metadata={} id='1'
content='我是老王,你是小王' additional_kwargs={} response_metadata={} id='2'
content='好的,我记住啦' additional_kwargs={} response_metadata={} id='3' tool_calls=[] invalid_tool_calls=[]
content='你是谁?' additional_kwargs={} response_metadata={} id='4'
content='我是小王' additional_kwargs={} response_metadata={} id='5' tool_calls=[] invalid_tool_calls=[]
3.2.3. 默认行为
如果某个 State 字段没有显式定义 Reducer,LangGraph 会使用默认的状态更新行为:后一次更新值会覆盖该字段原有的状态值。
换句话说,当节点返回的更新结果中包含某个字段时,如果该字段没有配置 Reducer,LangGraph 不会对新旧值进行追加、合并或累加,而是直接使用本次返回的新值替换原来的旧值。
示例代码如下:
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class OverAllState(TypedDict):
logs: list[str]
id: str
def node_a(state: OverAllState):
return {
"logs": ["node_a"],
"id": "node_a"
}
def node_b(state: OverAllState):
return {
"logs": ["node_b"],
"id": "node_b"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)
输出如下
============================== -> result <- ==============================
{'logs': ['node_b'], 'id': 'node_b'}
可以看到,logs字段和id字段都没有定义Reducer,因此,节点返回的新值会覆盖初始状态中的旧值,图运行结果中的状态值和最后一次更新保持一致。
3.3. 节点中访问State
3.3.1. 图节点中读取State
在 LangGraph 中,节点本质上是一个可调用对象,通常定义为普通 Python 函数。节点函数被执行时,LangGraph 会自动将当前图运行到该节点时的 State 传入节点函数。
节点函数的第一个参数通常是当前运行图的状态对象,也就是 State。
def node(state: StateSchema):
...
其中,state 表示当前节点执行时可以访问到的全局状态快照。节点可以通过读取 state 中的字段获取上游节点写入的数据,并基于这些数据完成当前节点的业务逻辑。
示例代码如下:
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from operator import add
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
id: str
def node_a(state: OverAllState):
for k, v in state.items():
print(f"k: {k}, v: {v}")
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)
graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
输出如下
k: logs, v: ['START']
k: id, v: start
3.3.2. 图节点更新State
在 LangGraph 中,节点函数通常不需要返回更新后的完整状态,只需要返回本节点对状态的局部更新。
也就是说,节点的返回值可以只包含需要修改的状态字段。
-
对于节点没有返回的字段,LangGraph 会保留其原有状态值;
-
对于节点返回的字段,LangGraph 会根据该字段是否配置了
Reducer来决定如何合并更新值。- 如果字段配置了
Reducer,则使用对应的Reducer函数将旧值和新值合并; - 如果字段没有配置
Reducer,则按照默认规则使用节点返回的新值覆盖原值。
- 如果字段配置了
LangGraph运行时会按照状态字段的Reducer函数将其与当前的最新状态合并。
示例代码如下:
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from operator import add
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
id: str
def node_a(state: OverAllState):
for k, v in state.items():
print(f"k: {k}, v: {v}")
return {
"logs": ["node_a 更新状态"]
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)
graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)
输出如下
k: logs, v: ['START']
k: id, v: start
============================== -> result <- ==============================
{'logs': ['START', 'node_a 更新状态'], 'id': 'start'}
在上述示例中:
logs字段通过Annotated[list[str], add]绑定了 Reducer 函数operator.add,因此 LangGraph 会将原有的logs值和node_a返回的新logs值进行列表拼接:
["START"] + ["node_a 更新状态"]
最终得到:
["START", "node_a 更新状态"]
id字段没有出现在node_a的返回值中,因此该字段不会被更新。图运行结束后,输出状态中的id仍然保持输入时的值:
"id": "start"
因此,LangGraph 节点更新 State 的核心规则可以概括为:
节点只返回需要更新的字段;未返回的字段保持不变;返回的字段根据是否配置 Reducer 决定是合并还是覆盖。
3.3.3. Overwrite绕过Reducer
在前面的示例中,如果某个状态字段定义了 Reducer,那么节点返回该字段的更新值时,LangGraph 默认会通过对应的 Reducer 将新值与已有状态值进行合并。
在某些场景下,我们可能并不希望继续执行 Reducer 的聚合逻辑,而是希望本次更新直接覆盖旧值。这时可以使用 Overwrite。
Overwrite 的作用是:告诉 LangGraph 本次状态更新不走该字段原本定义的 Reducer,而是直接用新值覆盖状态中的旧值。
需要注意的是,Overwrite 只影响当前这一次更新,并不会修改状态字段本身的 Reducer 定义。后续节点如果继续正常返回该字段的更新值,仍然会按照原来的 Reducer 逻辑进行合并。
示例代码如下:
from langgraph.graph import StateGraph, START, END
from langgraph.types import Overwrite
from typing import TypedDict, Annotated
from operator import add
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
id: str
def node_a(state: OverAllState):
return {
"logs": ["node_a"],
"id": "node_a"
}
def node_b(state: OverAllState):
return {
"logs": Overwrite(["node_b"]),
"id": "node_b"
}
def node_c(state: OverAllState):
return {
"logs": ["node_c"],
"id": "node_c"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", "node_c")
builder.add_edge("node_c", END)
graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)
输出如下
============================== -> result <- ==============================
{'logs': ['node_b', 'node_c'], 'id': 'node_c'}
上述代码中:
logs字段绑定了operator.add()函数- 如果没有
Overwrite,则最终输出的logs的值应为['START', 'node_a', 'node_b', 'node_c'] node_b返回更新时,用Overwrite包裹了logs字段的值,那么当前状态的logs会被["node_b"]覆盖,因此最终输出的logs字段值变成了['node_b', 'node_c']
- 如果没有
id字段按照默认行为,保留最后一次更新的值。
3.4. Multi Schema用法
3.4.1. 状态类型
LangGraph 支持在一个图中使用多个状态 Schema,用于区分图的外部输入、外部输出、内部共享状态以及节点间的临时状态。
常见状态类型可以分为以下几类:
- 全局状态 / 内部状态:图内部主要使用的状态,创建
StateGraph时传递给state_schema参数。它通常包含图运行过程中需要读写的大部分字段。 - 输入状态:图对外接收输入时使用的状态,创建
StateGraph时传递给input_schema参数。它用于约束调用图时允许传入哪些字段。 - 输出状态:图最终对外返回结果时使用的状态,创建
StateGraph时传递给output_schema参数。它用于约束图运行结束后只返回哪些字段。 - 私有状态:图内部节点之间传递的临时状态,通常不作为图的输入,也不作为图的最终输出。它可以通过节点函数的入参类型注解声明,并在节点返回值中写入。
需要注意,输入状态和输出状态主要面向图的边界,即“图如何接收外部输入”和“图如何返回外部结果”;而全局状态和私有状态主要面向图内部节点之间的数据传递。
3.4.2. 状态之间的关系
3.4.2.1. 设计规范
本节主要说明 LangGraph 状态设计中的规范。以下规则属于工程上的最佳实践,违反这些规范未必一定导致程序报错,但容易降低代码的可读性和可维护性。
-
输入状态和输出状态通常应是全局状态的子集
输入状态描述图对外需要接收的数据,输出状态描述图最终需要返回的数据。通常情况下,它们都应该是全局状态的一部分。
如:class InputState(TypedDict): username: str class OutputState(TypedDict): graph_output: str class OverAllState(TypedDict): username: str nickname: str graph_output: str其中,
InputState和OutputState的所有字段均存在于OverAllState中。 -
私有状态和全局状态应尽量避免字段重名
私有状态的定位是图内部某些节点之间传递的临时字段。如果私有状态字段和全局状态字段重名,虽然某些情况下程序仍然可以运行,但容易让人误以为该字段是全局共享字段,从而造成理解混乱。
因此,推荐让私有状态字段和全局状态字段保持清晰边界。 -
节点函数应明确声明入参状态类型和返回状态类型
节点函数的第一个参数通常是当前节点可读取的状态。通过类型注解声明该参数,可以明确表达该节点需要读取哪些字段。
同时,给节点函数声明返回状态类型,也可以帮助阅读者理解该节点会更新哪些字段。
例如:def node_1(state: InputState) -> OverAllState: return { "nickname": "Dear " + state["username"] } -
节点函数中不应该访问入参状态类型中不存在的字段
节点实际接收到的状态会按照其入参类型进行裁剪。因此,如果节点入参声明为InputState,就不应该在节点内部访问InputState中不存在的字段。
例如:def node_1(state: InputState) -> OverAllState: return { "nickname": state["username"] }如果在该函数中访问:
state["nickname"]而
nickname不属于InputState,运行时就可能抛出KeyError。 -
节点函数返回的字典应尽量和返回类型注解保持一致
从 Python 类型注解的角度看,函数返回类型只是静态提示,运行时不会自动强制校验。
从 LangGraph 的运行机制看,节点返回的是对状态的部分更新,不是完整状态。只要返回字段已经被图记录为可用状态字段,LangGraph 就可以将其作为状态更新处理。
不过,从工程规范上讲,节点返回字典中的字段最好和函数返回类型注解保持一致,这样更利于阅读、调试和维护。
3.4.2.2. 源码层面的约束
本节从底层机制角度说明 LangGraph 如何记录、裁剪和更新状态。
3.4.2.2.1. 状态的记录
LangGraph 的状态并不是简单保存在一个普通字典中,而是会被拆分成多个可读写的状态字段。每个状态字段在底层通常对应一个 Channel。
这些状态字段会在不同阶段被记录到状态图中。
-
StateGraph记录状态字段的核心方法是_add_schema()
_add_schema()会解析传入的状态 Schema,并将其中声明的字段记录到图中,使这些字段成为图运行时可以读写的状态字段。 -
创建
StateGraph时,会记录state_schema、input_schema和output_schema中的字段
当创建状态图时:builder = StateGraph( OverAllState, input_schema=InputState, output_schema=OutputState )LangGraph 会解析这些 Schema,并将其中涉及的字段加入图的状态管理体系。
-
调用
add_node()添加节点时,也可能记录节点入参声明的状态 Schema
当添加节点时,LangGraph会根据节点函数第一个参数的类型注解推断该节点的输入状态类型。
如果这个输入状态类型之前没有被图记录过,LangGraph 也会通过_add_schema()将其加入图中。
这也是私有状态能够生效的原因。
例如:class PrivateState(TypedDict): greeting: str def node_3(state: PrivateState) -> OutputState: return { "graph_output": state["greeting"] }当
node_3被添加到图中时,PrivateState中的greeting字段会被记录到图中,从而成为图内部可以传递的状态字段。 -
总结
- 全局状态、输入状态、输出状态通常在创建
StateGraph时被记录。 - 私有状态通常在调用
add_node()添加节点时,根据节点入参类型注解被记录。 - 被记录后的状态字段,底层会成为图运行时可以读写的状态字段。
- 全局状态、输入状态、输出状态通常在创建
3.4.2.2.2. 状态的访问
-
调用图时,输入会按照
input_schema进行约束
当调用图时:graph.invoke({"username": "小黄"})如果创建图时声明了
input_schema,那么外部输入会按照input_schema进行约束。
如果没有声明input_schema,则通常按照state_schema作为图的输入 Schema。
因此,input_schema的作用不是“只让第一个节点可见”,而是约束图的外部输入结构。
此处的约束是指:按照schema裁剪输入,只保留schema中出现的状态字段 -
节点接收到的状态会按照节点入参类型进行裁剪
每个节点能读取哪些字段,主要取决于该节点第一个参数的类型注解。
例如:def node_1(state: InputState) -> OverAllState: ...此时,
node_1接收到的state会按照InputState进行裁剪。即使图的全局状态中还有其他字段,node_1也不应该访问不属于InputState的字段。
如果访问了入参状态中不存在的字段,例如:state["nickname"]就可能抛出:
KeyError -
节点返回的是状态更新,而不是完整状态
节点函数不需要返回完整状态,只需要返回本节点想要更新的字段。
例如:def node_1(state: InputState) -> OverAllState: return { "nickname": "Dear " + state["username"] }这里虽然返回类型注解是
OverAllState,但函数实际只返回了nickname一个字段。这是允许的,因为 LangGraph 会把节点返回值视为对状态的部分更新。 -
节点返回值的应用主要由字段名称和图中已记录的状态字段决定
节点返回的字典会根据字段名称写入对应状态字段,并按照该字段的Reducer规则进行合并。
需要注意的是,函数返回类型注解主要用于表达代码意图,不是严格的运行时写入边界。
也就是说,如果某个字段已经被图记录为可用状态字段,那么节点即使没有在返回类型注解中声明该字段,也可能仍然可以返回并更新它。
不过,为了代码清晰,仍然推荐让节点的返回值和返回类型注解保持一致。 -
最终输出会按照
output_schema进行裁剪
图运行完成后,最终返回给外部调用方的结果会按照output_schema进行裁剪。
因此,output_schema的作用不是“只让最后一个节点可见”,而是约束图最终对外暴露哪些字段。
例如,图内部状态中可能同时存在:username nickname greeting graph_output但如果
output_schema只包含:graph_output那么最终
graph.invoke()的返回结果就只会包含graph_output。
3.4.3. 案例
下面通过一个简单案例说明四类状态的定义和使用。
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class InputState(TypedDict):
username: str
class OutputState(TypedDict):
graph_output: str
class OverAllState(TypedDict):
nickname: str
username: str
graph_output: str
class PrivateState(TypedDict):
greeting: str
def node_1(state: InputState) -> OverAllState:
# 向全局状态写入数据
return {
"nickname": "Dear " + state["username"]
}
def node_2(state: OverAllState) -> PrivateState:
# 从全局状态读取数据,写入私有状态
return {
"greeting": state["nickname"] + ", 早上好~"
}
def node_3(state: PrivateState) -> OutputState:
# 从私有状态读取数据,写入输出状态
return {
"graph_output": state["greeting"] + " 很高兴认识你!"
}
builder = StateGraph(OverAllState,input_schema=InputState,output_schema=OutputState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", "node_3")
builder.add_edge("node_3", END)
graph = builder.compile()
print(graph.invoke({"username":"小黄"}))
输出如下
{'graph_output': 'Dear 小黄, 早上好~ 很高兴认识你!'}
3.4.4. 案例执行过程分析
上述案例中,各类状态的作用如下。
1. 输入状态
class InputState(TypedDict):
username: str
InputState 用于约束图的外部输入。
因此调用图时,只需要传入:
{"username": "小黄"}
2. 全局状态
class OverAllState(TypedDict):
username: str
nickname: str
graph_output: str
OverAllState 是图内部主要使用的状态 Schema。
其中:
username来自图的输入;nickname由node_1写入;graph_output由node_3写入,并最终作为图的输出返回。
3. 私有状态
class PrivateState(TypedDict):
greeting: str
PrivateState 用于节点之间传递临时数据。
在本例中:
def node_2(state: OverAllState) -> PrivateState:
return {
"greeting": state["nickname"] + ", 早上好~"
}
node_2 写入了 greeting 字段。
随后:
def node_3(state: PrivateState) -> OutputState:
return {
"graph_output": state["greeting"] + " 很高兴认识你!"
}
node_3 通过 PrivateState 读取 greeting 字段,并生成最终输出。
4. 输出状态
class OutputState(TypedDict):
graph_output: str
OutputState 用于约束图最终返回给外部调用方的数据。
虽然图内部运行过程中还存在 username、nickname、greeting 等字段,但最终结果只返回:
{'graph_output': 'Dear 小黄, 早上好~ 很高兴认识你!'}
这是因为图创建时声明了:
output_schema=OutputState
所以最终输出会按照 OutputState 进行裁剪。
3.5. 预定义状态
3.5.1. MessagesState
LangGraph 构建的计算图通常会和 LLM 结合使用,而 LLM 在运行过程中通常需要维护一组消息列表。为了提升开发效率,LangGraph 官方提供了一个预定义状态类型:langgraph.graph.message.MessagesState。
开发者可以直接继承该状态类型,并在其基础上扩展自定义状态字段。
源码如下:
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
由此可知,MessagesState 只有一个字段:messages。
该字段的类型是列表,元素类型为 AnyMessage;同时,它通过 Annotated 绑定了内置 Reducer 函数 add_messages。
add_messages 的完全限定名(英文全称 fully qualified name)是:
langgraph.graph.message.add_messages,正是上文 3.2.2.3.2 节介绍的内置 Reducer 函数。
示例如下:
from langchain_deepseek import ChatDeepSeek
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langchain.messages import HumanMessage
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model='deepseek-v4-flash',
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(MessagesState):
username: str
output: str
def node_a(state: OverAllState) -> OverAllState:
return {
"messages": [HumanMessage("你好,我是 " + state["username"])]
}
def llm_node(state: OverAllState) -> OverAllState:
res = model.invoke(state["messages"])
return {
"messages": [res],
"output": res.content
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("llm_node", llm_node)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "llm_node")
builder.add_edge("llm_node", END)
graph = builder.compile()
response = graph.invoke({"username": "小黄"})
print(response)
输出如下
{
"messages": [
HumanMessage(
content="你好,我是 小黄",
additional_kwargs={},
response_metadata={},
id="155e1ef4-5bbc-4250-b978-62a7a5918cef",
),
AIMessage(
content="你好呀,小黄!😊 我是DeepSeek,很高兴认识你!有什么我可以帮你的吗?无论是聊天、解答问题、帮你写作、编程,还是其他任何需要,尽管告诉我吧!你名字里的“黄”是哪个黄呀?😄",
additional_kwargs={
"refusal": "None",
},
response_metadata={
"token_usage": {
"completion_tokens": 57,
"prompt_tokens": 10,
"total_tokens": 67,
"completion_tokens_details": "None",
"prompt_tokens_details": {
"audio_tokens": "None",
"cached_tokens": 0,
},
"prompt_cache_hit_tokens": 0,
"prompt_cache_miss_tokens": 10,
},
"model_provider": "deepseek",
"model_name": "deepseek-v4-flash",
"system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
"id": "17d57633-2ffb-4af3-be87-b3d58f9acd2b",
"finish_reason": "stop",
"logprobs": "None",
},
id="lc_run--019e6890-4544-7212-b4f2-aa20fc079911-0",
tool_calls=[],
invalid_tool_calls=[],
usage_metadata={
"input_tokens": 10,
"output_tokens": 57,
"total_tokens": 67,
"input_token_details": {
"cache_read": 0,
},
"output_token_details": {},
},
),
],
"username": "小黄",
"output": "你好呀,小黄!😊 我是DeepSeek,很高兴认识你!有什么我可以帮你的吗?无论是聊天、解答问题、帮你写作、编程,还是其他任何需要,尽管告诉我吧!你名字里的“黄”是哪个黄呀?😄",
}
上述案例中,OverAllState 继承了 MessagesState,因此可用的状态字段为
messages
username
output
只关注 messages 状态,执行流程如下:
node_a返回一条HumanMessage;LangGraph使用add_messages将该消息合并到messages状态字段中;llm_node从state["messages"]中读取完整消息列表,并调用模型;llm_node将模型生成的AIMessage作为状态更新返回;LangGraph再次通过add_messages将AIMessage合并到messages中;- 最终状态中包含完整消息列表。
MessagesState 帮开发者预先定义好了 messages 字段及其合并规则。在构建聊天机器人、Agent、工具调用流程、多轮对话流程时,它可以提升开发效率。
3.5.2. AgentState
AgentState 是 LangChain Agent 内部使用的状态类型。由于 LangChain Agent 底层也是基于 LangGraph 运行图构建的,所以从技术上讲,开发者也可以将 AgentState 或其子类作为自定义 LangGraph 的状态类型。
AgentState 的全类名,也可以称为类的完全限定名(英文全称: fully qualified class name)是:langchain.agents.middleware.types.AgentState
源码如下
class AgentState(TypedDict, Generic[ResponseT]):
"""State schema for the agent."""
messages: Required[Annotated[list[AnyMessage], add_messages]]
jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]
该状态中主要包含三个字段。
- messages
messages: Required[Annotated[list[AnyMessage], add_messages]]
messages 用于存储 Agent 运行过程中的消息列表。
该字段和 MessagesState 中的 messages 字段类似,也使用 add_messages 作为 Reducer。
- jump_to
jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
jump_to 是 LangChain Agent 内部使用的控制字段,主要服务于 Agent 中间件体系。
它通常用于表示运行流程的跳转意图,例如某些中间件希望影响 Agent 后续应该进入哪个节点。
需要注意的是,jump_to 并不是普通 LangGraph 状态图中的通用跳转机制。
在自定义 StateGraph 中,即使状态中定义了 jump_to 字段,LangGraph 也不会因为该字段的值自动跳转到某个节点。普通 LangGraph 运行图如果需要控制后续流向,通常应使用:
Command(goto="node_name")
见下文。
- structured_response
structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]
structured_response 用于存储 Agent 最终生成的结构化输出。
当使用 LangChain Agent 的结构化输出能力时,例如指定 response_format,Agent 最终生成的结构化结果通常会被写入该字段。
其中,OmitFromInput 表示该字段不应作为外部输入字段暴露给调用方,而是由 Agent 运行过程中内部生成。
总体来看,AgentState 是专门为 LangChain Agent 运行时设计的状态类型。
因此,在普通自定义 LangGraph 项目中,一般不建议直接基于 AgentState 扩展图状态。
04. 控制流
4.1. 顺序结构
4.1.1. add_edge
add_edge 用于在两个节点之间添加一条有向边。边是图结构中最基本的元素之一,节点之间的执行顺序、分支跳转以及循环控制,最终都依赖节点和边共同表达。
因此,在 LangGraph 中,基础的控制流结构都可以通过 add_edge 进行构建。
示例如下:
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class OverAllState(TypedDict):
username: str
greeting: str
output: str
def node_a(state: OverAllState) -> OverAllState:
return {
"greeting": "Dear " + state["username"]
}
def node_b(state: OverAllState) -> OverAllState:
return {
"output": state["greeting"] + ",你好!"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile()
res = graph.invoke({"username": "小黄"})
print(res)
from IPython.display import display
display(graph)
输出如下:
{
"username": "小黄",
"greeting": "Dear 小黄",
"output": "Dear 小黄,你好!"
}

在上述示例中,图的执行流程如下:
START -> node_a -> node_b -> END
其中:
START -> node_a表示图从node_a开始执行;node_a -> node_b表示node_a执行完成后,继续执行node_b;node_b -> END表示node_b执行完成后,图进入结束状态。
4.1.2. add_sequence
如果需要构建一组按顺序执行的节点,也可以使用 add_sequence。
add_sequence 支持传入一个可执行对象列表。LangGraph 会按照列表顺序依次添加节点,并在相邻节点之间自动添加边。默认情况下,函数名会被用作节点名称。
示例如下:
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class OverAllState(TypedDict):
username: str
greeting: str
output: str
def node_a(state: OverAllState) -> OverAllState:
return {
"greeting": "Dear " + state["username"]
}
def node_b(state: OverAllState) -> OverAllState:
return {
"output": state["greeting"] + ",你好!"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_edge(START, "node_a")
builder.add_sequence([node_a, node_b])
builder.add_edge("node_b", END)
graph = builder.compile()
res = graph.invoke({"username": "小黄"})
print(res)
from IPython.display import display
display(graph)
输出如下
{
"username": "小黄",
"greeting": "Dear 小黄",
"output": "Dear 小黄,你好!"
}

上述代码中:
builder.add_sequence([node_a, node_b])
大致等价于:
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge("node_a", "node_b")
因此,完整执行流程仍然是:
START -> node_a -> node_b -> END
相比手动调用多次 add_node 和 add_edge,add_sequence 更适合用于构建简单的线性执行流程。
4.1.3. 省略指向 END 的边
在 LangGraph 中,图的终止并不完全依赖某个真实执行的特殊节点。
从运行机制上看,LangGraph 会在每个 SuperStep 开始时,根据当前发生更新的 Channel 以及节点之间的触发关系,计算本轮需要执行的任务列表。如果没有新的节点被激活,也就没有新的任务需要执行,图运行自然结束。
需要注意的是,END 并不是运行阶段真正执行的节点。也就是说,指向 END 的边不会像指向普通节点的边那样,触发一个真实的节点任务。它更多用于表达图结构中的“终止语义”:当前路径执行到这里即可结束。
因此,在一些简单的线性流程中,即使省略指向 END 的边,最后一个节点执行完成后,如果没有后续节点被触发,图也可以正常结束。
示例如下:
from langgraph.graph import StateGraph, START
from typing import TypedDict
class OverAllState(TypedDict):
username: str
greeting: str
output: str
def node_a(state: OverAllState) -> OverAllState:
return {
"greeting": "Dear " + state["username"]
}
def node_b(state: OverAllState) -> OverAllState:
return {
"output": state["greeting"] + ",你好!"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
graph = builder.compile()
res = graph.invoke({"username": "小黄"})
print(res)
from IPython.display import display
display(graph)
输出如下
{
"username": "小黄",
"greeting": "Dear 小黄",
"output": "Dear 小黄,你好!"
}

在这个例子中,虽然没有显式添加:
builder.add_edge("node_b", END)
但是 node_b 执行完成后,没有新的节点被触发,图运行会自然结束。
不过,可以省略指向 END 的边,并不表示 END 没有意义。
显式添加 END 边可以让图结构更加完整,也能更清晰地表达“流程到此结束”的语义。尤其是在分支、条件跳转、循环退出等场景中,显式指向 END 通常更利于阅读和维护。
和 END 不同,START 通常不能省略。因为 START 不只是一个语义上的起点标记,它还用于告诉 LangGraph:图运行时应当从哪些节点开始执行。也就是说,下面这条边是启动图执行的关键:
builder.add_edge(START, "node_a")
如果没有从 START 出发的边,或者没有通过其他方式指定入口节点,LangGraph 就无法确定图的初始执行节点。
因此,在实际开发中,建议保留指向 END 的边。虽然在某些简单流程中省略这些边也能正常运行,但显式添加它们,可以让图结构更加完整、语义更加清晰,便于后续维护。
4.2. 分支结构
4.2.1. 静态分支(Static Branch)
- 定义:节点的下游候选节点 在图编译阶段就完全确定,只是运行时根据条件选择哪条边执行。
- 特点:
- 下游节点集合固定,数量、目标在编译时确定
- 运行时可选择一个或多个下游目标
- 可以用来做条件分支,但不生成新的节点
⚡ 核心判断:
编译期知道下游集合 → 静态分支
4.2.1.1. 并行节点
并行节点是最简单的静态分支形式。
当多个节点都从同一个上游节点触发时,它们会在同一个超步被激活。典型写法如下:
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
示例:两个节点并行执行
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
from langchain_deepseek import ChatDeepSeek
from langchain.messages import HumanMessage
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke(
[
HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")
]
).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke(
[
HumanMessage(f"写一个关于 {state['topic']} 的笑话")
]
).content
return {
"joke": joke
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
graph = builder.compile()
res = graph.invoke({"topic": "猫咪"})
print(res)
from IPython.display import display
display(graph)
上述案例中,node_a 和 node_b 都由 START 触发。图运行时,二者会在同一个超步中被调度,它们各自读取当前状态并独立执行。
需要注意的是:
- 这里的“并行”主要指调度语义上的并行;两个节点之间没有先后依赖;
- 它们的输出会在当前超步执行完成后统一合并到状态中;
- 如果两个节点写入同一个状态字段,则该字段通常需要配置合适的
Reducer,否则可能出现状态更新冲突,抛出InvalidUpdateError异常
输出如下
{
"topic": "猫咪",
"poem": "《猫咪》\n夜巡檐角步轻悄,昼卧花阴晒暖毛。\n偶逐流莺穿柳过,忽惊蝴蝶上眉梢。\n\n赏析:这首作品以猫咪日常为切入点,通过“夜巡檐角”、“昼卧花阴”展现其昼伏夜出的习性,“逐流莺”、“惊蝴蝶”则捕捉其灵动瞬间。全诗未著“猫”字,却以“轻悄步”、“晒暖毛”等细节勾勒出猫咪的形神特征,尾句“上眉梢”更将好奇神态融入蝴蝶意象,物我交融,余韵悠长。",
"joke": "有一只猫走进了一家披萨店,店员问:“您好,请问您要什么?”\n猫淡定地说:“喵~(我要一个12寸的海鲜披萨,加双倍芝士。)”\n店员愣了愣,说:“猫先生,您确定?”\n猫点点头,店员只好照做,然后猫掏出钱包付了钱,趴在小桌子上优雅地吃了起来。\n\n这时,旁边桌的客人看呆了,忍不住问店员:“它怎么能像人一样点餐、付钱、吃披萨?”\n店员耸耸肩,小声说:“我也不知道,但自从它上次用我的电脑打了一局《猫里奥》通关后,我就没敢拒绝它的任何要求。”"
}

4.2.1.2. 条件分支
StateGraph 提供了 add_conditional_edges 方法,用于从某个上游节点出发,根据运行时状态选择下游节点。
方法签名如下
def add_conditional_edges(
self,
source: str,
path: Callable[..., Hashable | Sequence[Hashable]]
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
| Runnable[Any, Hashable | Sequence[Hashable]],
path_map: dict[Hashable, str] | list[str] | None = None,
) -> Self:
不考虑 self,核心参数有三个:
source:条件分支的起始节点;path:路由规则,是一个可执行对象,通常是函数path_map:路由规则的返回值到真实节点名之间的映射关系。
其中,path 的返回值表示跳转的目标节点,可以是:
- 字符串或特殊对象
END表示的单个目标; - 字符串或特殊对象
END表示的多个目标组成的序列;
path_map
-
可以省略,即取默认值
None,此时path返回值中出现的字符串必须是合法的节点名称。 -
可以是字典,维护
path返回值和真实节点的映射。 -
也可以是列表,如下
path_map=["node_a", "node_b", "node_c"]相当于
path_map={ "node_a": "node_a", "node_b": "node_b", "node_c": "node_c", }此时也要求
path返回值中出现的字符串必须是合法的节点名称。
4.2.1.2.1. 不使用 path_map
如果不传 path_map,那么路由函数的返回值通常应当直接是图中的节点名称。如下
def router(state: OverAllState) -> Literal["node_a", "node_b"]:
if "诗" in state["content_type"]:
return "node_a"
return "node_b"
此时,router 返回的 "node_a" 和 "node_b" 必须能够直接对应图中已经注册的节点名。
完整案例如下
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
content_type: str
poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content
return {
"joke": joke
}
def router(state: OverAllState) -> Literal["node_a", "node_b"]:
if "诗" in state["content_type"]:
return "node_a"
return "node_b"
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_conditional_edges(START, router)
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
graph = builder.compile()
poem_res = graph.invoke({"topic": "布偶狗", "content_type": "诗"})
joke_res = graph.invoke({"topic": "布偶狗", "content_type": "笑话"})
print('=' * 30, '-> poem_res <-', '=' * 30)
print(poem_res)
print('=' * 30, '-> joke_res <-', '=' * 30)
print(joke_res)
from IPython.display import display
display(graph)
输出如下
============================== -> poem_res <- ==============================
{
"topic": "布偶狗",
"content_type": "诗",
"poem": "《咏布偶狗》\n绒身静卧似憨眠,无骨何妨守户前。\n不吠晨昏随主步,垂髫怜抱共跚连。\n\n注:我的诗中“无骨”暗喻布偶狗没有生命骨架,却依然守护着家门;“跚连”形容孩童与布偶狗相偕学步的蹒跚之态。通过“绒身”“垂髫”等意象,既保留了布偶狗的柔软特质,又赋予其超越玩偶的温情守护之意,在静态与动态的转换间实现了拟人化的诗意升华。"
}
============================== -> joke_res <- ==============================
{
"topic": "布偶狗",
"content_type": "笑话",
"joke": "有一天,一只布偶狗在街上闲逛,遇到了一只真狗。\n\n真狗好奇地问:“你是什么品种?怎么看起来毛这么长、这么滑,眼神还这么呆?”\n\n布偶狗骄傲地挺起胸膛:“我可是最顶级的仿真布偶狗!纯手工缝制,水晶眼睛,进口棉花填充,咬不坏、摔不烂,还不用溜。”\n\n真狗愣了一下,问:“那你平时都干嘛?”\n\n布偶狗叹了口气:“看家呗……主人说我最大的优点就是——丢不了,谁捡了都想还回来,因为太假了,连肉都不香。”"
}

观察图结构可以发现,__start__指向node_a和node_b的线是虚线,这表示节点的跳转是有条件的,不是固定的。
4.2.1.2.2. 使用 path_map
如果不希望路由函数直接返回节点名,而是返回业务语义更强的标识,可以使用 path_map 进行映射。如下:
def router(state: OverAllState) -> Literal["a", "b"]:
if "诗" in state["content_type"]:
return "a"
return "b"
builder.add_conditional_edges(
START,
router,
path_map={
"a": "node_a",
"b": "node_b",
}
)
此时返回的 "a" 和 "b" 可以不是图中已注册的节点名称,但要通过 path_map 映射到正确的节点
这种写法的好处是:
- 路由函数可以返回业务含义更清晰的标签;
- 图节点名称可以保持工程化命名;
- 渲染图结构时,边上可以显示路由标签,使图更容易理解。
示例如下
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
content_type: str
poem: str
ci_poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content
return {
"joke": joke
}
def router(state: OverAllState) -> Literal["a", "b"]:
if "诗" in state["content_type"]:
return "a"
return "b"
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_conditional_edges(
START,
router,
path_map={
"a": "node_a",
"b": "node_b",
}
)
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
graph = builder.compile()
poem_res = graph.invoke({"topic": "布偶狗", "content_type": "诗"})
joke_res = graph.invoke({"topic": "布偶狗", "content_type": "笑话"})
print('=' * 30, '-> poem_res <-', '=' * 30)
print(poem_res)
print('=' * 30, '-> joke_res <-', '=' * 30)
print(joke_res)
from IPython.display import display
display(graph)
输出如下
============================== -> poem_res <- ==============================
{
"topic": "布偶狗",
"content_type": "诗",
"poem": "《布偶狗》\n布偶妆成似犬形,绒毛细软眼如星。\n不吠不跳乖巧坐,疑是仙童降画屏。\n\n注:我的仿写创作思路是以玩具布偶狗为意象,通过“妆成似犬形”点明其拟态特征。后三句运用比喻(眼如星)、拟人(乖巧坐)、想象(仙童降画屏)手法,赋予静态玩偶动态神韵。末句以“仙童”暗喻其灵动气质,呼应首句的“布偶”属性,使全诗在虚实之间形成童趣与仙气的张力。"
}
============================== -> joke_res <- ==============================
{
"topic": "布偶狗",
"content_type": "笑话",
"joke": "好的,这里有一个关于布偶狗的笑话:\n\n有一只布偶狗,它从来不叫,也从来不跑。它的主人很担心,就带它去看兽医。\n\n兽医检查了半天,推了推眼镜说:“你这狗啊,没别的毛病,就是太乖了,乖到连狗的身份都忘了。你知道它为什么这么安静吗?”\n\n主人摇摇头。\n\n兽医叹了口气,说:“因为它是个‘布偶’,没有嘴,也没有腿。它唯一的技能,就是等你给它配个‘程序’,然后坐在那里,假装自己是一只不会乱咬东西的电子狗。说真的,这笑话比它还冷。”"
}

观察图结构可以发现,__start__指向node_a和node_b的虚线上出现了文本a和b,它们是路由函数返回的名称,通过path_map映射到具体的下游节点。
4.2.1.2.3. 同时路由至多个节点
add_conditional_edges 也支持一次路由到多个下游节点。
1. 不用path_map
示例如下
from collections.abc import Sequence
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
content_type: str
poem: str
ci_poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content
return {
"joke": joke
}
def node_c(state: OverAllState) -> OverAllState:
ci_poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的词")]).content
return {
"ci_poem": ci_poem
}
def router(state: OverAllState) -> Sequence[Literal["node_a", "node_b", "node_c"]]:
if "诗" in state["content_type"]:
return ["node_a", "node_c"]
return ["node_b", "node_c"]
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_conditional_edges(
START,
router
)
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
builder.add_edge("node_c", END)
graph = builder.compile()
poem_res = graph.invoke({"topic": "布偶狗", "content_type": "诗"})
joke_res = graph.invoke({"topic": "布偶狗", "content_type": "笑话"})
print('=' * 30, '-> poem_res <-', '=' * 30)
print(poem_res)
print('=' * 30, '-> joke_res <-', '=' * 30)
print(joke_res)
from IPython.display import display
display(graph)
在这个例子中:
- 当
content_type包含"诗"时,同时触发node_a和node_c; - 否则同时触发
node_b和node_c。
输出如下
============================== -> poem_res <- ==============================
{
"topic": "布偶狗",
"content_type": "诗",
"poem": "《咏布偶狗》\n铁骨棉裘本异俦,垂头摇尾学温柔。\n非关皮相失真性,恐向人间惹旧愁。\n\n赏析:这首作品以布偶狗为题,通过“铁骨棉裘”的悖论意象,巧妙揭示材质与形态的错位。后两句笔锋一转,从拟态之“学温柔”深入至本真之“失真性”,结句“恐向人间惹旧愁”更以物喻人,赋予玩偶以灵性,暗含对世情虚伪的讽喻,余韵悠长。",
"ci_poem": "《捣练子·布偶狗》\n针线巧,布绒柔。静卧妆台伴月幽。未解铃铛摇夜语,却随清梦上云舟。\n\n注:我的仿作将布偶狗拟人化,通过“针线巧,布绒柔”展现其手工质感,“伴月幽”营造静谧氛围。末句“随清梦上云舟”暗合现代人渴望逃离现实的隐喻,既保留古典词牌的意境美,又赋予布偶狗超越物象的情感寄托。"
}
============================== -> joke_res <- ==============================
{
"topic": "布偶狗",
"content_type": "笑话",
"ci_poem": "《鹧鸪天·布偶狗》\n绒目含星尾作虹,垂蹄静卧笑春风。\n无人自戏绒球转,得主频摇铃铎浓。\n\n偎暖日,扑帘栊。娇憨总在爪痕中。\n痴心不解浮生事,一抱虚温万事空。\n\n注:本词以布偶狗为意象,通过“绒目含星”、“垂蹄静卧”等细节摹写其柔顺之态。下阕“偎暖日”、“扑帘栊”拟人化笔法,寄寓物我相忘之趣。结句“一抱虚温”暗喻繁华终归寂灭之理,将玩偶之趣升华为对生命本真的哲思。",
"joke": "小明带他的布偶狗去宠物医院,医生检查后说:“你这狗没心跳没呼吸,已经死了。”\n小明急了:“不可能!它刚刚还对我摇尾巴!”\n医生叹了口气:“那是你兜里的钥匙串,走起路来叮当响,它尾巴上的铃铛在共振。”\n小明沉默片刻,掏出手机拍了张照发朋友圈:“我的狗死了,但它的尾巴还在听我走路。”\n评论区炸了:“建议主人也去查查脑子,可能也共振坏了。”"
}

观察图结构可以发现,node_a、node_b和node_c独立于图结构之外。
和上一节案例相比,router函数返回的是序列而非单个节点,渲染器无法推断节点间的映射关系。
2. 添加映射
通过 path_map 显示声明映射关系,明确下游节点集合,在提升代码可读性的同时,也有助于渲染器正确展示条件边。
当前场景下 path 返回值中的字符串就是合法的节点名称,path_map 可以是字典
path_map={
"node_a": "node_a",
"node_b": "node_b",
"node_c": "node_c",
}
也可以是列表
path_map=["node_a", "node_b", "node_c"]
完整案例如下
from collections.abc import Sequence
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
content_type: str
poem: str
ci_poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content
return {
"joke": joke
}
def node_c(state: OverAllState) -> OverAllState:
ci_poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的词")]).content
return {
"ci_poem": ci_poem
}
def router(state: OverAllState) -> Sequence[Literal["node_a", "node_b", "node_c"]]:
if "诗" in state["content_type"]:
return ["node_a", "node_c"]
return ["node_b", "node_c"]
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_conditional_edges(
START,
router,
path_map={
"node_a": "node_a",
"node_b": "node_b",
"node_c": "node_c",
}
# path_map=["node_a", "node_b", "node_c"]
)
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
builder.add_edge("node_c", END)
graph = builder.compile()
poem_res = graph.invoke({"topic": "布偶狗", "content_type": "诗"})
joke_res = graph.invoke({"topic": "布偶狗", "content_type": "笑话"})
print('=' * 30, '-> poem_res <-', '=' * 30)
print(poem_res)
print('=' * 30, '-> joke_res <-', '=' * 30)
print(joke_res)
from IPython.display import display
display(graph)
输出如下
============================== -> poem_res <- ==============================
{
"topic": "布偶狗",
"content_type": "诗",
"poem": "《布偶狗》\n布偶憨然卧锦茵,牵绳随主步香尘。\n偶因客至微昂首,原是无声座上宾。\n\n注:我的创作思路是抓住布偶狗静中有动的特质。首句以“憨然卧锦茵”勾勒其静态萌态,次句“牵绳随主步香尘”暗喻宠物与人的陪伴关系。转句“偶因客至微昂首”捕捉其反应,既保留布偶的拟真特性,又暗示机械性动作。结句“原是无声座上宾”点明其非生物属性,与开篇“布偶”形成闭环。全诗通过动静、真假的微妙转化,赋予静物以灵性。",
"ci_poem": "《醉花阴·布偶狗》\n绒耳垂垂星作眸,憨卧小窗幽。棉絮裹憨态,线描眉眼,缝就三分柔。\n旧尘不染春衫袖,伴我度春秋。虽无温舌语,月斜人静,也解倚床头。\n\n注:我以拟人笔法赋予布偶狗灵性,通过“绒耳垂垂”“棉絮裹憨态”等细节勾勒其柔软形态,又于“月斜人静”处暗藏陪伴深情。下阕“旧尘不染”暗喻其纯净守护,末句“解倚床头”更将静物写活,道出玩偶与主人之间无声的慰藉。"
}
============================== -> joke_res <- ==============================
{
"topic": "布偶狗",
"content_type": "笑话",
"ci_poem": "《南歌子·布偶狗》\n绒耳垂春水,圆睛印月痕。\n棉絮缝成憨态真。\n不向朱门摇尾,卧蓬门。\n\n旧线牵云袖,新绒补雪身。\n线头散作泪斑纹。\n但把残年缝进,掌中温。\n\n注:我的布偶狗,是外婆用旧棉裤的棉花和二姐穿小的灯芯绒外套一针一线缝成的。二十年过去,棉絮从它破了洞的耳朵里露出,像吐着舌头。我模仿陆游“我与狸奴不出门”的意境,通过“棉絮缝春水”喻指时光在针脚里流逝,而“线头泪斑”则是故意缝补的裂痕——我们总要在破碎的事物里,重新认出圆满的形状。",
"joke": "好的,这是一个关于布偶狗的笑话:\n\n有一只布偶狗,它特别特别懒,懒到什么程度呢?它的主人叫它去捡飞盘,它都懒得动。主人决定训练它,就扔出一个飞盘,大声喊:“布偶,快去捡!”\n\n布偶狗懒洋洋地抬头看了一眼飞盘落在远处,然后又趴下了。主人很生气,又扔了一个,它还是不理。\n\n主人无奈,只好采取了终极手段——假扮成一只会动的布偶狗,自己冲过去把飞盘叼了回来。\n\n布偶狗一看,浑身毛发都惊得炸了起来,它对旁边的一只小花狗低声说:“天哪,你看,那玩意儿居然能自己动!它还是个永动机!吓死狗了,好赖哦!”"
}

如图所示,图结构被正确渲染。
4.2.1.3. defer node execution
某些情况下,我们希望在所有常规任务节点执行完毕后,再进行日志、审计等收尾工作
此时可以在添加节点时设置defer=True,如下:
builder.add_node("audit_node", audit_node, defer=True)
defer=True 的含义是:
当前节点不会在其被触发后立即执行,而是被延迟到常规图运行流程结束后,再在额外的超步中触发执行。
这类节点适合用于:
- 日志记录;
- 审计检查;
- 结果汇总;
- 收尾清理;
- 统一校验前面节点是否已完成。
4.2.1.3.1. 底层实现机制

1. 编译阶段:使用特殊 Channel
-
LangGraph在编译状态图时,会为边创建对应的Channel。 -
此时会根据节点的
defer属性创建不同类型的Channel,如下。self.channels[branch_channel] = ( LastValueAfterFinish(Any) if node.defer else EphemeralValue(Any, guard=False) )defer默认值为False对于普通节点,边对应的通道类型是
EphemeralValue,可以理解为普通临时通道;而对于defer=True的节点,边对应的通道类型是特殊的LastValueAfterFinish。
2. 常规运行阶段-写入但不触发
- 在图运行过程中,每个节点执行完成后,会向其下游边对应的
Channel写入数据。 - 常规的
Channel在运行开始后处于可用状态,被写入后记录在updated_channels列表中,从而在下一个超步中触发下游节点的执行。 - 但是,
LastValueAfterFinish类型的通道起初是不可用的,首次被写入时不会添加到updated_channels列表中,下游节点自然不会被触发。
3. 常规流程结束后:调用finish()唤醒延迟节点
LangGraph底层用trigger_to_nodes维护了 边的Channel-> 节点 的映射,是一个字典。- 在每个超步结束后,运行时会根据
updated_channels判断是否还有新的节点需要被触发。 - 如果
updated_channels和trigger_to_nodes的 key 没有交集,说明当前没有新的普通节点需要继续执行,常规运行流程已结束。 - 此时,
LangGraph运行时会调用所有Channel的finish()方法。 - 对于普通
Channel,finish()通常不会产生新的触发效果;但对于LastValueAfterFinish类型通道,首次调用finish()时,会将内部的finished标记设置为True,并返回True。 - 一旦
finished=True,该通道的is_available()就会变为True。 - 于是,原本被延迟的通道会被加入
updated_channels,从而在额外的超步中触发对应的defer节点。
4. 总结
因此,defer=True 的运行机制可以概括为:
触发边的
Channel为特殊类型,首次写入不触发;常规流程结束后,特殊通道finish();通道变为可用;从而触发延迟节点,后者在额外超步中执行。
4.2.1.3.2. 案例
示例如下
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from loguru import logger
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content
return {
"joke": joke
}
def audit_node(state: OverAllState) -> OverAllState:
logger.info(f"任务节点全部执行完毕,诗 {'已生成' if state['poem'] else '未生成'},笑话 {'已生成' if state['joke'] else '未生成'}")
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("audit_node", audit_node, defer=True)
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
builder.add_edge(START, "audit_node")
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
builder.add_edge("audit_node", END)
graph = builder.compile()
res = graph.invoke({"topic": "布偶狗"})
print(res)
from IPython.display import display
display(graph)
在这个图中:
node_a和node_b是普通节点,在常规流程中被触发;audit_node虽然也由START触发,但由于设置了defer=True,不会立即执行;node_a和node_b都执行完成后,常规运行流程结束,audit_node才会在额外的超步中执行;- 因此,
audit_node中可以读取到node_a和node_b已经写入并提交后的状态。
输出如下
2026-05-28 17:07:09.618 | INFO | __main__:audit_node:40 - 任务节点全部执行完毕,诗 已生成,笑话 已生成
{
'topic': '布偶狗',
'poem': '《咏布偶狗》\n'
'绸身棉腑自憨娇,静卧无眠守寂寥。\n'
'不吠不嗔街巷事,偎人一晌解无聊。\n'
'\n'
'注:我的创作思路是通过拟人化手法,赋予布偶狗以温柔静默的品格。'
'首句以“绸身棉腑”点明材质,次句“守寂寥”暗合宠物陪伴的特质。'
'后两句通过“不吠不嗔”与“解无聊”的对比,凸显布偶狗无声胜有声的治愈力量,'
'在机械玩物中注入人文温度。',
'joke': '这是一个关于布偶狗的笑话:\n'
'\n'
'有一只布偶狗,它什么都好,毛茸茸的,蓝眼睛,性格温顺,'
'唯一的缺点就是——它是个布偶,不会动。\n'
'\n'
'它的主人每天都把它放在沙发上,假装它在看电视、在思考狗生。\n'
'\n'
'有一天,主人带了一只真的活狗回家,想让布偶狗有个伴。'
'活狗非常活泼,对着布偶狗又叫又摇尾巴,但布偶狗一动不动。\n'
'\n'
'活狗很奇怪,绕着布偶狗转了三圈,然后凑上去闻了闻。\n'
'\n'
'最后,活狗叹了口气,对主人说:\n'
'\n'
'“主人,这只狗……是不是在用‘静默模式’跟我交流?'
'它的蓝牙是不是断了?”'
}

4.2.1.3.3. 小结
defer=True 适合“最后执行”的收尾节点。
它的本质是通过特殊的 Channel 控制触发时机:
- 编译阶段为延迟节点创建
LastValueAfterFinish类型通道; - 常规运行阶段该通道可以被写入,但不可用,不会触发下游节点;
- 常规流程结束后调用通道的
finish()方法; finish()首次生效后将通道标记为可用;- 延迟节点在额外超步中被触发执行。
因此,
defer=True将节点的触发信号延迟释放,使该节点在常规流程结束后执行。
4.2.2. 动态分支(Dynamic Branch)
-
定义:节点的后续执行路径在 运行时 才确定,可以根据当前状态、输入数据或中间结果,动态决定要触发哪些下游任务或跳转到哪个下游节点。
-
特点:
- 下游执行目标可以在运行时选择;
- 下游任务数量可以在运行时决定;
- 可以为同一个下游节点动态创建多个执行任务;
- 适合实现 Map-Reduce 式动态扇出、多任务并行处理、运行时条件跳转等场景。
需要注意的是,所谓“动态”通常不是指运行时临时创建新的节点定义。节点本身一般仍需要在图编译前注册。动态性主要体现在:运行时决定触发哪些节点,以及为这些节点创建多少个执行任务。
-
典型用法:
Send(动态扇出任务/数据)Command(goto=...)(运行时跳转到下游节点)
⚡ 核心判断:
运行时决定后续执行目标或任务数量 → 动态分支
4.2.2.1. 并行节点
Send 结合 add_conditional_edges() 使用,可以用于动态扇出任务
所谓扇出(Fan-out),是指一个上游节点像扇子一样,向外分发出多个下游任务。例如,根据一个主题,同时生成诗、词、笑话三个任务;又如,根据一个列表,为列表中的每个元素动态启动一个处理任务。
具体来说,路由函数可以返回一个 Send 实例序列。每个 Send 实例都描述了一次独立的任务分发:
- 分发到哪个下游节点;
- 给这个下游节点传入什么私有状态。
运行时,LangGraph 会根据返回的每个 Send 实例创建对应的任务。这些任务通常会在同一个超步中并行执行。
Send 类构造器源码如下
def __init__(self, /, node: str, arg: Any) -> None:
"""
Initialize a new instance of the `Send` class.
Args:
node: The name of the target node to send the message to.
arg: The state or message to send to the target node.
"""
self.node = node
self.arg = arg
该构造器接收两个参数,并记录为实例属性:
- node:待启动的下游节点名称,必须是已在状态图中注册的合法名称
- arg:传递给下游节点的信息,仅对
node指向的节点可见,通常应是私有状态。每个Send实例接收到的arg是独立的,互不相干。
实例如下
from typing import TypedDict, Literal
from collections.abc import Sequence
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
CONTENT_TYPES = ["poem", "ci_poem", "joke"]
model = ChatDeepSeek(
model = "deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
poem: str
ci_poem: str
joke: str
class WorkerState(TypedDict):
"""
私有状态,只对 Worker 节点可用
"""
content_type: Literal["poem", "ci_poem", "joke"]
prompt: str
class InputState(TypedDict):
topic: str
class OutputState(TypedDict):
poem: str
ci_poem: str
joke: str
def worker_node(state: WorkerState) -> OutputState:
content_type = state["content_type"]
prompt = state["prompt"]
content = model.invoke([HumanMessage(prompt)]).content
return {
content_type: content
}
def router(state: InputState) -> Sequence[Send]:
prompt = "请生成关于 {} 的 {}"
english2chinese = {
"poem": "一首诗",
"ci_poem": "一首词",
"joke": "一个笑话"
}
topic = state["topic"]
return [Send(
"worker_node",
{
"content_type": content_type,
"prompt": prompt.format(topic, english2chinese[content_type]),
}
) for content_type in CONTENT_TYPES
]
builder = StateGraph(state_schema=OverAllState, input_schema=InputState, output_schema=OutputState)
builder.add_node("worker_node", worker_node)
builder.add_conditional_edges(
START,
router,
path_map=["worker_node"]
)
builder.add_edge("worker_node", END)
graph = builder.compile()
res = graph.invoke({"topic": "布偶狗"})
print(res)
from IPython.display import display
display(graph)
在这个案例中,router() 并没有直接返回某一个固定的下游节点名称,而是返回了多个 Send 实例:
[
Send("worker_node", {"content_type": "poem", ...}),
Send("worker_node", {"content_type": "ci_poem", ...}),
Send("worker_node", {"content_type": "joke", ...}),
]
因此,运行时会动态创建三个指向 worker_node 的任务。三个任务执行的是同一个节点函数,但它们接收到的私有状态不同,所以可以分别生成诗、词和笑话。
输出如下
{
"poem": "## 《布偶狗》\n\n它曾蜷在藤椅里数谷粒,\n一只耳朵垂向磨损的地板。\n现在檐角的水滴是它的心跳,\n牵动穿珠眼珠转动。\n\n穿碎花裙的小女孩,\n用膝盖丈量过它的静默。\n她松开橡皮筋的午后,\n线缝里渗出稚嫩的气流。\n\n有人将米尺抖成琴弦,\n折叠的假寐里,\n它代替远方的稻草人,\n在积雨的布纹中扎根。\n\n田野在练习告别,\n而它守着发芽的梯田。\n当月光填满每道接缝,\n皮毛间涌出成群的往事。",
"ci_poem": "《临江仙·布偶狗》\n绒作皮毛棉作骨,琉璃眸底春凝。\n也无吠影也无惊。倚床垂耳听,抱主索糖迎。\n\n不向残羹争冷炙,旧衾犹带余馨。\n学人解语却无声。偶沾星与月,懒问雨和晴。\n\n注:我以布偶狗为题,通过“绒作皮毛棉作骨”等句,以拟人手法赋予玩偶灵性。下阕“不向残羹争冷炙”暗喻玩偶超然物外的品性,末句“偶沾星与月”更添空灵意境,将玩偶与自然意象相融,表达出静默相伴的永恒温馨。",
"joke": "# 布偶狗的笑话\n\n有一天,一只布偶狗走进一家宠物店,对店主说:\n\n“老板,我想买一只真正的狗朋友。”\n\n店主看了看它,好奇地问:“你不是狗吗?”\n\n布偶狗叹了口气,说:“唉,我是布偶做的,只能陪主人睡觉和卖萌,但我不会汪汪叫,也不会摇尾巴,更不会追球。每次主人想带我出去散步,我只能在包里安静地坐着,感觉自己特别没用。”\n\n店主笑了笑,说:“那你想要什么样的狗朋友?”\n\n布偶狗眼睛一亮:“我想要一只真正的狗,可以带着我去跑、去叫、去玩!我要学习怎么当一只好狗!”\n\n店主想了想,指着一只活泼的小金毛说:“那这只怎么样?它精力旺盛,可以教你很多。”\n\n布偶狗激动地冲过去,结果因为自己是布做的,腿太软,直接摔了个跟头,四脚朝天躺在地上。\n\n小金毛跑过来,闻了闻它,然后叼起它的棉花尾巴,一路拖到了狗窝里。\n\n布偶狗挣扎着喊:“等等!我还没学会怎么站起来!”\n\n小金毛歪了歪头,汪汪了两声,像是说:“没事,我先教你躺平。”\n\n从此以后,布偶狗成了小金毛最好的“垫子”——每天被压在身下睡觉,还觉得特别温暖。\n\n布偶狗心想:“原来,当一只合格的布偶狗朋友,最重要的技能是——当沙发。” 🛋️🐾"
}

此处图结构能成功渲染的关键,是在 path_map 中显式声明了可能被路由到的下游节点:
path_map=["worker_node"]
因为 Send 的目标和数量可以运行时动态决定,图渲染器无法仅靠运行时返回值提前知道图结构。通过 path_map 声明候选下游节点后,渲染图才能正确展示 START 到 worker_node 的条件边关系。
补充说明:如果多个并行任务写入同一个状态字段,通常需要为该字段定义 reducer,用于合并多个任务的输出。本例中三个任务分别写入 poem、ci_poem、joke 三个不同字段,因此不会发生同一字段的并发合并问题。
4.2.2.2. 条件分支
4.2.2.2.1. Command介绍
Command 是 LangGraph 中用于控制图执行的多功能原语,它的构造器可以接受四个参数,并记录在同名类属性中:
update:更新图状态,效果等同于节点直接返回状态更新字典;goto:指定节点执行完成后的跳转目标,可用于运行时条件分支。当需要同时更新状态并控制跳转时,比单独使用条件边更合适;graph:存在子图时,用于指定跳转发生在哪一层图中,例如从子图跳转到父图;resume:用于恢复被中断的图执行,常见于human-in-the-loop场景。
4.2.2.2.2. 用Command实现条件分支
可以通过 Command(goto=...) 在节点内部实现条件分支。
与 add_conditional_edges() 相比,Command 更适合“状态更新”和“控制流跳转”需要放在同一个节点返回值中的场景。
例如,一个节点既要更新状态,又要根据当前状态决定下一步跳转目标,就可以返回:
return Command(
update={"foo": "bar"},
goto="next_node"
)
示例如下:
from typing import TypedDict, Literal
from langgraph.types import Command
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
content_type: Literal["poem", "joke"]
poem: str
joke: str
def router(state: OverAllState) -> Command[Literal["poem_node", "joke_node", END]]:
content_type = state["content_type"]
if content_type == "poem":
return Command (
goto="poem_node"
)
elif content_type == "joke":
return Command (
goto="joke_node"
)
return Command (
goto=END
)
def poem_node(state: OverAllState) -> OverAllState:
topic = state["topic"]
prompt = f"请生成一首关于 {topic} 的七言绝句"
poem = model.invoke([HumanMessage(prompt)]).content
return {
"poem": poem
}
def joke_node(state: OverAllState) -> OverAllState:
topic = state["topic"]
prompt = f"请生成一个关于 {topic} 的冷笑话"
joke = model.invoke([HumanMessage(prompt)]).content
return {
"joke": joke
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("router", router)
builder.add_node("poem_node", poem_node)
builder.add_node("joke_node", joke_node)
builder.add_edge(START, "router")
builder.add_edge("poem_node", END)
builder.add_edge("joke_node", END)
graph = builder.compile()
poem_res = graph.invoke({"topic": "布偶猫", "content_type": "poem"})
joke_res = graph.invoke({"topic": "布偶猫", "content_type": "joke"})
# 这里故意传入一个不符合 Literal["poem", "joke"] 的值,
# 用来观察运行时兜底分支。
# 注意:Literal 主要服务于静态类型检查,
# 默认不会在 Python 运行时阻止非法值传入。
illegal_res = graph.invoke({
"topic": "布偶猫",
"content_type": "illegal"
})
print('=' * 30, '-> poem <-', '=' * 30)
print(poem_res)
print('=' * 30, '-> joke <-', '=' * 30)
print(joke_res)
print('=' * 30, '-> illegal <-', '=' * 30)
print(illegal_res)
from IPython.display import display
display(graph)
上述代码中,router() 节点根据 content_type 的值决定下一步跳转目标:
- 当
content_type == "poem"时,跳转到poem_node; - 当
content_type == "joke"时,跳转到joke_node; - 当传入其他非法值时,跳转到
END,图直接结束。
结果如下
============================== -> poem <- ==============================
{
'topic': '布偶猫',
'content_type': 'poem',
'poem': '《布偶猫》\n雪砌绒身碧眼秋,一帘幽梦卧云舟。\n忽听银铃摇碎月,轻衔星子入西楼。\n\n(注:此诗通过“雪砌绒身”、“碧眼秋”等意象勾勒布偶猫雪白柔顺的毛发与澄澈眼眸;“卧云舟”、“衔星子”等超现实笔法,赋予其精灵般的气质;末句“入西楼”暗合其优雅步态与神秘秉性,整体形成月光浸染的梦幻意境。)'
}
============================== -> joke <- ==============================
{
'topic': '布偶猫',
'content_type': 'joke',
'joke': '给你分享一个关于布偶猫的冷笑话:\n\n---\n\n有一天,一只布偶猫跑去宠物店里应聘。 \n店员问:“你有什么特长呢?” \n布偶猫懒洋洋地回答:“我特别软。” \n店员说:“还有呢?” \n猫翻个身,露出肚皮:“你看,我连摸都不需要自己动——你只要一伸手,我自己就‘布偶’过去了。”\n\n---\n\n希望能让你会心一笑(或者冷到打个哆嗦)😺❄️'
}
============================== -> illegal <- ==============================
{
'topic': '布偶猫',
'content_type': 'illegal'
}

这里使用:
Command[Literal["poem_node", "joke_node", END]]
作为 router() 的返回值类型注解。
注解不能限制 goto 在运行时只能写这些值,而是为了让 LangGraph 和类型检查工具知道:这个节点可能跳转到哪些目标。对于图结构渲染来说,这个注解非常重要。如果不声明,渲染出来的图可能无法正确表达 router 节点的潜在跳转关系。
需要注意的是,如果某个节点使用 Command(goto=...) 控制后续跳转,一般不要再给这个节点额外添加普通下游边。否则,普通边和 Command 指定的跳转都可能生效,导致多个下游节点被同时触发。
因此,本例中只添加:
builder.add_edge(START, "router")
而不添加类似下面这样的边:
builder.add_edge("router", "poem_node")
builder.add_edge("router", "joke_node")
因为 router 的后续跳转已经由 Command(goto=...) 决定。
4.2.2.3. 小结
动态分支有两类典型应用场景:
| 场景 | 典型 API | 说明 |
|---|---|---|
| 动态扇出 | Send + add_conditional_edges() |
运行时创建多个任务,常用于 Map-Reduce |
| 动态跳转 | Command(goto=...) |
节点内部根据状态决定跳转目标 |
二者的区别如下:
Send更强调“一个节点动态分发多个任务实例”;Command(goto=...)更强调“当前节点执行完后动态跳转到哪个节点”。
Send解决的是:运行时要启动多少个任务实例。
Command(goto=...)解决的是:当前节点执行完后要去哪里。
不过,无论是 Send 还是 Command(goto=...),目标节点通常都需要提前注册到图中。所谓“动态”,主要是运行时动态决定任务数量、任务输入或跳转路径,而不是运行时临时创建新的节点。
4.3. 多分支汇聚:Fan-in
上文提到了扇出(Fan-out),它是指像打开扇子一样,由一个上游节点分发出多个下游分支。
与之相对,扇入(Fan-in)是指像合拢扇子一样,多个上游分支汇聚到同一个下游节点。
Fan-out 对应的是“分支结构”,Fan-in 对应的是“多分支汇聚结构”。
4.3.1. 静态扇入
在计算机中,“与”表示两个或多个条件同时满足,而“或”表示两个或多个条件任意一个满足。
当多个上游分支同时汇入同一个下游节点时,根据触发条件的不同,可以分成两种情况:
- “与”触发:上游所有分支全部到达才可触发下游节点
- “或”触发:任意一个分支到达,都可以触发下游节点
需要注意,这里的 “与 / 或” 只是帮助理解的类比,并不是 LangGraph 的官方术语。
4.3.1.1 “与”触发:等待所有上游分支到达
这是多分支汇聚中最常见的情况。
LangGraph 的计算图是在一系列超步中完成的,如果能看到每个节点实例运行时的超步编号,则节点执行顺序一目了然。
LangGraph 的节点可以通过名为 config 的有名参数获取运行时配置
config 的元数据中记录了当前节点所在的 SuperStep 序号,可以通过下面的方式获取:
config["metadata"]["langgraph_step"]
详见下文。
示例如下
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_core.runnables import RunnableConfig
from loguru import logger
class EmptyState(TypedDict):
pass
def node_a(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_a 被触发", cur_step)
return {}
def node_b(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_b 被触发", cur_step)
return {}
def node_c(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_c 被触发", cur_step)
return {}
def node_d(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_d 被触发", cur_step)
return {}
def node_e(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_e 被触发", cur_step)
return {}
builder = StateGraph(state_schema=EmptyState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_node("node_d", node_d)
builder.add_node("node_e", node_e)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_a", "node_c")
builder.add_edge("node_b", "node_d")
builder.add_edge(["node_c", "node_d"], "node_e")
graph = builder.compile()
graph.invoke({})
from IPython.display import display
display(graph)
运行结果如下
2026-06-05 14:55:01.705 | INFO | __main__:node_a:12 - cur_step: 1, node_a 被触发
2026-06-05 14:55:01.708 | INFO | __main__:node_b:17 - cur_step: 2, node_b 被触发
2026-06-05 14:55:01.710 | INFO | __main__:node_c:22 - cur_step: 2, node_c 被触发
2026-06-05 14:55:01.713 | INFO | __main__:node_d:27 - cur_step: 3, node_d 被触发
2026-06-05 14:55:01.714 | INFO | __main__:node_e:32 - cur_step: 4, node_e 被触发

由运行结果可知:
node_a在第 1 个超步执行;node_b和node_c在第 2 个超步并行执行;node_d在第 3 个超步执行;node_e在第 4 个超步执行。
虽然 node_c 在第 2 个超步已经执行完成,但是 node_e 并不会立刻触发。
因为这里使用的是:
builder.add_edge(["node_c", "node_d"], "node_e")
这表示 node_e 需要等待 node_c 和 node_d 两个上游节点全部完成后,才会被触发一次。
因此,这种写法对应的是 “与”触发:等待所有上游分支到达方可触发。
4.3.1.2 “或”触发:任意上游分支到达即可触发
另一种情况是:多个上游分支分别连接到同一个下游节点,但它们之间没有显式的同步等待关系。
示例如下
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_core.runnables import RunnableConfig
from loguru import logger
class EmptyState(TypedDict):
pass
def node_a(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_a 被触发", cur_step)
return {}
def node_b(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_b 被触发", cur_step)
return {}
def node_c(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_c 被触发", cur_step)
return {}
def node_d(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_d 被触发", cur_step)
return {}
def node_e(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("cur_step: {}, node_e 被触发", cur_step)
return {}
builder = StateGraph(state_schema=EmptyState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_node("node_d", node_d)
builder.add_node("node_e", node_e)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_a", "node_c")
builder.add_edge("node_b", "node_d")
# 独立触发:node_c 完成后可以触发 node_e
builder.add_edge("node_c", "node_e")
# 独立触发:node_d 完成后可以触发 node_e
builder.add_edge("node_d", "node_e")
graph = builder.compile()
graph.invoke({})
from IPython.display import display
display(graph)
运行结果如下
2026-06-05 15:09:11.715 | INFO | __main__:node_a:12 - cur_step: 1, node_a 被触发
2026-06-05 15:09:11.720 | INFO | __main__:node_b:17 - cur_step: 2, node_b 被触发
2026-06-05 15:09:11.723 | INFO | __main__:node_c:22 - cur_step: 2, node_c 被触发
2026-06-05 15:09:11.729 | INFO | __main__:node_d:27 - cur_step: 3, node_d 被触发
2026-06-05 15:09:11.729 | INFO | __main__:node_e:32 - cur_step: 3, node_e 被触发
2026-06-05 15:09:11.732 | INFO | __main__:node_e:32 - cur_step: 4, node_e 被触发

从运行结果可以看出,node_e 被触发了两次:
node_c在第 2 个超步执行完成后,触发node_e在第 3 个超步执行;node_d在第 3 个超步执行完成后,再次触发node_e在第 4 个超步执行。
原因是这里使用了两条独立的边:
builder.add_edge("node_c", "node_e")
builder.add_edge("node_d", "node_e")
这表示 node_c 和 node_d 都可以分别触发 node_e,它们之间没有同步等待关系。
因此,这种写法对应的是 独立触发,也可以类比为 “或”触发。
需要特别注意:
builder.add_edge(["node_c", "node_d"], "node_e")
和
builder.add_edge("node_c", "node_e")
builder.add_edge("node_d", "node_e")
不是等价写法。
前者表示 等待多个上游全部完成后才触发一次;
后者表示 多个上游分支分别独立触发下游节点。
4.3.2. 动态扇入-MapReduce结构
在上文我们通过 Send 动态生成了多个下游任务。
这种模式本质上是一种动态扇出:根据运行时数据,将一个任务拆分为多个子任务,并分别发送给下游节点处理。
如果这些子任务分别产生中间结果,并在后续通过 Reducer 进行合并,也就是重新 扇入 到一个下游节点中,最终得到统一的结果,那么就构成了典型的 MapReduce 结构。
MapReduce 是大数据计算中的经典模型,通常包含两个核心阶段:
-
Map:映射阶段
将输入数据映射为中间结果。在
LangGraph中,可以理解为通过Send将子任务分发给多个mapper节点实例,每个节点实例独立完成局部计算:将部分输入数据映射为中间结果。 -
Reduce:归约阶段
将多个子任务产生的中间结果进行汇总、合并或聚合,得到最终结果。在
LangGraph中,通常由一个特定的reducer节点完成归约:它接收上游mapper节点实例产生的中间结果,处理后得到计算图的最终输出。
本节实现一个经典的词频统计任务
输入数据
|
| hello world
| hello Atguigu
| hello LLM
v
Map:映射为中间键值对
|
| (hello, 1), (world, 1)
| (hello, 1), (Atguigu, 1)
| (hello, 1), (LLM, 1)
v
Shuffle / Group:按 Key 分组
|
| hello -> [1, 1, 1]
| world -> [1]
| Atguigu -> [1]
| LLM -> [1]
v
Reduce:归约 / 聚合
|
| hello -> 3
| world -> 1
| Atguigu -> 1
| LLM -> 1
v
最终结果
需要注意,本节示例中没有单独实现一个 Shuffle 节点,而是把 Shuffle / Group 的逻辑也放在了 reducer_node 中完成。
在下面的示例中,我们用 Send 动态派发任务,创建了多个 mapper_node 实例。
每个 mapper_node 实例负责处理一条输入数据,并输出中间产物:键值对。
随后,下游的 reducer_node 收集所有中间结果,完成 Shuffle / Group 和最终的 Reduce 聚合。
示例如下
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from _collections_abc import Sequence
from operator import add
from loguru import logger
class OverAllState(TypedDict):
input_values: list[str]
entries: Annotated[list[tuple[str, int]], add]
word_counts: dict[str, int]
def router_map(state: OverAllState) -> Sequence[Send]:
input_values = state["input_values"]
tasks = []
for input_value in input_values:
tasks.append(
Send(
"mapper_node",
{"input_value": input_value}
)
)
return tasks
class MaperInputState(TypedDict):
input_value: str
def mapper_node(state: MaperInputState) -> OverAllState:
input_value = state["input_value"]
words = input_value.split(" ")
entries = []
for word in words:
entries.append((word, 1))
return {
"entries": entries
}
def reducer_node(state: OverAllState) -> OverAllState:
entries = state["entries"]
logger.info("reducer entries: {}", entries)
shuffle_dict = {}
for k, v in entries:
if k not in shuffle_dict:
shuffle_dict[k] = [v]
else:
shuffle_dict[k].append(v)
logger.info("reducer shuffle entries: {}", shuffle_dict)
reduce_dict = {}
for k, v in shuffle_dict.items():
reduce_dict[k] = sum(v)
return {
"word_counts": reduce_dict
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("mapper_node", mapper_node)
builder.add_node("reducer_node", reducer_node)
builder.add_conditional_edges(START, router_map, path_map=["mapper_node"])
builder.add_edge("mapper_node", "reducer_node")
builder.add_edge("reducer_node", END)
graph = builder.compile()
word_counts = graph.invoke(
{"input_values": [
"hello world",
"hello Atguigu",
"hello LLM"
]}
)
print(word_counts)
from IPython.display import display
display(graph)
运行结果如下

2026-06-05 13:52:01.917 | INFO | __main__:reducer_node:45 - reducer entries: [('hello', 1), ('world', 1), ('hello', 1), ('Atguigu', 1), ('hello', 1), ('LLM', 1)]
2026-06-05 13:52:01.918 | INFO | __main__:reducer_node:55 - reducer shuffle entries: {'hello': [1, 1, 1], 'world': [1], 'Atguigu': [1], 'LLM': [1]}
{
"input_values": [
"hello world",
"hello Atguigu",
"hello LLM",
],
"entries": [
("hello", 1),
("world", 1),
("hello", 1),
("Atguigu", 1),
("hello", 1),
("LLM", 1),
],
"word_counts": {
"hello": 3,
"world": 1,
"Atguigu": 1,
"LLM": 1,
},
}
从结果可以看出,三个输入字符串分别被映射为了中间键值对:
("hello", 1), ("world", 1)
("hello", 1), ("Atguigu", 1)
("hello", 1), ("LLM", 1)
由于 entries 字段使用了 Reducer(LangGraph 状态的 Reducer,而不是当前示例中的 reducer_node):
entries: Annotated[list[tuple[str, int]], add]
所以多个 mapper_node 任务实例返回的 entries 会被合并到同一个状态字段中。
随后,reducer_node 读取合并后的 entries,先按单词进行分组:
{
"hello": [1, 1, 1],
"world": [1],
"Atguigu": [1],
"LLM": [1],
}
再对每个分组中的数值进行求和,得到最终词频统计结果:
{
"hello": 3,
"world": 1,
"Atguigu": 1,
"LLM": 1,
}
如果按照 任务实例 而不是 节点定义 来绘制运行图,那么这个示例实际的任务级运行图如下所示:
需要注意,display(graph) 展示的是节点级计算图,而不是运行时的任务实例图。
因此在可视化图中只会看到一个 mapper_node 节点;但在实际运行时,Send 会根据输入数据动态创建多个 mapper_node 任务实例。
4.4. 循环结构
4.4.1. 循环结构的实现
本节通过两种方式实现经典的 ReAct 循环结构。LangChain Agent 底层运行图架构正是 ReAct 。
ReAct 是 Reason + Action 的缩写,即“推理 + 行动”架构。其核心思想是:
- Reason:大模型根据当前消息状态进行推理,判断是否需要调用工具;
- Action:如果需要调用工具,则生成工具调用请求;
- Observation:工具执行后,将执行结果以
ToolMessage的形式返回给大模型; - Loop:大模型基于新的观察结果继续推理,决定是否继续调用工具;
- Final Answer:当大模型不再发起工具调用时,生成最终回答,流程结束。
因此,ReAct 本质上是一个典型的 “LLM → Tool → LLM → Tool → ... → LLM” 循环结构。
本节分别使用两种方式实现该循环:
- 静态实现:通过
add_conditional_edges()在图结构中显式定义条件路由; - 动态实现:通过
Command(goto=...)在节点返回值中触发运行时跳转。
4.4.1.1. 静态实现
所谓“静态实现”,并不是指执行路径完全固定,而是指:
节点的下游候选集合在图编译阶段已经确定,运行时只是从这些候选节点中选择下一步。
在本例中,llm_node 的下游候选节点是固定的:
- 如果大模型返回了
tool_calls,则进入tool_node; - 如果大模型没有返回
tool_calls,则进入output_node。
也就是说,图结构在编译阶段已经知道 llm_node 可能流向 tool_node 或 output_node,运行时只负责判断具体走哪一条路径。
示例如下
from typing import Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langchain.messages import HumanMessage, ToolMessage, SystemMessage
from langchain.tools import tool
from langchain_deepseek import ChatDeepSeek
from random import randint
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
@tool(parse_docstring=True)
def get_weather(city: str="北京"):
"""
查询指定城市当日天气
Args:
city: 城市名称
"""
return f"{city} 今天天气晴朗,东南风三级,气温 25~30 ℃"
@tool(parse_docstring=True)
def get_news(domain: Literal["AI", "食品安全"]):
"""
查询特定领域的当日热点
Args:
domain: 特定领域
"""
if domain == "AI":
return "Anthropic 发布了 Claude Opus-4.8,但通过 API 用中文向它发送“你是谁?”时,大多数情况下返回的却是“Qwen”或“Deepseek”。"
else:
return "双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍"
tools = [get_weather, get_news]
model_with_tools = model.bind_tools(tools=tools)
class OverAllState(MessagesState):
user_input: str
final_answer: str
def input_node(state: OverAllState) ->OverAllState:
return {
"messages": [HumanMessage(state["user_input"])],
}
def llm_node(state: OverAllState) -> OverAllState:
messages = state["messages"]
ai_msg = model_with_tools.invoke(messages)
return {
"messages": [ai_msg],
}
def tool_node(state: OverAllState) -> OverAllState:
messages = state["messages"]
ai_msg = messages[-1]
tool_calls = ai_msg.tool_calls
fail_prob = 6 # 失败概率,6表示 60% 概率失败,7 -> 70%,8 -> 80%,依次类推
for tool_call in tool_calls:
if tool_call["name"] == "get_weather":
# 生成 [0, 9] 范围内的随机数
if randint(0, 9) < fail_prob: # 60% 概率因 “网络波动” 而调用失败
messages.append(
ToolMessage(
content="网络波动,调用失败,请重试",
tool_call_id=tool_call["id"]
)
)
else:
messages.append(get_weather.invoke(tool_call))
elif tool_call["name"] == "get_news":
# 生成 [0, 9] 范围内的随机数
if randint(0, 9) < fail_prob: # 60% 概率因 “网络波动” 而调用失败
messages.append(
ToolMessage(
content="网络波动,调用失败,请重试",
tool_call_id=tool_call["id"]
)
)
else:
messages.append(get_news.invoke(tool_call))
else:
messages.append(
ToolMessage(
content="工具名称错误,调用失败,请重试",
tool_call_id=tool_call["id"]
)
)
return {
"messages": messages
}
def output_node(state: OverAllState) -> OverAllState:
return {
"final_answer": state["messages"][-1].content
}
def router(state: OverAllState) -> Literal["tool_node", "output_node"]:
messages = state["messages"]
lst_msg = messages[-1]
if lst_msg.tool_calls:
return "tool_node"
return "output_node"
builder = StateGraph(state_schema=OverAllState)
builder.add_node("input_node", input_node)
builder.add_node("llm_node", llm_node)
builder.add_node("tool_node", tool_node)
builder.add_node("output_node", output_node)
builder.add_edge(START, "input_node")
builder.add_edge("input_node", "llm_node")
builder.add_conditional_edges(
"llm_node",
router
)
builder.add_edge("tool_node", "llm_node")
builder.add_edge("output_node", END)
graph = builder.compile()
ai_res = graph.invoke({
"user_input": "帮我查询杭州当日天气和AI热点",
"messages": [SystemMessage("如果工具调用失败,必须重新调用直至成功")]
})
food_safety_res = graph.invoke({
"user_input": "帮我查询当日天气和食品安全相关的热点",
"messages": [SystemMessage("如果工具调用失败,必须重新调用直至成功")]
})
print('=' * 30, '-> ai_res <-', '=' * 30)
print("user_input: ", ai_res["user_input"])
print("final_answer: ", ai_res["final_answer"])
for msg in ai_res["messages"]:
msg.pretty_print()
print('=' * 30, '-> food_safety_res <-', '=' * 30)
print("user_input: ", food_safety_res["user_input"])
print("final_answer: ", food_safety_res["final_answer"])
for msg in food_safety_res["messages"]:
msg.pretty_print()
from IPython.display import display
display(graph)
运行结果如下
============================== -> ai_res <- ==============================
user_input: 帮我查询杭州当日天气和AI热点
final_answer: 查询成功!以下是结果汇总:
---
### 🌤️ 杭州当日天气
- **天气状况**:晴朗
- **气温范围**:25°C ~ 30°C
- **风力**:东南风三级
### 🤖 AI热点
**Anthropic 发布了 Claude Opus-4.8**,但通过 API 用中文向它发送"你是谁?"时,大多数情况下返回的却是"Qwen"或"Deepseek"。(这条AI新闻可能反映了当前大模型领域的一些有趣现象或争议)
---
杭州今天天气不错,晴朗舒适,适合外出活动!而AI领域似乎又有了新的模型发布动态。您还想了解其他信息吗?
================================[1m System Message [0m================================
如果工具调用失败,必须重新调用直至成功
================================[1m Human Message [0m=================================
帮我查询杭州当日天气和AI热点
==================================[1m Ai Message [0m==================================
好的,我来同时查询杭州的天气和AI热点。
Tool Calls:
get_weather (call_00_DPkgkfOYSBOuic3ipHj11702)
Call ID: call_00_DPkgkfOYSBOuic3ipHj11702
Args:
city: 杭州
get_news (call_01_clcon3yt50HhIKGKBaxl4926)
Call ID: call_01_clcon3yt50HhIKGKBaxl4926
Args:
domain: AI
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
==================================[1m Ai Message [0m==================================
两次调用都因网络波动失败了,让我重新尝试查询。
Tool Calls:
get_weather (call_00_ruPVWhBySBDG9B9AR7vN4587)
Call ID: call_00_ruPVWhBySBDG9B9AR7vN4587
Args:
city: 杭州
get_news (call_01_ENBOMtrHXaJn7u1o7PPS6090)
Call ID: call_01_ENBOMtrHXaJn7u1o7PPS6090
Args:
domain: AI
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
=================================[1m Tool Message [0m=================================
Name: get_news
Anthropic 发布了 Claude Opus-4.8,但通过 API 用中文向它发送“你是谁?”时,大多数情况下返回的却是“Qwen”或“Deepseek”。
==================================[1m Ai Message [0m==================================
天气查询仍然失败,我再次尝试查询天气。
Tool Calls:
get_weather (call_00_BSSyk3lOTQxZEzXLmbJr7873)
Call ID: call_00_BSSyk3lOTQxZEzXLmbJr7873
Args:
city: 杭州
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
==================================[1m Ai Message [0m==================================
Tool Calls:
get_weather (call_00_WQZzpmwpfIQfGjkO98752270)
Call ID: call_00_WQZzpmwpfIQfGjkO98752270
Args:
city: 杭州
=================================[1m Tool Message [0m=================================
Name: get_weather
杭州 今天天气晴朗,东南风三级,气温 25~30 ℃
==================================[1m Ai Message [0m==================================
查询成功!以下是结果汇总:
---
### 🌤️ 杭州当日天气
- **天气状况**:晴朗
- **气温范围**:25°C ~ 30°C
- **风力**:东南风三级
### 🤖 AI热点
**Anthropic 发布了 Claude Opus-4.8**,但通过 API 用中文向它发送"你是谁?"时,大多数情况下返回的却是"Qwen"或"Deepseek"。(这条AI新闻可能反映了当前大模型领域的一些有趣现象或争议)
---
杭州今天天气不错,晴朗舒适,适合外出活动!而AI领域似乎又有了新的模型发布动态。您还想了解其他信息吗?
============================== -> food_safety_res <- ==============================
user_input: 帮我查询当日天气和食品安全相关的热点
final_answer: 查询成功!以下是您需要的信息:
---
### 🌤️ 当日天气(北京)
- **天气状况:** 晴朗
- **气温:** 25~30℃
- **风力:** 东南风三级
### 🔍 食品安全热点
**双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍**
> 这是一则关于知名食品企业双汇旗下子公司猪肉产品被检出抗生素残留严重超标的新闻,超标幅度高达37.5倍,属于较为严重的食品安全问题。
---
如果您想查询其他城市的天气,也可以告诉我哦!
================================[1m System Message [0m================================
如果工具调用失败,必须重新调用直至成功
================================[1m Human Message [0m=================================
帮我查询当日天气和食品安全相关的热点
==================================[1m Ai Message [0m==================================
好的,我来同时查询当日天气和食品安全相关的热点。
Tool Calls:
get_weather (call_00_NkvnFHT4M4h9wwENZmyu9753)
Call ID: call_00_NkvnFHT4M4h9wwENZmyu9753
Args:
city: 北京
get_news (call_01_rbFBctitbMLZQivR1TYd2949)
Call ID: call_01_rbFBctitbMLZQivR1TYd2949
Args:
domain: 食品安全
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
=================================[1m Tool Message [0m=================================
Name: get_news
双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍
==================================[1m Ai Message [0m==================================
天气查询因为网络波动失败了,我重新查询一下天气信息。
Tool Calls:
get_weather (call_00_fWmSdUOseojF7PADvgOA7804)
Call ID: call_00_fWmSdUOseojF7PADvgOA7804
Args:
city: 北京
=================================[1m Tool Message [0m=================================
Name: get_weather
北京 今天天气晴朗,东南风三级,气温 25~30 ℃
==================================[1m Ai Message [0m==================================
查询成功!以下是您需要的信息:
---
### 🌤️ 当日天气(北京)
- **天气状况:** 晴朗
- **气温:** 25~30℃
- **风力:** 东南风三级
### 🔍 食品安全热点
**双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍**
> 这是一则关于知名食品企业双汇旗下子公司猪肉产品被检出抗生素残留严重超标的新闻,超标幅度高达37.5倍,属于较为严重的食品安全问题。
---
如果您想查询其他城市的天气,也可以告诉我哦!

本例运行图中的循环结构如下:
START
↓
input_node
↓
llm_node
├── 有 tool_calls → tool_node → llm_node
└── 无 tool_calls → output_node → END
其中,循环发生在:
llm_node → tool_node → llm_node
只要大模型持续返回工具调用,流程就会不断回到 llm_node,形成 ReAct 循环。
需要注意的是,本例中“工具调用失败后继续重试”主要依赖这条系统提示词:
SystemMessage("如果工具调用失败,必须重新调用直至成功")
也就是说,是否继续重试,在当前实现中主要由大模型决定,而不是由程序逻辑强制保证。
因此,如果模型在多次工具调用失败后选择停止重试并生成最终回答,运行图不会阻止它。
4.4.1.2. 动态实现
除了静态路由,我们还可以利用 Command 在运行时动态指定下一个要执行的节点。
在本节的实现中,llm_node 借助 Command 在返回值中同时完成两件事:
- 更新状态;
- 控制下一步的跳转。
这里的 动态 是指:
节点在运行时,根据自身的执行结果,直接返回“状态更新 + 下一跳控制指令”。
在本例中,llm_node 调用大模型后,会根据返回结果决定后续流程:
- 如果
ai_msg.tool_calls不为空,则goto="tool_node"; - 如果
ai_msg.tool_calls为空,则goto="output_node"。
这样一来,路由逻辑就被内聚在 llm_node 节点,不再需要额外定义 router() 函数,也不再需要调用 add_conditional_edges() 从 llm_node 添加条件边。
示例如下
from typing import Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langgraph.types import Command
from langchain.messages import HumanMessage, ToolMessage, SystemMessage
from langchain.tools import tool
from langchain_deepseek import ChatDeepSeek
from random import randint
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
@tool(parse_docstring=True)
def get_weather(city: str="北京"):
"""
查询指定城市当日天气
Args:
city: 城市名称
"""
return f"{city} 今天天气晴朗,东南风三级,气温 25~30 ℃"
@tool(parse_docstring=True)
def get_news(domain: Literal["AI", "食品安全"]):
"""
查询特定领域的当日热点
Args:
domain: 特定领域
"""
if domain == "AI":
return "Anthropic 发布了 Claude Opus-4.8,但通过 API 用中文向它发送“你是谁?”时,大多数情况下返回的却是“Qwen”或“Deepseek”。"
else:
return "双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍"
tools = [get_weather, get_news]
model_with_tools = model.bind_tools(tools=tools)
class OverAllState(MessagesState):
user_input: str
final_answer: str
def input_node(state: OverAllState) ->OverAllState:
return {
"messages": [HumanMessage(state["user_input"])],
}
def llm_node(state: OverAllState) -> Command[Literal["tool_node", "output_node"]]:
messages = state["messages"]
ai_msg = model_with_tools.invoke(messages)
if ai_msg.tool_calls:
goto = "tool_node"
else:
goto = "output_node"
return Command(
goto=goto,
update={
"messages": [ai_msg],
}
)
def tool_node(state: OverAllState) -> OverAllState:
messages = state["messages"]
ai_msg = messages[-1]
tool_calls = ai_msg.tool_calls
fail_prob = 6 # 失败概率,6表示 60% 概率失败,7 -> 70%,8 -> 80%,依次类推
for tool_call in tool_calls:
if tool_call["name"] == "get_weather":
# 生成 [0, 9] 范围内的随机数
if randint(0, 9) < fail_prob: # 60% 概率因 “网络波动” 而调用失败
messages.append(
ToolMessage(
content="网络波动,调用失败,请重试",
tool_call_id=tool_call["id"]
)
)
else:
messages.append(get_weather.invoke(tool_call))
elif tool_call["name"] == "get_news":
# 生成 [0, 9] 范围内的随机数
if randint(0, 9) < fail_prob: # 60% 概率因 “网络波动” 而调用失败
messages.append(
ToolMessage(
content="网络波动,调用失败,请重试",
tool_call_id=tool_call["id"]
)
)
else:
messages.append(get_news.invoke(tool_call))
else:
messages.append(
ToolMessage(
content="工具名称错误,调用失败,请重试",
tool_call_id=tool_call["id"]
)
)
return {
"messages": messages
}
def output_node(state: OverAllState) -> OverAllState:
return {
"final_answer": state["messages"][-1].content
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("input_node", input_node)
builder.add_node("llm_node", llm_node)
builder.add_node("tool_node", tool_node)
builder.add_node("output_node", output_node)
builder.add_edge(START, "input_node")
builder.add_edge("input_node", "llm_node")
builder.add_edge("tool_node", "llm_node")
builder.add_edge("output_node", END)
graph = builder.compile()
ai_res = graph.invoke({
"user_input": "帮我查询杭州当日天气和AI热点",
"messages": [SystemMessage("如果工具调用失败,必须重新调用直至成功")]
})
food_safety_res = graph.invoke({
"user_input": "帮我查询当日天气和食品安全相关的热点",
"messages": [SystemMessage("如果工具调用失败,必须重新调用直至成功")]
})
print('=' * 30, '-> ai_res <-', '=' * 30)
print("user_input: ", ai_res["user_input"])
print("final_answer: ", ai_res["final_answer"])
for msg in ai_res["messages"]:
msg.pretty_print()
print('=' * 30, '-> food_safety_res <-', '=' * 30)
print("user_input: ", food_safety_res["user_input"])
print("final_answer: ", food_safety_res["final_answer"])
for msg in food_safety_res["messages"]:
msg.pretty_print()
from IPython.display import display
display(graph)
运行结果如下所示
============================== -> ai_res <- ==============================
user_input: 帮我查询杭州当日天气和AI热点
final_answer: 以下是查询结果:
---
### 🌤️ 杭州当日天气
- **天气状况**:晴朗
- **气温**:25~30 ℃
- **风力**:东南风三级
### 🤖 AI 热点
- **Anthropic 发布了 Claude Opus-4.8**,但有趣的是,通过 API 用中文向它发送"你是谁?"时,大多数情况下返回的却是"Qwen"或"Deepseek"。
---
看起来杭州今天天气不错,适合外出活动!而AI领域这则关于Claude的有趣现象也引发了不少讨论。还有其他想了解的吗?
================================[1m System Message [0m================================
如果工具调用失败,必须重新调用直至成功
================================[1m Human Message [0m=================================
帮我查询杭州当日天气和AI热点
==================================[1m Ai Message [0m==================================
好的,我来同时查询杭州的天气和AI热点。
Tool Calls:
get_weather (call_00_QAXNeCIuy4MEe91ZiBQp0274)
Call ID: call_00_QAXNeCIuy4MEe91ZiBQp0274
Args:
city: 杭州
get_news (call_01_5hXZth3YLTZFZZKE48i62455)
Call ID: call_01_5hXZth3YLTZFZZKE48i62455
Args:
domain: AI
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
=================================[1m Tool Message [0m=================================
Name: get_news
Anthropic 发布了 Claude Opus-4.8,但通过 API 用中文向它发送“你是谁?”时,大多数情况下返回的却是“Qwen”或“Deepseek”。
==================================[1m Ai Message [0m==================================
天气查询遇到了网络波动,我重新尝试一下。
Tool Calls:
get_weather (call_00_ooZcTsB9ntQRU3tXDrVj7186)
Call ID: call_00_ooZcTsB9ntQRU3tXDrVj7186
Args:
city: 杭州
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
==================================[1m Ai Message [0m==================================
再次重试:
Tool Calls:
get_weather (call_00_EwVrJmFUZaYZfvkOxveK7822)
Call ID: call_00_EwVrJmFUZaYZfvkOxveK7822
Args:
city: 杭州
=================================[1m Tool Message [0m=================================
Name: get_weather
杭州 今天天气晴朗,东南风三级,气温 25~30 ℃
==================================[1m Ai Message [0m==================================
以下是查询结果:
---
### 🌤️ 杭州当日天气
- **天气状况**:晴朗
- **气温**:25~30 ℃
- **风力**:东南风三级
### 🤖 AI 热点
- **Anthropic 发布了 Claude Opus-4.8**,但有趣的是,通过 API 用中文向它发送"你是谁?"时,大多数情况下返回的却是"Qwen"或"Deepseek"。
---
看起来杭州今天天气不错,适合外出活动!而AI领域这则关于Claude的有趣现象也引发了不少讨论。还有其他想了解的吗?
============================== -> food_safety_res <- ==============================
user_input: 帮我查询当日天气和食品安全相关的热点
final_answer: 非常抱歉,天气查询接口持续出现网络波动,暂时无法获取到天气信息。以下是目前已获取到的信息:
---
### 📰 食品安全热点
**双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍**
> 这是今日食品安全领域的一条重要新闻,涉及知名肉制品企业双汇发展,其子公司检测出猪肉产品中抗生素残留超标情况较为严重。
---
### 🌤️ 天气查询
天气接口当前多次重试均因网络波动失败,暂时无法获取当日天气信息,建议稍后再尝试查询。
================================[1m System Message [0m================================
如果工具调用失败,必须重新调用直至成功
================================[1m Human Message [0m=================================
帮我查询当日天气和食品安全相关的热点
==================================[1m Ai Message [0m==================================
我来为你查询当日天气和食品安全相关的热点。
Tool Calls:
get_weather (call_00_JYYAWJk569VD0TZ4LCyr0840)
Call ID: call_00_JYYAWJk569VD0TZ4LCyr0840
Args:
city: 北京
get_news (call_01_YkzloPKV0DVwL1r5nBgi8164)
Call ID: call_01_YkzloPKV0DVwL1r5nBgi8164
Args:
domain: 食品安全
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
=================================[1m Tool Message [0m=================================
Name: get_news
双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍
==================================[1m Ai Message [0m==================================
天气查询遇到网络波动,我来重试一下。
Tool Calls:
get_weather (call_00_MjNdBAQz2gW6EzZavsZ91123)
Call ID: call_00_MjNdBAQz2gW6EzZavsZ91123
Args:
city: 北京
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
==================================[1m Ai Message [0m==================================
Tool Calls:
get_weather (call_00_pkcSelcAJiFDNpU7II0d9212)
Call ID: call_00_pkcSelcAJiFDNpU7II0d9212
Args:
city: 北京
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
==================================[1m Ai Message [0m==================================
Tool Calls:
get_weather (call_00_QKgt5xYTos0H1c6CIS9a6172)
Call ID: call_00_QKgt5xYTos0H1c6CIS9a6172
Args:
city: 北京
=================================[1m Tool Message [0m=================================
网络波动,调用失败,请重试
==================================[1m Ai Message [0m==================================
非常抱歉,天气查询接口持续出现网络波动,暂时无法获取到天气信息。以下是目前已获取到的信息:
---
### 📰 食品安全热点
**双汇发展子公司猪肉产品被抽检出抗生素超标37.5倍**
> 这是今日食品安全领域的一条重要新闻,涉及知名肉制品企业双汇发展,其子公司检测出猪肉产品中抗生素残留超标情况较为严重。
---
### 🌤️ 天气查询
天气接口当前多次重试均因网络波动失败,暂时无法获取当日天气信息,建议稍后再尝试查询。

本例和静态实现的核心区别体现在 llm_node 中:
def llm_node(state: OverAllState) -> Command[Literal["tool_node", "output_node"]]:
ai_msg = model_with_tools.invoke(state["messages"])
if ai_msg.tool_calls:
goto = "tool_node"
else:
goto = "output_node"
return Command(
update={
"messages": [ai_msg],
},
goto=goto,
)
其返回值类型:
Command[Literal["tool_node", "output_node"]]
表明该节点返回的是一个 Command 对象,且 goto 字段的值只能是:
"tool_node"- 或
"output_node"
这是静态约束,不会参与运行时校验。主要作用是提升代码可读性、通过静态类型检查,同时帮助 LangGraph 更准确地推断图结构。
4.4.1.3. 小结
| 静态实现 | 动态实现 | |
|---|---|---|
| 路由位置 | 路由逻辑在独立的 router() 函数中 |
路由逻辑写在节点返回值中 |
| 图结构表达 | 显式声明条件边,更直观 | 控制逻辑更内聚,代码更紧凑 |
| 适合场景 | 路由规则独立、希望图结构更清晰 | 节点执行结果直接决定下一跳 |
两种方式都能实现 ReAct 循环,本质区别不在于是否能循环,而在于:
路由逻辑写在哪里。
- 基于
add_conditional_edges()的静态实现:把控制逻辑放在图结构定义阶段; - 基于
Command(goto=...)的动态实现:把控制逻辑放在节点返回值中。
4.4.2. 引入递归限制
在存在循环结构的图中,如果没有合理的停止条件,运行图可能会一直循环执行。为了避免无限循环,LangGraph 提供了递归限制机制,用于限制单次图运行过程中允许执行的最大 SuperStep 数量。
当运行图在达到停止条件之前耗尽允许的最大步数时,LangGraph 会抛出 GraphRecursionError。开发者既可以在图内部提前检测剩余步数并优雅退出,也可以在图外部捕获异常并集中处理。
4.4.2.1. 步骤计数器
4.4.2.1.1. 运行时配置对象config
LangGraph 运行节点时,会向节点函数注入运行时配置对象 config。该对象通常使用 RunnableConfig 类型标注,用于记录本次运行的配置信息和部分运行时元数据。
节点函数的第一个参数必须是运行时状态。如果需要访问运行时配置,可以在状态参数之后声明额外参数 config。此时,LangGraph 会在调用节点函数之前,将运行时配置对象以关键字参数的方式传入节点函数。
config 的元数据中记录了当前节点所在的 SuperStep 序号,可以通过下面的方式获取:
config["metadata"]["langgraph_step"]
需要注意的是,这里的步骤编号对应图运行过程中的 SuperStep,从 1 开始计数。对于顺序执行的节点,不同节点通常位于不同的 SuperStep;对于并行执行的节点,多个节点可能处于同一个 SuperStep,因此它们读取到的 langgraph_step 可能相同。
示例如下
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_core.runnables import RunnableConfig
from loguru import logger
class EmptyState(TypedDict):
pass
def node_a(state: EmptyState, config: RunnableConfig) -> EmptyState | None:
current_step = config["metadata"]["langgraph_step"]
logger.info(current_step)
def node_b(state: EmptyState, config: RunnableConfig) -> EmptyState | None:
current_step = config["metadata"]["langgraph_step"]
logger.info(current_step)
def node_c(state: EmptyState, config: RunnableConfig) -> EmptyState | None:
current_step = config["metadata"]["langgraph_step"]
logger.info(current_step)
builder = StateGraph(state_schema=EmptyState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", "node_c")
builder.add_edge("node_c", END)
graph = builder.compile()
graph.invoke({})
from IPython.display import display
display(graph)
运行结果如下
2026-05-29 19:40:07.569 | INFO | __main__:node_a:12 - 1
2026-05-29 19:40:07.571 | INFO | __main__:node_b:16 - 2
2026-05-29 19:40:07.573 | INFO | __main__:node_c:20 - 3

可以看到,node_a、node_b、node_c 是顺序执行的三个节点,因此分别处于第 1、2、3 个 SuperStep。
4.4.2.2. 配置递归限制
recursion_limit 表示单次图运行过程中允许执行的最大 SuperStep 数量。可以在调用运行图时通过 config 显式配置:
graph.invoke(input, config={"recursion_limit": 10})
需要注意的是,recursion_limit 的默认值可能随版本变化、运行环境切换而变化。
4.4.2.2.1. 版本的变化
官方文档提到:从 LangGraph 1.0.6 开始,recursion_limit 默认为 1000,本项目运行在 LangGraph 1.1.2 版本,实测发现 recursion_limit 默认为 10000。
4.4.2.2.2. 环境的变化
阅读源码可知,LangGraph 运行时可能从多个来源读取默认值
如下:
| 来源 | 默认值 | 说明 |
|---|---|---|
langchain_core.runnables.config |
25 |
RunnableConfig 中定义的默认值,LangChain Agent 运行时生效 |
langgraph._internal._config |
10000 |
LangGraph 本地图运行时内部配置中的默认值,自定义 LangGraph 图结构时生效 |
langgraph_api.utils.config |
10011 |
LangGraph API / 服务侧相关配置中的默认值 |
对于自定义图结构的 LangGraph 运行时,其默认值取自 langgraph._internal._config,所以调试看到的默认值为 10000。
4.4.2.2.3. 最佳实践
在实际开发中,建议显式传入 recursion_limit,避免不同版本、不同运行入口或不同部署方式下默认值不同导致实际运行结果和预期不符。
4.4.2.3. 优雅退出:主动方法(Proactive Approach)
RemainingSteps 是 LangGraph 提供的特殊托管值,表示剩余可用步数,由运行时维护。
LangGraph 运行时会根据当前步数和 recursion_limit 计算剩余步数,并填充到 RemainingSteps 类型的状态字段中。开发者可以在状态中声明一个 RemainingSteps 类型的字段,来获取剩余可用步数。
借助 RemainingSteps,开发者可以在图内部提前判断剩余步数是否充足,并根据剩余步数动态路由。例如,当剩余步数较少时,不再继续循环,而是路由到 END 或兜底节点,从而让运行图正常结束。
这种方式是在达到递归限制之前主动处理,因此称为 主动方法(Proactive Approach)。
示例如下
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.managed import RemainingSteps
from langchain_core.runnables.config import RunnableConfig
from loguru import logger
class OverAllState(TypedDict):
remaining_steps: RemainingSteps
def loop_node(state: OverAllState, config: RunnableConfig) -> OverAllState:
cur_step = config["metadata"]["langgraph_step"]
remaining_steps = state["remaining_steps"]
logger.info("loop_node, cur_step: {}, remaining_step: {}", cur_step, remaining_steps)
def router(state: OverAllState) -> Literal["loop_node", END]:
remaining_steps = state["remaining_steps"]
if remaining_steps < 3:
logger.info("当前可用超步:{},已不足2步,终止运行图", remaining_steps)
return END
return "loop_node"
builder = StateGraph(state_schema=OverAllState)
builder.add_node("loop_node", loop_node)
builder.add_edge(START, "loop_node")
builder.add_conditional_edges("loop_node", router)
graph = builder.compile()
graph.invoke({}, config={"recursion_limit": 10})
from IPython.display import display
display(graph)
输出如下
2026-06-01 10:23:40.412 | INFO | __main__:loop_node:14 - loop_node, cur_step: 1, remaining_step: 9
2026-06-01 10:23:40.413 | INFO | __main__:loop_node:14 - loop_node, cur_step: 2, remaining_step: 8
2026-06-01 10:23:40.414 | INFO | __main__:loop_node:14 - loop_node, cur_step: 3, remaining_step: 7
2026-06-01 10:23:40.414 | INFO | __main__:loop_node:14 - loop_node, cur_step: 4, remaining_step: 6
2026-06-01 10:23:40.415 | INFO | __main__:loop_node:14 - loop_node, cur_step: 5, remaining_step: 5
2026-06-01 10:23:40.416 | INFO | __main__:loop_node:14 - loop_node, cur_step: 6, remaining_step: 4
2026-06-01 10:23:40.417 | INFO | __main__:loop_node:14 - loop_node, cur_step: 7, remaining_step: 3
2026-06-01 10:23:40.417 | INFO | __main__:loop_node:14 - loop_node, cur_step: 8, remaining_step: 2
2026-06-01 10:23:40.418 | INFO | __main__:router:19 - 当前可用超步:2,已不足2步,终止运行图

本例中,recursion_limit 被显式设置为 10。
第 1 个 SuperStep 执行时,剩余步数为 9;第 2 个 SuperStep 执行时,剩余步数为 8;依次类推。。。当前超步序号和剩余步数之和等于 recursion_limit。
第 8 个 SuperStep 执行时,剩余步数为 2。此时路由函数判断剩余步数较少,不再继续循环,而是路由到 END,运行图正常结束。
这种方式在运行图内部主动处理,不会抛出异常,因此是优雅退出的主动方法。
4.4.2.4. 异常中断:被动方法(Reactive Approach)
如果图中存在循环结构,并且运行图在达到停止条件之前耗尽了允许的最大步数,LangGraph 会抛出 GraphRecursionError。
开发者可以在图外部捕获该异常,处理递归限制超限的情况。由于这种方式是在异常已经发生之后再处理,因此称为 被动方法(Reactive Approach)。
示例如下
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.errors import GraphRecursionError
from langchain_core.runnables import RunnableConfig
from loguru import logger
class EmptyState(TypedDict):
pass
def loop_node(state: EmptyState, config: RunnableConfig) -> EmptyState:
cur_step = config["metadata"]["langgraph_step"]
logger.info("loop_node, cur_step: {}", cur_step)
builder = StateGraph(state_schema=EmptyState)
builder.add_node("loop_node", loop_node)
builder.add_edge(START, "loop_node")
builder.add_edge("loop_node", "loop_node")
graph = builder.compile()
try:
graph.invoke({}, config={"recursion_limit": 10})
except GraphRecursionError as e:
logger.info("超步数量达到最大限制,抛出异常: {}", e)
from IPython.display import display
display(graph)
运行结果如下
2026-06-01 10:13:06.116 | INFO | __main__:loop_node:13 - loop_node, cur_step: 1
2026-06-01 10:13:06.117 | INFO | __main__:loop_node:13 - loop_node, cur_step: 2
2026-06-01 10:13:06.118 | INFO | __main__:loop_node:13 - loop_node, cur_step: 3
2026-06-01 10:13:06.119 | INFO | __main__:loop_node:13 - loop_node, cur_step: 4
2026-06-01 10:13:06.119 | INFO | __main__:loop_node:13 - loop_node, cur_step: 5
2026-06-01 10:13:06.120 | INFO | __main__:loop_node:13 - loop_node, cur_step: 6
2026-06-01 10:13:06.120 | INFO | __main__:loop_node:13 - loop_node, cur_step: 7
2026-06-01 10:13:06.121 | INFO | __main__:loop_node:13 - loop_node, cur_step: 8
2026-06-01 10:13:06.121 | INFO | __main__:loop_node:13 - loop_node, cur_step: 9
2026-06-01 10:13:06.122 | INFO | __main__:loop_node:13 - loop_node, cur_step: 10
2026-06-01 10:13:06.122 | INFO | __main__:<module>:24 - 超步数量达到最大限制,抛出异常: Recursion limit of 10 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.
For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT

本例中,loop_node 的下游仍然是 loop_node 本身,因此运行图会不断循环。由于没有任何停止条件,当执行步数达到 recursion_limit=10 后,LangGraph 抛出 GraphRecursionError。
这种方式虽然可以避免异常导致整个进程崩溃,但图本身已经被异常中断,不能像主动方法那样正常结束。
4.4.2.5. 主动方法和被动方法的对比
主动方法和被动方法的最大区别在于:
主动方法是在图内部提前处理递归限制;被动方法是在图外部捕获
递归限制异常。
| 方法 | 超限检测 | 处理方法 | 控制流 |
|---|---|---|---|
主动(借助 RemainingSteps) |
达到限制之前 | 在图结构内部通过条件路由处理 | 图继续运行,正常结束 |
被动(捕获 GraphRecursionError) |
超出限制之后 | 图结构外部,通过 try/except 处理 |
图运行被异常中断 |
主动方法的优势:
- 在图内部实现优雅降级
- 可以将中间状态保存到检查点中
- 通过返回部分结果改善用户体验
- 图可以正常完成,不会抛出异常
被动方法的优势:
- 实现更简单
- 不需要修改图的内部逻辑
- 可以集中处理错误
实际开发中,如果循环结构本身是业务逻辑的一部分,例如 ReAct 循环、多轮反思、自我修正、工具调用重试 等,更推荐使用主动方法,在图内部提前处理递归限制。
如果只是为了兜底防止无限循环,也可以保留被动方法,在最外层捕获 GraphRecursionError,作为最后一道防线。
4.5. Edges总结:LangGraph中的边
上文我们已经学习了 LangGraph 中各种边的用法。本节对 LangGraph 中的边 做一个系统性总结。
4.5.1. LangGraph官方对边的定义和分类
Edges(边) 定义了节点之间的依赖关系,决定图如何根据当前状态决定下一步,以及何时停止运行。
边主要有以下几种类型:
4.5.1.1. 普通边(Normal Edges)
普通边 表示从当前节点直接流转到下一个节点。
例如
builder.add_edge("node_a", "node_b")
其含义是:node_a 执行完成后,下一步执行 node_b。
4.5.1.2. 条件边(Conditional Edges)
条件边 表示当前节点执行完成后,调用一个 路由函数 来决定下一步进入哪个或哪些节点。
例如
builder.add_conditional_edges("node_a", router)
其中,router 会根据当前图状态返回后续目标节点。
如果路由函数返回一个节点名,则下一步进入该节点;如果返回多个节点名,则这些目标节点会在下一个超步中并行执行。
4.5.1.3. 入口点(Entry Point)
入口点 表示图开始运行时,首先执行哪个节点。
本质上,入口点就是一条 以 START 为起点的普通边。
例如
builder.add_edge(START, "node_a")
其含义是:当用户输入进入图时,首先执行 node_a。
LangGraph 也提供了专门的 API 来声明入口点:
builder.set_entry_point("node_a")
二者是等价的。
4.5.1.4. 条件入口点(Conditional Entry Point)
条件入口点 表示图开始运行时,并不固定进入某一个节点,而是先调用一个 路由函数,再根据路由结果决定首先执行哪个节点,或者哪些节点。
本质上,条件入口点就是一条 以 START 为起点的条件边。
例如
builder.add_conditional_edges(START, router)
其含义是:图开始运行时,先执行 router,再根据 router 的返回值决定首个执行节点。
4.5.1.5. 注意
- 一个节点可以有多个出边。此时,这些出边对应的目标节点会在下一个超步中并行执行。
- 除了上述方式,条件边还可以通过在节点中返回
Command(goto=...)来实现,虽然方式不同,但边的性质和作用完全一致。 - 在实际开发中,建议同一个节点尽量只选择一种路由机制:
- 要么使用普通边,固定流转;
- 要么使用条件边或
Command(goto=...),动态流转。
不要在同一个节点上同时混用普通边和动态路由,否则多个路径可能同时生效,最终导致运行图逻辑混乱,与预期不符。
4.5.2. 指向END的终止边
除了官方文档中列出的几种边,还有一类非常重要的边:指向 END 的边。
严格来说,END 不是普通业务节点,而是 LangGraph 中的特殊终止标记。当流程流转到 END 时,表示该路径不再继续执行后续节点。
为了便于理解,可以将指向 END 的边分为两种情况。
4.5.2.1. 终止点(Finish Point)
终止点 表示某个节点执行完成后,图流程结束。
本质上,它是一条 以 END 为目标点的普通边。
例如
builder.add_edge("node_a", END)
其含义是:当 node_a 执行完成后,当前路径终止,不再继续执行其他节点。
LangGraph 也提供了专门的 API 来声明结束点:
builder.set_finish_point("node_a")
二者是等价的。
4.5.2.2. 条件终止点(Conditional Finish Point)
条件终止点 表示图在运行过程中,通过 路由函数 判断是继续执行后续节点,还是终止当前路径。
本质上,它是一条 路由结果中包含 END 的条件边。
例如
def router(state: EmptyState) -> Literal["node_b", END]:
if state["is_finished"]:
return END
return "node_b"
builder.add_conditional_edges("node_a", router)
其含义是:
- 如果
state["is_finished"]为真,则返回END,流程终止; - 否则返回
"node_b",继续执行node_b。
4.5.3. END和终止边的说明
- 在编译后的运行图中,终止边 和
END并没有具体的Channel和 节点 与之对应,它们只是终止标记,表示运行图的结束。 - 终止边的声明可以省略,
LangGraph运行时会在没有活跃节点时自然终止。但是,为了程序的可读性和图结构可视化渲染器的正常工作,最好显式声明指向END的边。
05. 节点执行与容错机制
节点执行失败时,如果未做任何处理,可能导致整个运行图执行失败。为了提升运行图的稳定性,处理临时故障、超时、异常恢复以及重复计算等问题,LangGraph 提供了一系列节点执行与容错机制:重试、超时设置、异常处理和节点缓存。对应关系如下。
| 问题/场景 | 对应机制 | 机制类型 | 说明 |
|---|---|---|---|
| 临时故障 | 重试 Retry | 节点容错机制 | 网络抖动、接口偶发失败、模型服务短暂不可用,可以通过重试机制再次执行节点。 |
| 超时 | 超时设置 Timeout | 节点容错机制 | 某个节点执行时间过长,可以通过超时限制避免图运行被长时间阻塞。 |
| 异常恢复 | 异常处理 Error Handling | 节点容错机制 | 节点抛出异常后,通过异常捕获、兜底逻辑、降级返回、路由到错误处理节点等方式恢复流程。 |
| 重复计算 | 缓存 Cache | 节点执行优化机制 | 相同输入反复触发耗时节点,可以通过节点缓存避免重复执行,提高性能。 |
5.1. LangGraph的节点容错机制
LangGraph 提供了三种异常处理机制
retries:重试机制。节点执行失败时,根据重试策略自动重新执行。Timeouts:超时控制。限制单次节点执行的最大等待时间。Error Handling:错误处理。重试次数耗尽后,执行特定的异常处理逻辑。
三者的执行顺序如下:
- 节点开始执行。
- 如果节点执行超时或抛出异常,本次节点尝试失败。
- 重试策略
RetryPolicy判断该异常是否需要重试。 - 如果满足重试条件,并且尚未达到最大尝试次数,则重新执行节点。
- 如果重试次数耗尽,节点仍然失败,则进入
error_handler错误处理逻辑。 - 如果没有配置错误处理逻辑,则异常继续向外抛出,可能导致整个图运行失败。
需要注意:
- 重试机制 是当前版本中最常用、最基础的节点容错能力。
- 超时控制 和 错误处理 是较新版本引入的能力,要求
langgraph>=1.2。 - 本课程当前环境为
langgraph==1.1.2,因此重点讲解重试机制和节点缓存,超时控制与错误处理仅做概念说明。
5.2. 重试机制
5.2.1. 基本用法
重试机制用于处理临时性异常,例如网络抖动、远程服务短暂不可用、API 偶发失败等。
在添加节点时,可以通过 retry_policy= 配置节点的重试策略。
示例如下
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import RetryPolicy
from requests.exceptions import HTTPError
from loguru import logger
class EmptyState(TypedDict):
pass
def node_a(state: EmptyState) -> EmptyState:
logger.info(f"node a 运行")
raise HTTPError("网络连接超时...")
builder = StateGraph(state_schema=EmptyState)
builder.add_node(
"node_a",
node_a,
retry_policy=RetryPolicy(
max_attempts=3,
jitter=False
)
)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)
graph = builder.compile()
try:
graph.invoke({})
except HTTPError as e:
logger.info("重试次数耗尽: {}", e)
from IPython.display import display
display(graph)
输出如下
2026-06-01 16:44:56.684 | INFO | __main__:node_a:12 - node a 运行
2026-06-01 16:44:57.185 | INFO | __main__:node_a:12 - node a 运行
2026-06-01 16:44:58.186 | INFO | __main__:node_a:12 - node a 运行
2026-06-01 16:44:58.187 | INFO | __main__:<module>:32 - 重试次数耗尽: 网络连接超时...

本例中:
max_attempts=3表示最多尝试 3 次。- 这 3 次包括首次执行,而不是首次失败后的 3 次重试。
- 因此,节点总共会被调用 3 次。
- 3 次全部失败后,异常继续向外抛出。
- 外层的
try...except捕获该异常,并打印日志。
5.2.2. 重试策略配置详解
超时策略通过 RetryPolicy 配置,其主要参数如下
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
max_attempts |
int |
3 |
最大尝试次数(包括首次执行) |
initial_interval |
float |
0.5 |
第一次重试前的等待时间,单位:秒 |
backoff_factor |
float |
2.0 |
每次重试后等待时间的放大倍数 |
max_interval |
float |
128.0 |
相邻两次重试之间的最大等待时间,单位:秒 |
jitter |
bool |
True |
是否为重试间隔添加随机抖动 |
retry_on |
**`type[Exception] | Sequence[type[Exception]] | Callable[[Exception], bool]`** |
5.2.2.1. max_attempts
max_attempts 表示最大尝试次数,包括首次执行。
例如:
RetryPolicy(max_attempts=3)
表示:
第 1 次:首次执行
第 2 次:第 1 次重试
第 3 次:第 2 次重试
如果第 3 次仍然失败,则不再继续重试。
5.2.2.2. initial_interval
initial_interval 表示第一次重试前的等待时间,单位为秒。
例如:
RetryPolicy(initial_interval=0.5)
表示首次失败后,等待 0.5s 再进行第一次重试。
5.2.2.3. backoff_factor
backoff_factor 表示重试间隔的增长倍数。
默认情况下,RetryPolicy 使用指数退避策略:
第一次失败后:等待 initial_interval
第二次失败后:等待 initial_interval * backoff_factor
第三次失败后:继续乘以 backoff_factor
例如:
RetryPolicy(
initial_interval=0.5,
backoff_factor=2.0
)
等待时间大致为:
0.5s -> 1.0s -> 2.0s -> 4.0s ...
5.2.2.4. max_interval
max_interval 表示相邻两次重试之间的最大等待时间。
即使指数退避计算出的等待时间继续增长,也不会超过 max_interval。
例如:
RetryPolicy(
initial_interval=1,
backoff_factor=2,
max_interval=5
)
等待时间大致为:
1s -> 2s -> 4s -> 5s -> 5s ...
5.2.2.5. jitter
jitter 表示是否为重试间隔添加随机抖动(添加随机数,可正可负)。
在并发场景中,如果大量任务同时失败,并且按照固定间隔同时重试,可能导致服务在某个时间点被大量请求打爆。
加入 jitter 后,不同任务的重试时间会被打散,从而降低瞬时流量峰值。
上述案例中,为了让日志时间更稳定,观察指数退避策略,所以设置了:
RetryPolicy(jitter=False)
如果是生产环境,通常建议保留默认值:
RetryPolicy(jitter=True)
5.2.2.6. retry_on
retry_on 用于设置哪些异常需要触发重试。
它支持三种写法:
写法一:指定单个异常类型
RetryPolicy(retry_on=HTTPError)
表示只有抛出 HTTPError 时才重试。
写法二:指定多个异常类型
RetryPolicy(
retry_on=(HTTPError, ConnectionError)
)
表示抛出 HTTPError 或 ConnectionError 时触发重试。
写法三:自定义判断函数
def should_retry(exc: Exception) -> bool:
return isinstance(exc, HTTPError)
RetryPolicy(retry_on=should_retry)
这种写法最灵活。
should_retry 接收一个异常实例,返回一个布尔值:
- 返回
True:表示该异常需要重试。 - 返回
False:表示该异常不需要重试。
5.2.3. 默认重试条件
retry_on 的默认值为 default_retry_on。
default_retry_on 是一个可调用对象,它接收一个 Exception 实例,并返回一个 bool 值:
True -> 需要重试
False -> 不需要重试
默认情况下,default_retry_on 会对大多数异常进行重试,但以下异常及其子类通常不会触发重试:
ValueErrorTypeErrorArithmeticErrorImportErrorLookupErrorNameErrorSyntaxErrorRuntimeErrorReferenceErrorStopIterationStopAsyncIterationOSError
这些异常通常更像是代码逻辑错误、类型错误、语法错误或运行时错误,单纯重试通常没有意义。
对于主流 HTTP 库中的异常,例如 requests 和 httpx,默认策略通常只对 5xx 状态码进行重试。
原因是:
4xx通常表示客户端请求错误,例如参数错误、鉴权失败、资源不存在等。5xx通常表示服务端异常,例如服务暂时不可用、网关错误、内部服务错误等。
因此,5xx 更适合通过重试进行恢复。
default_retry_on 的核心逻辑如下:
def default_retry_on(exc: Exception) -> bool:
import httpx
import requests
if isinstance(exc, ConnectionError):
return True
if isinstance(exc, httpx.HTTPStatusError):
return 500 <= exc.response.status_code < 600
if isinstance(exc, requests.HTTPError):
return 500 <= exc.response.status_code < 600 if exc.response else True
if isinstance(
exc,
(
ValueError,
TypeError,
ArithmeticError,
ImportError,
LookupError,
NameError,
SyntaxError,
RuntimeError,
ReferenceError,
StopIteration,
StopAsyncIteration,
OSError,
),
):
return False
return True
5.3. 超时控制
5.3.1. 使用要求
超时控制用于限制单次节点执行的最大耗时,避免某个节点因为外部服务无响应、网络阻塞或异步任务卡住而长时间不返回。
需要注意:
-
版本要求
节点级超时控制要求:
langgraph>=1.2 -
节点类型要求
节点级超时控制仅适用于异步节点,即使用
async def定义的节点。同步节点一旦开始执行,通常会阻塞当前线程。
Python进程内缺少一种通用、安全的机制,可以从外部强行终止正在运行的同步函数;如果强行中断,可能导致资源未释放、锁未释放或副作用处于不一致状态。因此,LangGraph的节点级超时更适合用于异步节点。对于同步节点,应优先在节点内部使用具体库提供的超时参数,例如HTTP客户端、数据库驱动或SDK自带的timeout配置。
本课程当前环境为:
langgraph==1.1.2
因此不支持该特性,本节只做概念说明。
5.3.2. 核心思想
超时控制的核心思想是:
为单次节点设置超时时间,运行时长超过超时时间,节点运行失败,并抛出超时异常。
在 add_node 中通过 timeout= 配置超时策略。
例如:
from langgraph.types import TimeoutPolicy
builder.add_node(
"call_model",
call_model,
timeout=TimeoutPolicy(run_timeout=60)
)
这里的含义是:
call_model节点单次执行最多运行 60 秒。- 如果超过 60 秒仍未完成,则触发超时。
- 超时后会抛出
NodeTimeoutError。 - 该异常会交给
RetryPolicy判断是否需要重试。 - 如果重试次数耗尽,才会进入错误处理逻辑。
5.3.3. TimeoutPolicy 简要说明
TimeoutPolicy 常见参数包括:
| 参数 | 描述 |
|---|---|
run_timeout |
超时时间,即单次节点运行的最长时间 |
idle_timeout |
节点没有可观察进展(如写状态、流式输出等)时的最长空闲时间 |
refresh_on |
空闲时间的刷新方式,可以是手动刷新或自动刷新。默认为自动刷新,此时写状态、流式输出等都会刷新 |
本课程环境暂不支持,不展开实操。
感兴趣的同学参考官方文档
https://docs.langchain.com/oss/python/langgraph/fault-tolerance#timeouts
5.4. 错误处理
5.4.1. 使用要求
错误处理机制要求:
langgraph>=1.2
本课程当前环境为:
langgraph==1.1.2
因此不支持该特性,本节只做概念说明。
5.4.2. 核心思想
错误处理机制用于在节点最终失败后,执行兜底逻辑。
节点执行失败时,整体顺序如下:
节点执行失败
↓
是否满足 retry_policy
↓
满足则继续重试,直到重试次数耗尽,否则直接进行下一步
↓
进入 error_handler
↓
执行兜底恢复逻辑
在 add_node 中通过 error_handler= 设置错误处理逻辑。
例如:
builder.add_node(
"call_api",
call_api,
retry_policy=RetryPolicy(max_attempts=3),
error_handler=handle_api_error
)
其含义是:
call_api执行失败。RetryPolicy判断是否重试。- 最多尝试 3 次。
- 3 次仍然失败后,调用
handle_api_error。 handle_api_error可以返回状态更新,也可以通过Command路由到其他节点。
错误处理适合以下场景:
- API 调用失败后返回兜底结果。
- 远程服务失败后切换到备用服务。
- 多步骤业务流程中执行补偿逻辑。
- 不希望整个图因为单个节点失败而直接终止。
当前环境不支持该特性,不展开实操
参考官方文档
https://docs.langchain.com/oss/python/langgraph/fault-tolerance#error-handling
5.5. 节点缓存
5.5.1. 什么是节点缓存?
节点缓存是指:
将节点的历史运行结果保存下来,后续当节点收到相同输入时,不再重复执行节点函数,而是直接返回之前缓存的结果。
节点缓存主要用于减少重复计算,降低延迟和成本。
例如,某个节点内部需要:
- 调用大模型
- 调用外部 API
- 查询远程数据库
- 执行复杂计算
- 进行耗时的数据处理
如果相同输入会多次出现,那么可以考虑启用节点缓存。
5.5.2. 适用场景
节点缓存通常适用于同时满足以下条件的场景:
-
节点函数具有确定性
相同输入应当产生相同输出。
例如:
输入 A -> 输出 B 再次输入 A -> 仍然输出 B -
节点执行成本较高
例如大模型调用、API 请求、复杂计算、耗时查询等。
-
相同输入会重复出现
例如多次调试图、重复执行相同流程、多个分支复用同一中间结果等。
-
输入能够被缓存键函数稳定处理
缓存是以 键值对 形式存储的,每个缓存都有唯一的
Key,缓存键函数 用于生成唯一键。默认情况下,
CachePolicy使用default_cache_key作为缓存键函数。后者基于节点输入调用pickle库的序列化功能生成缓存唯一键。使用默认缓存键函数时,建议输入尽量使用简单、稳定、可序列化的数据结构,例如:
strintfloatbooldictlisttuple
如果节点输入中包含复杂对象、文件句柄、数据库连接、模型实例、动态对象等,建议自定义
key_func,避免缓存Key不稳定或无法生成。
5.5.3. 基本用法
节点缓存需要两步配置。
第一步:为节点配置 cache_policy
在 add_node 调用时,通过 cache_policy= 设置节点缓存策略。
builder.add_node(
"node_a",
node_a,
cache_policy=CachePolicy(ttl=10)
)
第二步:编译图时启用缓存后端
仅仅为节点配置 cache_policy 还不够,还需要在编译图时指定缓存后端。
例如:
graph = builder.compile(cache=InMemoryCache())
其中:
CachePolicy:声明节点是否启用缓存,以及缓存策略是什么。InMemoryCache:声明缓存数据实际保存在哪里。
如果只配置 cache_policy,但编译图时没有传入 cache=,缓存不会真正生效。
示例如下
import time
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.cache.memory import InMemoryCache
from langgraph.types import CachePolicy
from operator import add
from loguru import logger
class EmptyState(TypedDict):
user: str
invoke_counts: Annotated[int, add]
def node_a(state: EmptyState) -> EmptyState:
logger.info("node_a 被调用, user: {}", state["user"])
time.sleep(3) # 模拟耗时操作
logger.info("node_a 耗时操作执行完毕")
return {
"invoke_counts": 1
}
builder = StateGraph(state_schema=EmptyState)
builder.add_node(
"node_a",
node_a,
cache_policy=CachePolicy(ttl=10)
)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)
graph = builder.compile(cache=InMemoryCache())
logger.info("首次调用图")
logger.info("运行结果: {}\n\n", graph.invoke({"user": "小明", "invoke_counts": 0}))
logger.info("相同输入再次调用图")
logger.info("运行结果: {}\n\n", graph.invoke({"user": "小明", "invoke_counts": 0}))
logger.info("不同输入再次调用图")
logger.info("运行结果: {}\n\n", graph.invoke({"user": "小花", "invoke_counts": 0}))
logger.info("不同输入再次调用图")
logger.info("运行结果: {}\n\n", graph.invoke({"user": "小花", "invoke_counts": 2}))
time.sleep(10) # 确保第一次调用缓存失效
logger.info("10秒后相同输入再次调用图,此时缓存已失效")
logger.info("运行结果: {}", graph.invoke({"user": "小明", "invoke_counts": 0}))
from IPython.display import display
display(graph)
运行日志如下
2026-06-01 18:46:22.959 | INFO | __main__:<module>:33 - 首次调用图
2026-06-01 18:46:22.960 | INFO | __main__:node_a:15 - node_a 被调用, user: 小明
2026-06-01 18:46:25.962 | INFO | __main__:node_a:17 - node_a 耗时操作执行完毕
2026-06-01 18:46:25.963 | INFO | __main__:<module>:34 - 运行结果: {'user': '小明', 'invoke_counts': 1}
2026-06-01 18:46:25.964 | INFO | __main__:<module>:35 - 相同输入再次调用图
2026-06-01 18:46:25.965 | INFO | __main__:<module>:36 - 运行结果: {'user': '小明', 'invoke_counts': 1}
2026-06-01 18:46:25.966 | INFO | __main__:<module>:37 - 不同输入再次调用图
2026-06-01 18:46:25.967 | INFO | __main__:node_a:15 - node_a 被调用, user: 小花
2026-06-01 18:46:28.968 | INFO | __main__:node_a:17 - node_a 耗时操作执行完毕
2026-06-01 18:46:28.970 | INFO | __main__:<module>:38 - 运行结果: {'user': '小花', 'invoke_counts': 1}
2026-06-01 18:46:28.970 | INFO | __main__:<module>:39 - 不同输入再次调用图
2026-06-01 18:46:28.971 | INFO | __main__:node_a:15 - node_a 被调用, user: 小花
2026-06-01 18:46:31.973 | INFO | __main__:node_a:17 - node_a 耗时操作执行完毕
2026-06-01 18:46:31.974 | INFO | __main__:<module>:40 - 运行结果: {'user': '小花', 'invoke_counts': 3}
2026-06-01 18:46:36.976 | INFO | __main__:<module>:42 - 11秒后相同输入再次调用图,此时缓存已失效
2026-06-01 18:46:36.977 | INFO | __main__:node_a:15 - node_a 被调用, user: 小明
2026-06-01 18:46:39.979 | INFO | __main__:node_a:17 - node_a 耗时操作执行完毕
2026-06-01 18:46:39.981 | INFO | __main__:<module>:43 - 运行结果: {'user': '小明', 'invoke_counts': 1}

在该示例中,缓存的 ttl 被设置为 10s。
缓存失效前:
- 相同输入会命中缓存。
- 命中缓存时,节点函数不会再次执行。
- 因此不会打印
node_a 被调用。 - 输入不同则不会命中缓存,节点函数仍然会执行。
缓存失效后:
- 即使输入相同,也会再次执行节点函数。
- 执行完成后,会重新写入缓存。
5.5.4. 缓存策略配置详解
缓存策略通过 CachePolicy 配置,只有两个配置项
| 参数 | 描述 |
|---|---|
key_func |
根据节点输入生成缓存 Key 的函数 |
ttl |
缓存键值对的存活时间,单位为秒 |
5.5.4.1. key_func
key_func 用于根据节点输入生成唯一的缓存 Key。
节点缓存本质上是一个 K-V 缓存:
缓存 Key -> 节点运行结果
当节点再次执行时,LangGraph 会先根据当前输入计算缓存 Key:
- 如果缓存 Key 已存在,并且没有过期,则直接返回缓存结果。
- 如果缓存 Key 不存在,或者已经过期,则正常执行节点函数,并将结果写入缓存。
默认缓存键函数是 default_cache_key。
其核心实现如下:
def default_cache_key(*args: Any, **kwargs: Any) -> str | bytes:
"""Default cache key function that uses the arguments and keyword arguments to generate a hashable key."""
import pickle
# protocol 5 strikes a good balance between speed and size
return pickle.dumps((_freeze(args), _freeze(kwargs)), protocol=5, fix_imports=False)
它会把节点输入参数经过处理后,使用 pickle 序列化为字节序列,并将后者作为缓存 Key。
5.5.4.2. ttl
ttl 表示缓存的存活时间,单位为秒。
例如:
CachePolicy(ttl=10)
表示缓存结果最多保存 10 秒。
如果设置为:
CachePolicy(ttl=None)
表示缓存不会因为时间原因自动过期。
不过在实际项目中,不建议随意设置永久缓存。
如果节点依赖外部状态,例如:
- 当前时间
- 远程 API 数据
- 数据库查询结果
- 用户权限
- 环境变量
- 模型版本
- 提示词版本
那么应该根据业务场景设置合适的 ttl,或者把这些变量纳入缓存 Key 的计算逻辑。
5.6. 全图默认配置
5.6.1. 使用要求
默认配置功能要求:
langgraph>=1.2
本课程当前环境为:
langgraph==1.1.2
因此不支持该特性,本节只做概念说明。
5.6.2. 核心思想
当多个节点都需要配置相同的执行策略时,如果每个节点都重复配置,繁琐且耗时。
例如:
builder.add_node(
"node_a",
node_a,
retry_policy=RetryPolicy(max_attempts=3)
)
builder.add_node(
"node_b",
node_b,
retry_policy=RetryPolicy(max_attempts=3)
)
builder.add_node(
"node_c",
node_c,
retry_policy=RetryPolicy(max_attempts=3)
)
在较新版本中,可以通过 set_node_defaults 为所有节点设置默认配置,即全图默认配置 Graph defaults。
默认配置可以包括:
retry_policytimeouterror_handlercache_policy
这样可以避免在每个 add_node 中重复相同配置。
本课程环境暂不支持,不展开实操。
参考官方文档
https://docs.langchain.com/oss/python/langgraph/fault-tolerance#graph-defaults
5.7. 小结
本章介绍了 LangGraph 中节点执行与容错相关的能力。
总结如下:
| 机制 | 机制类型 | 作用 | 当前课程是否重点讲解 |
|---|---|---|---|
重试机制 Retries |
节点容错机制 | 节点失败后自动重试 | 是 |
超时设置 Timeouts |
节点容错机制 | 限制单次节点执行时间 | 否,要求 langgraph>=1.2 |
错误处理 Error Handling |
节点容错机制 | 重试耗尽后执行兜底逻辑 | 否,要求 langgraph>=1.2 |
节点缓存 Node Cache |
节点执行优化机制 | 缓存节点历史运行结果 | 是 |
全图默认配置 Graph defaults |
默认策略配置 | 为所有节点设置默认执行策略 | 否,要求 langgraph>=1.2 |
实际使用时,可以按照以下原则选择:
- 临时性失败:优先使用
RetryPolicy。 - 外部服务可能卡住:使用
timeout控制单次节点执行时间。 - 失败后需要兜底恢复:使用
error_handler。 - 相同输入重复执行且成本较高:使用
CachePolicy。 - 多个节点使用相同策略:使用
set_node_defaults统一配置。
其中,重试机制 和 节点缓存 是本课程的重点。

浙公网安备 33010602011771号