案例 003:用 Agent 生成和维护 OpenAPI 文档
API 文档和代码不一致是团队协作的头号痛点:后端改了接口参数,前端不知道;新加了错误码,文档没更新;接口已经废弃了,还在文档里占位。本文用 Agent 实现"代码即文档"——从源码扫描接口定义,自动生成 OpenAPI 文档,检测版本差异,审查文档质量,并通过 CI/CD 自动发布。
一、业务背景
一个微服务架构的 SaaS 平台,6 个服务、380+ 个 API 端点。文档问题:
| 问题 | 频率 | 影响 |
|---|---|---|
| 接口改了但文档没更新 | 每周 5-10 次 | 前端调不通,联调浪费时间 |
| 新接口没写文档 | 每个版本 3-5 个 | 第三方集成方无法对接 |
| 错误码文档和实际不一致 | 持续存在 | 客户端无法正确处理异常 |
| 废弃接口没标记 | 累积 30+ 个 | 新人误用旧接口 |
目标:让文档和代码保持 100% 同步,每次 PR 自动检测文档差异。
二、自动化流程架构
┌────────────────────────────────────────────────────────────┐
│ OpenAPI 文档自动化流程 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ 源码扫描 │──▶│ 文档生成 │──▶│ 差异检测 │──▶│ 审查 │ │
│ │ (AST) │ │ (OpenAPI)│ │ (Diff) │ │(Agent)│ │
│ └──────────┘ └──────────┘ └──────────┘ └──┬───┘ │
│ │ │
│ ┌──────▼───┐ │
│ │ PR 评论 │ │
│ │ + 自动发布│ │
│ └──────────┘ │
└────────────────────────────────────────────────────────────┘三、源码扫描与文档生成
3.1 Agent 扫描配置
# openapi-scan-config.yaml
scan:
# 扫描目标
targets:
- service: "user-service"
source: "src/controllers/**/*.ts"
decorator: "@ApiOperation" # NestJS Swagger 装饰器
base_path: "/api/v1"
- service: "order-service"
source: "src/handlers/**/*.py"
decorator: "@router" # FastAPI 路由装饰器
base_path: "/api/v1"
# 提取规则
extract:
summary: "从 @ApiOperation(summary=...) 提取"
description: "从函数 docstring 提取"
request_body: "从 DTO 类型定义提取"
response: "从返回值类型 + @ApiResponse 提取"
parameters: "从路由参数 + @Param 提取"
authentication: "从 @UseGuards(AuthGuard) 推断"
# 输出
output:
format: "openapi-3.1"
path: "docs/openapi/{service}.yaml"
group_by: "tag" # 按模块分组3.2 Agent 文档生成 Prompt
name: "OpenAPI 文档生成"
prompt: |
你是一个 API 文档工程师。扫描以下源码,生成完整的 OpenAPI 3.1 文档。
## 输入
- 控制器源码:{{controller_source}}
- DTO 定义:{{dto_source}}
- 已有文档(用于 diff):{{existing_doc}}
## 任务
1. 扫描所有路由端点,提取:路径、方法、参数、请求体、响应
2. 从函数名和注释推断 summary 和 description
3. 为每个端点生成示例请求和响应
4. 标注已废弃的端点(有 @Deprecated 装饰器或注释中包含"废弃")
5. 为所有错误码生成 response 定义
## 输出
- 完整的 OpenAPI YAML 文件
- 与上一版本的 diff 报告(新增/修改/删除了哪些端点)3.3 差异检测脚本
# scripts/openapi-diff.py
"""
比较两个版本的 OpenAPI 文档,生成差异报告。
差异级别:
- BREAKING: 破坏性变更(删除端点、修改参数类型、删除必填参数)
- NON_BREAKING: 非破坏性变更(新增端点、新增可选参数)
- DEPRECATION: 废弃标记
"""
import yaml
import json
import sys
from deepdiff import DeepDiff
def load_spec(path):
with open(path) as f:
return yaml.safe_load(f)
def extract_endpoints(spec):
"""提取所有端点,标准化为 {method:path} -> endpoint_info"""
endpoints = {}
for path, methods in spec.get("paths", {}).items():
for method in ["get", "post", "put", "patch", "delete"]:
if method in methods:
key = f"{method.upper()} {path}"
endpoints[key] = methods[method]
return endpoints
def compare_specs(old_path, new_path):
old_spec = load_spec(old_path)
new_spec = load_spec(new_path)
old_endpoints = extract_endpoints(old_spec)
new_endpoints = extract_endpoints(new_spec)
changes = []
# 检测删除的端点(破坏性)
for key in old_endpoints:
if key not in new_endpoints:
changes.append({
"level": "BREAKING",
"type": "endpoint_removed",
"endpoint": key,
"message": f"端点 {key} 已被删除",
})
# 检测新增的端点
for key in new_endpoints:
if key not in old_endpoints:
changes.append({
"level": "NON_BREAKING",
"type": "endpoint_added",
"endpoint": key,
"message": f"新增端点 {key}",
})
# 检测修改的端点
for key in old_endpoints:
if key in new_endpoints:
old_ep = old_endpoints[key]
new_ep = new_endpoints[key]
# 检查参数变化
old_params = {p["name"]: p for p in old_ep.get("parameters", [])}
new_params = {p["name"]: p for p in new_ep.get("parameters", [])}
for name in old_params:
if name not in new_params:
changes.append({
"level": "BREAKING",
"type": "parameter_removed",
"endpoint": key,
"parameter": name,
"message": f"{key}: 参数 {name} 已被删除",
})
for name in new_params:
if name not in old_params:
required = new_params[name].get("required", False)
changes.append({
"level": "BREAKING" if required else "NON_BREAKING",
"type": "parameter_added",
"endpoint": key,
"parameter": name,
"message": f"{key}: {'新增必填' if required else '新增可选'}参数 {name}",
})
# 检查废弃标记
if new_ep.get("deprecated") and not old_ep.get("deprecated"):
changes.append({
"level": "DEPRECATION",
"type": "endpoint_deprecated",
"endpoint": key,
"message": f"端点 {key} 已标记为废弃",
})
return changes
def format_report(changes):
"""生成 Markdown 格式的差异报告"""
breaking = [c for c in changes if c["level"] == "BREAKING"]
non_breaking = [c for c in changes if c["level"] == "NON_BREAKING"]
deprecations = [c for c in changes if c["level"] == "DEPRECATION"]
report = ["# OpenAPI 差异报告\n"]
if breaking:
report.append(f"## 🔴 破坏性变更({len(breaking)} 项)\n")
for c in breaking:
report.append(f"- **{c['endpoint']}**: {c['message']}")
report.append("")
if deprecations:
report.append(f"## 🟡 废弃标记({len(deprecations)} 项)\n")
for c in deprecations:
report.append(f"- **{c['endpoint']}**: {c['message']}")
report.append("")
if non_breaking:
report.append(f"## 🟢 非破坏性变更({len(non_breaking)} 项)\n")
for c in non_breaking:
report.append(f"- **{c['endpoint']}**: {c['message']}")
report.append("")
if not changes:
report.append("✅ 无变更")
return "\n".join(report)
if __name__ == "__main__":
old_path, new_path = sys.argv[1], sys.argv[2]
changes = compare_specs(old_path, new_path)
print(format_report(changes))
# 如果有破坏性变更,退出码为 1(CI 会阻断)
has_breaking = any(c["level"] == "BREAKING" for c in changes)
sys.exit(1 if has_breaking else 0)四、CI/CD 集成
4.1 GitHub Actions 工作流
# .github/workflows/openapi-docs.yml
name: OpenAPI 文档自动化
on:
pull_request:
paths:
- "src/controllers/**"
- "src/handlers/**"
- "src/dto/**"
- "src/schemas/**"
jobs:
generate-and-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # 需要上一版本做 diff
- name: 生成新版 OpenAPI 文档
run: |
npm run openapi:generate
cp docs/openapi/user-service.yaml /tmp/new-spec.yaml
- name: 获取旧版文档
run: |
git show HEAD~1:docs/openapi/user-service.yaml > /tmp/old-spec.yaml || \
echo "{}" > /tmp/old-spec.yaml
- name: 差异检测
id: diff
run: |
python scripts/openapi-diff.py /tmp/old-spec.yaml /tmp/new-spec.yaml > diff-report.md
echo "has_breaking=$?" >> $GITHUB_OUTPUT
- name: Agent 审查文档质量
run: |
node scripts/agent-review-docs.js /tmp/new-spec.yaml >> review.md
- name: PR 评论
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const diff = fs.readFileSync('diff-report.md', 'utf-8');
const review = fs.readFileSync('review.md', 'utf-8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## OpenAPI 文档变更报告\n\n${diff}\n\n## Agent 质量审查\n\n${review}`
});
- name: 破坏性变更阻断
if: steps.diff.outputs.has_breaking == '1'
run: |
echo "❌ 检测到破坏性 API 变更,需要 Tech Lead 审批"
exit 1
publish:
needs: generate-and-diff
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 生成并发布文档
run: |
npm run openapi:generate
npm run docs:build
- name: 部署到文档站点
run: |
aws s3 sync docs/dist/ s3://api-docs-bucket/
aws cloudfront create-invalidation --distribution-id $DIST_ID --paths "/*"4.2 Agent 文档质量审查
# agent-doc-review.yaml
name: "OpenAPI 文档质量审查"
prompt: |
你是 API 文档审查专家。审查以下 OpenAPI 文档的质量。
## 审查清单
1. **完整性**:每个端点是否有 summary、description、示例请求、示例响应?
2. **错误码**:是否覆盖了常见错误(400/401/403/404/500)?
3. **参数描述**:每个参数是否有类型、是否必填、默认值、取值范围?
4. **命名一致性**:字段命名是否遵循项目规范(camelCase / snake_case)?
5. **废弃标记**:是否有缺少废弃说明的 deprecated 端点?
## 输出
按以下 JSON 格式输出审查结果:
{
"score": 0-100,
"issues": [{"severity": "HIGH|MEDIUM|LOW", "endpoint": "", "message": ""}],
"recommendations": []
}五、真实经验与踩坑
5.1 装饰器不等于文档
场景:Agent 从 @ApiOperation(summary="获取用户列表") 提取 summary,但很多开发者写的 summary 过于简短或没有意义(如"接口")。
问题:自动生成的文档可读性差,前端开发者看不懂。
解决方案:增加 Agent 审查步骤——如果 summary 少于 10 个字或者包含"接口"、"操作"等模糊词,标记为"需改进"并建议更具体的描述。同时在 lint 规则中强制 summary 最少 10 个字。
5.2 DTO 嵌套导致文档膨胀
场景:一个 DTO 引用了另一个 DTO,另一个又引用了第三个……生成出来的 OpenAPI 文档里 $ref 嵌套了 5 层。
问题:文档工具渲染时展开所有引用,一个接口定义有 200 行。开发者根本看不下去。
解决方案:限制 $ref 展开深度为 2 层,超过的在文档中显示为"详见 XXX 定义"。同时让 Agent 对过深的嵌套提出重构建议("建议把 UserDetailDTO 拆成 UserBasicInfo + UserContactInfo")。
5.3 文档发布和代码发布要解耦
场景:文档自动生成后直接发布到文档站,但代码还没部署。 问题:文档描述了新接口,但线上还没有,第三方调用报 404。 解决方案:文档发布和代码发布分开——PR 合并时只更新文档仓库的 main 分支,实际发布在代码部署成功后触发。在文档中标注"自 vX.Y.Z 起可用"的版本号。
六、参数说明表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scan_targets |
list | 必填 | 扫描的服务和源码路径 |
output_format |
string | "openapi-3.1" |
输出格式 |
output_path |
string | "docs/openapi/" |
输出目录 |
diff_level |
string | "all" |
差异检测级别:breaking / non_breaking / all |
block_on_breaking |
bool | true |
检测到破坏性变更时是否阻断 CI |
min_summary_length |
int | 10 |
summary 最小长度 |
max_ref_depth |
int | 2 |
$ref 最大展开深度 |
auto_publish |
bool | false |
是否自动发布文档 |
publish_on |
string | "deploy" |
发布时机:merge / deploy |
review_score_threshold |
int | 70 |
文档质量分低于此值需要人工审查 |
七、落地检查清单
- 源码中的路由装饰器能正确映射到 OpenAPI 端点
- 每个端点有 summary、description、示例请求、示例响应
- 错误码(400/401/403/404/500)在文档中有对应定义
- 差异检测能区分破坏性变更和非破坏性变更
- 破坏性变更在 CI 中阻断 PR 合并
- Agent 审查能检测出低质量 summary 和缺失描述
- 文档发布和代码发布解耦,不会出现"文档有但接口没有"
- 废弃端点有废弃说明和建议替代方案
- 第三方集成方能通过文档站的搜索功能快速找到接口
八、系列导航
上一篇:案例 002:用 Agent 维护 SaaS 后台的 CRUD 与权限模块 下一篇:案例 004:用 Agent 做数据库迁移前的风险评估