Auto Ontology OpenAPI 契约生成指南:FastAPI 路由如何零手写产出完整 API 文档
Auto Ontology OpenAPI 契约生成指南:FastAPI 路由如何零手写产出完整 API 文档
Auto Ontology 是一套 Python 框架,用于在数据之上构建语义化、基于图的可推理层,支持确定性的查询生成、更精准的上下文检索,以及更可解释的 LLM/Agent 工作流。它有一套巧妙的 OpenAPI 契约生成机制:FastAPI 路由定义完接口后,文档自动产出、永不手写,CI 还会自动检查契约是否"漂移"。本文将拆解这套机制。
为什么要自动生成 OpenAPI 契约?
手写 API 文档的痛苦人人都懂:接口改了、文档忘了改,消费者按旧文档调用就炸。
Auto Ontology 的做法是**"文档即构建产物"**——三份 OpenAPI 规范全部由代码生成并提交进仓库:
| 文件 | 描述对象 | 生成方式 |
|---|---|---|
auto-ontology-api.json |
对外公开 API(前端消费者用的就是它) | 前端执行 pnpm openapi |
backend.json |
内部 FastAPI 主服务(59 条路径) | uv run python -m dev_tools.generate_backend_openapi |
ingestion.json |
内部 Ingestion Service | 同上 |
这三份文件都是"生成物",仓库中专门的 README明确要求不要手改——CI 会重新生成并 diff,你提交的内容如果过期就会构建失败。
上图展示了 Auto Ontology 的整体架构:Python 服务(Backend、Ingestion Service)部署为 ClusterIP-only,外部流量全部经过 Next.js 前端代理。这个架构特点是理解契约生成的关键。
第一步:FastAPI 路由零成本导出内部 spec
FastAPI 的原生能力是这套机制的地基。开发者只需按惯例写路由——装饰器声明路径、Pydantic 模型声明请求/响应结构:
# auto_ontology/server/terms/router.py
@router.get("/terms", response_model=TermsPageResponse)
def list_terms(query: str | None = Query(...), skip: int = ..., limit: int | None = ...):
"""Return Term nodes. ..."""
response_model 里的 Pydantic 模型、Query 的描述文本、函数 docstring,FastAPI 都会自动翻译成 OpenAPI 结构。
关键设计在 create_app():它把 14 个业务路由(datasources、chat、terms、tags、search 等)挂到同一个 FastAPI 实例上,并且与应用启动完全解耦——
def create_app() -> FastAPI:
"""Build the API app without serving it.
Kept separate from ``main`` so tooling — the OpenAPI spec generator,
tests — can construct the app and read ``app.openapi()`` without
starting uvicorn.
"""
注意 lifespan 里的注释:数据库 schema 校验被刻意放在 lifespan 里而非 create_app 中,因为契约生成器构建应用时根本没有数据库。也就是说,生成一份完整的 API 文档,不需要启动服务器、不需要连数据库——纯内存中 create_app().openapi() 一步到位。
导出脚本 dev_tools/generate_backend_openapi.py 极简:
return {
"backend.json": create_app().openapi(),
"ingestion.json": ingestion_app.openapi(),
}
写盘时用了 sort_keys=True,保证每次生成的字节内容稳定——CI 的 drift 检查只对"真实变化"报警,而不是被 JSON 键序抖动误伤。
第二步:前端生成器合并出公开 spec
backend.json 描述的是"内部表面",集群外没人能直接调用它。真正对外的是 Next.js 的 route handler。于是 frontend/scripts/generate-openapi.ts 承担第二棒,它用 ts-morph 做静态 AST 分析,遍历 frontend/app/api/**/route.ts:
- 路径推导:文件式路由天然确定,
app/api/terms/[term_id]/route.ts→/api/terms/{term_id}; - 权限提取:从每个路由的
withPermission({...})包裹器读出所需权限,withPublic则标记为公开,并据此决定security字段和 401/403 响应; - 代理识别:调用
proxyToBackend的 handler 原样转发路径,于是直接按 path + method 从backend.json合并 FastAPI 的完整请求/响应 schema。
一个典型的代理路由只有 3 行代码,文档却自动完整:
// frontend/app/api/terms/route.ts
export const GET = withPermission({ catalog: ['read'] })((req) => proxyToBackend(req));
前端独有的路由(Prisma 直接支撑、无 FastAPI 对应)以及自己拼上游请求的 chat 路由,则通过同目录的 openapi.ts 声明模块用 zod 补充形状,契约定义见 frontend/types/openapi.ts:
export const openapi: OpenApiRoute = {
get: { responses: { 200: { description: '…', schema: z.array(acronym) } } },
};
之所以放在 route.ts 旁边的独立文件,是因为生成器是普通 Node 脚本,无法 import 带 next/headers、getPrisma() 的路由模块,而 openapi.ts 只依赖 zod,可以干净导入。
生成器最终组装出 OpenAPI 3.1 文档,并打上几个自定义扩展字段(详见 docs/openapi/README.md):
x-auto-ontology-permissions—— 该操作需要的权限;x-auto-ontology-source—— 提供该操作的route.ts路径;x-auto-ontology-proxied/x-auto-ontology-upstream—— 是否代理、代理到哪条后端路径。
生成时还会做一系列硬校验:读不懂 withPermission 参数直接报错、识别不出任何 HTTP 方法导出的路由文件报错、路径参数与后端不一致报错——宁可构建失败,也不发布一份"看起来完整但缺了一角"的契约。
第三步:一条命令 + CI 防漂移
再生成顺序有讲究(公开 spec 会读取已提交的 backend.json):
uv run python -m dev_tools.generate_backend_openapi # 产出 backend.json / ingestion.json
cd frontend && pnpm openapi # 产出 auto-ontology-api.json
生成的公开 spec 会先过 Prettier 统一格式,再同步拷贝一份进 MCP 包(mcp/auto_ontology_mcp/auto-ontology-api.json),因为 MCP 服务从 pip/uvx 直接安装运行时没有 docs/ 目录可读,两份副本 CI 都会 diff。
上图是契约所描述的对象世界——Terms、表格、列、SQL 属性等语义对象组成的图谱。公开 spec 里 81 条路径、155 个组件 schema,覆盖的正是操作这张图谱的全部能力。
契约的下游消费者:不止给人看
自动生成的契约在这里不只是"漂亮的文档",它是机器的一等公民:
- MCP 工具面:
mcp/auto_ontology_mcp/tools.py从公开 spec 中挑出一组 allow-list 操作,转成 Agent 可调用的工具。参数、请求体、字段描述全部直接取自 spec——注释里说得很直白:"参数保持正确,是因为 spec 一旦与路由漂移 CI 就会失败。" - 文档站点:公开 spec 已部署到静态页面供查阅,也可以粘贴到 Swagger 编辑器里浏览。
- CI 门禁:两份生成物都提交进仓库,PR diff 即评审界面;过期契约直接让构建变红。
这套机制值得借鉴的 5 个设计点
- 生成器零运行时依赖:
create_app()与main()分离,文档生成不碰数据库、不起服务器; - 静态分析 + 合并双通道:FastAPI 路由的 schema 自动合并,前端独有逻辑用 zod 旁路声明,各取所长;
- 排序写入保证 diff 稳定:
sort_keys=True让 drift 检查只报真变化; - 失败优于沉默:解析不了权限、匹配不上路径、参数错位,全部抛错终止构建;
- 命名规范写进约定:API 全线 snake_case,动态路由目录
[term_id]与后端参数名一一对应,使跨语言合并(Python ↔ TypeScript)可以逐字匹配。
相关文件索引
- 内部 spec 生成脚本:dev_tools/generate_backend_openapi.py
- 前端公开 spec 生成器:frontend/scripts/generate-openapi.ts
- 路由级 zod 声明契约:frontend/types/openapi.ts
- FastAPI 应用构建入口:auto_ontology/server/main.py
- Ingestion Service 入口:auto_ontology/ingestion_service/main.py
- 规范目录说明:docs/openapi/README.md
- MCP 工具面(契约下游消费者):mcp/auto_ontology_mcp/tools.py
💡 想动手试试?克隆仓库后按上文两条命令顺序执行,几秒内即可得到全新的三份 JSON 契约——这正是"文档零手写"的含义。

