Agent 跑到一半崩了,怎么从上一步接着跑?LangGraph StateGraph 实战:状态持久化、断点恢复与人工审批

如果 Agent 只回答一句话,直接调用模型通常够用;一旦任务要查资料、生成方案、等待审批、再调用外部系统,就得回答一个工程问题:进程重启后,任务还能不能接着跑?本文用 LangGraph StateGraph 搭一条可暂停、可恢复的流程,并说明检查点能解决什么、不能解决什么。

写在前面

Agent 最容易让人误以为"已经完成"的时刻,往往是它刚刚说完"正在处理"。实际任务可能还在查数据、等待人工确认,或者调用外部接口时发生了超时。

如果整个流程只放在一个 Python 函数里,服务进程一重启,内存里的中间结果就没了。用户只能重新提交任务;重试时还可能把已经完成的副作用再做一次。

这篇文章用一个"生成并发布系统变更方案"的例子拆解问题:

text 复制代码
接收需求 → 读取当前状态 → 生成变更计划 → 等待人工审批 → 执行变更 → 返回结果

我们用 LangGraph 的 StateGraph 表达步骤,用 checkpointer 保存线程状态,用 interrupt() 暂停并收集人工输入。示例里的执行动作是模拟函数,不会操作真实系统。真正上线时还要给外部动作加幂等键、权限校验和审计记录。

一、先分清 State、Checkpoint 和 Thread ID

三个概念容易混在一起:

  • State:当前工作流共享的数据,例如用户需求、计划、审批结果和执行状态。
  • Checkpoint:某个线程在图执行过程中的状态快照,用于恢复、回看和人工介入。
  • Thread ID :检查点的索引。同一个任务恢复时必须使用同一个 thread_id;换一个 ID 就是新任务。

LangGraph 官方文档把 checkpointer 用于线程级的短期状态和故障恢复;跨线程长期保存用户资料则是 Store 的职责,两者不要混为一谈。Persistence 文档

先看整体结构:

text 复制代码
START
  │
  ▼
读取请求 → 生成计划 → 人工审批(interrupt)
                               │
                    ┌──────────┴──────────┐
                    ▼                     ▼
                  拒绝结束              执行动作
                                          │
                                          ▼
                                         END

每个节点只负责一小步,状态通过 State 在节点间传递。这样不仅方便画流程,也能明确哪一步失败、哪一步需要重试、哪一步必须等待人。

二、安装依赖并定义最小状态

当前示例使用 Python 3.10+、LangGraph Python 包。建议在虚拟环境中安装:

bash 复制代码
python -m venv .venv
# Windows PowerShell
.venv\Scripts\Activate.ps1
# macOS / Linux
# source .venv/bin/activate

pip install -U langgraph

用 TypedDict 描述图状态。这里设置 total=False,让每个节点只返回自己负责更新的字段:

python 复制代码
from typing_extensions import TypedDict


class ChangeState(TypedDict, total=False):
    request: str
    current_snapshot: str
    plan: list[str]
    approved: bool
    result: str

State 里保存原始数据和必要的运行结果即可。不要把可以从其他字段推导出来的大段 Prompt、重复日志和临时格式化文本都塞进去。状态越臃肿,检查点就越大,也越难排查。

三、把工作流拆成节点

下面用简单函数模拟读取状态和生成计划。接入真实项目时,inspect_system 可以查内部 API,make_plan 可以调用模型;图结构和恢复逻辑不需要因此改变。

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


def inspect_system(state: ChangeState) -> dict:
    # 示例用固定值代替真实环境查询
    return {"current_snapshot": "服务 api-gateway 当前为 3 个副本,版本 v1.8.2"}


def make_plan(state: ChangeState) -> dict:
    request = state["request"]
    snapshot = state.get("current_snapshot", "未知")

    # 实际使用时可在此调用 Chat Model,返回结构化计划
    plan = [
        f"需求:{request}",
        f"当前状态:{snapshot}",
        "先将 1 个副本升级到目标版本并检查健康状态",
        "确认无异常后,再逐步升级剩余副本",
        "失败时停止后续步骤并保留当前状态供人工处理",
    ]
    return {"plan": plan}

此处故意把"生成计划"和"执行变更"拆开。模型给出方案不等于动作已经发生;是否批准以及批准后执行什么,由服务端状态和业务逻辑决定。

四、在危险动作前暂停,等待人工审批

LangGraph 的 interrupt() 会暂停图,使用 checkpointer 保存状态,并把 JSON 可序列化的审批信息返回给调用方。审批结束后,用相同 thread_id 和 Command(resume=...) 恢复图。Interrupts 文档

python 复制代码
from langgraph.types import interrupt


def approval_gate(state: ChangeState) -> dict:
    decision = interrupt({
        "type": "change_approval",
        "request": state["request"],
        "plan": state["plan"],
        "question": "是否批准执行这项变更?",
    })

    return {"approved": bool(decision)}


def route_after_approval(state: ChangeState) -> str:
    return "execute_change" if state.get("approved") else "rejected"


def execute_change(state: ChangeState) -> dict:
    # 这里只做模拟,不调用真实系统
    return {"result": "模拟执行完成:变更步骤已通过健康检查"}


def rejected(state: ChangeState) -> dict:
    return {"result": "审批未通过:没有执行变更"}

注意一个容易遗漏的细节:恢复时,包含 interrupt() 的节点会从节点开头重新运行。 所以 interrupt() 之前不要做不可逆副作用;审批恢复后再执行的外部操作,也要能处理重复请求。Interrupts 文档的恢复说明

五、编译图并启用检查点

把节点和边连起来,然后在编译时传入 checkpointer:

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


builder = StateGraph(ChangeState)
builder.add_node("inspect_system", inspect_system)
builder.add_node("make_plan", make_plan)
builder.add_node("approval_gate", approval_gate)
builder.add_node("execute_change", execute_change)
builder.add_node("rejected", rejected)

builder.add_edge(START, "inspect_system")
builder.add_edge("inspect_system", "make_plan")
builder.add_edge("make_plan", "approval_gate")
builder.add_conditional_edges(
    "approval_gate",
    route_after_approval,
    {
        "execute_change": "execute_change",
        "rejected": "rejected",
    },
)
builder.add_edge("execute_change", END)
builder.add_edge("rejected", END)

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

InMemorySaver 适合本地演示和快速验证流程,数据只在进程内存中。服务进程退出后,检查点也会消失;生产环境要换成持久化 checkpointer,例如 PostgreSQL。官方持久化说明

六、运行一次任务,再用同一个 ID 恢复

第一次调用会运行到审批节点并暂停:

python 复制代码
from langgraph.types import Command

config = {"configurable": {"thread_id": "change-2026-001"}}

paused = graph.invoke(
    {"request": "将 api-gateway 升级到 v1.9.0"},
    config=config,
)

print(paused["__interrupt__"])

暂停结果里包含交给审批界面的内容。真实 API 可以把它序列化为待审批卡片,包含请求、计划、风险说明和任务 ID。

用户批准后,使用相同配置恢复:

python 复制代码
resumed = graph.invoke(Command(resume=True), config=config)
print(resumed["result"])

拒绝时传 Command(resume=False)。如果误用了新的 thread_id,LangGraph 会创建另一条新线程,而不是恢复原审批任务。

完整的关键链路可以概括为:

text 复制代码
thread_id + checkpointer = 可定位的状态快照
interrupt()             = 暂停点和外部输入边界
Command(resume=...)     = 带着同一线程状态继续执行

七、断点恢复不等于外部操作"只执行一次"

检查点保存的是图状态,不会自动把数据库、云平台、支付系统或设备网关的副作用包进同一个事务。比如外部 API 已经执行成功,但应用在写入"完成"状态前断电,恢复后仍可能再次调用接口。

所以外部动作要有自己的幂等设计:

  1. 为每个业务动作生成稳定的 action_id,不要每次重试都换新 ID;
  2. 调用下游 API 时把 action_id 作为幂等键;
  3. 将动作状态做成 PENDING → RUNNING → SUCCEEDED / FAILED;
  4. 执行前用数据库条件更新抢占任务,只有一个 worker 能从 PENDING 进入 RUNNING;
  5. 记录审批人、审批时间、请求摘要、下游返回码和重试次数。

示意 SQL:

sql 复制代码
UPDATE change_action
SET status = 'RUNNING', started_at = CURRENT_TIMESTAMP
WHERE action_id = :action_id
  AND status = 'PENDING';

只有更新行数为 1 的 worker 才能继续执行。下游若支持幂等键,传同一个 action_id;若不支持,就需要在业务层实现去重或人工对账。LangGraph 的持久化能力与业务动作幂等是两层保障,不能相互替代。

八、常见故障怎么定位

现象 常见原因 检查方向
恢复后像新任务一样从头开始 thread_id 不一致,或 checkpointer 没有持久化 比较创建、暂停、恢复时的 thread ID;检查 checkpointer 类型
服务重启后找不到任务 使用了 InMemorySaver 换持久化 checkpointer,并在重启后用同一 thread ID 加载
审批后某个查询又执行了一次 interrupt() 所在节点恢复时从节点开头重跑 将纯查询缓存或幂等化;避免把副作用放在 interrupt 前
下游动作重复执行 图 checkpoint 不等于下游事务 action ID、条件状态迁移、下游幂等键和操作日志
状态越来越大 把完整历史、文件内容和重复派生数据都留在 State 仅保留恢复所需字段,大对象放数据库或对象存储,State 保存引用

排查时优先记录 thread_id、节点名、checkpoint 时间、action ID 和错误类型。日志不要写密钥、完整用户隐私数据或不必要的 Prompt 原文。

总结

Agent 工作流需要从"一个长函数"变成可以检查、暂停、恢复的状态机:

  • StateGraph 将流程拆成可观察的节点和边;
  • Checkpointer 保存线程级状态,thread_id 用来找回对应任务;
  • interrupt() 为人工输入建立明确暂停点;
  • InMemorySaver 只适合演示,进程重启后不能保留状态;
  • 检查点不能保证外部副作用只发生一次,执行端仍要设计幂等和审计。

这样设计之后,服务重启就不必等于任务丢失,审批也不再是聊天框里的一句"确定吗"。但要记住,恢复机制恢复的是工作流状态;对外部世界造成的改变,还需要应用自己的事务、幂等与权限边界。

参考资料


相关推荐
略略略咯咯1 小时前
33333
开发语言·windows·python
java_prinln1 小时前
11. 说说 Single-Agent 和 Multi-Agent 的设计方案?
python
垆边人似月.1 小时前
第三篇:函数、文件与异常处理 / Functions, Files & Exception Handling
开发语言·python
我的xiaodoujiao1 小时前
Django 权限系统项目实战详细图文教程 4--系统登录功能实现 2
后端·python·django
鬼手点金1 小时前
缓存作为架构约束
开发语言·前端·人工智能·python·microsoft·缓存·架构
恩喜玛生物2 小时前
AIDD系列02:ProteinMPNN序列设计,从已知骨架到可检验的肽候选
python·ai
FelixZhang0282 小时前
不懂金融或不会编程,如何开始量化?从策略搭建到回测验证
人工智能·python·算法·数学建模·均值算法·自然语言处理·金融
打工仔折腾 AI2 小时前
后端部署避坑:吃透IPv4地址、网络接口与CIDR前缀的实际应用
arm开发·人工智能·后端·python·性能优化
wangchunyu11413 小时前
JAVA CompletableFuture异步编程实战
java·开发语言·python