你的API测试全过,智能体(Agent)却看不见它

Dev Genius - Medium 2026-09-10T11:50:35.196543

九次响应中能返回干净 JSON,并不等于九成“对代理友好”。那就是坏的。你的 API 本身没问题:可用性良好,测试套件全绿,OpenAPI 规范已发布,开发者门户上甚至真有人认真写了篇入门指南。但代理一调用就卡死了。不是 500 错误。错误预算里什么都没有波动。代理发出一条请求,收到一个它根本无法解读的东西,然后停住——因为它没有同事可以问,没有搜索引擎可以查,也没有能力去猜。一个撞上同一堵墙的人类开发者,看一眼报错、骂一句、九十秒内就解决了。“人能弄明白”与“机器能解析”之间的这条鸿沟,正是目前大多数可用 API 所处的位置。下面按最容易踩坑的顺序,拆解这条鸿沟到底由什么构成。

你的 schema 现在就是契约,描述只是装饰

生产环境里的大多数 OpenAPI 规范是为人类写的。含义都藏在 description 字符串里,schema 只是对数据形态的一个模糊示意。

很多规范实际长这样:

parameters:
  - name: start_date
    in: query
    schema:
      type: string
    description: >
      The start of the reporting window. Use YYYY-MM-DD.
      Ranges longer than 90 days will be rejected.

人类读了会照做。而一个经由 Model Context Protocol 运作的代理读到的是 schema 对象,description 在它眼里只是可选的附加色彩。它看到 type: string,生成一个看起来合理的字符串,然后收到 400。你写进文档的那些约束,对调用方来说根本不存在——因为你是用英文写的,而不是写进 schema 里。

代理真正能用的长这样:

parameters:
  - name: start_date
    in: query
    required: true
    schema:
      type: string
      format: date
      pattern: '^\d{4}-\d{2}-\d{2}$'
      example: '2026-01-31'
    description: Start of the reporting window.

需要吸收的是这个颠倒。2018 年,我们为人写描述,把 schema 当作技术细节附加上去。现在这个顺序反过来了:schema 是功能性契约,description 是给仍在读文档的人类准备的兜底。任何调用方必须知道、却只存在于散文式叙述里的信息,实际上都等于没有文档。

你的 API 通过了所有测试,对 Agent 来说却是隐形的

那 90% 的错误问题

我会建议先检查这一类问题,因为几乎每个团队都有,但几乎没人注意到。你的成功响应是完美的 JSON,和公布的 schema 严格一致;而错误响应则是框架随手吐出来的东西:

HTTP/1.1 429 Too Many Requests
Content-Type: text/plain

Rate limit exceeded. Please try again later.

一个 API,90% 的响应返回合法 JSON,剩下 10% 返回纯文本,这不能叫「完成了 90%」。它是坏的——而且坏在最糟糕的方式上:静默地发生在已经出错的节骨眼上。

想想 agent 面对这串文本要做什么。它提取不出错误码,无法判断这个状态是不是临时的,更没法决定该重试、调整参数,还是转给人工处理。于是它只能停下来,而你的日志里记录的只是一个看起来再普通不过的 429。

这个问题有现成的标准,改起来只需要一个下午。RFC 9457(HTTP API 的问题详情格式,2023 年 7 月发布,取代 RFC 7807)定义了专门用于这种情况的 JSON 响应体:

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30

{
  "type": "https://api.example.com/problems/rate-limit",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "Quota of 1000 requests per hour exhausted.",
  "instance": "/v2/reports/8f2c",
  "retry_after_seconds": 30,
  "quota_reset_at": "2026-09-05T14:00:00Z"
}

信息量是一样的。区别在于:第二种格式是 agent 可以据此做决策的信息,而第一种只是它能记进日志的一句话。

值得采纳的规则是:API 能返回的每一个响应——包括框架替你生成的那些——都要符合已发布的 schema。去把所有响应枚举一遍。数量通常比团队预期的要多,而那些没有测试覆盖的路径,往往就是问题的集中地。

你的认证流程是一堵墙

Agent 不会「点击」。如果获取 token 需要浏览器跳转、同意授权页面、验证码,或者要有人在后台点一下 approve,那不管你的文档怎么写,你的 API 对自主调用者来说就是关闭的。

这不是什么假设性的故障场景。

OAuth 授权码流程是为坐在浏览器前的人设计的,而相当多 API 项目的入口只有一个。有效的方法并不华丽:采用客户端凭证授权的机器身份、限定范围且短期有效的令牌、程序化轮换,以及按机器流量而非人类浏览行为来计算的速率限制。如果调用者能在任何步骤都不需要人出现的情况下,从凭据走到一次成功的请求,那你就过关了;否则就没有。

智能体无从猜测的限制

调用你 API 的后端服务,是由一个懂你领域的人写的。而智能体是一个通用推理器,没有那样的上下文。它不知道客户标识符带校验和,不知道你的两个查询参数互斥,也不知道某个状态字段只接受四个值。凡是 schema 没有明说的,智能体都会弄错,而且会错得很有创意。

{
  "type": "object",
  "required": ["customer_id", "period"],
  "properties": {
    "customer_id": {
      "type": "string",
      "pattern": "^CUS-[0-9]{8}$"
    },
    "period":      { "type": "string", "enum": ["day", "week", "month"] },
    "segments":    { "type": "array", "maxItems": 10, "items": { "type": "string" } }
  },
  "oneOf": [
    { "required": ["customer_id"], "not": { "required": ["account_id"] } },
    { "required": ["account_id"],  "not": { "required": ["customer_id"] } }
  ],
  "additionalProperties": false
}

additionalProperties: false 是大多数团队会漏掉的那一行,也恰恰是能救你的那一行。没有它,智能体一旦编造出一个貌似合理的字段名,就会得到一次无声的部分成功;对所有人来说,这远比干脆利落的拒绝糟糕得多。

测试一个没有常识的调用方

你的测试套件是由知道答案的人写的——问题就出在这里。人手写的测试用例编码了人的假设。它们会传入合理的客户 ID、靠谱的日期范围,以及一个人会想到去尝试的参数组合。而一个根据 schema 生成调用请求的智能体完全没有这种判断力,所以那些真正有趣的失败,恰恰处在你的测试从未涉足的区域。

有三个值得加入的用例;一旦涉及自主调用者,这些就不再是边缘情况,而是寻常运行状态。在模式规定字符串类型的位置传入 null,然后检查响应是否为结构化拒绝,而不是堆栈跟踪或带着损坏记录的 200 响应。请求一个在上次部署中已被移除、但仍出现在代理一小时前缓存的模式里的字段,检查错误是否明确说明了这一点,而不是返回一个空对象。发送一个你的领域规则禁止、但模式允许的组合,看看是哪一层把它拦下来的。生成这些用例最省钱的方法是从模式本身入手:取文档,一次只改动一个约束,把结果发到预发环境端点,然后断言每个响应都可解析、每次拒绝都说明了哪里出错。这个循环只需几小时的工作量,却能发现一类无论系统运行多久都暴露不出的缺陷。

可调用并不等于可发现。以上所有讨论都假设代理已经知道你的端点存在,而它往往并不知道。人类开发者靠搜索找到你的 API,进入门户然后阅读;自主调用者则需要一条机器可读的入口:一个它能解析的目录,一份描述你的工具做什么、接收什么参数的清单,或者一个它运行时查询的注册表条目。一个规范写得很漂亮、却只能通过 JavaScript 渲染的门户才能被发现的 API,就这个目的而言,等于没有上架。鉴别方法很容易验证:用 curl 抓取你自己的文档,不带任何 JavaScript 引擎,看看返回什么。如果返回的是渲染后才填充内容的空壳,那么你引以为豪的每一个描述、示例和端点列表,对于不运行浏览器的调用者来说都是不可见的。过去五年建成的门户有一大批都过不了这一关,而维护它们的团队浑然不知,因为每个来访的人类看到的都是渲染后的版本。

这同时也是诱惑所在,值得点破。读到这里的你,最顺手的想法大概是在已有 API 前加一个 MCP 服务器,然后宣布问题解决。但问题并没有解决。

标题:你的 API 能通过所有测试,却对智能体隐形

协议适配器会继承底层接口的所有缺陷。如果底层错误是无结构的,适配器吐出来的错误同样无结构;如果 schema 本身很松散,适配器只会让这个松散 schema 看起来比原来更可信。适配器改变的只是调用方找到你的路径,并没有改变它到达之后看到的东西。而适配器试图掩盖的那个缝隙,恰恰决定了你会不会被调用两次。

重试风暴是你的问题,不是智能体的问题

智能体靠试错来工作。这是机制本身,不是缺陷。这意味着同一个智能体在几秒内可能多次访问同一个端点,只为了摸清你接口契约的结构。要让这种场景可控,需要两样东西:

POST /v2/orders
Idempotency-Key: 7f4e2c91-3a5d-4b8e-9c12-0d6a4f8b2e77

没有这个键,智能体只是在做它该做的事——重试一个超时的模糊请求——就会产生重复记录,然后某个开发者的周二下午就耗在了对账上。

二十分钟摸清自己的处境

拿你自己的 OpenAPI 文档,复制一份,把所有 description、summary 和 example 字段删掉,读一读剩下的内容。

如果剩下的文档无法告诉你:接受的格式是什么、枚举值有哪些、哪些字段互斥、每种错误长什么样——那它对智能体同样无法告知这些。而你自以为有文档记录的部分,其实完全靠那些调用方会丢弃的散文式描述在支撑。

然后,把你服务可能返回的所有状态码列出来,逐个检查哪些有对应的 schema。最后,试着只用 curl、不开浏览器拿到一个 token。

三项检查,一个下午,比做一次「就绪评估」有用得多。这项工作背后的有效指标,不是有多少开发者注册了你的开发者门户,而是:一个智能体能否在完全没有人工参与的情况下,发现你的 API、理解它的契约、发起调用并正确处理响应。这个数字的冷酷程度,是注册数远远比不上的。

这就是“可选”成为过去式的原因。2024年11月,Anthropic推出了模型上下文协议(Model Context Protocol),它的扩散速度远远超过了它所借鉴的那些规范。短短几个月内,各大智能体平台就纷纷提供了支持。2025年12月,该协议被捐赠给Linux基金会旗下的Agentic AI基金会,联合创始方包括Anthropic、Block和OpenAI——而当一个厂商协议走到这一步,就意味着它已经变成了基础设施。2026年7月的修订版沿着同一方向又推进了一步,将部分协议规范改为无状态运行,使其能够在并非为其设计的网关和负载均衡器中存活。这些变化并不会改变你的API需要做什么。它们改变的是后果到来的速度。智能体遇到无法解析的错误时,不会提交工单,也不会写一篇满腹牢骚的博客文章。它会直接选择另一个工具,而你对此毫无记录——因为那次并未发生的调用,在你的各项指标里留不下一丝痕迹。

我为此专门写了一整章内容,而在与真实API项目打交道的过程中,最令人不安的发现是:成熟度与就绪度其实是两个不同的维度。API项目做得最成熟的组织,在“对人类友好”的维度上得分最高,在“对智能体友好”的维度上得分却最低——因为他们投入的每一项资源,无论是开发者门户、SDK、教程还是社区,全部瞄准的都是会阅读文档的使用者。而那些什么积累都没有的团队,两个维度得分都很低。这个落差本身就是诊断结果。你的网关是为人类开发者而建的。下一个来调用它的对象,并不在乎你的文档质量、你的品牌形象,也不在乎你的开发者社区。它在乎的是你的schema、你的错误格式和你的认证协议——而这三样东西,它在一次请求里就会全部摸清。

《平台经济学:AI如何改写平台、API与合作伙伴关系的规则》现已出版。← mohammed-brueckner.com

《你的API通过了所有测试,却在智能体面前隐形》最初发表于Medium上的Dev Genius专栏,读者正通过点赞和回应继续着这场对话。

查看原文