跳到主要内容

Agent 友好型 API:当 AI 成为客户端时,后端工程师常犯的错误

· 阅读需 13 分钟
Tian Pan
Software Engineer

在 2024 年,自动化机器流量在互联网上首次超过了人类流量。Gartner 预测,到 2026 年,超过 30% 的新 API 需求将来自 AI Agent 和 LLM 工具。然而,只有 24% 的组织在设计 API 时明确考虑了 AI 客户端。

这一差距正是生产系统崩溃的地方。并不是因为 LLM 本身表现不佳,而是因为为人类开发者构建的 API 包含了一些默认假设,当调用者是自主 Agent 时,这些假设会悄无声息地失效。Agent 无法请求澄清,无法阅读文档网站,也无法自行判断 422 错误是指“修改你的请求”还是“几秒钟后重试”。

这篇文章是写给那些刚刚发现自己的服务正被 AI Agent 调用,或者即将构建此类服务的后端工程师的。

核心问题:API 是为人类设计的,而非机器

当人类开发者遇到模糊的错误时,他们会查阅文档、检查状态页,并根据经验推断下一步该做什么。当 AI Agent 遇到同样的错误时,它必须根据眼前的信息进行推理:HTTP 状态码、响应体,以及它在任务执行过程中积累的所有上下文。如果这些信号含糊不清,Agent 就会陷入猜测——而且是按照机器的速度进行猜测。

考虑一个在读取响应体之前立即调用 response.raise_for_status() 的 Python SDK。Agent 收到:"HTTP Error 422: Unprocessable Entity"。它真正需要的是:"Field 'first_name' required — provide a first_name string"。当 SDK 被修复以显示完整的错误体时,Agent 立即进行了自我修正。在错误模糊的情况下,它陷入了循环。这并非个案——这是大多数语言中大多数 HTTP 错误处理模式的默认行为。

同样的逻辑也适用于分页、身份验证流程、速率限制和 Schema 设计。你的 API 对调用者具备人类判断力所做的每一个假设,都是 Agent 系统中潜在的失效模式。

错误响应是 API 的核心部分,而非事后补充

你能为 AI 客户端做的杠杆率最高的改变就是改进错误响应。不是针对正常路径(happy path)——Agent 处理正常路径通常没问题。真正让 Agent 与人类行为产生灾难性偏差的是错误处理路径。

对于 AI Agent 来说,一个好的错误响应需要是机器可解析的,并且具有行动指导性。由 Stripe 推广的黄金标准通过以下方式构建错误:一个机器可读的 code 字符串(不仅仅是 HTTP 状态码)、一个人类可读的 message、一个指明导致问题的特定输入的 param 字段,以及一个直接链接到恢复文档的 doc_url。此外,还要增加两个 Agent 特别需要的字段:is_retriable(布尔值)和 retry_after_seconds(整数)。

有了这些字段,Agent 的错误处理就变成了一个确定性的决策树:读取 is_retriable → 如果为 false,停止并上报;如果为 true,等待 retry_after_seconds 秒,然后使用相同的幂等键(idempotency key)重试。如果没有这些,Agent 就只能仅凭状态码来推断是否可以重试——而这种推断经常出错。一个 409 Conflict 可能意味着“资源已存在,不要重试”、“存在并发写入,立即重试”或者“存在版本冲突,先获取最新状态”。三种完全不同的恢复路径,却对应同一个模糊的状态码。

RFC 9457 (application/problem+json) 标准化了响应封装格式。采用它只需要一天的工作量,就能立即让你所有的错误信息对任何理解该标准的 Agent、SDK 或监控系统变得可解析。

对于 Agent 调用者,幂等键(Idempotency Keys)是必选项

根据多个 Agent 框架的生产遥测数据,AI Agent 重试工具调用的概率约为 15-30%。重试发生的原因往往是 Agent 无法控制的:请求中途超时、瞬时网络错误、模型对前一次调用是否完成的不确定性,以及编排器重启。如果你的状态变更端点不支持幂等键,那么每一次重试都是一次潜在的重复操作——重复付款、重复发送电子邮件、重复的数据库记录。

实现模式已经非常成熟:客户端在第一次尝试前生成一个 UUID,在每次重试时将其作为 Idempotency-Key 请求头发送,服务器将该键对应的响应缓存 24 小时,并在收到重复提交时返回缓存的结果。生产环境中让团队栽跟头的关键细节是:内存缓存无法在 Pod 重启后存活。分布式 Agent 工作负载需要 Redis 或具有原子性 SET NX 语义的等效数据存储,这样当两个 Pod 同时收到带有相同键的请求时,只会执行一个。

根据重试语义对操作进行标记也很重要。“重试安全”与“幂等”并不等同——GET 请求是重试安全的,而带有幂等支持的写入操作也是重试安全的。Agent 需要知道哪些操作可以安全地重新发起,而无需上报给人类进行确认。

不以“人类正在观察”为前提的分页

基于偏移量(Offset-based)的分页有一种微妙的失效模式,人类 API 用户很少察觉,但智能体(Agent)却经常遇到:当页面之间有记录被创建或删除时,结果集中的项会发生偏移。第 2 页可能会返回已经在第 1 页出现过的项,或者跳过本应出现的项。对于人类使用的分页组件,这只是偶尔的视觉小瑕疵;但对于自主遍历整个数据集的智能体来说,这就是隐形的数据损坏。

基于游标(Cursor-based)的分页消除了这类失效。游标编码了结果集中的一个稳定位置,不受并发写入的影响。对智能体要求的契约应是精简且一致的:每个分页响应都需要 data(结果数组)、next_cursor(字符串,耗尽时为 null)和 has_more(布尔值)。有了这三个字段,智能体就可以编写一个通用的分页循环,适用于你 API 中的每一个端点。

脆弱性源于不一致性。如果你一半的端点使用游标分页,另一半使用偏移量分页,智能体就无法编写统一的可重用遍历模式。每个新端点都需要专门实现。在整个 API 层面标准化分页行为,其价值远超任何单一的性能优化。

速率限制应作为实时状态,而非事后惩罚

人类会在被限流后到仪表盘查看速率限制状态。智能体无法做到这一点。智能体知道自己接近速率限制的唯一方法是你通过每一个响应告诉它——而不仅仅是在为时已晚的 429 错误中。

在每一个响应中返回 X-RateLimit-RemainingX-RateLimit-LimitX-RateLimit-Reset,而不仅仅是 429。读取这些响应头的智能体可以在 Remaining 低于阈值时主动自我限流,在触发阻断其整个工作流的限制墙之前增加延迟。

当 429 错误确实发生时,Retry-After 必须是一个秒数,而不是一段文字。"Please try again in a few minutes" 是无法执行的。Retry-After: 47 则明确告诉智能体何时重试。在结构化的 JSON 正文字段(retry_after_seconds)中包含相同的值以及当前的配额状态,这样不解析响应头的智能体仍能确定性地恢复。

多智能体系统引入了单智能体速率限制无法解决的协调问题。在一次记录在案的生产事故中,九个共享同一个 API 配额的智能体因为同步重试,在不到八分钟的时间内耗尽了 5,000 次请求/小时的配额。一个后台轮询智能体(低优先级)阻塞了一个关键架构智能体(高优先级)——这在配额层发生了优先级反转(Priority Inversion)。由同步 429 重试引起的“惊群效应”(Thundering herd)导致了 60 多次链式故障,并在 90 秒内崩溃进入完全的 API 锁定状态。

缓解措施不应完全由 API 端承担。智能体框架需要在重试逻辑中加入抖动(Jitter)、对共享配额池的分级访问,以及基于实时速率限制响应头的主动限流。但 API 设计可以提供帮助:在 429 响应中返回足够的后续状态,使智能体框架无需单独的配额跟踪服务即可实现背压(Backpressure)机制。

OpenAPI 描述是语义路由信号

当智能体决定调用哪个工具时,它会读取你的 OpenAPI 描述。模型关注的不是你的 Schema 结构,而是通过阅读自然语言描述来将意图与功能进行匹配。一个描述为 "Get invoices" 的描述几乎无法给智能体提供信号。而描述为 "Returns a paginated list of invoices filtered by status, customer_id, and date range, sorted by created_at descending. Requires billing:read scope." 则告诉了智能体何时调用该端点、需要准备哪些参数,以及是否拥有正确的权限。

每个参数也需要语义化描述。不仅是它的类型,还包括有效值的含义、格式(ISO 8601?UUID?逗号分隔列表?),以及何时应包含可选参数。每个返回字段都需要指明其代表什么,以及在什么条件下为 null。枚举(Enums)需要为每个成员提供描述。

编写这些内容很枯燥,几乎没人会做。但这也是智能体调用错误工具或传递错误参数的首要原因——这不是因为模型失败了,而是因为描述太单薄,导致模型无法区分两个相似的端点。

Anthropic 的内部数据非常有启发性:在工具定义中提供 input_examples(展示最小化、部分和完整的参数用法),将复杂参数处理的准确率从 72% 提升到了 90%。Schema 定义了结构,而示例则捕捉了结构无法表达的行为模式,例如“仅当 status 为 'critical' 时才包含 escalation 字段”。

机器对机器身份验证

OAuth 授权码流程(Authorization code flow)需要浏览器。智能体没有浏览器。如果你唯一支持的身份验证方式涉及跳转到登录页面,那么无论你的 API 设计得多么完美,智能体都完全无法集成。

适用于智能体的可行身份验证模式包括:API 密钥(Bearer Token,简单且无状态)、OAuth 客户端凭据流程(Client Credentials Flow,机器对机器,无需用户交互)以及双向 TLS(mTLS)。如果你只支持交互式 OAuth,你就排除了整个智能体调用者群体。

与之相关的失效模式是权限过大的凭据。在一次记录在案的事故中,一个智能体被授予了一个特权令牌,拥有对组织中所有仓库的读取权限。一个公共 GitHub Issue 中的恶意提示词导致该智能体将私有仓库的内容泄露到了一个公共 Pull Request 中。任何提示词注入攻击(Prompt Injection Attack)的爆炸半径都受限于智能体凭据的权限。智能体应该获得完成特定任务所需的最小范围、可撤销的凭据——这与应用于自主系统的最小权限服务账号原则一致。

发现层:llms.txt

Cloudflare 在生产实践中注意到,向智能体暴露数千个 REST 端点是根本行不通的 —— 仅仅是描述信息就会在智能体开始推理该调用哪个端点之前填满其上下文窗口。llms.txt 规范于 2024 年 9 月提出,目前已被包括 Stripe 和 Anthropic 在内的超过 844,000 个网站采用,它在发现层解决了这一问题。

域名根目录下的 /llms.txt 文件是一个 Markdown 文档,它为 LLM 智能体精选了最重要的信息:首选的集成模式、应避免的已弃用端点、身份验证设置路径,以及明确的“大语言模型智能体指令”,这些指令揭示了仅凭 OpenAPI 规范无法察觉的隐患。它是 API 领域的优秀 README —— 只需一个下午的编写,就能消除整类集成错误。

它并不会取代详尽的 OpenAPI 文档。但它极大地降低了智能体理解你 API 接口范围的上下文成本,并且是记录智能体特有怪癖(例如“我们的 GraphQL 端点始终返回 200,请检查响应体中的 errors 数组”)的绝佳位置,而这些内容在其他地方都显得格格不入。

为你实际拥有的客户端进行设计

Stytch 工程团队对此有精辟的阐述:“如果 AI 智能体无法弄清楚你的 API 如何工作,你的用户也同样无法弄清楚。”智能体可读性是衡量 API 整体质量的先行指标,因为智能体能够暴露人类通过经验和查阅文档可以绕过的歧义性。

具体的改变并不复杂。幂等键、带有 is_retriable 的结构化错误、基于游标的分页、每个响应中的速率限制头、具有语义内容的 OpenAPI 描述、机器对机器身份验证选项 —— 这些都不需要重新架构你的 API。它们只是在你可能已经拥有的基础之上增加的约束。

它们需要的是将错误路径视为 API 的重要组成部分,而不是事后的补救。将你的模式描述(schema descriptions)视为语义契约,而不是填完就忘的注释字段。并且要意识到,无法请求进一步澄清的调用者理应获得更精确的信号,而不是更少 —— 因为在缺乏精确度的情况下,智能体并不会停止,它们会进行猜测。

References:Let's stay in touch and Follow me for more thoughts and updates