@TOC
让 AI「把订单接口整理得规范一点」,它改出来的东西往往确实更规范:参数加上了必填校验,金额改成字符串避免精度问题,顺手删掉一个「看起来没人用」的字段。
服务端的单元测试全部通过。上线之后,老版本的 App 开始报错。
问题不在 AI 代码写错了,而在它不知道有人在用这个接口。接口一旦对外,改动是否兼容,比代码写得漂不漂亮重要得多。这件事靠人盯 diff 很容易漏,最好交给工具,在合并前自动检查。
这篇用 oasdiff 实测一遍:先看它能查出什么、查不出什么,再把它接进 CI,最后给一段约束 AI 改接口的 Prompt。
一、哪些改动会让调用方出错
常见的不兼容改动
| 改动 | 为什么会出错 |
|---|---|
| 可选参数改成必填 | 老客户端没传这个参数,请求直接被拒 |
| 请求体新增必填字段 | 老客户端不知道这个字段,也就不会传 |
| 响应字段改类型 | 客户端按原来的类型解析,要么解析失败,要么算错 |
| 删除响应字段 | 读这个字段的客户端拿不到值 |
| 删除接口、改路径或方法 | 调用直接 404 或 405 |
| 请求参数的枚举值变少 | 老客户端传的值不再合法 |
反过来,新增可选参数、新增响应字段、新增接口,一般是安全的。例外是对未知字段直接报错的严格客户端,这个要看调用方的实现。
AI 为什么特别容易踩
AI 改代码时,看到的是服务端这一侧:handler、结构体、测试。它看不到调用方,也不知道哪些字段有人在读。「让代码更规范」这个目标本身,就会驱动它做出上面表里的改动。
二、准备新旧两份契约
旧契约
接口契约用 OpenAPI 描述,放在仓库里,比如 api/openapi.yaml。下面是一个精简的订单接口:
openapi: 3.0.3
info:
title: Order API
version: 1.0.0
paths:
/orders:
get:
parameters:
- name: status
in: query
required: false
schema:
type: string
responses:
"200":
description: ok
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Order"
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [amount]
properties:
amount:
type: integer
responses:
"201":
description: created
components:
schemas:
Order:
type: object
required: [id, amount]
properties:
id:
type: integer
amount:
type: integer
remark:
type: string
AI 的四处「优化」
模拟一次 AI 的「规范化」改动,一共四处:
- 查询参数
status从可选改成必填 - 创建订单的请求体新增必填字段
currency - 响应里的
amount从整数改成字符串 - 删掉响应里的
remark字段
对应的 diff:
@@ -8,7 +8,7 @@
parameters:
- name: status
in: query
- required: false
+ required: true
schema:
type: string
responses:
@@ -27,10 +27,12 @@
application/json:
schema:
type: object
- required: [amount]
+ required: [amount, currency]
properties:
amount:
type: integer
+ currency:
+ type: string
responses:
"201":
description: created
@@ -43,6 +45,4 @@
id:
type: integer
amount:
- type: integer
- remark:
type: string
最后一段 diff 看着有点绕:删掉的是 amount 原来的 type: integer 和整个 remark,留下的 type: string 现在归 amount。这正是人工 review 容易看走眼的地方。
三、用 oasdiff 对比
安装
# Go 1.26 及以上
go install github.com/oasdiff/oasdiff@latest
我本机是 Go 1.25,go install 会直接提示需要 1.26。Go 版本不够的话,去 oasdiff 项目的 GitHub Releases 下载预编译包更省事,macOS、Linux、Windows 都有。我用的是 v1.32.1。
查不兼容改动
oasdiff breaking old.yaml new.yaml
输出是 3 条 error:
| 级别 | 规则 | 说明 |
|---|---|---|
| error | request-parameter-became-required | GET /orders 的查询参数 status 变成了必填 |
| error | response-property-type-changed | GET /orders 响应里 items/amount 的类型从 integer 变成了 string |
| error | new-required-request-property | POST /orders 新增了必填的请求字段 currency |
四处改动查出了三处,而且每一条都写清楚了是哪个接口、哪个字段、从什么变成了什么。
第四处去哪了
删掉 remark 这一处,breaking 没有报。换成 changelog 看全部改动:
oasdiff changelog old.yaml new.yaml
这次一共 5 条,除了上面 3 条 error,还多了 2 条 info:
response-optional-property-removed:从 200 响应里删掉了可选字段items/remarkapi-version-not-bumped:有不兼容改动,但版本号还是 1.0.0
oasdiff 的逻辑是:remark 在契约里本来就是可选的,客户端理应能处理它不存在的情况,所以只算 info。道理没错,但如果你的某个客户端就是在读 remark,删掉它照样会出问题。
所以工具的默认规则,不等于你的调用方实际怎么用。删字段这类改动,就算工具没拦,也要去确认调用方有没有在用:查客户端代码,或者看看各版本客户端的请求里有没有带上这个字段。
前端或 SDK 和服务端在同一个仓库时,我会先在 WES Code 里问一句「项目里哪些地方读了 remark 这个字段」,有人在用就不删,改成标 deprecated。
第二条 info 也很实用:发现了不兼容改动,却没改版本号。这种提醒人工 review 时基本不会有人想到。
四、接进 CI:不兼容就不让合并
核心就两条命令
# 取出主干上的旧契约
git show origin/main:api/openapi.yaml > /tmp/base.yaml
# 有 error 级别的不兼容改动时,退出码为 1
oasdiff breaking /tmp/base.yaml api/openapi.yaml --fail-on ERR
--fail-on ERR 是关键:默认情况下,就算查出了不兼容改动,oasdiff 的退出码也是 0,CI 不会失败;加上这个参数,才会返回 1。想更严格,可以改成 --fail-on WARN。
我在本地建了个 git 仓库模拟过:main 上提交旧契约,feature 分支上换成新契约,跑这两条命令,退出码是 1,3 条 error 都列出来了。
GitHub Actions 示例
name: api-compat
on: pull_request
jobs:
oasdiff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 要拿到目标分支的历史,才能 git show 旧契约
- uses: actions/setup-go@v5
with:
go-version: "1.26"
- name: Check breaking changes
run: |
go install github.com/oasdiff/oasdiff@latest
git show origin/${{ github.base_ref }}:api/openapi.yaml > /tmp/base.yaml
"$(go env GOPATH)/bin/oasdiff" breaking /tmp/base.yaml api/openapi.yaml \
--fail-on ERR --format githubactions
--format githubactions 会把每条结果输出成 GitHub 的注释格式,直接显示在 PR 页面上。GitLab、Jenkins 也是同样的思路:取出目标分支的契约,对比,失败就拦。
五、让 AI 改接口时带上约束
可复制 Prompt
你在修改一个已经有调用方在用的 HTTP 接口,契约文件是 api/openapi.yaml。
硬性要求:
1. 不得引入不兼容改动:不能把可选参数改成必填,不能删除或改名字段,不能改字段类型,不能新增必填的请求字段
2. 确实需要不兼容改动时,先停下来告诉我原因,并给出兼容方案:新增字段并保留旧字段、给旧字段标 deprecated,或者新增 v2 接口
3. 先改契约,再改实现
4. 改完运行 oasdiff breaking <旧契约> api/openapi.yaml,把输出原样贴给我
第 4 条最管用:让它自己跑检查,把结果贴回来,比事后人工 review 可靠。
我在 WES Code 里改接口时用的就是这段约束:改完契约,让它在终端里跑一遍 oasdiff,有 error 就按第 2 条改成兼容的写法,再跑,直到通过。
六、不兼容改动非做不可时
新增,不删除。 需要新的字段或类型,就新增一个字段,旧的保留,并在契约里给旧字段标上 deprecated: true,等调用方迁移完再删。
必填参数给默认值。 与其把参数改成必填,不如在服务端给一个合理的默认值,老客户端不传也能正常工作。
版本化。 改动大到没法兼容时,新开 /v2/orders,和旧接口并存一段时间。
先通知,再下线。 列出受影响的调用方,约好兼容期,到期后再删旧字段或旧接口。
七、它查不出来的东西
契约和实现不一致。 oasdiff 只看契约文件。代码改了、yaml 没改,它就看不到。要么从代码自动生成契约,要么加一层契约测试,保证两边一致。
语义变化。 字段名和类型都没变,但含义变了,比如金额从「元」改成了「分」。这种改动工具查不出来,只能靠 review 和测试。
默认只算 info 或 warn 的改动。 比如上面删掉可选字段。这类改动要结合调用方的实际情况,决定要不要拦。
小结
- AI 改接口时看不到调用方,「规范化」改动很容易变成不兼容改动
- 接口契约放进仓库,改接口先改契约
- 合并前用
oasdiff breaking --fail-on ERR自动检查,不兼容就不让合并 - 工具没拦的删字段、语义变化,要结合调用方确认
文中的契约对比和检查流程,是我在 WES Code 里改接口时的做法,官网是 weisyn.com。你们团队是怎么管接口兼容性的,靠 review、契约测试还是工具,欢迎评论区聊聊。
觉得有用的朋友,欢迎点赞、收藏、关注,后面会继续分享 AI 编程的实战经验。