← Back to 文字

错误处理与重试

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 识别 Tool Calling 中最常见的失败点。
  • 为模型调用与工具执行设计最小错误处理。
  • 实现简单的重试策略,但不滥用重试。

前置知识

  • 已完成工具循环实现

1. Tool Calling 的常见失败点

  • 模型返回不存在的工具名
  • 模型返回参数缺失或类型错误
  • 工具内部执行异常
  • API 请求超时
  • 网络错误

月份 1 的目标不是消灭所有失败,而是让失败清楚、可定位、可恢复。

2. 错误处理优先级

第一优先级:错误要可见

不要吞异常。

第二优先级:错误要归类

区分:

  • 配置错误
  • 请求错误
  • 工具错误
  • 解析错误

第三优先级:只对值得重试的错误重试

例如网络波动可以重试,但参数不合法通常不应该重试。

3. 最小错误模型

from pydantic import BaseModel


class ErrorResponse(BaseModel):
    error_code: str
    message: str

推荐错误码示例:

  • CONFIG_ERROR
  • HTTP_ERROR
  • TOOL_NOT_FOUND
  • TOOL_EXECUTION_ERROR
  • VALIDATION_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. 实操任务

  1. 故意传一个不存在的工具名
  2. 故意传错误参数
  3. 模拟一次超时错误
  4. 为不同错误输出不同日志

8. 自测题

  1. 为什么参数错误通常不应重试?
  2. 为什么“错误可见”比“表面看起来不报错”更重要?
  3. 为什么 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_ERROR
  • HTTP_ERROR
  • TIMEOUT_ERROR
  • TOOL_NOT_FOUND
  • TOOL_EXECUTION_ERROR
  • VALIDATION_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_executellm_generate
  • 工具名
  • 模型名
  • 成功/失败
  • 错误类型
  • 错误消息

示例:

logger.info(
    "stage=tool_execute | tool_name=%s | success=%s | detail=%s",
    tool_name,
    success,
    detail,
)

19. 调试与排错:本章最常见问题

问题一:程序看起来“没有报错”,但结果不对

通常是因为异常被吞掉了。

问题二:日志里只有“失败了”,没有上下文

这种日志几乎等于没记。

问题三:重试掩盖了真正的根因

例如明明是参数错误,却被你重试了三次,最后只让问题更难看清。

20. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能区分暂时性错误和确定性错误。
  2. 能为工具系统设计最小错误码集合。
  3. 能写出有限次重试逻辑。
  4. 能在日志里记录关键执行上下文。

21. 如果你卡在这里,先回看哪几章

22. 从本章过渡到下一章的桥接说明

接下来进入 工具实验与验收.md

到这里你已经具备了 Tool Calling 的原理、实现和错误处理思路。下一步要做的不是继续加概念,而是通过实验把这套闭环真正跑顺,并形成可展示、可复盘的成果。

Fin