LangGraph 从状态图到可恢复 Agent:一篇入门与实践指南

文章目录

    • [1. LangGraph 解决什么问题](#1. LangGraph 解决什么问题)
    • [2. 核心心智模型:State、Node、Edge](#2. 核心心智模型:State、Node、Edge)
    • [3. 第一个可运行的状态图](#3. 第一个可运行的状态图)
      • [3.1 安装](#3.1 安装)
      • [3.2 定义状态、节点和边](#3.2 定义状态、节点和边)
      • [3.3 可视化图结构](#3.3 可视化图结构)
    • [4. State Schema 与 Reducer:状态设计的关键](#4. State Schema 与 Reducer:状态设计的关键)
      • [4.1 三种常见 Schema](#4.1 三种常见 Schema)
      • [4.2 默认覆盖与自定义合并](#4.2 默认覆盖与自定义合并)
      • [4.3 需要覆盖 reducer 时使用 `Overwrite`](#4.3 需要覆盖 reducer 时使用 Overwrite)
    • [5. 条件分支、并行与动态路由](#5. 条件分支、并行与动态路由)
      • [5.1 条件边](#5.1 条件边)
      • [5.2 用 `Send` 做 map-reduce](#5.2 用 Send 做 map-reduce)
      • [5.3 用 `Command` 同时更新状态并跳转](#5.3 用 Command 同时更新状态并跳转)
    • [6. Graph API 还是 Functional API](#6. Graph API 还是 Functional API)
    • [7. 持久化:让 Agent 可以暂停、恢复和记忆](#7. 持久化:让 Agent 可以暂停、恢复和记忆)
      • [7.1 Checkpointer 与 Store 的区别](#7.1 Checkpointer 与 Store 的区别)
      • [7.2 短期记忆与长期记忆](#7.2 短期记忆与长期记忆)
    • [8. Human-in-the-loop:在关键动作前暂停](#8. Human-in-the-loop:在关键动作前暂停)
    • [9. 流式输出与可观测性](#9. 流式输出与可观测性)
    • 结语

LangGraph 是 LangChain 生态中的低层编排框架与运行时。它不替你规定提示词或 Agent 架构,而是提供状态、节点、边、持久化、流式输出和人工介入等基础能力,让复杂工作流可以被显式描述、检查和恢复。

1. LangGraph 解决什么问题

一个简单的 LLM 调用可以写成"输入提示词,得到输出"。但真实应用通常还包含路由、工具调用、重试、并行、审核和跨轮次记忆。例如,一个研究助手可能需要:

  1. 判断问题属于哪个主题;
  2. 并行搜索多个来源;
  3. 汇总并检查证据;
  4. 在发送报告前请求人工确认;
  5. 进程重启后从上次成功的位置继续。

如果这些逻辑散落在一个巨大的 while 循环中,状态、异常和恢复会很难维护。LangGraph 把流程拆成可组合的节点和边,并在每个步骤之间保存状态快照。

官方对生态的定位可以概括为:LangChain 提供模型、工具和高层 Agent 抽象;LangGraph 提供底层编排运行时;LangSmith 负责追踪、评估和部署。刚开始构建普通工具调用 Agent 时,可以先使用 LangChain 的 create_agent;当需要精细控制流程、确定性步骤与模型步骤混合、持久化或人工介入时,再使用 LangGraph。

2. 核心心智模型:State、Node、Edge

LangGraph 图由三个基本要素组成:

  • State(状态):图运行过程中的共享数据结构,保存输入、上下文和中间结果。
  • Node(节点):一个可调用对象,读取当前状态并返回局部更新,通常是普通 Python 函数。
  • Edge(边):定义节点之间的流转,可以是固定边,也可以根据状态进行条件路由。

最小拓扑如下:

text 复制代码
START --> node_1 --> node_2 --> END

LangGraph 的运行时以 superstep(超步)推进:先根据边决定本轮要运行的节点,再执行节点,最后统一合并节点产生的更新。并行节点读取的是同一个本轮状态快照,它们的结果会在提交阶段通过对应的 reducer 合并。

3. 第一个可运行的状态图

3.1 安装

bash 复制代码
pip install -U langgraph

3.2 定义状态、节点和边

TypedDict 轻量、清晰,也最贴近"节点返回部分状态更新"的编程方式。没有特殊校验需求时,推荐优先使用它。

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

from langgraph.graph import END, START, StateGraph


class OverallState(TypedDict):
    # 每次节点返回的日志都会追加到列表,而不是覆盖旧值
    logs: Annotated[list[str], add]
    path: str


def node_1(state: OverallState):
    return {
        "logs": ["node_1 完成"],
        "path": state["path"] + " -> node_1",
    }


def node_2(state: OverallState):
    return {
        "logs": ["node_2 完成"],
        "path": state["path"] + " -> node_2",
    }


builder = StateGraph(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()
result = graph.invoke({"logs": [], "path": "START"})
print(result)

结果类似:

python 复制代码
{
    "logs": ["node_1 完成", "node_2 完成"],
    "path": "START -> node_1 -> node_2",
}

构建图始终分成三步:调用 StateGraph 定义图、调用 compile() 编译、调用 invoke()stream() 执行。STARTEND 是官方推荐的入口和终点写法。

3.3 可视化图结构

编译后的图可以导出 Mermaid:

python 复制代码
print(graph.get_graph().draw_mermaid())

在 Jupyter 中可直接展示 PNG:

python 复制代码
from IPython.display import Image, display

display(Image(graph.get_graph().draw_mermaid_png()))

可视化对调试条件分支、循环和并行路径尤其有帮助。生成 PNG 时使用的是 Mermaid 渲染服务,网络受限时优先导出 Mermaid 源码或使用本地渲染方案。

4. State Schema 与 Reducer:状态设计的关键

4.1 三种常见 Schema

LangGraph 支持 TypedDictdataclass 和 Pydantic 模型:

方式 特点 适用情况
TypedDict 轻量,仅提供类型提示 大多数工作流的默认选择
dataclass 以属性方式访问字段 希望使用对象语义的项目
Pydantic 运行时校验更强 输入边界需要严格验证

大型应用还可以拆分输入、输出和私有状态:

python 复制代码
class InputState(TypedDict):
    question: str


class OverallState(InputState):
    draft: str
    internal_notes: str


class OutputState(TypedDict):
    answer: str

这样可以限制外部输入和最终输出的字段,同时允许中间节点使用私有字段。状态 Schema 描述的是"哪些字段可以进入图和节点",节点返回的通常只是局部更新,不需要重建完整状态。

4.2 默认覆盖与自定义合并

没有 reducer 的字段采用覆盖策略:新值直接替换旧值。需要追加或聚合时,用 Annotated[类型, reducer] 声明合并函数。

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


class State(TypedDict):
    answer: str                         # 默认:覆盖
    logs: Annotated[list[str], add]     # 列表:追加合并

Reducer 是一个二元函数:(left, right) -> mergedleft 是当前值,right 是节点返回的新值。并行节点同时更新同一字段时,reducer 决定如何汇总结果,因此它必须满足类型兼容,并尽量保持确定性。

对消息历史,应使用官方的 add_messages,而不是简单的列表拼接:它会根据消息 ID 追加新消息,并用相同 ID 的新消息替换旧消息。

python 复制代码
from langgraph.graph import MessagesState


class ChatState(MessagesState):
    user_id: str

MessagesState 已经预置了 messages 字段和消息 reducer,适合聊天机器人、工具调用和 Agent 工作流。

4.3 需要覆盖 reducer 时使用 Overwrite

追加日志、消息历史很常见,但有时需要把聚合字段整体替换,例如加载一份人工编辑后的完整文档。此时可以使用 LangGraph 提供的 Overwrite 语义,明确表达"绕过 reducer,直接写入新值",避免依赖隐含的覆盖顺序。

5. 条件分支、并行与动态路由

5.1 条件边

固定边适合线性流程;条件边根据状态选择下一个节点:

python 复制代码
from typing import Literal
from typing_extensions import TypedDict


class RouteState(TypedDict):
    question: str


def route(state: RouteState) -> Literal["answer", "fallback"]:
    return "answer" if state["question"] else "fallback"


builder.add_conditional_edges(
    "classify",
    route,
    {"answer": "answer", "fallback": "fallback"},
)

路由函数只负责决定去哪里;业务结果仍由节点通过状态更新返回。这样能让控制流和数据处理各自清晰。

5.2 用 Send 做 map-reduce

当待处理项目数量运行时才知道,或者需要为每个项目创建独立输入,可以从路由函数返回多个 Send

python 复制代码
from typing_extensions import TypedDict
from langgraph.types import Send


class ResearchState(TypedDict):
    topics: list[str]


def fan_out(state: ResearchState):
    return [
        Send("research_one", {"topic": topic})
        for topic in state["topics"]
    ]


builder.add_conditional_edges("start_research", fan_out)

多个 research_one 会并行执行,结果再通过 reducer 汇总。聚合字段应提前定义可结合的 reducer,例如 Annotated[list[str], add]。不要依赖并行完成的先后顺序来决定业务结果。

5.3 用 Command 同时更新状态并跳转

当一个节点既要写状态又要动态决定下一节点,可以返回 Command

python 复制代码
from typing import Literal
from langgraph.types import Command
from typing_extensions import TypedDict


class ApprovalState(TypedDict):
    approved: bool
    status: str


def decide(state: ApprovalState) -> Command[Literal["publish", "revise"]]:
    if state["approved"]:
        return Command(update={"status": "ready"}, goto="publish")
    return Command(update={"status": "needs_revision"}, goto="revise")

如果只是路由,优先使用条件边;只有在"状态更新和路由必须绑定"时才使用 Command,这样图的拓扑更容易阅读。

6. Graph API 还是 Functional API

两套 API 共享同一个运行时,都支持检查点、恢复和人工介入,但表达方式不同:

对比项 Graph API Functional API
核心抽象 StateGraph、节点、边 @entrypoint@task
控制流 显式声明图结构 使用普通 if、循环和函数调用
可视化
更适合 复杂分支、并行、团队协作 线性流程、已有过程式代码、快速原型

Functional API 的最小形态如下:

python 复制代码
from langgraph.func import entrypoint, task


@task
def load_data(topic: str) -> str:
    return f"data for {topic}"


@entrypoint()
def workflow(topic: str) -> str:
    data = load_data(topic).result()
    return data.upper()

需要注意:为了支持检查点和恢复,entrypoint 的输入输出以及 task 的输出应当是可序列化数据;外部 API 调用、写文件、发邮件等副作用应封装在 task 中,并尽量设计成幂等操作。

7. 持久化:让 Agent 可以暂停、恢复和记忆

7.1 Checkpointer 与 Store 的区别

官方将持久化分成两层:

组件 保存内容 常见用途
Checkpointer 某个 thread 的图状态快照 短期记忆、故障恢复、人工介入、时间旅行
Store 应用自定义的长期键值数据 用户偏好、跨会话资料、业务知识

启用 checkpointer 后,调用时必须提供稳定的 thread_id

python 复制代码
from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {"configurable": {"thread_id": "user-42"}}
graph.invoke(input_state, config)

InMemorySaver 适合本地开发和测试,进程重启后数据会丢失。生产环境应根据部署方式选择 SQLite、PostgreSQL 等持久化实现,并规划 checkpoint 的清理策略,避免长对话无限增长。

7.2 短期记忆与长期记忆

同一个 thread_id 表示同一条会话线程,适合保存当前任务的工作上下文;跨线程共享的用户偏好、账号资料等,应放到 Store,而不是把所有数据塞进图状态。状态越大,序列化、传输和模型上下文成本越高。

8. Human-in-the-loop:在关键动作前暂停

interrupt() 可以暂停图执行,并把一个 JSON 可序列化的 payload 暴露给调用方。用户做出决定后,通过同一个 thread_id 使用 Command(resume=...) 继续:

python 复制代码
from typing import Literal
from typing_extensions import TypedDict
from langgraph.types import Command, interrupt


class ReviewState(TypedDict):
    recipient: str
    content: str


def approval(state: ReviewState) -> Command[Literal["send", "cancel"]]:
    approved = interrupt({
        "question": "是否发送邮件?",
        "recipient": state["recipient"],
        "preview": state["content"],
    })
    return Command(goto="send" if approved else "cancel")


# 第一次调用:运行到 interrupt 后暂停
result = graph.invoke(input_state, config)

# 人工确认后恢复
result = graph.invoke(Command(resume=True), config)

人工介入必须配合 checkpointer。节点恢复时会从节点边界重新执行,因此发送邮件、扣款等副作用必须封装为幂等任务,避免恢复导致重复执行。循环等待输入时,不要在同一个节点里用 while True 反复调用 interrupt();让每次恢复对应一次清晰的节点执行,更容易测试和追踪。

9. 流式输出与可观测性

invoke() 适合一次性拿到最终结果;长流程或聊天 UI 通常使用 stream()

python 复制代码
for chunk in graph.stream(
    {"messages": [{"role": "user", "content": "你好"}]},
    stream_mode="updates",
):
    print(chunk)

常用模式包括:

  • updates:每个节点产生的状态更新;
  • values:每个步骤后的完整状态;
  • messages:模型消息的增量 token;
  • custom:节点主动写出的自定义进度信息。

生产环境建议接入 LangSmith 追踪节点输入输出、状态变化和耗时。先让每个节点职责单一、输出结构稳定,再通过追踪定位慢调用、错误路由和不必要的模型调用。

结语

LangGraph 的核心不是某个"万能 Agent 模板",而是一套可靠的状态化编排模型:状态承载上下文,节点执行局部工作,边控制流转,reducer 决定并发更新如何合并,checkpointer 让执行可以恢复,interrupt 让人可以在关键点介入。

当应用从一次性问答走向长流程、多步骤和可审计的 Agent 时,这些原语比继续堆叠提示词更重要。先把状态和控制流设计清楚,再把模型放进需要推理的节点,通常能得到更容易调试、测试和上线的系统。

相关推荐
赵广陆19 小时前
企业实战:数据图与状态定义
pycharm·langchain·langgraph
AI大佬的小弟1 天前
大模型名词精讲14:Multi-Agent(多智能体协作)
多智能体·multiagent·langgraph·ai协作·ai入门·a2a·大模型基础概念
赵广陆3 天前
企业实战:主体识别
langchain·pdf·langgraph
刀锋00014 天前
从0到1手搓生产级 AI Agent:LangGraph 1.2 + LangChain 1.3 保姆级实战(全部代码已跑通)
人工智能·python·langchain·ai agent·langgraph
Joy T7 天前
Agent 开源项目全景解析(下):LlamaIndex、Dify、FastGPT 与真实工程选型
langchain·开源·框架·agent·springai·langgraph·mcp
寻道码路9 天前
大模型工程化实战(一):概率坍塌的救赎 - 给LLM输出加锁
大模型·agent·langgraph·ai工程化·llm确定性
weixin_4713830310 天前
07 LangGraph 集成 RAG
python·langchain·agent·langgraph
Tbisnic12 天前
LangChain的 六大核心组件与 RAG 知识库构建
人工智能·python·ai·langchain·rag·langgraph
zl_dfq12 天前
LangGraph 之 【持久化能力】(线程级\跨会话持久化、重放更新状态、get_state、PostgresSaver、PostgreasStore)
langgraph