让 AI 帮你设计一套 RESTful API,出来的代码确实能跑。但你 review 一遍就会发现,路径一会儿单数一会儿复数,错误码有的返回 -1 有的返回 1001 有的返回字符串,响应结构有的字段是 user_name 有的字段是 userName,分页接口一会儿 offset 一会儿 cursor。代码 review 这件事 AI 帮你做了,但 review 出来的问题它从来不会自己改。
这不是 prompt 写得不到位。接口设计是有客观标准的工程行为,而 AI 的训练语料里这些标准是碎片化的。它见过好的接口也见过烂的,但没有一条"什么是好"的清晰边界。所以它生成出来的东西平均能用,但永远不会好。
AI 写 API 的五个通病
通病一:路径命名风格飘忽
AI 起路径名的习惯可以总结成"想到哪个写哪个"。同一个 /users 资源下面,它可能给你 POST /users/createUser、GET /user/list、GET /users/{id}/getInfo、DELETE /users/deleteUser。
RESTful 的路径命名规则其实不复杂,但 AI 老是违反:
- 资源路径必须是名词复数(
/users、/orders),不是动词 - 路径里不能有动词(
/getUser是反模式,正确是GET /users/{id}) - 子资源用嵌套路径(
/users/{id}/orders) - 批量操作可以接受命名动词(如
/users/batch-import),但单个资源操作全部用 HTTP 方法表示
AI 的问题不是不知道这些规则,是它没法把这些规则一致地应用在一整个项目里。它每次都是"重新思考"路径名,而不是遵循项目已有的命名约定。
通病二:HTTP 方法被当成装饰
你让 AI 写 CRUD 接口,它大概率给你这样的代码:
java
@PostMapping("/user/create")
public Result<UserDTO> createUser(@RequestBody UserCreateRequest req) { ... }
@PostMapping("/user/update")
public Result<UserDTO> updateUser(@RequestBody UserUpdateRequest req) { ... }
@PostMapping("/user/delete")
public Result<Void> deleteUser(@RequestBody UserDeleteRequest req) { ... }
@PostMapping("/user/list")
public Result<List<UserDTO>> listUsers(@RequestBody UserQueryRequest req) { ... }
4 个接口全部用 POST,因为"传 JSON 方便"。HTTP 方法在这里完全失去了语义。GET/POST/PUT/PATCH/DELETE 不是技术选型,是 API 的契约层。客户端通过 HTTP 方法就知道这个操作是否幂等、是否安全、是否会修改服务器状态。
RESTful 风格从来不是政治正确,是工程协作的成本问题。统一方法语义后,前端不需要看文档就知道怎么写,监控可以按方法打点,网关可以按方法做限流(GET 一般不限流,POST 限流严),CDN 可以缓存 GET 响应。这些东西 AI 默认不会替你做。
通病三:错误码系统像一锅粥
AI 返回错误码的姿势有几种典型:
- 直接抛 HTTP 状态码当业务错误(404 Not Found 表示"用户不存在",503 Service Unavailable 表示"余额不足")
- 自定义 code,类型一会儿 string 一会儿 int("1001"、"-1"
、INVALID_PARAM) - 错误信息和 code 重复(code 已经说不清楚了,message 又说一遍)
- 没有错误分类,全靠 message 字符串判断(前端写
if (msg.includes("余额")))
错误码系统的工程标准其实有共识:
- HTTP 状态码只表示 HTTP 层的状态(网络成功/失败、参数格式错误、认证失败)
- 业务错误用独立的 code 字段(如
biz_code: 10001表示余额不足、biz_code: 10002表示库存不足) - 错误分类按业务模块划分(用户模块 10000-10999、订单模块 11000-11999),便于监控聚合
- code 是数字或固定字符串,不带变量信息;详细错误信息放 message 字段;排查用 trace_id
AI 写出来的错误码系统,每个接口都不一样,因为每次都是"自由发挥"。这不是 prompt 写得不好,是缺少错误码规范的设计原则。
通病四:响应结构无法统一
同一个项目里,AI 可能给你这样的接口:
GET /users/{id}直接返回 User 对象{id: 1, name: "x"}GET /orders返回{data: [...], total: 100}POST /users直接返回新创建的 UserPOST /login返回{token: "xxx", userInfo: {...}}
没有统一的响应包装层,前端拿到响应还得先看文档判断结构。这种接口前端写着很难受------有的接口返回 {data: ...}、有的直接返回对象、有的列表包了一层、有的没包。
统一的响应结构工程标准通常是:
json
{
"code": 0,
"message": "ok",
"data": { ... } 或 [...] 或 null,
"trace_id": "abc123"
}
code=0 表示成功,非 0 表示失败;data 才是真正的业务数据;trace_id 用于排查。这种结构在前端可以统一拦截处理(code 非 0 自动弹错误提示、code 等于某个特定值跳登录页)。
AI 默认不会主动设计响应结构包装层,因为它假设"前端会处理"。这就是接口设计与 AI 生成之间的 gap。
通病五:分页策略随便选
AI 写分页接口,可能给你:
?page=1&pageSize=20(offset 分页)?offset=0&limit=20(offset 另一种风格)?cursor=xxx(cursor 分页)- 响应里有的有
total、有的有hasMore、有的都没有
分页策略不是风格问题,是性能问题。offset + limit 在数据量大的时候会慢------MySQL 跳过 100 万行去取第 100001 行,要扫 100 万行。Cursor 分页(基于最后一条记录的 ID)不管翻到第几页都是 O(log n)。
工程上的选择通常是:
- 后台管理列表(数据量小、需要 total 做分页器)→ 用 offset
- 用户面向的滚动列表(数据量大、性能敏感)→ 用 cursor
- 不要在一个接口里同时返回 cursor 和 total(cursor 模式下 total 没意义)
AI 默认用 page=1&pageSize=20,因为它最常见。但它不会告诉你这个选择在大数据量下会有性能问题。
sharp-api-design 怎么管住 AI 的接口输出
sharp-skills 的 api-design 模块做的事情跟其他模块一样:把接口设计的品控标准写成 AI 能执行的规则文件。
规则分三级:
MUST 级规则(违反就重写):
- 路径必须是名词复数形式,禁止使用动词(
/users不是/getUsers) - CRUD 操作必须使用对应的 HTTP 方法(GET 查询、POST 创建、PUT 全量更新、PATCH 部分更新、DELETE 删除)
- 业务错误必须使用独立的
biz_code字段,不能直接复用 HTTP 状态码表示业务错误 - 响应必须有统一的包装结构(
code/message/data/trace_id) - 所有列表接口必须明确分页策略(offset 或 cursor 二选一,全项目一致)
- 请求参数必须有边界校验注解(
@NotNull、@Size、@Pattern),不能信任客户端 - 错误码必须按业务模块分区间(用户模块 10xxx,订单模块 11xxx)
SHOULD 级规则(尽量遵守):
- 路径应使用 kebab-case(
/user-orders不是/userOrders) - 字段命名应使用 snake_case(保持 JSON 序列化风格统一)
- 接口路径前缀应包含版本号(
/api/v1/users) - 列表接口应支持按字段排序(
?sort=-created_at) - 批量操作应支持事务性参数(
?atomic=true) - 接口文档应自动生成(OpenAPI 注解)
MAY 级规则(锦上添花):
- 可以添加请求签名头(防重放)
- 可以添加幂等键参数(POST 接口防重复提交)
- 可以添加 ETag 头(GET 接口支持 304 缓存)
- 可以添加
X-RateLimit-Remaining响应头
这套规则的核心思路是:接口设计是有客观标准的工程行为,不是"看风格"。 路径命名、错误码、分页策略这些都有对错之分,不是"我习惯就行"。
一个实际对比
同一个需求:实现用户管理模块的 CRUD 接口。
AI 默认输出:
sql
POST /user/create
POST /user/update
POST /user/delete
POST /user/list
GET /user/getInfo
错误返回 Result.fail("参数错误")、Result.fail("用户不存在")、Result.fail(-1) 三种风格混用。响应结构 POST /user/create 直接返回 User 对象,POST /user/list 返回 {data: [...], total: 100}。分页用 page=1&size=10。
sharp-api-design 约束下的输出:
bash
GET /api/v1/users # 列表,cursor 分页
GET /api/v1/users/{id} # 详情
POST /api/v1/users # 创建
PUT /api/v1/users/{id} # 全量更新
PATCH /api/v1/users/{id} # 部分更新
DELETE /api/v1/users/{id} # 删除
错误统一为 biz_code(用户不存在 10001、邮箱已注册 10002、参数校验失败 10400)。响应统一包装层 code/message/data/trace_id。分页统一 cursor 模式。
同样的功能,前者是"能跑的代码",后者是"能协作的接口"。前端拿到第二种可以无脑写拦截器,拿到第一种得写一堆适配。
接口设计规则为什么不能直接写进 prompt
你当然可以在 prompt 里写"请使用 RESTful 风格"、"错误码用业务分类"、"响应统一包装结构"。但跟数据可视化一样的三个问题:
第一,prompt 衰减。第一轮对话写下的约束,到第三轮 AI 就忘了一半。这是 LLM 的注意力机制决定的,不以你的 prompt 水平为转移。
第二,重复成本。每个项目都写一遍这些规则,prompt 比接口文档还长。规则文件化之后,写一次,复用一百次。
第三,标准漂移。今天你写的"错误码用业务分类",明天 AI 帮你生成新接口的时候可能就退化成"直接返回 HTTP 状态码"。品控的核心是可复现,每次新生成都得跟上次一致,规则文件就是锁定这个一致性。
更关键的是,接口设计跟其他领域不一样,它的"对错"是有判断人的------前端、客户端、QA、监控、运维都会按照规则检验你的接口。规则文件 AI 执行一次就过 review,prompt 执行十次可能有八次过不了 review。这是工程化思维在做接口设计这件事上的具体体现。
api-design 规则不是万能的
说句实话,sharp-api-design 能管住的是"接口命名规范"、"错误码体系"、"响应结构统一"这些契约层的标准。它管不了的是"这个业务该不该拆成 5 个接口还是 3 个接口"、"这个字段该不该冗余"、"这个操作该不该异步"。
这些是业务建模的判断,不是接口设计的规则。一个订单系统是按状态拆接口(创建订单/支付订单/取消订单)还是按资源拆接口(订单/支付/退款),取决于产品设计,不是规则文件能定的。
规则文件管的是"怎么写对",不管"该不该这样写"。后者需要人判断,AI 短期内也做不好这个判断。但契约层的标准用规则固化下来,至少不会让团队里不同的人写出风格完全不一致的接口。
我做过的对比实验:拿同一个用户管理需求,让 3 个 prompt 模板(不加载规则)和加载 sharp-api-design 规则的 AI 同时生成 5 套接口方案,拿给 5 个有 3 年以上经验的后端开发盲评。规则约束下的接口在"路径规范"、"错误码可识别"、"响应一致性"、"分页策略合理性"四个维度上评分平均高出 50%。差距最大的就是错误码一致性------自由模式下 AI 5 次输出用了 3 套不同的错误码风格,规则模式下 5 次输出完全一致。
接口设计这件事,审美可以讨论,工程标准不能含糊。 AI 缺的不是创意,是落地的标准。规则文件落下去的标准比 prompt 里写一百句"请遵循规范"管用得多。
顺便说一句,我在做的那个小程序「爪爪代码冒险记」里有个接口设计关卡,用卡皮巴拉做 API 网关拦截各种不符合规范的请求,比直接讲规则好玩。感兴趣可以搜搜看。