API 文档和代码不一致是团队协作的头号痛点:后端改了接口参数,前端不知道;新加了错误码,文档没更新;接口已经废弃了,还在文档里占位。本文用 Agent 实现"代码即文档"——从源码扫描接口定义,自动生成 OpenAPI 文档,检测版本差异,审查文档质量,并通过 CI/CD 自动发布。

案例 003:用 Agent 生成和维护 OpenAPI 文档

API 文档和代码不一致是团队协作的头号痛点:后端改了接口参数,前端不知道;新加了错误码,文档没更新;接口已经废弃了,还在文档里占位。本文用 Agent 实现"代码即文档"——从源码扫描接口定义,自动生成 OpenAPI 文档,检测版本差异,审查文档质量,并通过 CI/CD 自动发布。

一、业务背景

一个微服务架构的 SaaS 平台,6 个服务、380+ 个 API 端点。文档问题:

问题 频率 影响
接口改了但文档没更新 每周 5-10 次 前端调不通,联调浪费时间
新接口没写文档 每个版本 3-5 个 第三方集成方无法对接
错误码文档和实际不一致 持续存在 客户端无法正确处理异常
废弃接口没标记 累积 30+ 个 新人误用旧接口

目标:让文档和代码保持 100% 同步,每次 PR 自动检测文档差异。

二、自动化流程架构

text
┌────────────────────────────────────────────────────────────┐
│                   OpenAPI 文档自动化流程                     │
│                                                            │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────┐ │
│  │ 源码扫描  │──▶│ 文档生成  │──▶│ 差异检测  │──▶│ 审查  │ │
│  │ (AST)    │    │ (OpenAPI)│    │ (Diff)   │    │(Agent)│ │
│  └──────────┘    └──────────┘    └──────────┘    └──┬───┘ │
│                                                      │     │
│                                               ┌──────▼───┐ │
│                                               │ PR 评论   │ │
│                                               │ + 自动发布│ │
│                                               └──────────┘ │
└────────────────────────────────────────────────────────────┘

三、源码扫描与文档生成

3.1 Agent 扫描配置

yaml
# 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

yaml
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 差异检测脚本

python
# 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 工作流

yaml
# .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 文档质量审查

yaml
# 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 做数据库迁移前的风险评估