AI 智能体正在读你的 API 错误
3 月,Mintlify 统计了它托管的文档站点一个月的流量。总计约 7.9 亿次请求,其中 45.3% 来自 AI 编程智能体,浏览器占 45.8%。仅 Claude Code 一家就发出了全部请求的四分之一(Mintlify 的数据)。
这是访问文档的流量,不是访问 API 的。但读文档的智能体,正是随后编写请求、运行请求、阅读返回结果的那些智能体。在 Postman 的2025 年 State of the API 调查中,24% 的开发者表示已经在设计 API 时考虑智能体。这在刚发布时听起来还早,现在已经不是了。
我们在这里构建的大部分东西都和失败有关:返回 500 的端点、卡住的端点、响应到一半就中断的端点。所以我们一直在问一个很具体的问题:当智能体从你的 API 收到一个错误时,它有足够的信息去做正确的下一步吗?
智能体会照字面理解你的错误信息
开发者收到 {"error":"E4021"} 时,会去搜这个代码,搜不到就去问同事。智能体没法问任何人。它只能依靠你发来的响应体,如果响应体里没有有用的信息,它就会猜:重试同一个请求,或者改掉一个原本没问题的字段。
当 ID 不存在时,我们的商品端点会这样回答:
$ curl -i https://api.testingapis.com/api/Products/99999
HTTP/1.1 404 Not Found
content-type: application/json; charset=utf-8
{"status":404,"error":"Not Found","message":"Product with ID 99999 does not exist."}这句话很平淡,但没关系。它点明了资源和 ID,所以无论读它的是人还是程序,都知道问题出在 ID 上,重发也没用。
如果你想要一种标准的错误格式,RFC 9457(Problem Details for HTTP APIs)定义了 type、title、status、detail 和 instance,以 application/problem+json 返回。它在 2023 年取代了 RFC 7807。我们认为具体格式没那么重要,重要的是里面某处有一句清楚的话。
告诉对方要等多久
智能体会重试。如果你的 429 或 503 没说要等多久,客户端只能自己挑一个数字,而一个在循环中处理任务的智能体会一直挑下去。Retry-After 几十年前就是 HTTP 的一部分,一个响应头就能解决这个问题:
$ curl -i https://api.testingapis.com/api/status/429
HTTP/1.1 429 Too Many Requests
content-type: application/json; charset=utf-8
retry-after: 5
{"status":429,"statusText":"Too Many Requests","category":"client-error","message":"This is a 429 response. Use /api/status/{code} to request any status."}/api/status/429IETF 还在制定 RateLimit 和 RateLimit-Policy 响应头,让客户端在配额用完之前,而不是之后,就能看到自己的配额。第 11 版草案在 5 月发布。语法是这样的:
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=50;t=30意思是“每 60 秒 100 个请求,还剩 50 个,窗口 30 秒后重置”。它仍是草案,所以我们不会去掉客户端已经在读的 X-RateLimit-* 响应头。两者都发,成本很低。
重试会造成重复
重试 GET 没有害处。重试 POST 就不一定了:如果第一次请求已经到达服务器,只是响应丢了,你就会得到两个订单。人通常会注意到重复扣款,而智能体看到第二次尝试成功了,就继续往下做。
常见的解决办法是幂等键。客户端在写操作中附带一个唯一的 Idempotency-Key 请求头,如果同一个键再次到达,服务器就返回第一次的结果,而不是把工作再做一遍。Stripe 多年来一直这样做,这个请求头也有一份IETF 草案,不过还不是 RFC。如果你的 API 收款,或者会创建清理起来很麻烦的东西,那么在智能体开始调用它之前加上这个很值得。
MCP 服务器正在变成普通的 HTTP API
很多智能体不会直接调用你的 REST API,而是通过一个包装它的 MCP 服务器。7 月 28 日发布的 MCP 规范去掉了会话握手和 Mcp-Session-Id 请求头。现在每个请求都带着自己的协议版本和客户端信息,方法名和工具名通过 HTTP 请求头传递,所以网关无需解析请求体就能路由和限流。
结果是,MCP 服务器现在的行为和其他任何无状态 HTTP 服务都差不多。它可以放在普通的负载均衡器后面,上面的建议原样适用。当工具调用失败时,智能体需要从你的 MCP 服务器得到原因和等待时间,就像从你的 API 那里一样。
快速检查一下
这是我们写这篇文章时运行的脚本。它发送四个会失败的请求,对每个请求检查两件事:有没有程序能读懂的原因,有没有 Retry-After?
// For each failure: is there a reason a program can read, and a time to wait?
const BASE = "https://api.testingapis.com";
const cases = [
["missing product", "/api/Products/99999"],
["rate limited", "/api/status/429"],
["server down", "/api/Products/1?error=503"],
["bad request", "/api/status/400"],
];
for (const [name, path] of cases) {
const res = await fetch(BASE + path);
const body = await res.json().catch(() => ({}));
const reason = body.message ?? body.detail ?? body.title;
const wait = res.headers.get("retry-after");
console.log(
name.padEnd(16),
res.status,
reason ? "reason: yes" : "reason: NO ",
wait ? `retry-after: ${wait}s` : "retry-after: -",
);
}missing product 404 reason: yes retry-after: -
rate limited 429 reason: yes retry-after: 5s
server down 503 reason: yes retry-after: -
bad request 400 reason: yes retry-after: -每个响应都有原因,这很好。但看第三行。/api/status/503 会发送 Retry-After: 5,而在真实端点上加 ?error=503 却什么都不发。针对它测试的客户端永远学不会何时该重试。这一条已经列入我们的计划。
把同一个脚本对准你自己 API 的失败路径。如果你没法让自己的 API 按需失败,错误响应指南介绍了如何从我们的 API 得到 500、429、超时或被截断的响应体。
这些都不是新建议。清楚的错误信息和 Retry-After 早在有人构建智能体之前就是好主意。改变的是:困惑的人最终会给你发邮件,而智能体只会再试一次。