本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。
学习目标
学完本节后,你应当能够:
- 识别 Tool Calling 中最常见的失败点。
- 为模型调用与工具执行设计最小错误处理。
- 实现简单的重试策略,但不滥用重试。
前置知识
- 已完成工具循环实现
1. Tool Calling 的常见失败点
- 模型返回不存在的工具名
- 模型返回参数缺失或类型错误
- 工具内部执行异常
- API 请求超时
- 网络错误
月份 1 的目标不是消灭所有失败,而是让失败清楚、可定位、可恢复。
2. 错误处理优先级
第一优先级:错误要可见
不要吞异常。
第二优先级:错误要归类
区分:
- 配置错误
- 请求错误
- 工具错误
- 解析错误
第三优先级:只对值得重试的错误重试
例如网络波动可以重试,但参数不合法通常不应该重试。
3. 最小错误模型
from pydantic import BaseModel
class ErrorResponse(BaseModel):
error_code: str
message: str
推荐错误码示例:
CONFIG_ERRORHTTP_ERRORTOOL_NOT_FOUNDTOOL_EXECUTION_ERRORVALIDATION_ERROR
4. 简单重试思路
适合重试的场景:
- 临时网络错误
- 超时
- 服务端偶发错误
不适合重试的场景:
- API Key 错误
- 工具名不存在
- 参数结构不合法
示例思路:
import asyncio
async def retry_call(callable_func, retries: int = 3):
last_error = None
for _ in range(retries):
try:
return await callable_func()
except TimeoutError as error:
last_error = error
await asyncio.sleep(1)
raise last_error
5. 为什么不能盲目重试
因为有些错误重试 100 次也不会成功,反而会:
- 浪费 token
- 增加调用成本
- 让错误更难定位
6. 日志记录建议
每次失败至少记录:
- 出错阶段
- 模型名
- 工具名
- 错误类型
- 错误消息
7. 实操任务
- 故意传一个不存在的工具名
- 故意传错误参数
- 模拟一次超时错误
- 为不同错误输出不同日志
8. 自测题
- 为什么参数错误通常不应重试?
- 为什么“错误可见”比“表面看起来不报错”更重要?
- 为什么 Tool Calling 中错误分类很重要?
9. 作业与验收
作业:
- 为
call_with_tools()增加最小错误处理和简单重试逻辑
验收标准:
- 至少覆盖 3 类错误
- 至少有 1 类错误会重试
- 日志中可以定位错误来源
10. 常见错误
- 所有异常都用同一种错误码
- 无论什么错误都重试
- 出错后不记录请求上下文
11. 本章与前文关系
前面一章已经把工具注册、执行和基础结果结构搭起来了。现在问题变成:
当工具系统真正跑起来时,出错怎么办?
这一步非常关键,因为 Tool Calling 一旦进入真实使用,失败通常不是例外,而是日常情况的一部分。
12. 本章在研发助手项目中的位置
研发助手项目里可能出现的失败,不止是模型调用失败,还包括:
- 工具不存在
- 工具参数错误
- 工具内部逻辑出错
- 网络超时
- API Key 缺失
如果这些失败没有被分类和记录,项目很快就会变成“偶尔成功、偶尔神秘失败”的黑盒。
13. 错误处理为什么不是“最后再补”
因为错误处理会反向影响你的接口设计、日志设计和对象设计。
例如:
- 你是否保留统一错误码
- 你是否把 success 状态结构化保留
- 你是否在日志里记录 tool name
这些都不是“最后再加一个 try/except”就能补齐的。
14. 什么错误可以重试,什么错误不能重试
可以考虑重试的错误
- 网络临时失败
- 超时
- 服务端偶发异常
不应优先重试的错误
- API Key 缺失
- 工具名不存在
- 参数结构错误
- JSON 结构错误
一个很简单的判断原则是:
如果错误来源是“输入或配置必然不对”,重试通常没有意义。
15. 错误示例 vs 正确示例
错误示例:所有错误都重试
这会让系统:
- 变慢
- 更贵
- 更难排查
正确示例:只为“可能暂时性失败”的错误重试
比如只对超时和临时 HTTP 错误进行有限次重试,其余错误直接暴露。
16. 一个更完整的错误分类建议
月份 1 推荐至少区分这些错误码:
CONFIG_ERRORHTTP_ERRORTIMEOUT_ERRORTOOL_NOT_FOUNDTOOL_EXECUTION_ERRORVALIDATION_ERROR
为什么要这么做?因为后面日志、接口返回和排查流程都要用到这些分类。
17. 一个更接近工程的最小重试逻辑
import asyncio
async def retry_on_timeout(callable_func, retries: int = 3) -> object:
"""只对超时类错误重试。"""
last_error: Exception | None = None
for attempt in range(1, retries + 1):
try:
return await callable_func()
except TimeoutError as error:
last_error = error
print(f"第 {attempt} 次超时,准备重试")
await asyncio.sleep(1)
raise RuntimeError("重试后仍然失败") from last_error
这里最重要的不是代码长短,而是它体现出的边界:
- 只重试特定错误
- 有最大次数
- 最终仍要暴露失败
18. 日志里应该记录哪些上下文
月份 1 至少记录:
- 当前阶段,例如
tool_execute、llm_generate - 工具名
- 模型名
- 成功/失败
- 错误类型
- 错误消息
示例:
logger.info(
"stage=tool_execute | tool_name=%s | success=%s | detail=%s",
tool_name,
success,
detail,
)
19. 调试与排错:本章最常见问题
问题一:程序看起来“没有报错”,但结果不对
通常是因为异常被吞掉了。
问题二:日志里只有“失败了”,没有上下文
这种日志几乎等于没记。
问题三:重试掩盖了真正的根因
例如明明是参数错误,却被你重试了三次,最后只让问题更难看清。
20. 本章完成后你应该具备的能力
完成本章后,你应当做到:
- 能区分暂时性错误和确定性错误。
- 能为工具系统设计最小错误码集合。
- 能写出有限次重试逻辑。
- 能在日志里记录关键执行上下文。
21. 如果你卡在这里,先回看哪几章
- 异常处理基础不稳:回看 04-异常-文件-JSON.md
- 工具系统结构不稳:回看 02-工具循环实现.md
22. 从本章过渡到下一章的桥接说明
接下来进入 工具实验与验收.md。
到这里你已经具备了 Tool Calling 的原理、实现和错误处理思路。下一步要做的不是继续加概念,而是通过实验把这套闭环真正跑顺,并形成可展示、可复盘的成果。