一个需求从口头到上线,会经过很多双手:
PM 在 PRD 里写"每人限领 3 张",原型上标了"每人最多领 3 张",后端接口叫 limitPerUser,前端页面显示"x/3",测试用例断言"第 4 次返回 403"——这些字段名和边界值,有没有在同一个地方统一定义?
大多数项目没有。结果是:
maxPerUser: 5——我该信哪个?"这就是缺一份"契约真源"的症状。
在 MumuSpec 体系里,09 API接口规格就是来回答这个问题的。
一层管一层,不混层。09 只回答一个问题:后端向外部承诺了什么行为?
十四份文档有一条明确的主链:
Proposal(03-04) → Spec(05-06-07) → Design(08-09-10-11) → Plan(12) → Test(13) → Trace(14)09 属于 Design 阶段,是"怎么实现"那一层的核心契约。
它的上下游关系:
方向 | 文档 | 关系 |
|---|---|---|
上游 | 04 PRD | 09 的每个端点对应 PRD 的一条业务场景 |
上游 | 05 UserStory | 09 的每个字段对应 US 的一条验收条件 |
下游 | 10 数据模型 | 09 的响应字段与 10 的存储字段拼写一致 |
下游 | 06 FSD | 前端读 09 知道调什么接口、拿什么字段 |
下游 | 13 测试 | 测试用例断言以 09 的响应格式和错误码为准 |
下游 | 14 RTM | 每个端点必须出现在追溯矩阵中 |
一条规则落到 09,才算"可编码"。PM 的 R-02 说"每人限领 3 张",业务侧这条规则写在 04。但它真正变成可编码的输入,是在 09 定义了 limitPerUser: integer, 1-5、在 10 定义了 coupon_claims.limit_per_user INT NOT NULL之后。
这是很多团队踩过的坑:
"我们让 AI 直接读 PRD 生成代码,结果同一个字段在不同 API 里被理解成不同名字。"
原因很简单:PRD是给人读的,09 是给人+机器一起读的。
PRD 的写法是自然语言:"当用户领取数量达到上限时,返回错误提示"。09 的写法是结构化规格:
POST /coupons/receive
请求: { "coupon_type": string }
响应: { "limitPerUser": integer, "claimed": integer }
错误: 403 COUPON_LIMIT_EXCEEDEDAI 不需要从一段散文里猜接口规格。它只需要读 09 的端点、字段、类型、约束——然后生成代码。
在实际项目中,09 写得越精确,AI 生成的代码质量就越高:
09 的精确度 | AI 生成效果 |
|---|---|
只有端点路径 | 生成骨架,字段名靠猜 |
有端点 + 请求字段 | 生成 Controller + DTO,但可能遗漏约束 |
有端点 + 字段 + 类型 + 约束 | 生成完整 CRUD + 参数校验 |
有端点 + 字段 + 类型 + 约束 + 错误码 + 示例 | 生成完整 API + 校验 + 错误处理 + 测试桩 |
03 到 05 决定"做什么",09 决定"AI 能不能一次写对"。
用真实的模板结构说明。完整模板见 Spec模板-投研助手/09-API接口规格.md,这里摘核心几块:
一张表看清全部对外接口:
# | 端点 | 方法 | 功能 | 成功码 |
|---|---|---|---|---|
1 | /api/v1/agent/capabilities | GET | 能力探测 | 200 |
2 | /api/v1/agent/ask | POST | 问答提交 | 200 |
3 | /api/v1/agent/sessions | GET | 会话列表 | 200 |
4 | /api/v1/agent/sessions | POST | 新建会话 | 201 |
5 | /api/v1/agent/sessions/<id> | DELETE | 删除会话 | 200 |
6 | /api/v1/agent/sessions/<id>/records | GET | 问答记录 | 200 |
每个端点都与 04 PRD 的场景编号对应,与 05 UserStory 的需求编号对应。没有"游离端点"——不可能出现接口文档里有个端点但没人知道对应哪个需求。
对核心端点逐字段展开。例如 POST /ask:
请求体:
字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
query | string | 是 | 1–500 字符 | 用户提问原文 |
session_id | string | 是 | UUID | 目标会话 ID |
成功响应(200):
字段 | 类型 | 必有 | 说明 |
|---|---|---|---|
traceId | string | 是 | 链路追踪 ID |
answer | string | 是 | 答案文本 |
llm_used | boolean | 是 | 是否使用真实 LLM |
model | string|null | 是 | 模型标识 |
response_time_ms | integer | 是 | 响应耗时(毫秒) |
answer_source | string | 是 | copaw / bailian / demo |
每个字段名都是全库统一的。06 FSD 引用 answer_source来展示"数据来源"标签,13 测试用例断言 response_time_ms < 5000,14 追溯矩阵记录字段到 TC 的映射——用的都是 09 里定义的名字。
HTTP | error.code | 触发条件 | details |
|---|---|---|---|
400 | EMPTY_QUERY | query 为空/null | {} |
400 | INVALID_QUERY | query 超 500 字符 | {"max_length":500} |
403 | SESSION_NOT_FOUND | session_id 不存在 | {"session_id":"..."} |
前端拿这个写错误提示,测试拿这个写异常断言,AI 拿这个生成错误处理代码。
端点 | 字段 | 规则 | 失败 HTTP | error.code |
|---|---|---|---|---|
POST /ask | query | 非空/非空白 | 400 | EMPTY_QUERY |
POST /ask | query | ≤ 500 字符 | 400 | INVALID_QUERY |
POST /ask | session_id | 非空 | 400 | INVALID_QUERY |
所有校验逻辑集中写在 09,不散落在各端代码里。后端按此实现校验,前端按此做预校验,测试按此设计边界用例。
09 里写定的字段名,就是全库的字段名。
maxCount,在 09 里用 limitPerUser,在数据库里用 max_claims_per_user写 09 的时候,每个端点旁边标注对应 04 的场景编号和 05 的需求编号:
# 端点 POST /ask
# 对应: 04 SC-02 · 05 US-002 REQ-M1QA-002这样做的好处:
很多项目的 09 错误码是"到时候再补"。结果联调时前端问"这个场景返回什么码?"后端说"我还没想好"。
错误码应该在 09 冻结的同时就定义好:
SCREAMING_SNAKE_CASE)角色 | 读 09 为了什么 |
|---|---|
后端 | 写 09 的人。定义端点、字段、约束、错误码 |
前端 | 知道调什么接口、传什么参数、拿什么字段、报什么错 |
测试 | 设计接口测试用例、断言响应字段和错误码 |
PM | 确认接口字段与业务规则一致("limitPerUser = 3 对吗?") |
AI 工具 | 直接消费 09,生成代码骨架和校验逻辑 |
在 MumuSpec 十四文档体系里,09 是一个很小的文件——通常 3-5 页 markdown。但它是被引用最多的文件:
09 写的质量,直接决定了 AI 生成代码的准确率,也决定了前后端联调的心累程度。
写 09 不需要复杂的工具,一个 markdown 文件 + 一张字段表 + 一个错误码清单就够了。
关键是:把它当合同写,而不是当笔记写。