前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8836
  • Agent记忆正常仍出错:问题在状态不在记忆
  • Mercury 2.5 推理速度达 770 tokens/秒
  • GitHub Copilot 代码审查新增个人配置选项
  • MCP单人上手容易,团队规模落地是另一回事
  • 影子测试100%一致率背后:模型实际正确率仅75%
  • 两个都通过的测试,代价却不同:重试的隐性成本
  • 定时运行AI Agent输出飘移的根因与修复
  • Anthropic如何两周将Claude.ai速度提升3倍
  • claude-code-templates:一键装配 Claude Code 开发套件,含 100+ Agent/MCP
  • Agent 删改测试必须拦截:CI 合并门禁实操方案
  • Google Antigravity SDK 支持本地 AI 模型:Gemma 4 26B 可离线跑
  • NVIDIA Warp 与 MjWarp 加速机器人仿真工作流
  • HEMA 用 MCP 和 Amazon Bedrock 实现内部 AI 助手转型
  • 五大LLM网关工具生产环境横评
  • GitHub Copilot应用如何渲染百万行PR
  • 基于 AWS 构建 Agent 式视频智能对话系统架构解析
  • Bedrock 上用开源权重模型做 AI 编程助手
  • Anthropic 实验室:Claude 自主发现类 CRISPR 新型酶系统
  • ChatGPT Voice 集成邮件、日历和 Slack:Altman 心中的"Her"更近一步
  • OpenAI GPT-6 Sol/Luna 和 Claude Opus 5.5 同步降价 50%
  • AI 工具循环必须显式传递 Retry-After 头否则必死循环
  • AI 代码补丁静默引入新工具调用:merge 前必须强制契约检查
  • AI 写 API 文档无法区分 null/0/缺省三态:OpenAPI 契约必须显式约束
  • AI 编程 Agent 工具输出遭截断:应记录 stdout_bytes 和截断标志
  • Anthropic工程师揭秘:Claude为何越进化写作越差
  • Gemini 3.8 Flash / Flash-Lite TTS 发布:千款语音、30秒克隆、逐行台词控制
  • 工程师详解:新版Claude为何写作风格变得怪异
  • 小米MiMo-V3将搭载HySparse 2:100万Token下KV缓存缩小4.5倍
  • GitHub Copilot 应用新增本地沙箱隔离功能
  • AI Agent 调试指南:重启不是调试,七层架构定位根因
  • AI 加剧软件供应链攻击威胁,行业如何应对
  • 阿里 Qwen Audio 3.1 发布:语音识别/TTS 多模型,API 价格最高降 95%
  • Claude Code部署到Lizard平台实战指南
  • Claude Opus 5.5降价却破坏四个Agent依赖项
  • OpenAI GPT-6 Sol/Luna 半价发布,缓存机制或为更大降本杠杆
  • treg:聚合 3000+ Agent 工具的统一网关
  • 向量检索权限校验应内嵌到 pgvector 查询中
  • Univer:面向 AI Agent 的开源办公套件 SDK
  • DeepSeek公开Agent训练新论文,梁文锋署名
  • 为 AI 编程 Agent 构建可复用技能系统的实践
  • DeepMind研究:百个AI智能体协作求解时出现作弊与告密现象
  • Google AX:开源Agent编排运行时
  • 阿里千问发布Qwen-Audio-3.1:TTS降价70%、ASR降价95%
  • 诺基亚开源AnyJev:无训练即可将任意开源LLM转为校准决策模型
  • 2026年AI网关横评:Bifrost领跑,多路 failover 哪家强
  • GPT-6 Sol/Luna 发布:准确率翻倍、成本减半,价格战开启
  • Claude Opus 5.5 登场,AI 模型价格普降 40-50%
  • Kyutai开源语音模型Voice of Reason,口算GSM8K准确率从27%升至77%
  • 微软Copilot大促:10万席最高半价,向超级AI应用转型
  • OpenAI GPT-6 Sol/Luna:API价格腰斩,长任务Prompt缓存优化
  • AI 编码 Agent 在移动端多久“看”一次才够用
  • 已加载 51 / 8836
8.0
热点
AI SCORE
技术实践2026-09-24 00:32

AI 工具循环必须显式传递 Retry-After 头否则必死循环

dev.to · AI#AI Agent#可靠性#HTTP
Editor brief · 编辑速览

作者用本地测试环境验证:当 AI Agent 工具循环忽略 429 响应头中的 Retry-After,会在限流时无意义重试直至服务崩溃,需主动将头信息注入工具结果。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

实验室的时钟从第五次轮询返回 429 时开始计时,而就在 800 毫秒后,agent 又问了同样的状态。提示词没有任何问题。HTTP 层已经告诉调用者要等待了。模型从未看到那句话。

这是来自本地测试环境的 48 小时实战记录,不是客户案例,也不是基准测试。问题很具体:如果 AI 工具循环把所有非 2xx 响应一视同仁,Retry-After 能否到达下一个模型轮次?简短的回答是:不能,除非你主动把响应头拷贝到工具结果里。

我搭建了一个小小的状态服务,接入一个朴素轮询器,然后把它和一个真正尊重 Retry-After 响应头的循环进行比较。这个服务故意表现得"无礼"——来自同一客户端 key 的四次成功 GET 之后,开始返回 429 并带上 Retry-After: 3。这足以让一个工具调用型 agent 在无需负载生成器的情况下打爆一个免费服务器。

那个一直挥之不去的类比是:Retry-After 就像一个交警在路口伸出手掌。而一个只返回状态码和响应体的工具 schema,就像一个只看信号灯颜色的司机。掌依然在。路口依然在。

前十二小时我做了什么

第一版用的是标准库服务器,所以唯一的可变部分就是 HTTP 和一个字典。没有框架,没有队列。一个进程,一把锁,一个从客户端 key 到命中次数的映射。

# retry_after_lab.py — run: python retry_after_lab.py
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from collections import defaultdict
import threading, json, time

HITS = defaultdict(int)
LOCK = threading.Lock()
LIMIT = 4  # 429 after this many GETs per client key

class Handler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        return

    def do_GET(self):
        if self.path != "/status":
            self.send_response(404); self.end_headers(); return
        key = self.headers.get("X-Client-Key", "anon")
        with LOCK:
            HITS[key] += 1
            n = HITS[key]
        if n > LIMIT:
            self.send_response(429)
            self.send_header("Retry-After", "3")
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({
                "error": "rate_limited",
                "hits": n,
            }).encode())
            return
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({
            "state": "open",
            "hits": n,
            "ts": time.time(),
        }).encode())

if __name__ == "__main__":
    ThreadingHTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

用 curl 确认了契约之后才让任何模型进入场景。四次 200,然后是一个带响应头的 429,shell 可以打印出这个头,JSON body 看起来像一个普通错误。

python retry_after_lab.py &
for i in 1 2 3 4 5 6; do
  echo "--- $i"
  curl -sD - -H "X-Client-Key: lab" http://127.0.0.1:8765/status
  echo
done

这部分无聊得很,但正是有用的那种无聊。响应头是稳定的。body 很小。我关心的故障点不是服务器,而是下一跳:一个只把 JSON body 序列化进模型对话记录的工具包装器。

当循环开始调用工具时,什么坏了

大多数 agent 运行时把 HTTP 暴露为一个函数,有名字、有 URL、有解析后的 body。状态码有时能透传过去。响应头往往不能。我把下面的包装器标注为一个提案,符合我在追踪中看到的样子;它不是从生产 agent 里导出的 dump。

# Proposed tool result — headers dropped on purpose
def http_get(url: str, headers: dict | None = None) -> dict:
    import urllib.request
    req = urllib.request.Request(url, headers=headers or {})
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            return {
                "ok": True,
                "status": resp.status,
                "body": resp.read().decode(),
            }
    except urllib.error.HTTPError as e:
        return {
            "ok": False,
            "status": e.code,
            "body": e.read().decode(),
        }

把这个字典作为工具结果喂回去,模型收到的是 {"ok": false, "status": 429, "body": "{\"error\": \"rate_limited\", \"hits\": 5}"}。这个对象里没有任何睡眠指令。合理的下一个 token 序列是再次对同一个 URL 调用 http_get。交警的手掌从未进入对话。

然后我运行了第二个客户端,做了一件仓促的 agent 会做的事:任何失败都重试,间隔 200ms,无抖动,用同一个客户端 key。两秒内额外收到了六次 GET。服务器尽职了。客户端没有。

# naive_poller.py — this is the broken loop under test
import json, time, urllib.request, urllib.error

URL = "http://127.0.0.1:8765/status"
HEADERS = {"X-Client-Key": "naive"}

def once():
    req = urllib.request.Request(URL, headers=HEADERS)
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            return resp.status, dict(resp.headers), resp.read().decode()
    except urllib.error.HTTPError as e:
        return e.code, dict(e.headers), e.read().decode()

for i in range(10):
    status, headers, body = once()
    print(f"{i} status={status} retry_after={headers.get('Retry-After')} body={body}")
    if status == 200:
        break
    time.sleep(0.2)  # the bug: ignores Retry-After

这个打印输出就是全部的教训。retry_after=3 和 sleep(0.2) 出现在同一行。这个数字对 Python 可见。只要工具结果不拷贝它,它对模型就不可见。这就是那道接缝。

一个正确的工具结果不需要什么精巧设计。它只是更宽一些。

# Proposed tool result — header survives into the next model turn
{
  "ok": False,
  "status": 429,
  "retry_after_seconds": 3,
  "instruction": "Do not call http_get again until retry_after_seconds have elapsed.",
  "body": {"error": "rate_limited", "hits": 5}
}

即便如此,如果运行时在模型层之下重试工具,那还是不够。有些编排器会捕获 ok: false 然后在请求新的 completion 之前重新调用同一个函数。这时响应头已经在 JSON 里了但仍然没被用上。sleep 必须放在编排器里,而不是提示词里。模型做节拍器很糟糕。

产物:一个失败闭口的探针

第二天做的是测试,不是仪表盘。探针记录来自同一客户端 key 的 429 时间戳。如果两个 429 之间的距离小于 Retry-After 减去 150ms 时钟偏差,测试就失败。这是我会真正遵守的契约。

# test_retry_after.py — run after the server is up
import time, urllib.request, urllib.error, json, sys

URL = "http://127.0.0.1:8765/status"
KEY = "probe"

def hit():
    req = urllib.request.Request(URL, headers={"X-Client-Key": KEY})
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            return resp.status, dict(resp.headers), time.monotonic()
    except urllib.error.HTTPError as e:
        return e.code, dict(e.headers), time.monotonic()

# Warm the limiter
for _ in range(5):
    status, headers, ts = hit()

assert status == 429, status
wait = float(headers.get("Retry-After", "0"))
assert wait >= 1, headers

# Immediate retry must still be 429
status2, _, ts2 = hit()
assert status2 == 429, status2
gap = ts2 - ts
if gap < wait - 0.15:
    # This assert is the point of the lab.
    print(f"FAIL: second 429 arrived after {gap:.3f}s, header asked for {wait}s")
    sys.exit(1)

time.sleep(wait)
status3, _, _ = hit()
print(json.dumps({"after_wait_status": status3, "gap_before_sleep": round(gap, 3)}))
# Naive pollers never reach this line with a 200 unless LIMIT resets.

我不会把这个作为负载测试来交付。它是一个针对客户端的契约测试。服务器是一个 fixture。如果你把同一个探针指向一个你并不拥有的公开 API,你就不再是在调试工具循环了。你正在为你声称要研究的 429 做贡献。

接下来的时间里,桌上一直放着一张紧凑的决策表。状态码 429 加 Retry-After 意味着在编排器里 sleep,然后重试一次,然后停止。状态码 429 没有响应头意味着指数退避加上限,然后停止。状态码 200 但 body 里有应用层错误不在 HTTP 层重试。状态码 204 是带空 body 的成功,不是解析失败。把这四种情况混在一起,就是一个 agent 如何把一个礼貌的限流器变成重试风暴的方式。

一个免费模型车道和一个免费服务器真正帮上忙的地方

上面的测试环境跑在一台笔记本上。把它从笔记本上移出来的原因是追踪过夜日志:你想要模型轮次、工具 JSON 和 HTTP 日志放在一起,在你睡觉的时候也能看到。披露:本文是 MonkeyCode 产品推广的一部分。

MonkeyCode 是一个开源编程 agent 项目,就本次写作所提供的版本,提供免费模型访问和免费服务器选项。我用这个组合作为一个地方,重新对同一个 fixture 运行同样的轮询器,而不需要编一个 GPU 故事。我不在这里附上模型名称、token 上限、硬件或持续时间声称。这些数字一周内就会过时,而这篇笔记讲的是一个响应头,不是一份计划表。

有用的部分是 logistical 的。启动 fixture,启动朴素轮询器,启动第二个把 Retry-After 拷贝进工具结果的轮询器,然后保留对话记录。如果免费服务器正忙或容量未定,测试在本地依然成立。产品为方法提供了便利,而不是方法本身。

我会重复做的一件事:用原始字符串记录工具参数,在 JSON 解析之前。一些追踪显示函数调用看起来像 {"url": "http://127.0.0.1:8765/status" 没有右花括号。运行时重试了解析器,然后重试了 HTTP 调用,然后限流器介入了。被截断的工具 JSON 和被忽略的响应头会叠加。它们看起来像两个 bug。它们是一条没有背压的管道。

我会重复做的,和我不会做的

我会保持 fixture 的简洁。四次 200 和一个 429 就够了。我会把 Retry-After 作为一级字段放在工具 schema 里,而不是寄希望于模型去读 body。我会把 sleep 放在进程代码里。我会保留那个在两个 429 太近时失败的探针。

我不会让模型凭感觉选择睡眠时长。我不会只解析 body。我不会对不是我操作的 host 运行这个。我不会把一个免费的共享服务器当作 SLO。未知的上限不是无限的上限。它们就是未知的。

局限性很直白。这个实验室不测量任何模型的吞吐量、成本或质量。它不能证明每个 agent 框架都会丢弃响应头;它只是展示了一个常见的包装器形状如何使丢弃变得必然。时钟偏差、HTTP/2、剥离 Retry-After 的代理、以及发送日期而不是时间增量 delta 的服务器都不在讨论范围内。如果你的限流器返回 503 而且没有响应头,这个探针救不了你。

谁应该跳过这个做法:用轮询器作为武器攻击第三方 API 的人;需要保证远程容量的人;指望用提示词替换互斥锁的人。如果你的工具结果已经是 {status, headers, body} 而且你的编排器会 sleep,你不需要这篇文章。你需要那个 assert。

四十八小时后,fixture 在第五次命中时仍然返回 429。朴素的循环仍然忽略交警。探针仍然失败闭口。这就是整篇笔记。如果你想要一个已经接好免费模型访问和免费服务器车道的沙盒,MonkeyCode 是重新运行这个测试环境的一个地方——上面的测试不依赖它。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
OpenAI GPT-6 Sol/Luna 和 Claude Opus 5.5 同步降价 50%
下一篇
AI 代码补丁静默引入新工具调用:merge 前必须强制契约检查