搭 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:
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: boolAnnotated[list, operator.add] 告诉 LangGraph:多个节点往 messages 里写时,用追加(operator.add)合并而不是后写覆盖先写。这是 typed state 的关键——每个字段的合并策略是显式声明的,不会出现状态竞争。
定义好 state 后搭图骨架:
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):
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 的多次调用共享状态:
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 硬限(秒):
async def draft_node(state: AgentState) -> AgentState:
# 调 LLM 生成回复
...
builder.add_node("draft", draft_node, timeout=30) # 30 秒硬限更精细的控制用 TimeoutPolicy,同时设 wall-clock 超时和 idle 超时:
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_timeout 和 idle_timeout 的区别:run_timeout 是不管怎样到时间就掐的总时限;idle_timeout 在节点有进度信号时(比如流式输出收到一个 chunk)会重置,适合「偶尔卡住但通常在动」的长任务。超时后 LangGraph 抛 NodeTimeoutError,如果有 retry_policy 则自动决定是否重试。
关键限制:timeout 只对 async 节点生效。同步节点用的是 time.sleep / CPU 密集操作,没法在进程内安全取消。所以生产环境节点统一用 async def。
五、重试策略:RetryPolicy
API 偶发失败(限流、网关 5xx)是常态,不能让它崩整条链。add_node 的 retry_policy 参数接一个 RetryPolicy:
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) - 不重试:
ValueError、TypeError、KeyError(LookupError)、OSError等程序逻辑错误
这个默认行为很合理——5xx 是服务端临时故障值得重试,ValueError 是你的代码 bug 重试也没用。如果你要自定义,传一个判断函数:
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 参数让你指定一个兜底节点,原始节点抛异常时自动路由过去,不崩整图:
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 做动态路由:
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=...) 恢复:
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"}恢复执行:
# 第一次 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:
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。
十、参数速查表
| 参数 | 所属 | 类型 | 作用 |
|---|---|---|---|
timeout | add_node | float | timedelta | TimeoutPolicy | None | 节点超时控制,数字=wall-clock 硬限 |
run_timeout | TimeoutPolicy | float | timedelta | None | wall-clock 硬限,不刷新 |
idle_timeout | TimeoutPolicy | float | timedelta | None | 空闲超时,进度信号重置 |
refresh_on | TimeoutPolicy | 'auto' | 'heartbeat' | idle 超时刷新方式,默认 auto |
retry_policy | add_node | RetryPolicy | None | 节点重试策略 |
max_attempts | RetryPolicy | int(默认 3) | 最大尝试次数(含首次) |
initial_interval | RetryPolicy | float(默认 0.5) | 首次重试前等待秒数 |
backoff_factor | RetryPolicy | float(默认 2.0) | 重试间隔倍增因子 |
jitter | RetryPolicy | bool(默认 True) | 随机抖动,防重试风暴 |
retry_on | RetryPolicy | type | Sequence | Callable | 触发重试的异常类型,默认 default_retry_on |
error_handler | add_node | StateNode | None | 兜底节点,原节点失败时路由过去 |
checkpointer | compile | Checkpointer | None | 状态持久化器,interrupt 必需 |
interrupt_before | compile | list[str] | None | 指定节点前暂停 |
interrupt_after | compile | list[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 存在进程内存,进程重启状态全没。生产环境必须换 SqliteSaver 或 PostgresSaver(需 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 包。
参考来源