免费端点触发429时不断重试会烧尽可用窗口和token;提出级联路由方案——免费层承载流量,检测到限速后优雅降级到备份付费端点,配合熔断器防止免费层持续失败。
当免费端点返回 429 时,大多数 Agent 会做最昂贵的操作:重试。重试看起来无害。一个 200 毫秒的请求变成 2 秒等待,然后再来一次。但在峰值负载下,这个循环会变成 30 秒的卡顿,同时你的 Agent 在空响应上不断点击刷新。如果在卡顿期间配额窗口重置了,每次重试都在燃烧你本可用于实际工作的 token。
重试循环假设故障是临时的。对于速率限制,这个假设通常不成立。配额计数器按固定周期重置,而不是按你的方便来。你不只是在等待;你在燃烧本可以用于其他地方的实际时间。
级联路由器是另一种方案。它将请求发送到免费端点,在收到速率限制信号时退让,然后优雅地降级到备用端点。免费层承担负载;备用端点只在需要时存在。你既获得了免费层的成本优势,又获得了付费层的可靠性。
这个设计有三部分:一个端点抽象层、一个速率限制检测器,以及一个在免费端点反复失败时跳闸的断路器。以下是核心代码:
# cascade_router.py — free tier first, paid/self-hosted as fallback.
import json
import os
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
@dataclass
class Endpoint:
name: str
url: str
api_key: str
model: str
cooldown_until: float = 0.0
consecutive_failures: int = 0
def available(self) -> bool:
return time.time() >= self.cooldown_until
class CascadeRouter:
def __init__(self, endpoints: list[Endpoint]):
self.endpoints = endpoints
def _call_one(self, ep: Endpoint, messages: list[dict]) -> tuple[int, dict]:
body = json.dumps({"model": ep.model, "messages": messages, "max_tokens": 256}).encode()
req = urllib.request.Request(ep.url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {ep.api_key}",
})
start = time.perf_counter()
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read())
ep.consecutive_failures = 0
return 200, {"latency_ms": round((time.perf_counter() - start) * 1000), "endpoint": ep.name, **data}
except urllib.error.HTTPError as e:
ep.consecutive_failures += 1
if e.code == 429:
backoff = min(60, 5 * (2 ** (ep.consecutive_failures - 1)))
ep.cooldown_until = time.time() + backoff
return e.code, {"error": str(e), "endpoint": ep.name}
def complete(self, messages: list[dict]) -> dict:
for ep in self.endpoints:
if not ep.available():
continue
status, result = self._call_one(ep, messages)
if status == 200:
return result
return {"error": "all endpoints unavailable", "endpoint": "none"}
router = CascadeRouter([
Endpoint("free-tier", os.environ["FREE_URL"], os.environ["FREE_KEY"], os.environ["FREE_MODEL"]),
Endpoint("paid-backup", os.environ["PAID_URL"], os.environ["PAID_KEY"], os.environ["PAID_MODEL"]),
])
result = router.complete([{"role": "user", "content": "Summarize this PR diff in 3 bullets."}])
print(json.dumps(result, indent=2))
退避计算是关键细节。它从 5 秒开始,每次连续失败翻倍,上限为 60 秒。这防止了重试风暴,同时仍然让免费端点有机会恢复。断路器是隐式的:一旦冷却时间超过 60 秒,免费端点实际上就被跳闸了,直到窗口重置。
60 秒的上限是经过考虑的。超过这个时间,等待免费端点的成本就超过了直接使用付费端点的成本。断路器说:我给你一分钟了,现在我要走了。
并非所有错误都是平等的。429 表示"你被速率限制了,稍后再试"——冷却是合理的。5xx 表示"服务器端出了点问题"——一次快速重试然后继续是更好的策略,因为服务器可能正在重启,几秒钟就会恢复。你的路由器应该区别对待它们。
配额窗口的形状决定了你的退避上限。如果配额每分钟重置一次,5 秒冷却就够了。如果每小时重置一次,60 秒上限意味着你将在剩余一小时内都被跳闸。在配置路由器之前,先了解你的配额窗口。一次快速的 curl 测试就能告诉你:发送请求直到遇到 429,然后测量恢复需要多长时间。这个数字就是你的退避上限。
对于 60 秒配额窗口来说,简单的 time.sleep(retry_count * 2) 是灾难性的。你的 Agent 在 1 秒、3 秒、5 秒时重试——都在窗口内——然后放弃,而窗口在 40 秒后才重置。如果它在 45 秒时重试,就会成功。朴素的循环永远到不了那里。级联路由器可以。
现在是质量陷阱。免费模型和付费模型产生不同的输出。如果免费模型是 7B 参数模型,付费的是 70B,降级响应在质量上会有差异。对于摘要或分类,这没问题。对于精确的代码生成,可能就是个问题。缓解措施:将降级限制在无状态请求上,或者接受上下文切换。
日志是这个模式中被低估的部分。路由器已经在响应中返回了 endpoint 字段,所以你可以记录它。这不仅仅是成本跟踪;这是调试。当输出看起来奇怪时,你需要知道是哪个模型生成的。简单的一条结构化日志——时间戳、端点、模型、延迟、状态——在事后分析中是无价的。
这个模式在哪里运行?在常驻服务器上。你的笔记本会休眠,VPN 会抖动,共享 Wi-Fi 会增加延迟——所有这些都会污染测量值。MonkeyCode 是一个开源的 Agent 工具链,其免费层包含 1000 万 token 的模型配额和免费服务器选项。披露:本文是 MonkeyCode 产品推广的一部分。免费服务器是运行这个路由器的好地方,因为它保持在线。免费配额充当第一层;你现有的付费端点或自托管模型充当第二层。
有三类人不应该使用这个。第一,如果免费端点和付费端点之间的质量差距破坏了下游任务——比如说免费模型经常产生格式错误的 JSON,而你的解析器很严格——级联只是延迟了失败。第二,如果数据边界禁止将 prompt 发送到外部免费端点,级联在架构上就是不可能的。第三,如果配额窗口非常短(例如每分钟一个请求),冷却时间永远不够短,路由器会不断降级——在这种情况下,直接使用付费端点,跳过免费层。
这个模式的价值在于它把免费层当作一个真正的组件,而不是赠品。免费层有约束;级联路由器管理这些约束。当免费端点可用时,你省钱。当它不可用时,你的 Agent 仍然工作。这就是免费层应有的使用方式:不是作为唯一端点,而是作为阶梯的第一级。如果你想在真实环境中看到它,MonkeyCode 的免费层包含 1000 万 token 配额和免费服务器,配额详情可在仓库中验证。运行路由器,观察降级发生,你就会知道免费层是否在你的技术栈中占有一席之地。