AI 集成场景下的 API 错误分类体系

AI 集成场景下的 API 错误分类体系
Spec Coding 编辑部 · Spec-First 工程实践内容

怎样设计一份 AI 生成的客户端真正用得上的 API 错误分类:稳定的 error code、机器可读的 category,以及区分可重试失败和永久失败的关键字段。

发布于 2026-03-10 · ✓ 已更新 2026-05-06 · 阅读约 6 分钟 · 作者:Spec Coding 编辑部 · 审校:编辑与事实核查政策

内容复查说明

复查日期:2026-05-06。本文已作为可索引的专题参考重新放出,并与 AI 编码治理 Hub 形成内链。内容保留了可复用的评审材料、失败模式和下一步阅读路径,适合直接用于实际团队讨论。

纯文本错误信息就是个陷阱

我见过的集成失败,栽在错误信息上的远多于栽在主流程上的。套路都差不多:服务端返回 400 Bad Request,body 里写一句 "Something went wrong processing your request. Please try again later."。人类运维看一眼大概能猜到下一步该干嘛,可程序化客户端猜不出来。而现在越来越多消费我 API 的是 AI 生成的客户端,它卡在两者之间——自信地编造出一套重试逻辑,把永久失败误判成瞬时失败,然后拿着一张已拒付的信用卡把客户的 rate limit 耗光。

解决方法不是把英文写得更漂亮,而是把 error envelope 的主要读者当成机器,人类只是次要读者。

光靠 HTTP 状态码撑不起契约

我评审过的每一个 API 都太依赖 HTTP 状态码。400 代表校验失败——除非它代表支付被拒,或者账户被冻结,或者 feature flag 没开。500 代表可以重试——除非你违反的那条数据库约束在接下来 47 次尝试里会继续失败。状态码只是个路由提示,不是一套分类体系。如果我让一个代码生成模型"碰到 5xx 就重试",它会一丝不苟地照做,结果有一半场景都是错的。

我依然会返回正确的 HTTP 状态码,只是不指望任何客户端仅凭它就能做决定。

我在生产里用的四字段错误信封

我服务返回的每个错误响应,都恰好有四个顶层字段,含义永远一致:

四个字段。再多就是压在消费者身上的认知税,再少则是把解析问题甩给客户端。

稳定的 code 才是关键

我把 code 写成 snake_case.hierarchical 的字符串——能做到就用三段式:domain、condition、subcondition。payment.card_declined.insufficient_fundsauth.token.expiredrate_limit.tenant.daily。层级结构意味着客户端就算不认识叶子节点,也能靠 domain 前缀做模式匹配,干点合理的事。一个 AI 生成的客户端,只读过我那一页参考文档,就能在没见过的 code 上给出正确的重试语义,因为前缀已经足够用来路由了。

我在评审里死守一条规则:code 一旦发布,就被冻结。我可以新增 code,可以给 code 标上 sunset 日期并弃用,但不能改名、拆分,也不能改变它描述的条件。违反这条规则,所有已经跑在外面的生成客户端都会跟我一起崩。

一套真正管用的 category 分类

category 字段是一个封闭集合。我用的就这八个值,从来没觉得需要第九个:

这个列表小到可以背下来,边界也清晰到每个 code 正好对应唯一的 category。当 AI 生成的客户端认不出 code 时,它会回落到 category,照样能做对事。

字段级校验要用结构化数组

校验错误在 details 里有自己的形状。不能是单个字符串,不能是一坨 HTML,而是一个数组,每个条目都有 fieldcodemessage

"details": {
  "field_errors": [
    {"field": "email", "code": "validation.format.email", "message": "Must be a valid email address"},
    {"field": "amount", "code": "validation.range.min", "message": "Must be at least 1"}
  ]
}

嵌套结构的 field 用点号路径表示(billing.address.postal_code)。code 与顶层 code 共享同一套稳定命名空间。表单 UI 不用解析英文就能把错误映射到输入框。生成的客户端也可以按字段 code 构造强类型异常。

重试提示要放在两个地方

响应可重试的时候,我会把提示发两遍:给懂标准的客户端用 Retry-After HTTP 头,给其他人用 details 里的 retry_after_seconds。冗余成本很低,指望每一个生成客户端都能正确读 header 的成本很高。我见过太多 SDK 悄无声息地把 header 丢掉,body 却解析得好好的。

对于 rate_limit,我还会带上 details.limitdetails.remaining,让聪明的客户端可以自行调速,而不是一头撞进下一个 429。

关联 ID 不容妥协

每个错误 body 都带一个 details.trace_id,和我日志里的 ID 对得上。客户把错误贴进工单时,我希望一次复制粘贴就能把我带到 tracing 后端里那条精确的请求。这个 ID 是客户端可见失败与服务端证据之间的桥梁,也是我唯一从来没为添加它后悔过的字段。

本地化是元数据,不是身份

message 字段永远是英文。调用方想要本地化字符串,就传 Accept-Language,我会填充 details.localized_messagecode 不会因 locale 而变。这个教训是从一个按语言返回错误码的 API 那里学来的——法语返回 error_carte_refusee,英语返回 error_card_declined——结果每一个跨区域的客户端都挂了。

一个真实的例子

下面是我某个服务对信用卡拒付的真实响应:

HTTP/1.1 402 Payment Required
Retry-After: 0
Content-Type: application/json

{
  "code": "payment.card_declined.insufficient_funds",
  "category": "permanent",
  "message": "Card declined: insufficient funds",
  "details": {
    "trace_id": "01HX8K3M9P2QZ7N4R6S8T0V2W4",
    "retry_after_seconds": 0,
    "issuer_decline_code": "51",
    "localized_message": null,
    "field_errors": []
  }
}

category 是 permanent,告诉客户端不要拿同一张卡重试。code 是 payment.card_declined.insufficient_funds,告诉聪明点的客户端应该提示换一张卡,而不是弹一个泛泛的"payment failed"模态框。trace ID 直接把支持人员带到日志里。

Given/When/Then 形式的验收标准

- Given a request that exceeds the per-tenant rate limit
  When the API rejects the request
  Then the response has code starting with "rate_limit."
   And category is "rate_limit"
   And details.retry_after_seconds is a positive integer
   And the Retry-After header matches details.retry_after_seconds
   And details.trace_id is present and matches the log record

- Given a validation failure on two fields
  When the API rejects the request
  Then category is "validation"
   And details.field_errors contains one entry per failing field
   And each entry has field, code, and message populated

单页参考文档这个套路

我维护一个单页文档——就是字面意义上的一份 Markdown 文件——列出每一个 code、它的 category、是否可重试,以及一个示例 body。这一页既是我 SDK 生成器的单一事实来源,也是客户基于我的 API 写 AI 辅助客户端时的依据。新 code 上线前,先落到这一页,再进生产。当文档和运行时对不上时,文档赢,运行时就是 bug。一页、一份契约、一个查阅入口。

错误分类要让机器能做决定

“请求失败”不是错误分类。AI 生成的客户端会抓住你给它的字段做分支,所以错误响应至少要告诉它:是否可重试、是否需要用户动作、是否可以安全重放、是否应该报警。

{
  "error": {
    "code": "payment_method_declined",
    "category": "user_action_required",
    "retryable": false,
    "safe_to_retry_with_same_idempotency_key": true,
    "message": "Ask the customer for another payment method.",
    "docs_url": "https://api.example.com/errors/payment_method_declined"
  }
}

边界:不要把内部异常类名暴露成公开 error code。公开 code 是契约,一旦 SDK 和合作方依赖它,就要按 API 版本管理。

客户端测试也要进入规格

错误分类写完后,要补一个客户端测试:相同 code、category、retryable、message 字段进入旧 SDK 时,状态是否被正确映射,重试是否停止,日志是否保留 request_id。没有这条证据,API 团队只能证明服务端返回了 JSON,不能证明集成方真的能用。

最后给 taxonomy 加一条回归测试:同一个错误 code 在 API、SDK、日志表和告警规则里含义一致。字段名可以不同,语义不能漂。这个测试很便宜,却能提前发现“服务端可重试、客户端不可重试”的分裂状态。

关键词:API error taxonomyerror codesretry semanticsmachine-readable errorsAPI contractsAI-generated clients

编辑说明与免责声明

最近复核:2026-05-06。编辑部检查了示例、专题内链和可复制评审片段,确保内容更适合真实项目使用。

本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。