给Agent工具调用增加超时与重试
课程归属:打造能干活的Agent
一、为什么 Agent 调用工具需要超时与重试
在资料分析任务助手中,Agent 可能调用本地文件读取函数、CSV 分析函数,也可能调用模型服务或其他外部接口。这些操作并不总能顺利完成:网络请求可能迟迟没有返回,服务可能临时不可用,工具也可能因为输入错误直接失败。
如果没有超时控制,一次调用就可能长期占用执行流程;如果所有异常都立即终止任务,短暂故障又会导致整份报告无法生成。
本文使用 Python 资料分析任务助手构建一个可复现案例:通过 LangGraph 为异步工具节点设置执行超时和有限重试,并在重试耗尽后生成明确的失败结果。最终,我们可以判断任务是正常完成、工具失败,还是超出重试上限,而不是仅凭 Agent 的文字回复判断成功。
二、先区分三种故障处理机制
2.1 超时、重试和兜底分别解决什么问题
超时(Timeout):为一次调用设置最长允许执行时间,超限后将其视为失败。
重试(Retry):对符合条件的失败重新执行。重试应该有上限,并可以逐步延长等待时间,避免持续请求故障服务。
失败兜底(Fallback):重试耗尽后,记录失败原因、保留必要上下文,并让流程进入可控的失败分支。
这三个机制不是互相替代的关系。超时负责发现执行时间异常,重试负责应对可能恢复的故障,兜底负责处理无法恢复的故障。
图 1:工具调用的容错处理流程
图中最重要的是:调用返回不代表调用成功。工具即使没有抛出异常,也可能返回缺少必要字段的数据,因此还需要进行结果校验。重试结束后,兜底逻辑必须明确记录任务失败,不能将错误结果伪装成正常报告。
2.2 哪些故障适合重试
| 故障类型 | 是否建议重试 | 处理依据 |
|---|---|---|
| 临时网络错误 | 通常可以 | 网络恢复后可能成功 |
| 外部服务短暂不可用 | 通常可以 | 应限制尝试次数 |
| 请求超时 | 视操作类型决定 | 需要判断重试是否安全 |
| 参数缺失或格式错误 | 不建议 | 应先修正输入 |
| 权限不足或密钥错误 | 不建议 | 应修复配置或权限 |
| 写入结果不确定 | 不能直接重试 | 应先查询结果或使用幂等机制 |
这里的“幂等”是指同一个操作重复执行,不会产生额外的业务副作用。例如,读取文件通常可以重复执行;向数据库插入订单、发送邮件或覆盖文件,则需要更谨慎地处理重复操作。
三、案例设计与项目环境
本案例将一个异步工具节点加入 LangGraph,模拟读取资料时遇到临时故障的情况。第一次调用抛出可重试异常,第二次调用成功;随后再运行一个持续超时的工具,验证重试耗尽后的失败处理。
为了不依赖真实模型密钥、网络服务或企业数据,案例使用确定性的教学模拟函数。这样可以单独验证超时和重试机制,不会把外部服务的波动误判成代码问题。
3.1 本文的交付标准
| 项目 | 约定 |
|---|---|
| 工作目录 | agent_lab/ |
| 运行环境 | WSL2 Ubuntu、Python 3.11 |
| 核心依赖 | LangGraph |
| 单次工具超时 | 1 秒 |
| 最大尝试次数 | 3 次,包含第一次调用 |
| 成功判据 | 工具返回有效结果,图执行结束 |
| 失败判据 | 超时重试耗尽,记录明确错误,不返回成功状态 |
以上数值是为了演示机制而设定的教学参数,不代表生产系统的通用配置。生产环境需要根据外部接口的响应时间、业务时限和服务端限流策略调整。
3.2 安装依赖
在 WSL2 Ubuntu 终端中执行:
mkdir -p agent_lab
cd agent_lab
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "langgraph==1.2.17"
本文版本选择依据是截至 2026 年 10 月 26 日可查到的 LangGraph 官方发行记录,记录显示 1.2.17 已于 10 月 24 日发布。该版本要求 Python 不低于 3.10,因此 Python 3.11 满足其基础版本要求。具体安装是否成功,仍应以读者本地环境的依赖解析结果为准。
可通过以下命令记录实际安装版本:
python -c "from importlib.metadata import version; print(version('langgraph'))"
python --version
如果安装失败,先检查 Python 版本、虚拟环境是否激活以及包索引是否可访问,不要直接混装多个虚拟环境中的依赖。
3.3 目录结构
本文只需要一个主程序文件:
agent_lab/
├── .venv/
└── main.py
main.py 包含工具函数、图状态定义、节点容错配置和验证入口。这个最小案例暂时不引入 SQLite、Streamlit 或真实模型调用,因为本篇只验证工具调用的容错行为。
四、理解 LangGraph 的执行顺序
LangGraph 可以将任务拆分为节点,并在节点之间传递状态。工具调用可以被封装为一个节点,由框架管理节点执行、异常传播和重试策略。
图 2:一次工具调用的时序
该时序图表达的是整体处理关系。实际项目中,重试策略是否捕获某类异常,取决于配置的异常类型和框架规则;失败处理也必须按照图的状态结构返回数据,不能只打印异常后继续假装成功。
4.1 为什么优先使用异步节点
LangGraph 官方文档明确指出,Python 图节点的超时机制适用于异步节点;同步节点不能通过框架的节点超时配置安全地中止。因此,下面采用 async def 节点,并使用 asyncio.sleep() 模拟异步操作。
需要特别注意:超时取消并不等于能够强制终止所有底层工作。如果节点内部启动了外部进程,或调用的底层库忽略取消信号,实际资源仍可能继续运行。对于同步阻塞函数,应优先使用底层客户端自身的连接超时、读取超时和总超时,不能只在外层包一个异步函数就认为所有阻塞操作都已受到控制。
五、编写完整的最小案例
5.1 创建 main.py
在 agent_lab/main.py 中写入以下完整代码:
import asyncio
from typing import TypedDict
from langgraph.errors import NodeTimeoutError
from langgraph.graph import END, START, StateGraph
from langgraph.types import RetryPolicy
class AgentState(TypedDict, total=False):
task_id: str
result: str
status: str
attempts: int
error: str
# 模拟一个会暂时失败的外部工具。
# 第一次调用抛出 TimeoutError,第二次调用成功。
async def flaky_tool(state: AgentState) -> dict:
attempt = state.get("attempts", 0) + 1
if attempt == 1:
raise TimeoutError("模拟临时网络故障")
return {
"attempts": attempt,
"result": "已读取教学资料:Agent 工具调用需要容错控制。",
"status": "completed",
"error": "",
}
# 模拟一个每次都超过时限的工具。
async def slow_tool(state: AgentState) -> dict:
await asyncio.sleep(3)
return {
"attempts": state.get("attempts", 0) + 1,
"result": "",
"status": "completed",
"error": "",
}
def on_tool_error(state: AgentState) -> dict:
"""在工具重试耗尽后,输出明确失败状态。"""
return {
"status": "failed",
"result": "",
"error": "工具调用失败:已达到超时或重试上限。",
}
def build_graph(tool):
builder = StateGraph(AgentState)
builder.add_node(
"call_tool",
tool,
timeout=1.0,
retry_policy=RetryPolicy(
max_attempts=3,
initial_interval=0.1,
backoff_factor=2.0,
max_interval=0.5,
jitter=False,
retry_on=(TimeoutError, NodeTimeoutError),
),
error_handler=on_tool_error,
)
builder.add_edge(START, "call_tool")
builder.add_edge("call_tool", END)
return builder.compile()
async def main():
# 用例一:第一次失败,后续重试成功。
success_graph = build_graph(flaky_tool)
success = await success_graph.ainvoke({
"task_id": "demo-success",
"attempts": 0,
"status": "running",
})
print("正常用例:", success)
# 用例二:每次都超时,最终进入失败处理。
failure_graph = build_graph(slow_tool)
failure = await failure_graph.ainvoke({
"task_id": "demo-timeout",
"attempts": 0,
"status": "running",
})
print("超时用例:", failure)
if __name__ == "__main__":
asyncio.run(main())
这份程序使用 LangGraph 1.2 系列提供的节点 timeout、retry_policy 和 error_handler 配置。官方文档说明,节点超时会产生 NodeTimeoutError,之后由重试策略决定是否再次执行;重试耗尽后才会调用错误处理函数。
代码中有两点值得注意:
max_attempts=3表示最多执行三次,而不是失败后再额外执行三次。error_handler返回的是任务状态更新。对于最终失败的任务,status被设置为failed,且result保持为空,不会把失败伪装成成功报告。
5.2 运行程序
在 agent_lab/ 目录下执行:
python main.py
正常用例的预期结果包含:
status为completed;result包含教学资料文本;attempts为2,表示模拟工具第二次执行成功。
超时用例的预期结果包含:
status为failed;result为空字符串;error说明工具调用已达到超时或重试上限。
**验证状态说明:**上述是根据代码和 LangGraph 官方机制给出的预期输出,不是本次环境的实测日志。应以实际运行结果为准;如果错误处理函数的参数签名与安装版本不兼容,应先核对对应版本的官方文档和实际异常信息,不要直接将预期结果记录为测试通过。
5.3 为什么不能只用 try/except
在普通 Python 函数中,使用 try/except 可以捕获异常,但它本身不提供完整的超时、退避重试和流程状态管理。
例如:
try:
result = await flaky_tool({"attempts": 0})
except TimeoutError:
result = "失败"
这段代码只处理了一次调用的异常。它没有定义最大尝试次数,没有统一管理超时策略,也没有区分可重试异常和永久性错误。
当工具数量增加后,如果每个节点都复制一套重试循环,代码会越来越难维护。将容错规则放在图节点配置中,可以让业务逻辑与执行策略分离。
六、把容错机制用于真实资料分析任务
教学模拟只证明了执行机制的设计思路。接入真正的文件、CSV 或模型服务时,还需要把工具的输入、输出和异常边界定义清楚。
表 2:工具接入时的容错约定
| 工具类型 | 推荐超时边界 | 重试注意事项 |
|---|---|---|
| 本地文本读取 | 通常不需要很长的超时 | 文件不存在属于输入错误,不应盲目重试 |
| CSV 分析 | 根据文件规模和资源限制设置 | 参数错误、列缺失应直接返回错误 |
| 模型 API | 使用客户端连接与读取超时 | 只对适合重试的错误重试,注意限流和费用 |
| 远程 HTTP 查询 | 配置连接超时与总超时 | 区分 4xx 输入/权限错误与可恢复的服务端错误 |
| 文件或数据库写入 | 同时设置执行边界和业务保护 | 先判断操作是否已经完成,避免重复副作用 |
对于固定公共接口 read_text_file(relative_path: str) 和 analyze_csv(relative_path: str),建议保留统一的逻辑结果结构:
{
"status": "ok",
"data": {"text": "示例资料内容"},
"source": ["data/input/example.txt"],
"error": None
}
发生错误时,返回 status="error",并将错误代码和可读消息放入 error 字段。需要注意,工具返回 ok 只代表工具逻辑执行成功,并不等于整个任务已经 completed;任务仍需检查结果完整性后才能进入完成状态。
图 3:从工具错误到任务状态的转换
这条链路强调:只有输出检查通过,任务才能完成。即使模型最终生成了一段看似合理的回答,如果工具读取失败、来源缺失或报告文件没有成功写出,也不应把任务标记为成功。
七、常见故障与处理方法
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
TypeError 提示不接受 timeout 参数 | LangGraph 版本过旧,或节点配置方式不匹配 | 查看安装版本并核对对应文档 |
| 超时后程序仍有后台工作 | 底层同步函数或外部操作未响应取消 | 配置底层客户端超时,检查资源释放 |
| 参数错误也被反复执行 | 重试范围设置过宽 | 只对明确可恢复的异常重试 |
| 重试后写入了重复数据 | 操作存在副作用且没有幂等保护 | 使用业务唯一键或先查询操作结果 |
| 所有重试结束后任务仍显示成功 | 兜底状态没有传回任务状态 | 检查图状态更新和最终验收逻辑 |
| 任务执行时间仍然过长 | 每次重试都重新计时,且退避时间累积 | 计算总时间预算,并限制整体任务时限 |
7.1 为重试设置总时间预算
单次超时不是整个任务的总超时。假设单次超时为 1 秒、最大尝试次数为 3 次,退避间隔分别为 0.1 秒和 0.2 秒,那么仅按这些配置估算,执行时间就可能接近 3.3 秒,还没有计算调度开销。
生产系统还应设置任务级截止时间。如果总预算只剩很少时间,就不应继续发起一个注定无法在任务时限内完成的重试。
此外,重试可能增加外部 API 的调用次数和费用。模型调用、支付、发邮件、文件覆盖等操作,不能只因为出现超时就直接再次执行。客户端超时可能只表示“没有及时收到结果”,并不能证明服务端没有完成操作。
八、验收清单与扩展边界
表 3:本案例的验收用例
| 用例 | 预期行为 | 判断依据 |
|---|---|---|
| 临时故障后恢复 | 重试后成功 | 最终状态为 completed |
| 工具持续超时 | 达到最大尝试次数后失败 | 最终状态为 failed |
| 参数无效 | 不进行无意义的重复调用 | 返回可识别的参数错误 |
| 外部写入结果不确定 | 不直接盲目重放 | 查询结果或使用幂等键 |
| 任务超过整体预算 | 停止后续不必要操作 | 任务截止时间被执行层检查 |
完成验证时,不要只看终端有没有输出文字。应核对实际的状态、工具调用次数、错误内容,以及报告文件是否真实存在且内容完整。当前教学代码将结果保存在 LangGraph 的调用返回值中,并没有持久化数据库,因此进程退出后不会自动保留任务历史;后续课程可以再使用 SQLite 保存任务和步骤记录。
本篇的范围是为异步工具节点增加超时、有限重试和失败处理。它不等同于完整的任务恢复、分布式锁或业务幂等方案。需要恢复中断任务时,应引入持久化检查点;需要防止重复写入时,应在业务层增加唯一标识或幂等键;需要长期运行的工具,则应同时设计取消、资源清理和日志追踪。
本文基于LangGraph框架,为Python Agent的异步工具节点实现超时控制、有限重试与失败兜底机制。重点解析三类容错策略的分工:超时识别执行异常、重试应对瞬时故障、兜底保障流程可控。通过最小可运行案例验证节点级超时配置、RetryPolicy与TimeoutPolicy协同机制,并强调结果校验、状态明确返回及生产环境总时间预算约束。
15

被折叠的 条评论
为什么被折叠?



