1. Agent 工具调用失败时,为什么不能只靠 try-catch
Agent 工具调用失败和普通 HTTP 请求失败,看起来都是「报错」,但处理逻辑完全不同。普通请求失败,你重试一次大概率就好了;Agent 工具调用失败,如果你只是无脑重试,可能会把一次 429 限流放大成连续十几次无效请求,甚至让整个任务链路雪崩。
我最近在做一个日志分析 Agent,它需要调用 bash_exec、search_db、get_order 三个工具。跑了两天发现一个规律:失败不是均匀分布的,而是集中在三类场景——超时、限流、5xx。超时通常是网络抖动或后端响应慢,重试一次就能过;限流是 API 配额被打满,需要冷却或换通道;5xx 是服务端临时故障,退避后重试有效。但如果你把这三类混在一起处理,用同一个重试策略,结果就是该快的慢、该停的还在跑。
更麻烦的是,Agent 是多轮迭代的。一次工具调用失败后,LLM 会拿到错误信息继续推理,如果错误信息不结构化,模型可能反复调用同一个失败工具,几分钟烧掉大量 token。所以异常处理的核心不是「重试」,而是「分类 + 退避 + 降级」三件事。
这篇文章聚焦一个具体问题:Agent 工具调用失败后,如何用统一 Key/API 通道承接多模型请求,实现超时、限流、5xx 三类异常的自动重试与降级切换。我会给出可复制的 config.toml 和 settings.json 骨架,以及用日志验证降级是否生效的具体动作。
2. TaoToken 统一通道的前置准备
在讲重试和降级之前,先解决一个基础问题:你的 Agent 要能切换模型,前提是多个模型走同一个入口。如果每个模型都要单独配 Key、单独改 BaseURL,降级逻辑会变得非常臃肿。
TaoToken 在这里的角色是统一通道。你只需要一个 API Key,就可以在同一个 BaseURL 下请求不同模型。这对 Agent 降级特别有用——当主模型限流时,你不需要改代码里的 endpoint,只需要换 model 字段。
前置准备分三步:
第一步,获取 API Key。访问 https://taotoken.net/api-keys 创建一个 Key,复制保存。这个 Key 后面会用在 config.toml 和 settings.json 里。
第二步,确认 BaseURL。TaoToken 的 API 入口是 https://taotoken.net/api,所有模型请求都走这个地址。注意不要加多余的路径后缀,OpenAI 兼容接口会自动拼接 /v1/chat/completions。
第三步,确认你要用的模型名。在模型对话页面可以查看当前支持的模型列表,常见的有 claude-sonnet-4-5、gpt-4o 等。降级策略里至少准备两个模型,一个主模型一个备用模型。
注意:API Key 不要硬编码在代码里,也不要提交到 Git。建议用环境变量注入,config.toml 里引用环境变量名而不是值。
如果你还没有 Key,可以先到 https://taotoken.net/api-keys 创建。整个准备过程不超过两分钟,但它是后面所有重试和降级逻辑的基础。
3. 可复制的 config.toml 与 settings.json 骨架
这一节给出两个配置文件骨架。config.toml 用于定义模型通道、重试参数和降级链;settings.json 用于定义工具调用的超时、重试预算和错误分类规则。两个文件配合使用,Agent 启动时加载。
3.1 config.toml:模型通道与降级链
# config.toml # Agent 模型通道配置:统一走 TaoToken,支持多模型降级 [api] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,不写明文 timeout_seconds = 60 # 单次 LLM 调用超时 max_retries = 3 # 全局最大重试次数 retry_base_delay = 1.0 # 指数退避基础延迟(秒) retry_max_delay = 30.0 # 单次退避上限 retry_jitter_ratio = 0.1 # 随机抖动比例,防止惊群 # 降级链:按 priority 从小到大尝试 [[models]] name = "claude-sonnet-4-5" provider = "anthropic" priority = 1 max_retries = 2 # 主模型最多重试 2 次 timeout_seconds = 60 [[models]] name = "gpt-4o" provider = "openai" priority = 2 max_retries = 1 # 备用模型最多重试 1 次 timeout_seconds = 45 [[models]] name = "gpt-4o-mini" provider = "openai" priority = 3 max_retries = 0 # 保底模型不重试,直接返回 timeout_seconds = 30 [circuit_breaker] failure_threshold = 3 # 连续失败 3 次触发熔断 recovery_timeout = 30 # 熔断后 30 秒进入半开 half_open_max_calls = 1 # 半开状态放行 1 个试探请求这个配置的关键点:降级链按 priority 排序,主模型失败后自动切到下一个。每个模型有独立的 max_retries,避免主模型重试太多次拖慢整体。熔断器参数控制什么时候停止请求某个模型。
3.2 settings.json:工具调用超时与错误分类
{ "tool_call": { "default_timeout_seconds": 30, "max_retries": 2, "retry_budget": { "global": 5, "per_tool": 2, "per_model_switch": 1 }, "error_classification": { "transient": { "patterns": ["TimeoutError", "ConnectionReset", "SSLHandshakeError"], "action": "retry_with_backoff", "max_retries": 3 }, "rate_limited": { "patterns": ["429", "RateLimitExceeded", "QuotaExhausted"], "action": "cooldown_then_switch", "cooldown_seconds": 5 }, "server_error": { "patterns": ["500", "502", "503", "504"], "action": "retry_with_backoff", "max_retries": 2 }, "permanent": { "patterns": ["400", "401", "403", "404", "InvalidParameter"], "action": "fail_fast", "max_retries": 0 } } }, "fallback": { "enabled": true, "on_context_overflow": "switch_to_long_context_model", "on_model_misbehavior": "switch_to_next_priority", "on_all_failed": "return_friendly_error" } }settings.json 里最重要的是 error_classification。它把错误分成四类:transient 可重试、rate_limited 需冷却后切换、server_error 退避重试、permanent 直接失败。每类对应不同的 action,Agent 在执行工具前先查这张表,决定是重试还是降级。
提示:retry_budget 是防止自杀式重试的关键。global 限制整个 run 的总重试次数,per_tool 限制单个工具的重试次数,per_model_switch 限制换模型的次数。超过预算直接终止,避免无限循环。
4. 验证请求与降级是否生效
配置文件写好后,需要验证两件事:一是正常请求能走通,二是降级逻辑真的会触发。这一节给出具体的验证命令和日志观察方法。
4.1 基础连通性验证
先用 curl 确认 TaoToken 通道可用:
export TAOTOKEN_API_KEY="你的Key" curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "回复 OK"}], "max_tokens": 10 }' | jq '.choices[0].message.content'如果返回 "OK",说明通道正常。接着换模型名再试一次,确认多模型可用:
curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "回复 OK"}], "max_tokens": 10 }' | jq '.choices[0].message.content'4.2 模拟限流触发降级
要验证降级是否生效,最直接的方法是模拟 429。你可以临时把主模型的 max_retries 设为 0,然后故意用一个不存在的模型名请求,观察 Agent 是否自动切到备用模型。
更可控的方式是在代码里注入一个 mock 错误。以下是一个 Python 验证脚本:
import os import time import logging from openai import OpenAI logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") client = OpenAI( base_url="https://taotoken.net/api", api_key=os.environ["TAOTOKEN_API_KEY"], ) FALLBACK_CHAIN = [ {"model": "claude-sonnet-4-5", "max_retries": 2}, {"model": "gpt-4o", "max_retries": 1}, {"model": "gpt-4o-mini", "max_retries": 0}, ] def call_with_fallback(messages): for idx, entry in enumerate(FALLBACK_CHAIN): model = entry["model"] for attempt in range(entry["max_retries"] + 1): try: logging.info(f"尝试 model={model} attempt={attempt+1}") resp = client.chat.completions.create( model=model, messages=messages, max_tokens=50, ) logging.info(f"成功 model={model} content={resp.choices[0].message.content}") return {"model": model, "content": resp.choices[0].message.content} except Exception as e: err = str(e) logging.warning(f"失败 model={model} error={err[:80]}") if "429" in err or "rate" in err.lower(): time.sleep(2 ** attempt + 0.1) continue if "500" in err or "502" in err or "503" in err: time.sleep(2 ** attempt + 0.1) continue break logging.info(f"降级到下一个模型,当前 priority={idx+1}") return {"model": None, "content": "所有模型均失败,请稍后重试"} if __name__ == "__main__": result = call_with_fallback([{"role": "user", "content": "用一句话解释什么是熔断器"}]) print(result)运行这个脚本,观察日志。如果主模型正常,你会看到成功 model=claude-sonnet-4-5。如果主模型限流,你会看到失败 model=claude-sonnet-4-5 error=...429...,然后降级到下一个模型,接着成功 model=gpt-4o。这就是降级生效的证据。
4.3 用日志验证降级链路
在生产环境里,建议把每次尝试和降级都打到结构化日志里。关键字段包括:timestamp、model、attempt、error_type、action、final_model。以下是一个日志片段示例:
{"ts":"2026-01-15T10:23:01Z","model":"claude-sonnet-4-5","attempt":1,"error_type":"rate_limited","action":"cooldown_then_switch"} {"ts":"2026-01-15T10:23:06Z","model":"gpt-4o","attempt":1,"error_type":null,"action":"success","final_model":"gpt-4o"}如果你看到final_model和请求时的主模型不一致,说明降级生效了。如果final_model为 null,说明所有模型都失败,需要检查 Key 余额或网络。
5. 本篇常见错误排查
这一节列出配置和验证过程中最容易踩的坑,以及对应的排查动作。
5.1 429 限流后立即重试,导致持续失败
现象:日志里连续出现 429,每次间隔很短,重试多次后仍然失败。
原因:没有冷却时间,或者冷却时间太短。限流通常是按时间窗口计算的,立即重试只会继续撞墙。
排查:检查 settings.json 里 rate_limited 的 cooldown_seconds 是否设置。建议至少 5 秒,如果限流严重可以设到 10 秒。同时确认 retry_budget 里的 global 是否被耗尽。
修复:把 cooldown_seconds 调大,或者在限流后直接切换到备用模型,而不是等待。
5.2 5xx 重试次数过多,拖慢整体响应
现象:一次工具调用花了 30 秒以上,日志显示 5xx 重试了 4 次。
原因:max_retries 设置过大,或者退避延迟没有上限。
排查:检查 config.toml 里 retry_max_delay 是否设置。如果没有上限,指数退避会变成 1、2、4、8、16、32 秒,累计延迟很高。
修复:设置 retry_max_delay = 30,并且把 5xx 的 max_retries 控制在 2 次以内。如果 2 次都失败,直接降级到备用模型。
5.3 降级后模型不支持工具调用,导致报错
现象:主模型失败后切到备用模型,但备用模型返回「不支持 function calling」。
原因:降级链里的模型能力不一致。有些轻量模型不支持工具调用,切过去后 Agent 无法执行工具。
排查:检查降级链里每个模型是否支持 function calling。可以在模型对话页面确认模型能力。
修复:把不支持工具调用的模型从降级链里移除,或者把它放在最后作为纯文本保底。如果必须用,需要在降级时移除 tools 参数,只保留对话能力。
5.4 熔断器误触发,健康模型被跳过
现象:主模型只失败了 2 次,但熔断器已经打开,后续请求直接跳过主模型。
原因:failure_threshold 设置过小,或者失败统计没有区分错误类型。永久性错误(如 400)不应该计入熔断统计。
排查:检查 circuit_breaker 的 failure_threshold 和错误分类逻辑。确认只有 transient、rate_limited、server_error 才计入失败,permanent 不计入。
修复:把 failure_threshold 调到 5 以上,并且在熔断统计里排除 permanent 错误。同时设置 half_open_max_calls,让半开状态能快速恢复。
5.5 API Key 环境变量未生效
现象:请求返回 401,日志显示 api_key 为空。
原因:环境变量没有导出,或者 config.toml 里引用的变量名和实际不一致。
排查:运行echo $TAOTOKEN_API_KEY确认变量存在。检查 config.toml 里 api_key_env 的值是否和实际变量名一致。
修复:在启动 Agent 前执行export TAOTOKEN_API_KEY="你的Key",或者把变量写入 .env 文件并用 dotenv 加载。不要直接把 Key 写在 config.toml 里。
6. 接入文档与后续动作
配置和验证跑通后,下一步是把这套逻辑接入你的实际 Agent 框架。不同框架的接入方式不同,但核心思路一致:加载 config.toml 和 settings.json,在工具调用外层包一层重试和降级逻辑,把每次尝试打到结构化日志。
如果你在接入过程中遇到报错,可以先查接入文档:https://taotoken.net/doc 。文档里有各语言的 SDK 示例和常见错误码说明。如果问题出在 Key 或配额上,到 API Keys 页面检查:https://taotoken.net/api-keys 。
对于长期跑编码任务或 Agent 任务的场景,建议关注 Coding Plan:https://taotoken.net/coding-plan 。它适合需要稳定通道和较高配额的场景,能减少限流触发的频率。
最后提醒一点:重试和降级是手段,不是目的。真正重要的是错误分类要准。分类错了,后面所有策略都会失效。我试过把 400 当成 transient 重试,结果白白浪费了 3 次请求。所以先把 error_classification 里的 patterns 调准,再调重试参数。