ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

第 12 篇:错误处理 —— 重试、降级、熔断,用 TaoToken 统一 Key 跑通 FastMCP 容错链路

第 12 篇:错误处理 —— 重试、降级、熔断,用 TaoToken 统一 Key 跑通 FastMCP 容错链路 1. FastMCP 工具调用总在 429 和超时上翻车问题到底出在哪FastMCP 是一个用 Python 快速构建 MCP Server 的框架它把工具注册、参数校验、协议通信这些脏活都封装好了你只需要写mcp.tool()装饰的函数就能对外暴露能力。适合谁适合已经在用 Claude Code、Cline、Cursor 这类客户端接 MCP 工具但发现工具一多、外部 API 一抖整条链路就开始间歇性失败的开发者。我试过在本地把三个工具串起来跑一个查天气、一个查商品、一个查用户资料。本地测试全绿一放到真实网络环境问题就来了——天气 API 偶尔 503商品搜索偶尔 429用户服务偶尔连接超时。单独看每个错误都不致命但组合起来Agent 收到的就是一堆ECONNREFUSED、TimeoutError、429 Too Many Requests。更麻烦的是LLM 看到这些原始错误后不会像人类开发者那样等等再试它可能直接编造一个结果或者陷入重试还是放弃的循环。这就是 FastMCP 错误处理和传统 Web API 最大的区别错误的消费者是 LLM不是人类。人类看到ECONNREFUSED知道是连接被拒LLM 看到这串字符只会懵。所以我们需要三层策略——重试应对瞬时故障、降级保住核心功能、熔断防止级联崩溃——并且把错误包装成 LLM 能理解的语义化结构。这篇会交付可复制的重试次数与退避参数、降级兜底响应模板、熔断阈值配置以及触发 429 和超时后的验证动作。所有外部调用统一走 TaoToken 的 API 通道用一个 Key 管理多工具接入省得每个工具配一套凭证。2. 用 TaoToken 统一 Key 接入 FastMCP 多工具容错链路在写重试和熔断之前先把调用通道统一掉。多工具接入最烦的就是每个外部服务一套 Key、一套 Base URL出错时你都不知道是哪个凭证的问题。TaoToken 的做法是给你一个统一的 API 入口和一把 Key模型对话、编码计划、控制台、API Keys 管理都在同一套体系里。官网入口在这里https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。你需要在控制台生成 Key然后所有 FastMCP 工具的外部模型调用都指向同一个 Base URL。为什么这对容错链路重要因为重试和熔断需要可观测的失败信号。如果每个工具走不同的通道429 的限流窗口、超时的判定标准都不一样你没法统一配置退避参数。统一通道后429 就是 429超时就是超时熔断器的失败计数才有意义。具体操作登录控制台 → 进入 API Keys 页面 → 创建一把 Key → 复制保存。然后在 FastMCP 项目里用环境变量注入不要硬编码。模型 ID 根据你用的场景选编码类任务和对话类任务的模型 ID 不同在控制台的模型列表里能看到。这里有个关键点TaoToken 是合规的 API 聚合通道不是让你去搞什么灰色中转。你把它当成一个统一的模型调用入口就行所有请求走标准 HTTPS不需要任何额外网络配置。配置好之后你的 FastMCP Server 里所有httpx.AsyncClient的 base_url 都指向https://taotoken.net/apiheaders 里带Authorization: Bearer 你的Key。这样重试逻辑只需要处理一种通道的失败模式熔断器也只需要监控一个下游的健康状态。3. 可复制的重试、降级、熔断配置片段这一节直接给能跑的配置。先建项目结构fastmcp-resilience/ ├── .env ├── config.toml ├── server.py └── resilience.py.env文件TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/apiconfig.toml放重试和熔断参数方便不改代码调阈值[retry] max_retries 3 base_delay 1.0 max_delay 30.0 jitter_ratio 0.5 [circuit_breaker.weather] failure_threshold 3 recovery_timeout 30.0 half_open_max_calls 2 [circuit_breaker.search] failure_threshold 5 recovery_timeout 60.0 half_open_max_calls 3 [degradation] cache_ttl 3600resilience.py实现三个核心组件。重试用指数退避加抖动import asyncio import random from typing import Callable, TypeVar T TypeVar(T) async def retry_with_backoff( func: Callable[..., T], max_retries: int 3, base_delay: float 1.0, max_delay: float 30.0, jitter_ratio: float 0.5, retryable_exceptions: tuple (ConnectionError, TimeoutError), ) - T: last_exception None for attempt in range(max_retries 1): try: return await func() except retryable_exceptions as e: last_exception e if attempt max_retries: break delay min(base_delay * (2 ** attempt), max_delay) jittered delay * (1 - jitter_ratio random.random() * jitter_ratio) await asyncio.sleep(jittered) raise last_exception熔断器用三态模型状态转换逻辑写清楚import time from enum import Enum from dataclasses import dataclass, field class CircuitState(Enum): CLOSED closed OPEN open HALF_OPEN half_open dataclass class CircuitBreaker: failure_threshold: int 5 recovery_timeout: float 30.0 half_open_max_calls: int 3 _state: CircuitState field(defaultCircuitState.CLOSED, reprFalse) _failure_count: int field(default0, reprFalse) _success_count: int field(default0, reprFalse) _last_failure_time: float field(default0.0, reprFalse) _half_open_calls: int field(default0, reprFalse) property def state(self) - CircuitState: if (self._state CircuitState.OPEN and time.time() - self._last_failure_time self.recovery_timeout): self._state CircuitState.HALF_OPEN self._half_open_calls 0 self._success_count 0 return self._state def allow_request(self) - bool: state self.state if state CircuitState.CLOSED: return True if state CircuitState.OPEN: return False if self._half_open_calls self.half_open_max_calls: self._half_open_calls 1 return True return False def record_success(self): if self._state CircuitState.HALF_OPEN: self._success_count 1 if self._success_count self.half_open_max_calls: self._reset() else: self._failure_count 0 def record_failure(self): self._failure_count 1 self._last_failure_time time.time() if self._state CircuitState.HALF_OPEN: self._state CircuitState.OPEN elif self._failure_count self.failure_threshold: self._state CircuitState.OPEN def _reset(self): self._state CircuitState.CLOSED self._failure_count 0 self._success_count 0 self._half_open_calls 0降级用缓存兜底server.py里把三者串起来import os import time import httpx from fastmcp import FastMCP from fastmcp.exceptions import ToolError from resilience import retry_with_backoff, CircuitBreaker mcp FastMCP(容错链路示例) weather_breaker CircuitBreaker(failure_threshold3, recovery_timeout30.0) _cache: dict[str, tuple[float, str]] {} mcp.tool() async def get_weather(city: str) - str: 获取天气信息。 if not weather_breaker.allow_request(): raise ToolError( {error:{code:CIRCUIT_OPEN,message:天气服务熔断中请30秒后重试, recovery_hint:RETRY_LATER,retry_after:30}} ) async def _call(): async with httpx.AsyncClient( base_urlos.environ[TAOTOKEN_BASE_URL], headers{Authorization: fBearer {os.environ[TAOTOKEN_API_KEY]}}, timeout10.0, ) as client: resp await client.get(f/weather/{city}) resp.raise_for_status() return resp.text try: result await retry_with_backoff(_call, max_retries3, base_delay1.0) weather_breaker.record_success() _cache[city] (time.time(), result) return result except Exception as e: weather_breaker.record_failure() if city in _cache: ts, data _cache[city] if time.time() - ts 3600: return f[降级] 使用缓存数据\n{data} raise ToolError( {error:{code:UPSTREAM_ERROR,message:天气服务暂时不可用, recovery_hint:RETRY_LATER,retry_after:15}} )这套配置里重试次数 3 次、基础延迟 1 秒、最大延迟 30 秒、抖动比例 0.5熔断阈值天气服务 3 次失败、恢复 30 秒、半开试探 2 次降级缓存 TTL 1 小时。参数都在config.toml里改完重启即可。4. 验证请求触发 429 和超时后看什么配置写完不算完得验证它真的生效。我实测下来验证分三步先确认正常请求能通再人为触发 429 和超时最后看熔断器状态。第一步正常请求验证。启动 Serverpython server.py用 MCP 客户端调用get_weather传一个正常城市名。预期返回天气数据且weather_breaker状态是closed。你可以在 Server 里加一个 Resource 暴露熔断器状态mcp.resource(server://breakers) def breaker_status() - str: return fweather: {weather_breaker.state.value}, failures: {weather_breaker._failure_count}第二步触发 429。把_call里的请求指向一个会返回 429 的端点或者临时把 TaoToken 的 Key 换成无效的观察重试日志。预期看到重试 1/3等待 0.7s... 重试 2/3等待 1.4s... 重试 3/3等待 2.8s...三次重试后仍失败熔断器record_failure被调用。连续触发 3 次熔断器状态从closed变open。此时再调用get_weather直接返回CIRCUIT_OPEN错误不再发起真实请求。第三步触发超时。把timeout改成 0.001 秒任何请求都会超时。观察重试逻辑是否捕获TimeoutError以及降级缓存是否生效——如果之前成功请求过这次应该返回[降级] 使用缓存数据。验证成功的标志429 触发后重试次数正确、退避时间符合指数增长、熔断器在阈值后跳闸、降级缓存命中。如果重试没生效检查异常类型是否在retryable_exceptions里如果熔断器不跳闸检查record_failure是否被调用。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth实际跑的时候报错五花八门。我踩过的坑整理成对照表401 Unauthorized。最常见的原因是 Key 没注入或格式不对。检查.env里TAOTOKEN_API_KEY是否以sk-开头headers 里是否写成Bearer sk-xxx。如果用的是 Claude Code 或 Cline检查它们的配置文件里 Base URL 是否指向https://taotoken.net/apiKey 是否填对。三件套缺一不可Base URL、Key、Model ID。local proxy failed。这个报错通常出现在客户端配置了本地代理但代理没启动。检查你的 MCP 客户端设置里有没有多余的 proxy 配置有的话删掉。FastMCP Server 本身不需要任何代理直接走 HTTPS 就行。reading choices 相关报错。这通常是响应格式解析失败比如模型返回的 JSON 结构和你预期的不一样。检查你的format_weather之类的解析函数加一层 try-except解析失败时返回语义化错误而不是抛原始异常。OAuth 相关报错。如果你在 Claude Code 里配置 MCP Server 时遇到 OAuth 问题检查~/.claude/settings.json或项目级.mcp.json里的配置。Claude Code 的 MCP 配置需要指定command、args、env三个字段env 里放TAOTOKEN_API_KEY和TAOTOKEN_BASE_URL。如果用的是 Codex检查auth.json里的凭证是否过期。CC Switch / Cline MCP 配置。如果你用 CC Switch 管理多个 Claude Code 配置确保每个 profile 里的 Base URL 都是https://taotoken.net/apiKey 是同一把。Cline 的 MCP 配置在cline_mcp_settings.json里同样三件套Base URL、Key、Model ID。排查顺序建议先看 401 确认凭证再看网络连通性最后看响应解析。大部分问题出在凭证和 Base URL 上把这两个确认了80% 的报错就没了。6. 把容错链路跑稳之后下一步做什么重试、降级、熔断三层配好你的 FastMCP Server 在面对 429 和超时时就不会直接崩了。但容错只是第一步你还需要知道什么时候在失败、失败了多少次、哪个工具最不稳定。这些信息靠日志和指标来暴露。如果你还在用零散的 Key 管理多个工具建议先把通道统一到 TaoToken这样失败信号才一致熔断阈值才有统一标准。API Keys 管理在 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc 模型对话验证在 https://taotoken.net/chat 。长期跑编码类 Agent 任务的话Coding Plan 在 https://taotoken.net/coding-plan 比按次调用更划算。下一篇会讲可观测性——结构化日志、关键指标、分布式追踪让你在问题发生前就发现征兆。但那是下一篇的事这篇你先把重试参数、降级模板、熔断阈值跑通用 429 和超时各触发一次确认熔断器真的跳闸、缓存真的兜底。跑通了再往下走。
返回列表