用 AI 生成技术规格书——从需求到 OpenAPI Spec 的工程化
用 AI 生成技术规格书——从需求到 OpenAPI Spec 的工程化
去年下半年我们团队做了一次接口规范化的专项治理。起因很简单:前后端联调的时候,大量时间花在「这个字段是什么意思」「这个接口的返回值格式是什么」上,光是对 API 文档,每个迭代周期就要消耗 2-3 天。
解法方向也很明确:用 AI 自动生成 OpenAPI Spec,从需求文档直接产出规范化的接口定义,减少人工翻译需求的时间,同时保证输出格式符合公司标准。
但做完之后,我对「用 AI 生成 API Spec」这件事有了不少新的认识。这篇文章不只是讲怎么做,更多是想把整个工程化过程里的判断和踩坑说清楚。
一、问题定义:不是让 AI 乱写
先把问题定义清楚,否则很容易陷入「用 AI 生成了一堆垃圾」的困境。
错误的期望: 给 AI 一段需求描述,它帮你生成完整的 OpenAPI 3.0 Spec,直接可用。
正确的期望: 在结构化引导下,AI 帮你完成 Spec 的初稿,然后经过验证和校正,输出符合公司标准的 API 定义。
这两者的差异在于:
- 错误期望忽略了「公司标准」这个约束
- 错误期望假设 AI 能完全理解业务上下文
- 正确期望把 AI 定位成「加速器」而不是「替代者」
所以整个工程化方案的核心是:结构化引导 + 标准约束 + 自动验证,而不是一个「输入需求、输出 Spec」的黑盒。
二、OpenAPI Spec 生成的挑战
在讲具体方案前,先说清楚挑战在哪里。
2.1 公司标准差异
每个公司对 API 设计都有自己的规范,比如:
- 错误码格式:
{"code": 10001, "message": "..."}还是{"error": "NOT_FOUND", "detail": "..."} - 分页格式:
page + size还是cursor-based - 时间字段:Unix 时间戳还是 ISO 8601 字符串
- 命名风格:camelCase 还是 snake_case
- 认证方式:JWT Bearer 还是 API Key
如果没有把这些标准注入到生成过程里,AI 会「发明」一套自己的规范,和项目其他接口格式不一致,反而增加工作量。
2.2 业务上下文缺失
需求文档通常写的是业务逻辑,不是接口设计。「用户可以查询自己的订单」这句话,翻译成接口有很多种可能:
- GET
/orders还是 GET/users/{id}/orders - 支持分页吗?支持按状态过滤吗?
- 返回的订单字段有哪些?金额用分还是元?
这些问题需要额外的业务上下文才能回答,不能指望 AI 凭空猜对。
2.3 一致性要求
同一个系统里,相似的接口要用一致的设计模式。比如所有的「批量操作」接口,入参格式应该一致;所有的「查询列表」接口,分页参数和返回格式应该一致。
单次 AI 调用无法感知这种全局一致性要求,需要额外的约束机制。
三、工程化方案:结构化引导流程
整个流程分五步,每一步都有明确的输入输出和验证节点。
四、核心 Prompt 设计
Prompt 设计是这个方案的灵魂。好的 Prompt 会让 AI 产出接近标准的 Spec 初稿,减少后续修正工作量。
4.1 系统 Prompt(注入公司标准)
@Service
public class ApiSpecGenerator {
private static final String SYSTEM_PROMPT_TEMPLATE = """
你是一个专业的 API 设计专家,负责根据需求描述生成符合 OpenAPI 3.0 规范的接口定义。
## 公司 API 设计标准
### 1. 通用响应格式
所有接口统一使用以下响应结构:
```json
{
"code": 0, // 0 表示成功,非 0 表示业务错误码
"message": "ok", // 成功时为 "ok",失败时为错误描述
"data": {} // 实际返回数据,失败时为 null
}
```
### 2. 错误码规范
- 0: 成功
- 1000-1999: 通用错误(1001=参数错误,1002=未授权,1003=禁止访问,1004=资源不存在)
- 2000-2999: 订单模块错误
- 3000-3999: 用户模块错误
### 3. 分页规范
列表接口统一使用以下分页参数:
- 请求:pageNum(从1开始,默认1)、pageSize(默认20,最大100)
- 响应 data 字段包含:total、list、pageNum、pageSize
### 4. 时间字段规范
- 所有时间字段使用 ISO 8601 格式字符串:yyyy-MM-dd'T'HH:mm:ss+08:00
- 字段名以 Time 结尾,如 createTime、updateTime
### 5. 命名规范
- 字段名使用 camelCase
- 路径参数使用小写字母加短横线,如 /order-items/{itemId}
- 枚举值使用大写下划线分隔,如 PENDING_PAYMENT
### 6. 认证规范
所有接口使用 Bearer Token 认证,请在 SecuritySchemes 中定义 BearerAuth
### 7. 版本规范
API 版本号前缀:/api/v1/
## 输出要求
- 严格按 OpenAPI 3.0.3 格式输出
- 只输出 YAML 格式,不要有任何解释文字
- 所有字段必须有 description
- 请求体和响应体都必须有完整的 schema 定义
- 必须包含至少一个 400 和 401 的错误响应示例
%s
""";
private final ChatClient chatClient;
/**
* 生成 OpenAPI Spec
* @param requirement 需求描述
* @param existingSpecs 已有接口的 Spec(用于一致性参考)
* @param moduleContext 模块上下文(数据模型、业务规则等)
*/
public SpecGenerationResult generate(
String requirement,
List<String> existingSpecs,
String moduleContext) {
// 构建完整的 system prompt
String systemPrompt = buildSystemPrompt(existingSpecs, moduleContext);
// 构建用户 prompt
String userPrompt = buildUserPrompt(requirement);
String rawSpec = chatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.options(OpenAiChatOptions.builder()
.temperature(0.1) // 低温度,保证输出格式稳定
.build())
.call()
.content();
// 提取 YAML 内容(有时模型会加代码块标记)
String yamlSpec = extractYaml(rawSpec);
return new SpecGenerationResult(yamlSpec, requirement);
}
private String buildSystemPrompt(List<String> existingSpecs, String moduleContext) {
StringBuilder additionalContext = new StringBuilder();
if (!existingSpecs.isEmpty()) {
additionalContext.append("\n## 已有接口示例(保持与这些接口的一致性)\n");
// 只取前 2 个示例,避免 Token 过多
existingSpecs.stream().limit(2).forEach(spec ->
additionalContext.append("```yaml\n").append(spec).append("\n```\n")
);
}
if (moduleContext != null && !moduleContext.isBlank()) {
additionalContext.append("\n## 模块上下文\n").append(moduleContext);
}
return String.format(SYSTEM_PROMPT_TEMPLATE, additionalContext.toString());
}
private String buildUserPrompt(String requirement) {
return String.format("""
请根据以下需求描述,生成 OpenAPI 3.0 格式的接口定义。
## 需求描述
%s
请直接输出 YAML 格式的 OpenAPI Spec,不需要任何解释。
""",
requirement
);
}
private String extractYaml(String response) {
// 移除 markdown 代码块标记
if (response.contains("```yaml")) {
int start = response.indexOf("```yaml") + 7;
int end = response.lastIndexOf("```");
if (end > start) {
return response.substring(start, end).trim();
}
}
if (response.contains("```")) {
int start = response.indexOf("```") + 3;
int end = response.lastIndexOf("```");
if (end > start) {
return response.substring(start, end).trim();
}
}
return response.trim();
}
}4.2 分步生成(大型接口的策略)
当一个需求描述包含多个相关接口时,一次性生成容易出现格式不一致。分步生成更稳定:
@Service
public class StepwiseSpecGenerator {
private final ChatClient chatClient;
private final ApiSpecGenerator specGenerator;
/**
* 分步骤生成复杂的 API Spec
* Step 1: 先生成接口清单(路径、方法、功能摘要)
* Step 2: 逐个生成每个接口的详细 Schema
* Step 3: 合并并检查一致性
*/
public String generateStepwise(String requirement) {
// Step 1: 生成接口清单
List<ApiEndpoint> endpoints = extractEndpoints(requirement);
log.info("从需求中识别出 {} 个接口", endpoints.size());
// Step 2: 并行生成每个接口的详细定义
List<CompletableFuture<String>> futures = endpoints.stream()
.map(endpoint -> CompletableFuture.supplyAsync(
() -> generateSingleEndpoint(endpoint, requirement)
))
.toList();
List<String> endpointSpecs = futures.stream()
.map(CompletableFuture::join)
.toList();
// Step 3: 合并成完整的 OpenAPI Spec
return mergeSpecs(endpointSpecs);
}
private List<ApiEndpoint> extractEndpoints(String requirement) {
String prompt = String.format("""
分析以下需求描述,提取所有需要的 REST API 接口,返回 JSON 数组。
每个接口包含:method(HTTP方法)、path(路径)、summary(功能摘要,20字以内)
需求:%s
只返回 JSON 数组,格式:
[{"method":"GET","path":"/api/v1/xxx","summary":"..."}]
""",
requirement
);
String response = chatClient.prompt().user(prompt).call().content();
try {
ObjectMapper mapper = new ObjectMapper();
String json = extractJson(response);
return mapper.readValue(json, new TypeReference<List<ApiEndpoint>>() {});
} catch (Exception e) {
log.error("解析接口清单失败: {}", e.getMessage());
return List.of();
}
}
private String generateSingleEndpoint(ApiEndpoint endpoint, String fullRequirement) {
String prompt = String.format("""
从以下需求中,提取与接口 %s %s 相关的需求,生成该接口的 OpenAPI 3.0 路径定义。
完整需求:%s
只输出该接口的 paths 部分 YAML,不需要 openapi、info 等顶层字段。
""",
endpoint.method(), endpoint.path(), fullRequirement
);
return chatClient.prompt()
.system(buildStrictSystemPrompt())
.user(prompt)
.call()
.content();
}
}五、Spec 验证与合规检查
生成的 Spec 必须经过验证才能使用,这一步是工程化的关键。
5.1 OpenAPI 格式验证
@Service
public class SpecValidator {
/**
* 使用 swagger-parser 验证 OpenAPI Spec 格式
*/
public ValidationResult validateFormat(String yamlSpec) {
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
SwaggerParseResult result = new OpenAPIV3Parser()
.readContents(yamlSpec, null, parseOptions);
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
if (result.getMessages() != null) {
for (String message : result.getMessages()) {
if (message.startsWith("attribute") || message.contains("not valid")) {
errors.add(message);
} else {
warnings.add(message);
}
}
}
if (result.getOpenAPI() == null) {
errors.add("无法解析为有效的 OpenAPI 对象");
}
return new ValidationResult(errors.isEmpty(), errors, warnings, result.getOpenAPI());
}
}5.2 公司标准合规检查
这是自定义的检查逻辑,验证生成的 Spec 是否符合公司规范:
@Service
public class ComplianceChecker {
/**
* 检查 Spec 是否符合公司 API 设计标准
*/
public ComplianceReport check(OpenAPI openAPI) {
List<ComplianceIssue> issues = new ArrayList<>();
// 检查路径前缀
checkPathPrefix(openAPI, issues);
// 检查响应格式
checkResponseFormat(openAPI, issues);
// 检查认证定义
checkAuthentication(openAPI, issues);
// 检查分页格式
checkPaginationFormat(openAPI, issues);
// 检查时间字段格式
checkTimeFieldFormat(openAPI, issues);
// 检查字段命名风格
checkNamingConvention(openAPI, issues);
return new ComplianceReport(
issues.stream().noneMatch(i -> i.severity() == IssueSeverity.ERROR),
issues
);
}
private void checkPathPrefix(OpenAPI openAPI, List<ComplianceIssue> issues) {
if (openAPI.getPaths() == null) return;
openAPI.getPaths().keySet().forEach(path -> {
if (!path.startsWith("/api/v")) {
issues.add(new ComplianceIssue(
IssueSeverity.ERROR,
"PATH_PREFIX",
String.format("路径 '%s' 不符合版本前缀规范,应以 /api/v1/ 开头", path)
));
}
});
}
private void checkResponseFormat(OpenAPI openAPI, List<ComplianceIssue> issues) {
if (openAPI.getPaths() == null) return;
openAPI.getPaths().values().forEach(pathItem -> {
List<Operation> operations = getAllOperations(pathItem);
operations.forEach(operation -> {
if (operation.getResponses() == null) return;
ApiResponse successResponse = operation.getResponses().get("200");
if (successResponse == null) {
issues.add(new ComplianceIssue(
IssueSeverity.ERROR,
"RESPONSE_FORMAT",
"操作 '" + operation.getOperationId() + "' 缺少 200 响应定义"
));
return;
}
// 检查是否使用了统一响应结构(code + message + data)
boolean hasUnifiedFormat = checkHasUnifiedResponseFormat(successResponse);
if (!hasUnifiedFormat) {
issues.add(new ComplianceIssue(
IssueSeverity.ERROR,
"RESPONSE_FORMAT",
"操作 '" + operation.getOperationId() +
"' 的响应格式不符合统一响应结构(应包含 code/message/data 字段)"
));
}
});
});
}
private boolean checkHasUnifiedResponseFormat(ApiResponse response) {
if (response.getContent() == null) return false;
MediaType mediaType = response.getContent().get("application/json");
if (mediaType == null || mediaType.getSchema() == null) return false;
Schema schema = mediaType.getSchema();
if (schema.getProperties() == null) return false;
return schema.getProperties().containsKey("code") &&
schema.getProperties().containsKey("message") &&
schema.getProperties().containsKey("data");
}
private void checkTimeFieldFormat(OpenAPI openAPI, List<ComplianceIssue> issues) {
// 遍历所有 Schema,找出时间相关字段
if (openAPI.getComponents() == null ||
openAPI.getComponents().getSchemas() == null) return;
openAPI.getComponents().getSchemas().forEach((schemaName, schema) -> {
if (schema.getProperties() == null) return;
schema.getProperties().forEach((fieldName, fieldSchema) -> {
// 时间字段应以 Time 结尾
boolean isTimeField = fieldName.toLowerCase().contains("time") ||
fieldName.toLowerCase().contains("date") ||
fieldName.toLowerCase().contains("at");
if (isTimeField) {
// 检查是否是 string 类型 + ISO 8601 格式
if (!"string".equals(fieldSchema.getType())) {
issues.add(new ComplianceIssue(
IssueSeverity.WARNING,
"TIME_FORMAT",
String.format("Schema '%s' 中的时间字段 '%s' 应为 string 类型(ISO 8601 格式)",
schemaName, fieldName)
));
}
}
});
});
}
}5.3 自动修正(小问题自动补丁)
对于一些规律性的合规问题,可以自动修正:
@Service
public class SpecAutoFixer {
/**
* 对常见的合规问题进行自动修正
*/
public String autoFix(String yamlSpec, List<ComplianceIssue> issues) {
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
try {
Map<String, Object> specMap = yamlMapper.readValue(
yamlSpec, new TypeReference<Map<String, Object>>() {}
);
for (ComplianceIssue issue : issues) {
switch (issue.type()) {
case "PATH_PREFIX" -> fixPathPrefix(specMap);
case "AUTH_MISSING" -> addBearerAuth(specMap);
case "MISSING_ERROR_RESPONSE" -> addDefaultErrorResponses(specMap);
// 其他自动修正规则
}
}
return yamlMapper.writeValueAsString(specMap);
} catch (Exception e) {
log.error("自动修正失败: {}", e.getMessage());
return yamlSpec; // 修正失败则返回原始 Spec
}
}
@SuppressWarnings("unchecked")
private void addBearerAuth(Map<String, Object> specMap) {
Map<String, Object> components = (Map<String, Object>)
specMap.computeIfAbsent("components", k -> new LinkedHashMap<>());
Map<String, Object> securitySchemes = (Map<String, Object>)
components.computeIfAbsent("securitySchemes", k -> new LinkedHashMap<>());
if (!securitySchemes.containsKey("BearerAuth")) {
securitySchemes.put("BearerAuth", Map.of(
"type", "http",
"scheme", "bearer",
"bearerFormat", "JWT",
"description", "JWT 认证 Token,格式:Bearer <token>"
));
}
// 在顶层添加全局安全要求
specMap.putIfAbsent("security", List.of(Map.of("BearerAuth", List.of())));
}
}六、需求解析的 Prompt 设计细节
需求文档的质量参差不齐,有时候是产品经理写的 PRD,有时候是开会讨论的会议纪要,有时候只是几句话。需要先把需求「标准化」,再生成 Spec。
@Service
public class RequirementParser {
private final ChatClient chatClient;
/**
* 将非结构化需求文档转换为结构化的 API 需求
*/
public StructuredRequirement parse(String rawRequirement) {
String prompt = String.format("""
请分析以下需求描述,提取 API 设计所需的关键信息。
## 需求描述
%s
## 提取任务
请识别并提取:
1. **实体**(Entity):涉及哪些业务对象?它们有哪些属性?
2. **操作**(Operations):对这些实体要做哪些操作(查询/创建/修改/删除)?
3. **业务规则**(Business Rules):有哪些约束条件?
4. **权限要求**(Auth):哪些操作需要什么角色权限?
5. **数据量级**(Scale):是否有分页需求?数据量大概是多少?
## 输出格式(JSON)
{
"entities": [
{
"name": "实体名",
"description": "实体描述",
"fields": [
{"name": "字段名", "type": "数据类型", "required": true/false, "description": "字段说明"}
]
}
],
"operations": [
{
"entity": "操作的实体",
"action": "CREATE|READ|UPDATE|DELETE|LIST",
"description": "操作说明",
"filters": ["可能的过滤条件"],
"requiresAuth": true/false,
"requiredRole": "需要的角色(可选)"
}
],
"businessRules": ["规则1", "规则2"],
"hasListOperations": true/false,
"estimatedDataScale": "SMALL(<100条)|MEDIUM(100-10000条)|LARGE(>10000条)"
}
只返回 JSON,不要有任何其他内容。
""",
rawRequirement
);
String response = chatClient.prompt()
.user(prompt)
.options(OpenAiChatOptions.builder().temperature(0.0).build())
.call()
.content();
return parseStructuredRequirement(response);
}
}七、完整示例:从需求到 Spec
给一个具体的例子,走一遍完整流程。
输入需求:
商品管理模块需要支持以下功能:
1. 管理员可以创建商品,商品包含名称、价格(分)、库存、分类、描述(可选)、上架状态
2. 所有用户可以查询商品列表,支持按分类过滤,支持按价格排序,需要分页
3. 用户可以查询单个商品详情
4. 管理员可以更新商品信息,上架/下架商品
5. 管理员可以删除商品(逻辑删除)
价格字段用整数表示分,创建和更新时间自动记录经过需求解析后的中间结构(StructuredRequirement):
实体:Product(商品)
- 字段:id、name、priceInCents(价格/分)、stock、categoryId、description(可选)、status(ONLINE/OFFLINE)、createTime、updateTime
操作:
- LIST Product(全部用户,支持分类过滤、价格排序、分页)
- GET Product(全部用户)
- CREATE Product(管理员角色 ADMIN)
- UPDATE Product(管理员角色 ADMIN)
- DELETE Product(管理员角色 ADMIN,逻辑删除)
生成的 OpenAPI Spec 片段:
openapi: 3.0.3
info:
title: 商品管理 API
version: 1.0.0
description: 商品管理模块接口定义
servers:
- url: https://api.example.com
description: 生产环境
security:
- BearerAuth: []
paths:
/api/v1/products:
get:
operationId: listProducts
summary: 查询商品列表
description: 查询商品列表,支持按分类过滤和按价格排序,支持分页
tags:
- 商品管理
security: [] # 此接口无需认证
parameters:
- name: categoryId
in: query
required: false
description: 商品分类ID,不传则返回所有分类
schema:
type: string
- name: sortBy
in: query
required: false
description: 排序字段,支持 priceAsc(价格升序)或 priceDesc(价格降序)
schema:
type: string
enum: [priceAsc, priceDesc]
- name: pageNum
in: query
required: false
description: 页码,从1开始,默认1
schema:
type: integer
minimum: 1
default: 1
- name: pageSize
in: query
required: false
description: 每页记录数,默认20,最大100
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 查询成功
content:
application/json:
schema:
$ref: '#/components/schemas/ProductListResponse'
'400':
$ref: '#/components/responses/BadRequest'
components:
schemas:
Product:
type: object
description: 商品信息
properties:
id:
type: string
description: 商品ID
name:
type: string
description: 商品名称
priceInCents:
type: integer
description: 商品价格(单位:分),如 9900 表示 99.00 元
minimum: 0
stock:
type: integer
description: 库存数量
minimum: 0
categoryId:
type: string
description: 商品分类ID
description:
type: string
description: 商品描述,可选
status:
type: string
description: 商品状态:ONLINE=上架中,OFFLINE=已下架
enum: [ONLINE, OFFLINE]
createTime:
type: string
format: date-time
description: 创建时间,ISO 8601 格式
example: "2026-04-24T10:30:00+08:00"
updateTime:
type: string
format: date-time
description: 最后更新时间,ISO 8601 格式
required:
- id
- name
- priceInCents
- stock
- categoryId
- status
- createTime
- updateTime八、工程实践总结
在这套方案里,有几个判断我想着重说一下:
1. 不要追求一步到位
需求 → Spec 不是一步操作,中间的「需求解析 → 结构化表示 → 生成 → 验证 → 修正」每一步都有价值。把这个链路拆清楚,每一步都可以单独调试和优化。
2. 标准约束要写进 Prompt,不是靠后处理
一开始我们的方案是「先让 AI 随意生成,再后处理纠正格式」,实践下来效果很差,要纠正的地方太多。把公司标准写进 System Prompt,让 AI 从一开始就按标准生成,后处理只做兜底验证,效率高很多。
3. 温度设低一点
生成 Spec 这类结构化输出任务,温度设在 0 到 0.2 之间。温度高了输出格式会飘,格式验证通过率会下降。
4. 人工 review 不能省
自动化流水线能保证格式和合规性,但不能保证「业务语义正确」。生成的 Spec 在合规性检查通过后,还需要业务方过一遍,确认接口的设计是否符合实际业务逻辑。这一步是省不掉的。
实际效果: 在我们团队,一个包含 5-8 个接口的功能模块,以前从需求到 Spec 要 0.5-1 天,现在用这套流程,自动生成初稿 + 人工确认,大概 1-2 小时。时间缩短了 60%-80%,更重要的是格式一致性大幅提升,联调时的沟通成本显著下降。
