嵌了类型定义后,API 客户端代码还得过哪几道静态检查才敢合入

直接把类型定义塞进提示词,只能解决“模型知道该长什么样”的问题,离“这段代码能合入”还差三道硬关卡:语法与类型可编译、契约与语义对齐、生成代码与既有工程规范不冲突。下面按“先能编译,再验语义,最后查工程一致性”的顺序逐层拆。

第一道:可编译性校验——先让代码真的能跑起来

生成代码第一步必须是“能通过编译器/解释器的语法与类型检查”。这不是废话,很多从提示词直接产出的客户端代码连 import 都缺,或者字段名大小写对不上。

具体做法是把生成结果扔进真实工具链,而不是靠模型自己说“我写的是对的”:

  • TypeScript 场景:跑 tsc --noEmit,配合 strict: true。如果项目开了 noUncheckedIndexedAccessexactOptionalPropertyTypes 这类严格选项,必须用同一份 tsconfig 来查,否则本地过了 CI 上照样挂。
  • Python 场景:用 mypy --strictpyright,并且要把嵌入提示词里的那套类型定义(TypedDict、dataclass、Pydantic model)实际 import 进来检查,而不是只做语法解析。
  • Go/Java/Kotlin 等:直接调 go build / javac / gradle compileKotlin,让编译器对照真实的接口类型做全量类型推导。

这里有一个容易踩的坑:模型生成的代码可能“局部自洽”,比如自己定义了一个跟真实类型同名的 interface,然后编译通过,但根本没 import 项目里那份真实类型。所以编译检查必须发生在生成代码与真实类型定义同处一个编译单元或模块图的前提下,而不是单独编译生成片段。

第二道:契约与语义校验——编译过了不代表调对了

编译通过只说明类型对得上,不说明字段语义、请求方法、路径参数、错误处理真的跟接口契约一致。这一步要拿生成代码去和“权威契约源”做比对。

2.1 用 JSON Schema / OpenAPI 做结构校验

如果接口契约有 OpenAPI 或 JSON Schema 描述,直接把生成代码里构造的请求体、解析的响应体做运行时或静态比对。一个实用的做法是:让生成代码输出一个“请求模板 + 响应解析逻辑”的中间表示,再用工具校验这个中间表示是否覆盖了 schema 里的 required 字段、枚举值、嵌套结构。

比如生成代码里写了:

const body = { name: user.name, age: user.age };

但 OpenAPI 里 agenullable: trueminimum: 0,静态比对就要能发现生成代码没处理 null 分支。这类问题编译期抓不到,只有拿 schema 逐字段 diff 才能暴露。

2.2 关键业务规则断言

有些约束是类型系统表达不了的,必须在生成后写针对性断言。比如:

  • 分页参数 page_size 不能超过 100,但生成代码里写死了 page_size: 500
  • 时间字段要求 RFC3339 格式,生成代码用了 toISOString() 没问题,但用 Date.toString() 就错了;
  • 某个接口要求幂等键 idempotency_key 必填,生成代码漏了。

这些规则最好预先整理成结构化清单(哪怕是 YAML 或一组正则规则),每次生成后自动跑一遍。不要指望 prompt 里“顺便提一句”就能稳定生效——实测把业务规则写成可执行断言,比在提示词里重复十遍更可靠。

2.3 错误路径与状态码覆盖

生成代码最常见的隐性缺陷是“只写了 happy path”。静态检查要确认生成代码对非 2xx 响应有处理分支,且错误类型跟契约里定义的错误模型对得上。比如契约里 409 返回 ConflictError,生成代码却统一 throw new Error(res.statusText),这就是语义丢失。

可以通过 AST 分析检查:每个请求函数里是否显式处理了契约中声明的非成功状态码;错误对象是否从响应体反序列化,而不是只读 status。

第三道:工程一致性校验——能编译能跑还不够,得“像人写的”

这一层最容易被忽略,但实际合入时卡得最多的就是这里。生成代码要过 lint、格式化、安全扫描、依赖约束,以及团队自己的架构约定。

3.1 格式化与 lint

直接上项目现有的 eslint + prettierruff + black 配置,不要给生成代码开白名单。模型产出的 import 顺序、未使用变量、any 泄漏、命名风格,一跑 lint 全会现形。如果 CI 里 lint 挂掉,代码就不该合入——这个标准对生成代码和手写代码必须一致。

3.2 安全检查

客户端代码有两个高频安全问题:

  • 密钥/令牌硬编码:用 gitleakstrufflehog 或 Semgrep 的 secrets 规则扫一遍。模型有时会把示例 token 当真值写进去。
  • 注入与不安全反序列化:如果生成代码里有 evalinnerHTMLpickle.loadsyaml.load 这类调用,直接拦截。用 Semgrep 或 CodeQL 跑项目现有的安全规则集即可。

3.3 依赖与 import 约束

生成代码可能引入项目里不存在的依赖,或者绕过了内部封装。比如团队统一用 src/api/http.ts 里的 request 函数发请求,生成代码却直接 import axios from 'axios';或者团队禁用了 lodash,生成代码里 import _ from 'lodash'。这类问题用 eslint-plugin-import 的自定义规则或 dependency-cruiser 的禁止规则就能卡住。

3.4 架构边界与命名约定

如果项目按模块划分目录,生成代码应该落在指定模块内,且不能跨模块 import 内部实现。命名上,请求函数是否遵循 fetchXxx / getXxx 约定,类型是否带 Dto / Model 后缀,这些用 AST 规则或简单的文件名/符号名正则就能查。合入前跑一遍团队自己的 archguardmadge 依赖图检查,能避免生成代码把模块依赖关系搅乱。

落地建议:把三道检查串成一条流水线

实际工程里,我会把这三道检查串成生成后的“合入门禁”,顺序固定:

  1. 编译检查(tsc / mypy / go build,使用项目真实配置);
  2. 契约比对(OpenAPI / JSON Schema 结构 diff + 业务规则断言 + 错误路径 AST 检查);
  3. 工程一致性(lint + 格式化 + 安全扫描 + 依赖/架构规则)。

每一道失败就返回给生成阶段重试,并把具体错误信息作为反馈塞进下一轮 prompt,而不是无脑让模型重写。这样迭代两三轮,基本能稳定产出“敢合入”的客户端代码。重点在于:类型定义只是输入,真正的质量门槛是这些可执行检查,而不是模型的一次性输出。

常见问题

问:已经有 OpenAPI 生成了客户端,为什么还要自己写提示词再生成?

OpenAPI 生成器产出的是“通用结构正确”的代码,但往往不符合团队内部封装习惯、命名风格、错误处理策略。用提示词嵌入类型定义再生成,是为了让代码贴近项目现有写法;而本文说的三道检查,就是保证这种“定制生成”不会因为自由度变高而失控。

问:契约比对具体怎么做,有没有现成工具?

结构比对可以用 openapi-diff 或自己写脚本把生成代码的请求/响应类型提取成 JSON Schema,再和契约 schema 做 ajv 校验;业务规则断言建议整理成结构化规则文件,用自定义脚本或 Semgrep 规则执行;错误路径覆盖用 AST 遍历(如 TypeScript 的 ts-morph、Python 的 ast 模块)检查分支是否存在。

问:生成代码跑 lint 挂了,是改 lint 规则还是让模型重写?

改规则是下策,等于为生成代码降低工程标准。正确做法是把 lint 错误作为反馈让模型重写,通常一两轮就能收敛。如果反复修不掉,说明 prompt 里给的示例代码本身就不符合 lint 规则,需要先修正示例。

问:这三道检查要全部自动化吗,还是人工 review 兜底就行?

编译和 lint 必须自动化,否则生成代码量一大根本看不过来。契约比对至少要自动化结构校验,业务规则断言可以逐步积累。人工 review 应该聚焦在生成代码的业务逻辑合理性上,而不是重复检查机器已经能查出的问题。