实战 SOP
实战 SOP

LangGraph 生产级 Agent 搭建 SOP:超时、重试与容错

用 LangGraph 1.2 搭建生产级 Agent 的完整 SOP:从 typed state 与 checkpointer 持久化、per-node timeout 防卡死,到 RetryPolicy 重试、error_handler 兜底、interrupt 人工审批。附完整 Python 代码、参数速查表与踩坑记录。

发布于 2026年8月1日9 分钟阅读
<!-- langgraph-production-agent-sop | sop | LangGraph 生产级 Agent 搭建 SOP:超时、重试与容错 -->

搭 AI Agent 最容易翻车的不是 prompt 写不好,是跑起来就崩:调 LLM API 超时整条链断掉、工具异常没有兜底直接报错、跑到一半进程被杀状态全丢、人工审核节点卡住没法暂停。这些在 demo 里不会暴露,一上生产就连环爆。LangGraph 1.x 把这些问题拆成了可配置的工程参数——每个节点独立设超时、重试策略和错误处理器,全局用 checkpointer 持久化状态,用 interrupt 做人工审批断点。

这篇 SOP 走一遍用 LangGraph 搭生产级 Agent 的全流程:定义 typed state、挂 checkpointer 保状态、给节点配 TimeoutPolicy 防卡死、用 RetryPolicy 自动重试、挂 error_handler 做 fallback、用 interrupt 做人工审批。每步附真实 Python 代码和参数表,最后是完整可运行的 Agent 示例加踩坑记录。以下 API 基于 LangGraph 1.2,可直接对照官方文档跑通。


一、为什么选 LangGraph:durable state 是地基

生产级 Agent 和 demo Agent 的根本区别在一个词:durable state(持久化执行状态)。普通 Agent 脚本跑一半崩了,所有中间结果丢失,得从头来。LangGraph 的 StateGraph 把每一步的执行状态持久化到 checkpointer,崩了能从断点恢复,长时运行的任务能跨进程、跨机器续跑。

据独立 2026 对比(2000-run,5 任务×100 次),LangGraph 在延迟和 token 成本可预测性上表现最好——每个 LLM call 是离散已知量,不像有些框架把多步调用揉成黑盒。在 LangGraph、CrewAI、AutoGen(AG2)三者中,LangGraph 被评为「最生产就绪」,原因就是 durable execution + 细粒度错误处理 + 可观测性这三件套齐了。

LangGraph v0.4(2026-04)进一步改进了 state persistence 和 human-in-the-loop checkpoint,到 1.x 这些特性已稳定。下面六步搭一个完整的生产级 Agent。


二、核心:StateGraph 与 typed state

LangGraph 的中心是 StateGraph——一个有向图,每个节点读写一个共享的 typed state。先定义 state:

python
from typing import Annotated
from typing_extensions import TypedDict
import operator

class AgentState(TypedDict):
    messages: Annotated[list, operator.add]  # reducer: 追加而非覆盖
    query: str
    research: str
    draft: str
    approved: bool

Annotated[list, operator.add] 告诉 LangGraph:多个节点往 messages 里写时,用追加(operator.add)合并而不是后写覆盖先写。这是 typed state 的关键——每个字段的合并策略是显式声明的,不会出现状态竞争。

定义好 state 后搭图骨架:

python
from langgraph.graph import StateGraph, START, END

builder = StateGraph(AgentState)
# 节点稍后添加
builder.add_edge(START, "research")
builder.add_edge("research", "draft")
builder.add_edge("draft", "review")

这定义了执行顺序:research → draft → review。现在往里填生产特性。


三、durable state:checkpointer 保状态

让图「崩了能恢复」靠的是 checkpointer。compile 时传入一个 checkpointer,LangGraph 会自动在每个节点执行后保存状态快照(checkpoint):

python
from langgraph.checkpoint.memory import InMemorySaver

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

InMemorySaver 把状态存在内存里,适合开发和测试。生产环境需要持久化存储——LangGraph 支持 SqliteSaver 和 PostgresSaver(需单独安装 langgraph-checkpoint-sqlite / langgraph-checkpoint-postgres),状态存数据库,进程崩了重启能从最后一个 checkpoint 恢复。

调用图时传 thread_id,这是状态的唯一标识,同一个 thread_id 的多次调用共享状态:

python
import uuid

config = {"configurable": {"thread_id": str(uuid.uuid4())}}
result = graph.invoke({"query": "对比 LangGraph 和 CrewAI", "messages": []}, config)

四、per-node timeout:防卡死

LLM API 调用卡住是生产环境最常见的故障。LangGraph 1.x 给 add_node 加了 timeout 参数,可以给每个节点单独设硬限。

最简单的用法——传一个数字,设 wall-clock 硬限(秒):

python
async def draft_node(state: AgentState) -> AgentState:
    # 调 LLM 生成回复
    ...

builder.add_node("draft", draft_node, timeout=30)  # 30 秒硬限

更精细的控制用 TimeoutPolicy,同时设 wall-clock 超时和 idle 超时:

python
from langgraph.types import TimeoutPolicy

builder.add_node("research", research_node, timeout=TimeoutPolicy(
    run_timeout=60,    # wall-clock 硬限:整个节点最多跑 60 秒
    idle_timeout=15,   # idle 超时:15 秒无进度推进就超时,进度信号会重置计时
    refresh_on="auto",  # 自动监听回调事件刷新 idle 计时(默认值)
))

run_timeoutidle_timeout 的区别:run_timeout 是不管怎样到时间就掐的总时限;idle_timeout 在节点有进度信号时(比如流式输出收到一个 chunk)会重置,适合「偶尔卡住但通常在动」的长任务。超时后 LangGraph 抛 NodeTimeoutError,如果有 retry_policy 则自动决定是否重试。

关键限制:timeout 只对 async 节点生效。同步节点用的是 time.sleep / CPU 密集操作,没法在进程内安全取消。所以生产环境节点统一用 async def


五、重试策略:RetryPolicy

API 偶发失败(限流、网关 5xx)是常态,不能让它崩整条链。add_noderetry_policy 参数接一个 RetryPolicy

python
from langgraph.types import RetryPolicy

builder.add_node("research", research_node,
    timeout=TimeoutPolicy(run_timeout=60, idle_timeout=15),
    retry_policy=RetryPolicy(
        max_attempts=3,           # 最多尝试 3 次(含首次)
        initial_interval=1.0,     # 首次重试前等 1 秒
        backoff_factor=2.0,       # 每次重试间隔翻倍:1s → 2s → 4s
        max_interval=128.0,      # 重试间隔上限 128 秒
        jitter=True,              # 加随机抖动,避免重试风暴
    ),
)

retry_on 参数控制哪些异常才重试。LangGraph 的默认策略 default_retry_on 的行为是:

  • 重试ConnectionError、HTTP 5xx 响应(httpx.HTTPStatusError / requests.HTTPError 状态码 500-599)
  • 不重试ValueErrorTypeErrorKeyErrorLookupError)、OSError 等程序逻辑错误

这个默认行为很合理——5xx 是服务端临时故障值得重试,ValueError 是你的代码 bug 重试也没用。如果你要自定义,传一个判断函数:

python
from langgraph.errors import NodeTimeoutError

def retry_on_timeout_or_api_error(exc: Exception) -> bool:
    """超时和 API 错误才重试,其他直接崩。"""
    if isinstance(exc, NodeTimeoutError):
        return True
    return False

builder.add_node("research", research_node,
    retry_policy=RetryPolicy(max_attempts=3, retry_on=retry_on_timeout_or_api_error),
)

六、错误处理器:error_handler 做 fallback

重试到上限还失败怎么办?error_handler 参数让你指定一个兜底节点,原始节点抛异常时自动路由过去,不崩整图:

python
async def research_fallback(state: AgentState) -> AgentState:
    """research 节点连续失败后的兜底逻辑。"""
    return {
        "research": "外部数据源不可用,使用缓存摘要。",
        "messages": [{"role": "system", "content": "research 降级为缓存模式"}],
    }

builder.add_node("research", research_node,
    timeout=TimeoutPolicy(run_timeout=60, idle_timeout=15),
    retry_policy=RetryPolicy(max_attempts=3),
    error_handler=research_fallback,  # 3 次重试全失败 → 走这里
)

error_handler 接收的是一个节点函数(callable),它的返回值会写回 state,图继续往下执行。这比 try/except 包裹节点函数干净——错误处理逻辑和业务逻辑分离,图结构上也能看到 fallback 路径。


七、结构化输出与自纠:conditional edges

生产 Agent 需要结构化输出——不是让 LLM 自由发挥,而是约束它按 schema 返回。typed state 本身就是 schema:每个节点返回 AgentState 的部分字典,字段类型是声明的。

要加自纠循环(输出质量不够时回去重做),用 add_conditional_edges 做动态路由:

python
def should_approve(state: AgentState) -> str:
    """审核节点:通过则结束,不通过则回 draft 重写。"""
    return END if state["approved"] else "draft"

builder.add_conditional_edges("review", should_approve)

path 函数返回的字符串是下一个节点名(或 END),add_conditional_edges 据此路由。这构成了一个 draft → review → (不通过) → draft 的自纠循环,直到审核通过才结束。


八、human-in-the-loop:interrupt 审批断点

有些操作(发邮件、执行交易、删除数据)必须人工确认。LangGraph 的 interrupt 函数能在节点里暂停图执行,等人工输入后用 Command(resume=...) 恢复:

python
from langgraph.types import interrupt, Command

def review_node(state: AgentState) -> AgentState:
    # 暂停图执行,把草稿发给人工审核
    answer = interrupt({
        "type": "approval_request",
        "draft": state["draft"],
        "message": "请审核以上回复,回复 yes 批准、no 打回重写。",
    })
    # interrupt 在首次调用时抛 GraphInterrupt,暂停执行
    # 人工用 Command(resume=...) 恢复后,answer 接收人工输入
    return {"approved": answer == "yes"}

恢复执行:

python
# 第一次 invoke 触发 interrupt,图暂停
for chunk in graph.stream({"query": "...", "messages": []}, config):
    print(chunk)
# 输出: {'__interrupt__': (...)}

# 人工审核后恢复
for chunk in graph.stream(Command(resume="yes"), config):
    print(chunk)
# approved=True → 图继续执行到 END

关键前提:interrupt 必须配 checkpointer,因为暂停时状态要持久化存下来,恢复时才能读回来。没有 checkpointer 的图不能用 interrupt。


九、完整生产级 Agent 代码

把以上六个特性组合成一个完整可运行的 Agent:

python
import uuid
import operator
from typing import Annotated
from typing_extensions import TypedDict

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command, RetryPolicy, TimeoutPolicy
from langgraph.errors import NodeTimeoutError

# ---- 1. Typed State ----
class AgentState(TypedDict):
    messages: Annotated[list, operator.add]
    query: str
    research: str
    draft: str
    approved: bool

# ---- 2. Nodes(全部 async,timeout 才生效)----
async def research_node(state: AgentState) -> AgentState:
    """调用搜索 API 获取信息。"""
    # 实际替换为你的搜索工具调用
    result = f"关于「{state['query']}」的搜索结果..."
    return {"research": result, "messages": [{"role": "tool", "content": result}]}

async def draft_node(state: AgentState) -> AgentState:
    """调用 LLM 生成回复草稿。"""
    # 实际替换为你的 LLM 调用
    draft = f"基于「{state['research']}」生成的回复..."
    return {"draft": draft, "messages": [{"role": "assistant", "content": draft}]}

def review_node(state: AgentState) -> AgentState:
    """人工审核断点:暂停等待审批。"""
    answer = interrupt({"draft": state["draft"], "msg": "批准 yes / 打回 no"})
    return {"approved": answer == "yes"}

# ---- 3. Error Handler(fallback 节点)----
async def research_fallback(state: AgentState) -> AgentState:
    return {"research": "数据源不可用,使用缓存。"}

# ---- 4. 自定义重试判断 ----
def retry_on_timeout(exc: Exception) -> bool:
    return isinstance(exc, NodeTimeoutError)

# ---- 5. 构建图 ----
builder = StateGraph(AgentState)

builder.add_node("research", research_node,
    timeout=TimeoutPolicy(run_timeout=60, idle_timeout=15),
    retry_policy=RetryPolicy(max_attempts=3, retry_on=retry_on_timeout),
    error_handler=research_fallback,
)
builder.add_node("draft", draft_node, timeout=30)
builder.add_node("review", review_node)

builder.add_edge(START, "research")
builder.add_edge("research", "draft")
builder.add_edge("draft", "review")

def should_approve(state: AgentState) -> str:
    return END if state["approved"] else "draft"

builder.add_conditional_edges("review", should_approve)

# ---- 6. 编译(挂 checkpointer)----
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# ---- 7. 运行 ----
config = {"configurable": {"thread_id": str(uuid.uuid4())}}

# 第一次调用:跑到 review 节点触发 interrupt,暂停
for chunk in graph.stream(
    {"query": "LangGraph 生产级最佳实践", "messages": []},
    config,
):
    print(chunk)

# 人工审批后恢复
for chunk in graph.stream(Command(resume="yes"), config):
    print(chunk)

跑通这段代码你就有了完整的生产级 Agent 骨架:durable state + timeout + retry + error handler + human-in-the-loop。


十、参数速查表

参数所属类型作用
timeoutadd_nodefloat | timedelta | TimeoutPolicy | None节点超时控制,数字=wall-clock 硬限
run_timeoutTimeoutPolicyfloat | timedelta | Nonewall-clock 硬限,不刷新
idle_timeoutTimeoutPolicyfloat | timedelta | None空闲超时,进度信号重置
refresh_onTimeoutPolicy'auto' | 'heartbeat'idle 超时刷新方式,默认 auto
retry_policyadd_nodeRetryPolicy | None节点重试策略
max_attemptsRetryPolicyint(默认 3)最大尝试次数(含首次)
initial_intervalRetryPolicyfloat(默认 0.5)首次重试前等待秒数
backoff_factorRetryPolicyfloat(默认 2.0)重试间隔倍增因子
jitterRetryPolicybool(默认 True)随机抖动,防重试风暴
retry_onRetryPolicytype | Sequence | Callable触发重试的异常类型,默认 default_retry_on
error_handleradd_nodeStateNode | None兜底节点,原节点失败时路由过去
checkpointercompileCheckpointer | None状态持久化器,interrupt 必需
interrupt_beforecompilelist[str] | None指定节点前暂停
interrupt_aftercompilelist[str] | None指定节点后暂停

十一、踩坑记录

坑一:timeout 只对 async 节点生效。 add_node(timeout=30) 传给同步 def 节点不报错但不生效——同步函数阻塞 GIL,没法在进程内安全取消。生产环境所有节点统一用 async def,否则超时形同虚设。

坑二:忘了挂 checkpointer 就用 interrupt。 interrupt()GraphInterrupt 后状态要持久化才能恢复,没有 checkpointer 的图直接报错。compile 时必须传 checkpointer=InMemorySaver() 或生产级 saver。

坑三:retry_policy 重试了不该重试的异常。 default_retry_on 对未识别的异常默认返回 True(重试)。如果你自定义的节点抛了一个业务逻辑错误(如「用户余额不足」),会被无意义重试 3 次浪费时间。用自定义 retry_on 函数精确控制。

坑四:InMemorySaver 生产环境丢状态。 InMemorySaver 存在进程内存,进程重启状态全没。生产环境必须换 SqliteSaverPostgresSaver(需 pip install langgraph-checkpoint-sqlite / langgraph-checkpoint-postgres),状态落盘。

坑五:idle_timeout 和 run_timeout 混淆。 run_timeout=60 是不管怎样 60 秒必掐;idle_timeout=15 是 15 秒没进度就掐,但有进度会重置。长任务流式输出场景用 idle_timeout 更合理——只要在产出就有进度信号,不会误杀。

坑六:error_handler 里又调了可能失败的逻辑。 fallback 节点本身也可能失败(比如调缓存服务也挂了)。error_handler 要尽量简单——返回静态兜底值或本地缓存,别在里面再调外部 API。


十二、常见问题 FAQ

Q1:LangGraph 和 CrewAI / AutoGen 怎么选? 据独立 2026 对比(2000-run),LangGraph 在延迟和 token 成本可预测性上最优,被评为「最生产就绪」。CrewAI 上手更快适合快速原型,AutoGen(AG2)强在多 Agent 对话。需要 durable state、细粒度错误控制、长时运行的生产 Agent 选 LangGraph;快速验证想法选 CrewAI。

Q2:timeout 参数传数字和传 TimeoutPolicy 有什么区别? 传数字(timeout=30)等价于 TimeoutPolicy(run_timeout=30),是 wall-clock 硬限不刷新。传 TimeoutPolicy(run_timeout=60, idle_timeout=15) 能同时设硬限和空闲超时,空闲超时在节点有进度信号时重置,适合流式输出的长任务。

Q3:retry_policy 和 error_handler 什么关系? retry_policy 在节点抛异常时先重试(最多 max_attempts 次),全部重试失败后才触发 error_handler。error_handler 接管后返回兜底结果,图继续执行,不中断。两者是「先重试后兜底」的分层策略。

Q4:interrupt 暂停后怎么恢复?Command(resume="人工输入值") 配合同一个 thread_id 再次调用 graph.stream(Command(resume=...), config)。图会从 review 节点开头重新执行,interrupt() 函数返回你传入的 resume 值。

Q5:生产环境 checkpointer 选哪个? 开发用 InMemorySaver(内存,重启丢失),单机生产用 SqliteSaver(SQLite 文件),多实例部署用 PostgresSaver(PostgreSQL,跨实例共享状态)。后两者需单独安装对应的 checkpoint 包。


参考来源

本文由 AI 辅助生成,经人工审核编辑。最后更新:2026-08-01

常见问题

LangGraph 和 CrewAI / AutoGen 怎么选?
据独立 2026 对比(2000-run),LangGraph 延迟和 token 成本可预测性最优,被评为「最生产就绪」。CrewAI 上手快适合原型,AutoGen 强在多 Agent 对话。需要 durable state 和细粒度错误控制选 LangGraph。
timeout 传数字和传 TimeoutPolicy 有什么区别?
传数字等价于 TimeoutPolicy(run_timeout=数字),是 wall-clock 硬限不刷新。TimeoutPolicy 能同时设 run_timeout 和 idle_timeout,后者在节点有进度信号时重置,适合流式长任务。
retry_policy 和 error_handler 什么关系?
retry_policy 先重试(最多 max_attempts 次),全部失败后才触发 error_handler。handler 返回兜底结果,图继续执行。两者是「先重试后兜底」的分层策略。
interrupt 暂停后怎么恢复?
用 Command(resume="人工输入值") 配合同一个 thread_id 再次调用 graph.stream(Command(resume=...), config)。图从 review 节点开头重新执行,interrupt 返回你传入的 resume 值。
生产环境 checkpointer 选哪个?
开发用 InMemorySaver(内存),单机生产用 SqliteSaver(SQLite 文件),多实例部署用 PostgresSaver(PostgreSQL,跨实例共享状态)。后两者需单独安装对应包。

相关文章