LangGraph 实战:如何用图结构解决大模型应用的“状态管理”难题?

阅读建议: 本文约 11200 字,适合已使用过大语言模型(LLM)、LangChain 构建过简单 RAG/Agent 但遇到状态混乱、流程不可控、断点续传难等问题的开发者。我们将从痛点背景出发,剖析传统方法的缺陷,再引入 LangGraph 的核心概念,最后通过 3个完整的工业级场景代码实战(多轮对话检索增强 RAG、带工具调用的反思式 Agent、动态分支的多 Agent 协作系统),带你真正掌握 LangGraph 的核心逻辑与最佳实践。


引言

痛点引入:你是不是也在大模型应用里踩过这些“状态的坑”?

想象一下这个场景:你花了一周时间,用 LangChain 0.1.x 版本的 ConversationChain 搭配 ChromaDB 做了一个“法律合同多轮问答助手”——

  1. 第一轮对话还挺顺: 用户问“这份合同里的违约条款有哪些?”,系统检索出相关文本,LLM 准确提炼。
  2. 第二轮开始出问题: 用户追问“那如果甲方逾期付款30天,违约金怎么算?”——糟糕!ChromaDB 检索到的是另一份完全无关的房屋租赁合同的违约金,而不是之前那份供应链合同的!哦不对,不对,我明明把检索的 source 加在了 prompt 里?再看 prompt 历史,哦我的天,原来之前的多轮对话里用户提过“帮我找一份房屋租赁合同模板”,系统把那堆模板也扔进了上下文(ConversationBufferMemory存的太多了?),导致第二轮检索虽然源是对的,但 LLM 在长上下文中混淆了“用户这次提问的主体”和“之前提到的模板”。
  3. 第三轮彻底崩溃: 你赶紧想加个“当前处理文档ID”的变量来控制检索,但是 ConversationChain 没有地方存自定义变量啊?哦对了,LangChain 有 ConversationSummaryMemory 或者 ConversationBufferWindowMemory,但自定义变量怎么办?那换成 Agent 吧,Agent 可以有工具调用,比如加一个“设置当前文档ID”和“获取当前文档ID”的工具,但工具调用的返回值只能塞回 LLM 的 prompt 里,状态不是全局共享的——如果 Agent 在调用工具A的时候更新了文档ID,但在调用工具B的时候工具B的代码拿不到这个新ID,只能再从 prompt 里解析(而且解析很容易出错!比如 LLM 返回的工具调用参数里文档ID写错了格式,比如写成 123abc 而不是 UUID)。
  4. 第四轮你心态崩了: 为了调试状态,你想在某个步骤(比如用户输入验证后)暂停流程,检查一下当前的所有变量(当前文档ID、检索到的 chunks、之前的关键信息摘要),但 LangChain 0.1.x 的链和 Agent 都是黑盒式的线性执行,一旦启动就只能等到结果或者报错,根本没法断点调试!更别说,如果用户在第二轮对话的时候网络断了,用户刷新页面,之前的状态(除了你存到 Redis 里的 prompt 片段)全丢了——你要从 Redis 里把所有 prompt 片段读出来,重新构建 ConversationChain/Agent,重新执行所有步骤?那如果之前的步骤涉及到调用外部 API(比如第三方支付验证、大规模数据库查询),重新执行成本太高了啊!

这还只是一个简单的多轮 RAG 场景,如果换成更复杂的多 Agent 协作(比如产品经理 Agent 写需求→设计师 Agent 生成 UI 稿原型描述→前端工程师 Agent 写代码→测试工程师 Agent 写测试用例→最后迭代),或者带反思的复杂推理任务(比如先让 LLM 生成一个数学题的解法,然后让另一个“反思者” Agent 检查解法的错误,然后修正,再检查,直到正确为止),状态管理的问题会指数级放大:

  • 状态的一致性怎么保证?多个 Agent 同时更新同一个状态变量怎么办?
  • 状态的持久化怎么实现?要持久化哪些状态?什么时候持久化?怎么恢复?
  • 流程的可控性怎么提升?能不能根据当前的状态动态跳转分支?能不能在某个节点手动审核结果?
  • 调试的便利性怎么增强?能不能可视化整个流程的执行路径和每个节点的状态变化?

解决方案概述:为什么是 LangGraph?

如果你之前被这些问题折磨过,那恭喜你——LangGraph 就是为解决这些问题而生的!

LangGraph 是 LangChain 团队在 2024 年初推出的、专门用于构建 有状态、可控、可调试、可持久化的复杂 LLM 应用(尤其是 Agent 和多 Agent 系统)的图结构框架。它的核心思想是:把 LLM 应用的逻辑拆分成一个个“节点(Node)”,把节点之间的跳转逻辑拆分成“边(Edge)”,把所有的状态集中到一个“全局状态字典(State)”里统一管理

和传统的 LangChain 0.1.x 的线性 Chain/Agent 相比,LangGraph 有哪些革命性的优势呢?我们先列个核心对比表(后面会详细展开):

维度LangChain 0.1.x 线性 Chain/AgentLangGraph
执行方式严格线性(或有限的内置分支,比如 RouterChain)完全灵活的图结构(支持条件分支、循环、子图)
状态管理分散式(Chain 用 Memory、Agent 用 Prompt/临时变量)集中式全局状态字典(State Schema 定义类型,支持原子更新)
断点调试几乎不可能(黑盒式执行)原生支持(可以在任何节点暂停、检查、修改状态、手动恢复)
状态持久化需要自己实现(比如用 Redis 存 Memory)原生支持 Checkpointing(自动持久化状态、支持断点续传)
多 Agent 协作需要复杂的自定义 Chain/Agent 嵌套原生支持(每个 Agent 是一个节点,边定义协作逻辑)
动态分支/循环RouterChain 有限制,循环很难实现条件边(Conditional Edge)轻松实现分支,循环边(Loop Back Edge)轻松实现循环
可视化执行路径无原生支持原生支持(可以画出图的结构,也可以画出某次执行的路径)

最终效果展示(先剧透,后面会一步步实现)

在这篇文章里,我们会实现 3 个完整的工业级场景:

场景1:有状态的多轮对话检索增强 RAG(Stateful Multi-Turn RAG)
  • 效果: 可以记住用户当前处理的文档 ID、当前的查询意图(比如“分析违约条款”还是“查看付款流程”)、之前检索到的关键 chunks,下次用户提问时自动过滤无关的文档、优化检索词。
  • 可视化图结构:

    用户输入验证

    当前文档ID是否存在?

    请求用户提供文档ID

    查询意图识别与优化

    更新全局状态

    等待用户下一轮输入

    根据文档ID和优化后的查询词检索ChromaDB

    更新全局状态

    LLM生成回答

    更新全局状态

场景2:带工具调用的反思式数学解题 Agent(Reflective Math Agent with Tools)
  • 效果: 可以先调用计算器工具计算,然后让“反思者”LLM 检查解题步骤和结果,如果有错误就修正,再检查,直到正确为止(循环次数可配置)。
  • 可视化图结构:

    错误且未超限

    正确或超限

    用户输入数学题

    解题者Agent:生成解题思路

    需要调用工具吗?

    调用计算器工具

    更新全局状态

    反思者Agent:检查解题步骤和结果

    解题结果正确吗?循环次数是否超限?

    修正思路提示

    生成最终回答

    结束

场景3:动态分支的多 Agent 协作产品需求评审系统(Dynamic Branch Multi-Agent Product Requirement Review System)
  • 效果: 产品经理 Agent 先写需求,然后根据需求的类型(比如“新功能”还是“优化现有功能”)动态跳转分支:
    • 如果是“新功能”:先跳转到市场分析 Agent,再跳转到技术可行性 Agent,再跳转到成本评估 Agent,最后跳转到产品总监 Agent 审核;
    • 如果是“优化现有功能”:先跳转到用户反馈分析 Agent,再跳转到技术可行性 Agent,最后跳转到产品总监 Agent 审核;
  • 可视化图结构(简化版,完整版后面会画):

    新功能

    优化现有功能

    通过

    不通过

    产品经理Agent:写需求

    需求类型识别

    市场分析Agent

    用户反馈分析Agent

    技术可行性Agent

    新功能吗?

    成本评估Agent

    产品总监Agent:审核

    审核通过吗?

    生成最终评审报告

    返回产品经理Agent修改


准备工作

环境/工具

在开始实战之前,我们需要准备好以下开发环境和工具:

工具/库名称推荐版本说明
Python3.10 或 3.11LangGraph 的官方要求,避免版本兼容性问题
LangChain0.2.x 或 0.3.xLangGraph 是 LangChain 的子库,需要配套使用
LangGraph0.2.x(最新稳定版)本文的核心库
LangChain OpenAI0.2.x 或 0.3.x用于调用 OpenAI 的 GPT 模型和嵌入模型
LangChain Chroma0.2.x 或 0.3.x用于场景1的向量数据库
python-dotenv最新版用于加载环境变量(比如 OpenAI API Key)
ipython最新版(可选)用于交互式调试,也可以用 VS Code 的 Jupyter Notebook
安装步骤
  1. 创建虚拟环境(强烈推荐):

    # 用 conda 创建虚拟环境(推荐)
    conda create -n langgraph-demo python=3.11 -y
    conda activate langgraph-demo
    
    # 或者用 venv 创建虚拟环境
    python -m venv langgraph-demo
    # Windows 激活虚拟环境
    langgraph-demo\Scripts\activate
    # macOS/Linux 激活虚拟环境
    source langgraph-demo/bin/activate
    
  2. 安装依赖库:

    pip install --upgrade langchain langchain-openai langgraph langchain-chroma python-dotenv ipython
    
  3. 配置环境变量:
    在项目根目录下创建一个 .env 文件,内容如下:

    # OpenAI API Key(必须配置,从 https://platform.openai.com/api-keys 获取)
    OPENAI_API_KEY=sk-your-openai-api-key-here
    
    # OpenAI API Base(可选,如果你用的是国内的镜像站)
    OPENAI_API_BASE=https://api.openai.com/v1
    
    # OpenAI 模型名称(可选,默认是 gpt-4o-mini,便宜又好用)
    LLM_MODEL_NAME=gpt-4o-mini
    
    # OpenAI 嵌入模型名称(可选,默认是 text-embedding-3-small)
    EMBEDDING_MODEL_NAME=text-embedding-3-small
    

基础知识

在开始实战之前,你需要具备以下前置知识:

  1. 大语言模型(LLM)的基本原理: 知道什么是 prompt engineering、什么是工具调用(Function Calling/Tool Use)。
  2. LangChain 的基本用法: 知道什么是 ChatOpenAI、什么是 Chroma 向量数据库、什么是 PromptTemplate
  3. 图结构的基本概念: 知道什么是节点(Node)、边(Edge)、有向图(Directed Graph)。

如果你对这些知识不太熟悉,可以参考以下学习资源:


第一章:LangGraph 的核心概念与架构(打好基础,后面才能飞)

在开始实战之前,我们必须先搞清楚 LangGraph 的核心概念——只有把这些概念吃透了,后面写代码的时候才不会一头雾水,遇到问题也能快速定位和解决。

1.1 核心概念:从图的三要素到 LangGraph 的三要素

我们都知道,一个有向图由三个基本要素组成:节点(Vertex/Node)、边(Edge)、起点(Start Node)和终点(End Node)。LangGraph 也是基于这个基本原理构建的,但它为了适配 LLM 应用的场景,给这三个要素赋予了更具体的含义,还增加了一个最核心的要素:全局状态字典(State)

我们先来看 LangGraph 的官方定义的核心概念列表(后面会逐一详细展开):

LangGraph 核心概念说明
State(状态)全局共享的字典/数据类,用于存储整个应用的所有状态(比如用户输入、LLM 输出、工具调用结果、自定义变量)
State Schema(状态模式)定义 State 的结构(字段名、字段类型、字段的更新方式)的类型提示或 TypedDict
Node(节点)图中的一个逻辑单元,接收当前的 State 作为输入,执行特定的逻辑(比如调用 LLM、调用工具、更新 State),然后返回更新后的 State(或者返回 State 的一部分)
Edge(边)连接两个节点的有向箭头,定义节点之间的跳转逻辑(比如“从节点A跳转到节点B”、“根据 State 的某个字段值跳转到不同的节点”)
Start Node(起点)图的入口节点,LangGraph 会自动为我们创建一个名为 START 的起点
End Node(终点)图的出口节点,LangGraph 会自动为我们创建一个名为 END 的终点,也可以自定义多个终点
Conditional Edge(条件边)根据 State 的某个字段值或某个自定义函数的返回值,动态跳转到不同节点的边
Checkpoint(检查点)图在执行过程中某个时刻的 State 的快照,用于断点续传、调试和回滚
Checkpointer(检查点管理器)用于管理 Checkpoint 的组件,LangGraph 内置了基于内存的 MemorySaver,也支持基于 Redis、SQLite 的 Checkpointer
Subgraph(子图)嵌入到另一个图中的图,用于模块化复杂的逻辑
Compiled Graph(编译后的图)我们定义好节点和边之后,调用 compile() 方法编译出来的可执行的图对象

好,现在我们来逐一详细展开这些核心概念——这是本文最重要的部分之一,请务必认真阅读


1.1.1 State(状态):LangGraph 的“心脏”

如果说 LangChain 0.1.x 的 Chain/Agent 是“没有心脏的行尸走肉”(状态分散,难以管理),那么 State 就是 LangGraph 的“心脏”——所有的节点都围绕着 State 工作,所有的逻辑都依赖于 State,所有的结果都存储在 State 里。

什么是 State?

简单来说,State 就是一个全局共享的字典(或数据类),它贯穿整个图的执行过程,从起点到终点,所有的节点都可以读取它、修改它

举个简单的例子,在我们的场景1(有状态的多轮对话 RAG)中,State 可能包含以下字段:

# 这是一个简化版的 State(用 Python 的字典表示)
state = {
    "user_input": "那如果甲方逾期付款30天,违约金怎么算?",  # 当前用户的输入
    "current_doc_id": "123e4567-e89b-12d3-a456-426614174000",  # 当前处理的文档ID
    "query_intent": "供应链合同-违约条款-违约金计算",  # 当前的查询意图
    "retrieved_chunks": [{"page_content": "...", "metadata": {...}}],  # 检索到的关键chunks
    "llm_response": "根据合同第10.2条,甲方逾期付款30天的话,违约金是应付金额的0.5%/天...",  # LLM的输出
    "chat_history": [("用户之前的问题", "系统之前的回答")]  # 整个对话的历史
}
State Schema(状态模式):给 State 加上“紧箍咒”

在 Python 中,字典是动态的——你可以随时添加、删除、修改字段,这在简单的场景中很方便,但在复杂的 LLM 应用中,这可能会导致状态混乱(比如某个节点不小心把 current_doc_id 改成了 None,或者添加了一个拼写错误的字段 retreived_chunks)。

为了解决这个问题,LangGraph 要求我们定义一个 State Schema(状态模式)——也就是用 Python 的 TypedDictPydantic BaseModel 来定义 State 的结构(字段名、字段类型、字段的更新方式)。这样一来,LangGraph 就可以在编译和执行图的时候对 State 进行类型检查,避免状态混乱的问题。

在 LangGraph 中,最常用的 State Schema 定义方式是 TypedDict,因为它简单、轻量,而且和 LangGraph 的更新机制配合得很好。我们来看一下如何用 TypedDict 定义场景1的 State Schema:

from typing import TypedDict, List, Tuple, Annotated
from langchain_core.documents import Document
import operator

# 定义 State Schema
class State(TypedDict):
    # 用户输入:每次用户输入都会覆盖这个字段
    user_input: str
    # 当前处理的文档ID:如果有的话就用,没有的话就是 None
    current_doc_id: str | None
    # 当前的查询意图:LLM 识别出来的,会覆盖之前的
    query_intent: str | None
    # 检索到的关键 chunks:每次检索都会追加到这个列表里(而不是覆盖)
    # 注意:这里用了 Annotated 来指定字段的更新方式!!!这是 LangGraph 的一个核心特性
    # operator.add 表示:如果新的 State 里有这个字段,就把新的值和旧的值相加(对于列表来说就是追加)
    retrieved_chunks: Annotated[List[Document], operator.add]
    # LLM 的输出:每次都会覆盖
    llm_response: str | None
    # 整个对话的历史:每次都会追加(用户输入是第一个元素,系统回答是第二个元素)
    chat_history: Annotated[List[Tuple[str, str]], operator.add]

哦,对了!这里有一个 LangGraph 最核心、最容易被忽略但又最重要的特性State 字段的更新方式(Reducer)

在上面的代码中,我们用了 Annotated[List[Document], operator.add] 来定义 retrieved_chunks 字段——这里的 operator.add 就是这个字段的 Reducer(归约函数)

State 字段的更新方式(Reducer):怎么把新值合并到旧值里?

在 LangGraph 中,当一个节点执行完逻辑之后,它会返回一个更新字典(Update Dictionary)——这个字典里只包含需要更新的 State 字段,而不是整个 State。然后,LangGraph 会根据 State Schema 中定义的 Reducer(归约函数),把更新字典里的新值合并到旧的 State 里。

这一点和传统的编程方式完全不同——传统的编程方式是“修改全局变量”,而 LangGraph 是“返回更新字典,然后 LangGraph 自动根据 Reducer 合并”。这样做的好处是什么呢?

  1. 状态的一致性:所有的状态更新都是通过 LangGraph 统一管理的,避免了多个节点同时修改同一个变量导致的竞态条件(Race Condition)。
  2. 状态的可追溯性:每次状态更新都是一个增量更新,我们可以很容易地追溯到哪个节点在什么时候更新了哪个字段。
  3. 代码的简洁性:节点只需要返回需要更新的字段,不需要关心整个 State 的结构。

好,现在我们来详细讲一下 Reducer 的类型

在 LangGraph 中,Reducer 可以分为以下几类:

1. 覆盖式 Reducer(默认)

如果我们在 State Schema 中没有用 Annotated 指定 Reducer,那么默认的 Reducer 就是覆盖式 Reducer——也就是说,如果更新字典里有这个字段,就用新值覆盖旧值;如果更新字典里没有这个字段,就保留旧值。

比如,在我们的 State Schema 中,user_inputcurrent_doc_idquery_intentllm_response 都是覆盖式 Reducer。

举个例子:

# 旧的 State
old_state = {
    "user_input": "这份合同里的违约条款有哪些?",
    "current_doc_id": "123e4567-e89b-12d3-a456-426614174000",
    "query_intent": "供应链合同-违约条款-列举",
    "llm_response": "根据合同第10条,违约条款包括...",
    ...
}

# 节点返回的更新字典(只包含需要更新的字段)
update_dict = {
    "user_input": "那如果甲方逾期付款30天,违约金怎么算?",  # 覆盖旧的 user_input
    "query_intent": "供应链合同-违约条款-违约金计算",  # 覆盖旧的 query_intent
    "llm_response": None  # 覆盖旧的 llm_response 为 None
}

# LangGraph 合并后的新 State
new_state = {
    "user_input": "那如果甲方逾期付款30天,违约金怎么算?",  # 被覆盖了
    "current_doc_id": "123e4567-e89b-12d3-a456-426614174000",  # 保留旧值(因为更新字典里没有)
    "query_intent": "供应链合同-违约条款-违约金计算",  # 被覆盖了
    "llm_response": None,  # 被覆盖了
    ...  # 其他字段也保留旧值
}
2. 追加式 Reducer(operator.add 或自定义函数)

如果我们在 State Schema 中用 Annotated 指定了 Reducer 为 operator.add,那么对于列表类型的字段来说,就是追加式 Reducer——也就是说,如果更新字典里有这个字段,就把新值(列表)追加到旧值(列表)的后面;如果更新字典里没有这个字段,就保留旧值。

比如,在我们的 State Schema 中,retrieved_chunkschat_history 都是追加式 Reducer(用了 operator.add)。

举个例子:

from langchain_core.documents import Document
import operator

# 旧的 State
old_state = {
    "retrieved_chunks": [
        Document(page_content="合同第10条:违约条款", metadata={"source": "供应链合同.pdf", "page": 10})
    ],
    "chat_history": [
        ("这份合同里的违约条款有哪些?", "根据合同第10条,违约条款包括...")
    ],
    ...
}

# 节点返回的更新字典(只包含需要更新的字段)
new_chunk = Document(page_content="合同第10.2条:逾期付款违约金", metadata={"source": "供应链合同.pdf", "page": 10})
update_dict = {
    "retrieved_chunks": [new_chunk],  # 注意:这里是一个列表!因为 operator.add 是两个列表相加
    "chat_history": [
        ("那如果甲方逾期付款30天,违约金怎么算?", None)  # 注意:这里第二个元素是 None,后面 LLM 生成回答之后会更新
    ]
}

# LangGraph 合并后的新 State
new_state = {
    "retrieved_chunks": [
        Document(page_content="合同第10条:违约条款", metadata={"source": "供应链合同.pdf", "page": 10}),
        Document(page_content="合同第10.2条:逾期付款违约金", metadata={"source": "供应链合同.pdf", "page": 10})  # 被追加了
    ],
    "chat_history": [
        ("这份合同里的违约条款有哪些?", "根据合同第10条,违约条款包括..."),
        ("那如果甲方逾期付款30天,违约金怎么算?", None)  # 被追加了
    ],
    ...
}

注意:在使用 operator.add 作为列表的 Reducer 时,更新字典里的字段值必须是一个列表——哪怕你只需要追加一个元素,也要把它放在列表里(比如 [new_chunk])。

除了 operator.add,我们还可以自定义 Reducer 函数。比如,如果我们想把 retrieved_chunks 字段的最大长度限制在 5 个(避免 State 太大),我们可以自定义一个 Reducer 函数:

from typing import List
from langchain_core.documents import Document

# 自定义 Reducer 函数:限制列表的最大长度为 5
def limit_chunks(old_chunks: List[Document], new_chunks: List[Document]) -> List[Document]:
    # 先合并旧的和新的
    merged = old_chunks + new_chunks
    # 然后只保留最后 5 个
    return merged[-5:]

# 修改 State Schema,用自定义的 Reducer 函数
class State(TypedDict):
    ...
    retrieved_chunks: Annotated[List[Document], limit_chunks]
    ...

自定义 Reducer 函数的规则是:

  • 接收两个参数:第一个参数是旧的字段值,第二个参数是更新字典里的新字段值
  • 返回一个值:合并后的新字段值。

1.1.2 Node(节点):LangGraph 的“手脚”

如果说 State 是 LangGraph 的“心脏”,那么 Node 就是 LangGraph 的“手脚”——它接收当前的 State 作为输入,执行特定的逻辑(比如调用 LLM、调用工具、验证用户输入、更新 State),然后返回更新字典(Update Dictionary)。

什么是 Node?

简单来说,Node 就是一个 Python 函数(或类的方法),它的签名必须符合 LangGraph 的要求

LangGraph 要求 Node 函数的签名是什么样的呢?我们来看一下:

# Node 函数的标准签名
def node_function(state: State) -> dict:
    # 1. 从 state 中读取需要的字段
    user_input = state["user_input"]
    current_doc_id = state["current_doc_id"]
    ...

    # 2. 执行特定的逻辑(比如调用 LLM、调用工具、验证用户输入)
    ...

    # 3. 返回更新字典(Update Dictionary)——只包含需要更新的字段
    return {
        "query_intent": "供应链合同-违约条款-违约金计算",
        "retrieved_chunks": [new_chunk],
        ...
    }

注意:

  • Node 函数的第一个参数必须是 State(也就是我们定义的 State Schema 类型)。
  • Node 函数的返回值必须是一个字典——也就是更新字典(Update Dictionary),只包含需要更新的 State 字段。

好,现在我们来看一下场景1中的“用户输入验证节点”的代码示例:

# 场景1:用户输入验证节点
def validate_user_input_node(state: State) -> dict:
    """
    验证用户输入是否为空,如果为空就返回错误提示,否则就保留用户输入。
    """
    # 1. 从 state 中读取用户输入
    user_input = state["user_input"].strip()

    # 2. 验证用户输入
    if not user_input:
        # 用户输入为空,返回错误提示(存储在 llm_response 字段里,后面会显示给用户)
        return {
            "llm_response": "请输入有效的问题或指令!",
            "chat_history": [(user_input, "请输入有效的问题或指令!")]  # 追加到对话历史
        }
    else:
        # 用户输入有效,不需要更新任何字段(返回空字典即可)
        return {}

哦,对了!如果 Node 函数不需要更新任何 State 字段,那么它可以返回空字典,或者返回 None——LangGraph 会忽略空字典和 None。

Node 的类型

在 LangGraph 中,Node 可以分为以下几类:

  1. 普通节点(Normal Node):我们上面讲的就是普通节点——接收 State,返回更新字典。
  2. 工具节点(Tool Node):专门用于调用工具的节点——LangGraph 内置了 ToolNode 类,我们只需要把工具列表传给它即可,不需要自己写调用工具的逻辑。
  3. 条件节点(Conditional Node):不执行任何逻辑,只用于根据 State 的某个字段值或某个自定义函数的返回值跳转到不同的节点——不过,在 LangGraph 中,我们通常用 Conditional Edge(条件边) 来实现条件跳转,而不是用条件节点(因为条件边更简洁、更灵活)。
  4. 子图节点(Subgraph Node):把一个子图作为一个节点嵌入到另一个图中——用于模块化复杂的逻辑。

我们后面会在实战中详细讲 ToolNode 和子图节点。


1.1.3 Edge(边):LangGraph 的“神经”

如果说 State 是 LangGraph 的“心脏”,Node 是 LangGraph 的“手脚”,那么 Edge 就是 LangGraph 的“神经”——它连接节点和节点,定义节点之间的跳转逻辑。

什么是 Edge?

简单来说,Edge 就是一个有向箭头,从一个节点(源节点)指向另一个节点(目标节点),表示“源节点执行完之后,跳转到目标节点”

LangGraph 要求我们在定义图的时候,先添加所有的节点,然后再添加所有的边。

Edge 的类型

在 LangGraph 中,Edge 可以分为以下几类:

1. 普通边(Normal Edge)

普通边是最简单的边——它从一个固定的源节点指向一个固定的目标节点,表示“源节点执行完之后,无条件跳转到目标节点”。

我们用 graph.add_edge(source_node, target_node) 方法来添加普通边。

举个例子,在场景1中,“用户输入验证节点”执行完之后,无条件跳转到“当前文档ID检查节点”——这就是一条普通边:

# 先添加节点
graph.add_node("validate_user_input", validate_user_input_node)
graph.add_node("check_current_doc_id", check_current_doc_id_node)

# 再添加普通边:从 validate_user_input 跳转到 check_current_doc_id
graph.add_edge("validate_user_input", "check_current_doc_id")
2. 条件边(Conditional Edge)

条件边是 LangGraph 中最强大、最灵活的边——它从一个固定的源节点出发,根据 State 的某个字段值或某个自定义函数的返回值,动态跳转到不同的目标节点。

我们用 graph.add_conditional_edges(source_node, condition_function, edge_mapping) 方法来添加条件边。

其中:

  • source_node:源节点的名称(字符串)。
  • condition_function:条件函数——它接收当前的 State 作为输入,返回一个字符串(目标节点的名称)。
  • edge_mapping:边映射字典——可选参数,用于把条件函数的返回值映射成目标节点的名称(如果条件函数的返回值已经是目标节点的名称,那么这个参数可以省略)。

举个例子,在场景1中,“当前文档ID检查节点”执行完之后,我们需要根据 State 中的 current_doc_id 字段是否存在,跳转到不同的节点:

  • 如果 current_doc_id 存在(不是 None),就跳转到“查询意图识别与优化节点”;
  • 如果 current_doc_id 不存在(是 None),就跳转到“请求用户提供文档ID节点”。

我们来看一下如何用条件边实现这个逻辑:

# 1. 先添加所有需要的节点
graph.add_node("check_current_doc_id", check_current_doc_id_node)
graph.add_node("identify_query_intent", identify_query_intent_node)
graph.add_node("ask_for_doc_id", ask_for_doc_id_node)

# 2. 定义条件函数
def check_current_doc_id_condition(state: State) -> str:
    """
    条件函数:根据 current_doc_id 是否存在,返回目标节点的名称。
    """
    current_doc_id = state["current_doc_id"]
    if current_doc_id:
        # current_doc_id 存在,跳转到 identify_query_intent 节点
        return "identify_query_intent"
    else:
        # current_doc_id 不存在,跳转到 ask_for_doc_id 节点
        return "ask_for_doc_id"

# 3. 添加条件边:从 check_current_doc_id 出发,根据条件函数的返回值跳转
# 注意:这里 edge_mapping 可以省略,因为条件函数的返回值已经是目标节点的名称
graph.add_conditional_edges(
    source="check_current_doc_id",
    path=check_current_doc_id_condition
)

再举一个稍微复杂一点的例子:在场景3(多 Agent 协作产品需求评审系统)中,“需求类型识别节点”执行完之后,我们需要根据 State 中的 requirement_type 字段的值,跳转到不同的节点:

  • 如果 requirement_type 是 “new_feature”,就跳转到 “market_analysis_agent” 节点;
  • 如果 requirement_type 是 “optimize_existing_feature”,就跳转到 “user_feedback_analysis_agent” 节点;
  • 如果 requirement_type 是其他值,就跳转到 “invalid_requirement_type” 节点。

我们来看一下如何用条件边和 edge_mapping 实现这个逻辑:

# 1. 先添加所有需要的节点
graph.add_node("identify_requirement_type", identify_requirement_type_node)
graph.add_node("market_analysis_agent", market_analysis_agent_node)
graph.add_node("user_feedback_analysis_agent", user_feedback_analysis_agent_node)
graph.add_node("invalid_requirement_type", invalid_requirement_type_node)

# 2. 定义条件函数(注意:这里返回的是 requirement_type 的值,而不是目标节点的名称)
def identify_requirement_type_condition(state: State) -> str:
    """
    条件函数:返回 requirement_type 的值。
    """
    return state["requirement_type"]

# 3. 定义 edge_mapping:把 requirement_type 的值映射成目标节点的名称
edge_mapping = {
    "new_feature": "market_analysis_agent",
    "optimize_existing_feature": "user_feedback_analysis_agent",
    "other": "invalid_requirement_type"
}

# 4. 添加条件边:从 identify_requirement_type 出发,根据条件函数的返回值和 edge_mapping 跳转
graph.add_conditional_edges(
    source="identify_requirement_type",
    path=identify_requirement_type_condition,
    path_map=edge_mapping
)

注意:如果条件函数的返回值不在 edge_mapping 的键中,那么 LangGraph 会抛出一个错误——所以,我们通常会在 edge_mapping 中添加一个 “other” 或 “default” 的键,用于处理未知的返回值。

3. 循环边(Loop Back Edge)

循环边其实就是一条普通边或条件边,从一个节点指向它自己,或者指向它之前的某个节点——用于实现循环逻辑(比如反思式 Agent 的“生成→检查→修正→生成”循环)。

举个例子,在场景2(带工具调用的反思式数学解题 Agent)中,“反思者 Agent 节点”执行完之后,我们需要根据 State 中的 is_correct 字段和 loop_count 字段的值,跳转到不同的节点:

  • 如果 is_correct 是 True,或者 loop_count 大于等于 5,就跳转到 END 节点;
  • 如果 is_correct 是 False,并且 loop_count 小于 5,就跳转到“解题者 Agent 节点”(循环)。

我们来看一下如何用条件边实现这个循环逻辑:

# 1. 先添加所有需要的节点
graph.add_node("problem_solver_agent", problem_solver_agent_node)
graph.add_node("reflector_agent", reflector_agent_node)

# 2. 定义条件函数
def reflector_condition(state: State) -> str:
    """
    条件函数:根据 is_correct 和 loop_count 的值,返回目标节点的名称。
    """
    is_correct = state["is_correct"]
    loop_count = state["loop_count"]
    if is_correct or loop_count >= 5:
        # 解题结果正确,或者循环次数超限,跳转到 END
        return END
    else:
        # 解题结果错误,并且循环次数未超限,跳转到 problem_solver_agent(循环)
        return "problem_solver_agent"

# 3. 添加条件边:从 reflector_agent 出发,根据条件函数的返回值跳转
graph.add_conditional_edges(
    source="reflector_agent",
    path=reflector_condition
)

1.1.4 Start Node(起点)和 End Node(终点):LangGraph 的“入口”和“出口”

LangGraph 会自动为我们创建两个特殊的节点:

  1. START 节点:图的入口节点——所有的图都必须从 START 节点开始执行。
  2. END 节点:图的出口节点——当图跳转到 END 节点时,执行就结束了。

我们需要做的是:

  • 添加一条从 START 节点到我们的第一个业务节点的普通边。
  • 在适当的时机(比如业务逻辑执行完了),跳转到 END 节点。

举个例子,在场景1中,我们的第一个业务节点是“用户输入验证节点”——所以我们需要添加一条从 STARTvalidate_user_input 的普通边:

from langgraph.graph import StateGraph, START, END

# 创建图对象
graph = StateGraph(State)

# 添加业务节点
graph.add_node("validate_user_input", validate_user_input_node)
...

# 添加从 START 到第一个业务节点的普通边
graph.add_edge(START, "validate_user_input")
...

# 添加从最后一个业务节点到 END 的普通边
graph.add_edge("wait_for_next_input", END)

注意:STARTEND 是 LangGraph 内置的常量,我们不需要自己定义,直接导入使用即可。


1.1.5 Checkpoint(检查点)和 Checkpointer(检查点管理器):LangGraph 的“时光机”

如果说前面的概念是 LangGraph 的“硬件”,那么 Checkpoint 和 Checkpointer 就是 LangGraph 的“软件增值服务”——它让我们可以:

  1. 断点续传:如果图在执行过程中因为网络中断、程序崩溃等原因停止了,我们可以从最近的 Checkpoint 恢复执行,不需要重新执行所有的步骤。
  2. 调试:我们可以在任何节点暂停执行,检查当前的 State,修改 State,然后手动恢复执行。
  3. 回滚:我们可以回滚到之前的某个 Checkpoint,重新执行。
  4. 状态持久化:我们可以把 Checkpoint 存储到磁盘、Redis、SQLite 等持久化存储中,即使程序重启了,状态也不会丢失。
什么是 Checkpoint?

简单来说,Checkpoint 就是图在执行过程中某个时刻的 State 的快照——它包含了当前的 State、当前执行到的节点、以及一些元数据(比如执行时间、执行ID等)。

什么是 Checkpointer?

Checkpointer 是用于管理 Checkpoint 的组件——它负责:

  1. 保存 Checkpoint:在图执行到某个节点之前或之后,自动保存当前的 State 到持久化存储中。
  2. 加载 Checkpoint:从持久化存储中加载之前的 Checkpoint,恢复图的执行。
  3. 列出 Checkpoint:列出某个执行ID的所有 Checkpoint。
  4. 删除 Checkpoint:删除某个执行ID的所有 Checkpoint。

LangGraph 内置了几种 Checkpointer:

  1. MemorySaver:基于内存的 Checkpointer——Checkpoint 存储在程序的内存中,程序重启后就会丢失。适合用于开发和调试。
  2. SqliteSaver:基于 SQLite 的 Checkpointer——Checkpoint 存储在 SQLite 数据库文件中,程序重启后不会丢失。适合用于小型生产环境。
  3. PostgresSaver:基于 PostgreSQL 的 Checkpointer——Checkpoint 存储在 PostgreSQL 数据库中,适合用于大型生产环境。
  4. RedisSaver:基于 Redis 的 Checkpointer——Checkpoint 存储在 Redis 中,适合用于需要高性能的生产环境。

我们后面会在实战中详细讲如何使用 MemorySaverSqliteSaver


1.1.6 Compiled Graph(编译后的图):LangGraph 的“可执行文件”

当我们定义好所有的节点、边、Checkpointer 之后,我们需要调用 graph.compile() 方法来**

更多推荐