首页 / 资讯中心 / 文章详情

Spec:AI编程时代的轻量级契约式开发协议

Spec:AI编程时代的轻量级契约式开发协议 ★ FEATURED ARTICLE
1. 项目概述Spec 不是文档而是开发流程的“交通信号灯”“什么是 Spec”——这个问题最近在多个技术社区高频出现尤其在 AI 编程工具如 Copilot、CodeWhisperer、Cursor 等普及后越来越多开发者发现光靠自然语言描述需求让 AI 写代码结果常常是“写得快、改得累、跑不通、不敢交”。我带过三个不同规模的团队做 AI 辅助开发落地从某高校实验室的轻量 Demo 开发到某公司中台系统的模块重构再到某跨平台工具链的持续集成升级反复验证了一个事实没有 Spec 的 AI 编程就像没有红绿灯的十字路口——表面热闹实则拥堵、冲突、事故频发。SpecSpecification规格说明在这里不是传统意义上那种几十页、锁在 Confluence 里、上线前才被翻一次的“形式主义文档”。它是一种轻量、结构化、可执行、且与开发节奏强同步的意图表达协议。核心关键词就三个高效、可控、可演进。高效是指它能大幅压缩“人脑→语言→AI理解→代码生成→人工校验”的信息衰减链可控是指它把模糊的“大概这样”变成明确的“必须满足 A/B/C 三条约束”可演进是指它天然支持版本比对、变更追溯、自动化校验而不是写完就扔。适合谁看如果你正面临这些情况这篇就是为你写的用 AI 工具写代码但每次都要反复调 prompt改三遍才勉强可用团队协作时后端说“接口已按文档实现”前端一联调发现字段名全对不上项目交接给新人对方花两天读代码再花三天猜当初为什么这么设计想做自动化测试或契约测试但连“接口该返回什么”都找不到权威定义。Spec 就是解决这些问题的底层基础设施。它不替代编程而是让编程这件事本身变得更确定、更少返工、更容易沉淀。接下来我会从设计逻辑、核心构成、实操落地、问题排查四个维度拆解它到底怎么用、为什么有效、以及踩过哪些坑。2. Spec 的整体设计思路为什么不能只靠自然语言2.1 从“人话翻译失败”说起AI 编程的三大信息断层我做过一组对照实验同一段业务逻辑用户注册后发送欢迎邮件触发积分奖励分别用两种方式输入给 CursorA 方式纯自然语言描述“用户注册成功后要发一封欢迎邮件内容包含用户名和注册时间同时给账户加 100 积分积分变动要记录日志。”B 方式结构化 SpecYAML 格式name: user_registration_post_action trigger: event:user_registered inputs: - user_id: string, required - username: string, required - registered_at: datetime, required outputs: - email_sent: boolean - points_added: integer 100 - log_recorded: boolean constraints: - email_template: welcome_v2.jinja - points_source: registration_bonus - log_level: INFO结果很直观A 方式生成的代码7 次中有 4 次漏掉日志记录2 次用了错误的邮件模板名还有 1 次把积分加成了字符串B 方式生成的代码7 次全部通过基础单元测试且代码结构高度一致——函数名、参数顺序、返回值类型完全对齐 Spec 定义。为什么因为自然语言存在三重不可靠性第一重歧义性。“发送欢迎邮件”——是立即发还是进队列异步发失败了重试几次这些在中文里一个词就带过了但对系统可靠性至关重要。Spec 强制你把“异步”“重试上限3”“超时5s”作为显式字段填进去。第二重隐含假设。“触发积分奖励”——奖励是实时到账还是 T1 结算是否需要风控校验这些在口头沟通里常被默认但 AI 没有“默认”概念它只能按字面猜。Spec 要求你把所有隐含条件外化为preconditions和postconditions。第三重粒度失配。人习惯说“整个流程”而代码必须拆成函数、参数、异常分支。自然语言描述越宏观AI 生成的代码越容易在边界条件上出错。Spec 的结构天然倒逼你按“输入/输出/约束”切分这恰好匹配现代编程的契约式思维。提示Spec 不是给 AI 看的“说明书”而是给你自己写的“设计备忘录”。当你开始写 Spec你就已经在做架构决策了——哪些是必须保证的哪些是可以妥协的哪些是未来要扩展的。这个过程本身就在提升开发质量。2.2 Spec 的定位介于需求文档与代码之间的“可信中间层”很多人误以为 Spec 是“写给 AI 看的”这是本末倒置。Spec 的首要读者永远是人——是写代码的你、是 Code Review 的同事、是三个月后接手维护的新人、是测试同学写用例的依据。AI 只是第一个、最严格的“执行校验者”。我把 Spec 在开发流中的位置画成一个三层模型上层需求层Why——产品 PRD、用户故事、业务目标。关注价值不关心实现。中层Spec 层What——定义“这个功能必须做到什么程度才算完成”。不涉及“怎么写”只约定“做到什么样子”。它是需求到代码的翻译器也是代码到需求的回溯锚点。下层代码层How——具体实现包括算法、框架、数据库操作等。可以优化、重构、换语言但只要符合 Spec对外行为就不变。这三层之间Spec 是唯一能双向映射的层。你可以从 PRD 提炼出 Spec比如把“用户能快速找回密码”拆解为“邮箱验证码 5 分钟有效”“重置链接单次有效”“新密码需含大小写字母和数字”也可以从代码反向生成 Spec用工具扫描函数签名、HTTP 接口定义、数据库 schema自动生成初版 Spec。而自然语言描述无法稳定支撑这种双向映射——它太软太易变。所以 Spec 的设计哲学第一条就是最小完备性。它不追求穷尽所有细节但必须覆盖所有影响外部行为的关键点。比如一个支付回调接口Spec 必须定义HTTP 方法、路径、请求体结构、成功响应格式、失败响应格式、幂等性要求、重试策略。至于内部用 Redis 还是数据库存状态那是代码层的事Spec 不该管。2.3 为什么选 YAML 而不是 JSON 或 MarkdownSpec 的载体格式我们团队实测对比过 YAML、JSON、Markdown、Protocol Buffer IDL 四种方案最终锁定 YAML。原因很实际JSON太“硬”没有注释能力。Spec 的核心价值之一是承载设计意图比如# 此处不校验手机号格式由前端统一处理这类说明JSON 无法表达而 YAML 的#注释是标准语法。Markdown太“软”缺乏结构校验。你可以写一段漂亮的文字描述但没法用工具自动检查“是否遗漏了 outputs 字段”或“constraints 中的 key 是否拼写错误”。YAML 天然支持 schema 校验用 JSON Schema 或专用 linter能提前拦截低级错误。Protocol Buffer IDL功能强大但学习成本高且过度绑定于 gRPC 场景。我们的项目既有 HTTP API也有 CLI 工具、数据管道任务需要一种通用、轻量、编辑器友好的格式。YAML 满足VS Code 原生支持高亮和折叠GitHub 上可直接渲染查看非技术人员也能大致看懂结构。我们定制了一套极简的 Spec Schemav1.0只有 7 个顶层字段name,description,trigger,inputs,outputs,constraints,examples。每个字段都有明确语义和校验规则。例如inputs必须是数组每个元素必须包含name和typetype只允许string/integer/boolean/datetime/array/object六种基础类型不支持嵌套复杂类型避免过度设计。这套约束不是为了限制而是为了确保 Spec 本身可读、可查、可自动化。注意不要一上来就追求“完美 Spec”。我们团队的实践是先用 5 分钟写一个最简版只填 name、trigger、inputs、outputs让 AI 生成骨架代码再在开发过程中随着理解加深逐步补全 constraints 和 examples。Spec 是活的不是一次写完就封存的。3. Spec 的核心构成与实操要点从定义到落地的完整闭环3.1 Spec 的七个核心字段详解每个字段都在解决一个具体问题Spec 文件虽小但每个字段都对应开发中的一个痛点。下面结合真实案例某内部数据分析工具的“导出报表”功能逐条解析name功能的唯一标识符不是“导出报表”这种模糊名称而是report_export_to_csv_v2。规则很简单小写字母下划线版本号。好处是代码中可直接用作函数名、API 路径、配置项 key支持语义化版本管理v1 是导出 Excelv2 切换为 CSVv3 增加压缩选项避免命名冲突比如user_export和report_export不会混淆。我们曾因没加版本号在一次灰度发布中新旧两个导出逻辑共存导致监控告警混乱排查了 4 小时才发现是 Spec 名称未区分。description用一句话说清“为什么需要这个功能”不是复述需求而是点明设计动机。例如“为满足审计合规要求所有报表导出操作必须留痕且原始数据不可修改。”这句话直接决定了后续constraints中要加入audit_log_required: true和immutable_source: true。如果描述写成“让用户能下载报表”那就失去了指导意义。trigger明确功能被调用的上下文常见类型有http:POST /api/v1/reports/export、event:report_generated、cli:export --format csv。关键在于精确到最小可测单元。我们曾把 trigger 写成user_clicks_export_button结果 AI 生成的代码试图去监听前端 DOM 事件完全跑偏。改成http:POST /api/v1/reports/export后生成的才是真正的后端接口。inputs定义“进来的是什么”强制类型与必填校验每个 input 必须有name、type、requiredtrue/false、description可选但强烈推荐。特别注意type不是编程语言类型如 Python 的str或List[Dict]而是领域通用类型string,array。Spec 层不关心语言细节对于array类型必须用items_type指定元素类型如items_type: stringrequired: false的字段必须提供default值或明确nullable: true否则 AI 可能生成空指针异常。在报表导出案例中format字段required: false但我们写了default: csv这样即使前端不传后端也确定行为。outputs定义“出去的是什么”是契约的核心同样要求name、type、description。重点在于输出必须是可序列化的即能转成 JSON。避免outputs中出现file_handle或database_connection这类运行时对象对于错误场景outputs应包含error_code和error_message字段并在constraints中定义常见错误码如ERR_INVALID_FORMAT如果有副作用如发邮件、写数据库必须在outputs中声明side_effects: [email_sent, db_updated]这是后续自动化测试的依据。constraints定义“必须满足的规则”Spec 的灵魂所在这是最容易被忽略、也最有价值的部分。我们团队总结出四类必填约束行为约束idempotent: true幂等、timeout_ms: 30000超时、max_retries: 2重试数据约束min_length: 3用户名、regex: ^[a-z0-9_]$ID 格式、enum: [csv, xlsx]格式枚举安全约束auth_required: true需鉴权、scope: [report:read]权限范围、sensitive_fields: [user_email]敏感字段标记可观测约束log_level: INFO、metrics_emit: [export_duration_ms, export_rows_count]上报指标。没有 constraints 的 Spec就像没有交通规则的马路——车能开但没人知道会不会撞。examples提供“典型场景的输入输出对”是 AI 最有效的训练样本每个 example 是一个对象包含input和output两个子对象。例如examples: - name: export_small_report input: {report_id: rpt_001, format: csv} output: {file_url: https://cdn.example.com/rpt_001.csv, rows_count: 127, file_size_kb: 42} - name: export_with_filter input: {report_id: rpt_002, format: csv, filter: {status: active}} output: {file_url: ..., rows_count: 89, file_size_kb: 31}AI 对 example 的学习效果远超自然语言描述。我们测试发现提供 2 个高质量 example比写 200 字 prompt 更能控制生成代码的字段名和结构。3.2 Spec 的生命周期管理如何让它真正“活”起来Spec 写完不是终点而是协作的起点。我们建立了四步闭环流程第一步Spec First编码之前必评审任何新功能开发PRPull Request的第一项提交必须是.spec.yaml文件。Code Review 时首先检查 Spec 是否完备trigger 是否明确inputs/outputs 是否覆盖所有路径constraints 是否有遗漏只有 Spec 通过才允许提交代码。这一步把 70% 的设计问题挡在编码前。第二步Spec 与代码双向绑定在代码中用注释关联 Spec# spec: report_export_to_csv_v2用脚本Python PyYAML扫描所有代码文件提取# spec:标签生成《Spec-代码映射表》在 CI 流程中校验“每个 Spec 是否有对应代码实现”“每个代码文件是否关联了 Spec”当 Spec 变更时CI 自动触发告警要求更新关联代码。这套机制让我们杜绝了“Spec 写一套代码写另一套”的情况。第三步Spec 驱动自动化测试我们用 Spec 自动生成三类测试接口契约测试基于trigger和inputs/outputs生成 Postman Collection 或 pytest 用例验证 HTTP 接口行为边界值测试扫描constraints中的min_length、regex、enum自动生成非法输入用例如传空字符串、传非法枚举值性能基线测试从constraints.timeout_ms提取阈值作为性能测试的 Pass/Fail 标准。以前写测试用例要半天现在 Spec 定稿后3 分钟内生成 20 个可运行测试。第四步Spec 版本归档与追溯所有 Spec 文件存放在独立 Git 仓库specs/与代码仓库分离。每次变更都打 tag如spec-report-export-v2.1.0并关联 Jira Issue。这样当线上出现问题我们可以查当前生产环境部署的 Spec 版本对比该版本与上一版本的 diff快速定位是哪条 constraint 调整导致的行为变化甚至用旧版 Spec 生成“兼容模式”代码临时回滚。这比翻 Git Log 查代码变更高效得多。实操心得Spec 文件不要放在项目根目录而应统一放在specs/子目录下。我们曾把 Spec 散落在各模块里结果 CI 脚本漏扫导致一个关键约束变更没触发测试上线后才发现。集中存放统一路径是自动化落地的前提。4. Spec 的实操过程从零开始构建一个可运行的 Spec 工程4.1 工具链搭建5 分钟完成本地环境初始化Spec 的威力在于工程化而非单个文件。我们用一套极简但高效的开源工具链全部基于命令行无 GUI 依赖适配 Mac/Linux/WindowsWSL。核心工具清单spec-lint轻量 YAML linter校验 Spec 是否符合我们定义的 Schemav1.0。安装pip install spec-lintspec-gen根据 Spec 自动生成代码骨架、测试用例、API 文档。安装npm install -g spec-genspec-diff比较两个 Spec 版本的语义差异不是文本 diff高亮行为变更如timeout_ms从 10000 改为 5000。安装go install github.com/spec-tools/spec-difflatestVS Code 插件Spec Helper提供语法高亮、字段自动补全、Schema 错误实时提示。初始化步骤全程命令行复制粘贴即可创建项目mkdir my-report-tool cd my-report-tool初始化 Spec 目录mkdir specs touch specs/export_report.spec.yaml安装 lint 工具pip install spec-lint创建本地校验脚本echo #!/bin/bash\nspec-lint specs/*.spec.yaml validate-spec.sh chmod x validate-spec.sh第一次运行校验./validate-spec.sh此时会报错因为文件为空这是预期的。就这么简单。不需要 Docker、不需要 Kubernetes、不需要云服务。Spec 工程的门槛应该低到一个实习生花 10 分钟就能上手。4.2 编写第一个 Spec以“报表导出”为例的完整 walkthrough现在我们动手写specs/export_report.spec.yaml。记住原则先骨架后血肉先运行后完善。Step 1写最简骨架2 分钟打开文件填入name: report_export_to_csv_v2 description: 导出指定报表为 CSV 格式供下游系统消费 trigger: http:POST /api/v1/reports/export inputs: - name: report_id type: string required: true - name: format type: string required: false default: csv outputs: - name: file_url type: string - name: rows_count type: integer - name: file_size_kb type: integer保存运行./validate-spec.sh。报错Missing required field: constraints。好说明 linter 生效了。Step 2补全 constraints3 分钟在outputs下方添加constraints: idempotent: true timeout_ms: 30000 max_retries: 2 auth_required: true scope: [report:read] enum: {format: [csv, xlsx]} regex: {report_id: ^[a-z0-9_]{6,32}$}再次校验报错Field examples is required。继续补。Step 3添加 examples2 分钟examples: - name: export_basic input: {report_id: rpt_001, format: csv} output: {file_url: https://cdn.example.com/rpt_001.csv, rows_count: 127, file_size_kb: 42}现在./validate-spec.sh应该显示OK。恭喜你的第一个可校验 Spec 诞生了Step 4生成代码骨架1 分钟运行spec-gen --spec specs/export_report.spec.yaml --lang python --output src/它会自动生成src/export_report.py一个空函数参数和返回值严格按 Spec 定义src/export_report_test.py一个 pytest 文件包含test_export_basic用例输入输出与 example 一致docs/api_export_report.mdOpenAPI 风格的接口文档。打开src/export_report.py你会看到def report_export_to_csv_v2( report_id: str, format: str csv ) - dict: 导出指定报表为 CSV 格式供下游系统消费 Spec: report_export_to_csv_v2 # TODO: Implement business logic raise NotImplementedError(Not implemented)这就是 Spec 的力量——它把“我要做什么”和“代码长什么样”瞬间对齐。你不用再纠结函数名该叫export_report还是generate_csv_exportSpec 已经替你决定了。4.3 Spec 驱动的开发流程一次真实的迭代记录我们用这个 Spec 开发“报表导出”功能记录真实耗时与决策点Day 1 AMSpec 定稿 代码骨架生成9:00-9:15与产品确认constraints.enum.format是否要支持xlsx原 Spec 只写了csv产品说下周要加9:15-9:20更新 Spec增加xlsx到 enum并加一条# TODO: xlsx support to be implemented in v2.2注释9:20-9:25重新运行spec-gen生成新骨架9:25-10:00实现 CSV 导出核心逻辑读 DB、写 CSV、上传 CDN耗时 35 分钟。Day 1 PM契约测试与问题暴露14:00运行pytest src/export_report_test.pytest_export_basic通过14:30手动测试format: xlsx报错NotImplementedError—— 这正是我们想要的Spec 明确承诺支持xlsx代码却没实现测试立刻失败15:00在constraints中将xlsx条目改为注释并更新examples移除xlsx相关用例15:10重新校验 Spec通过。Day 2CI 集成与上线10:00在 GitHub Actions 中添加validate-spec和run-tests步骤11:00合并 PRCI 自动运行spec-lint→ OKpytest→ OKspec-diff对比上一版→ 检测到constraints.enum.format从[csv]变为[csv, xlsx]但examples未更新CI 报告警告非阻断12:00上线。监控显示所有formatcsv请求 100% 成功平均耗时 12.3s低于timeout_ms: 30000的约束。整个过程从 Spec 创建到上线不到 2 个工作日。没有会议、没有文档返工、没有联调扯皮。Spec 让开发回归到最本质的状态把确定的需求变成确定的代码。注意事项Spec 的examples必须与当前代码能力严格一致。我们曾因忘记更新 example在 CI 中误判“测试失败是代码问题”实际是 Spec 描述超前了。现在规则是example 只描述已实现的功能未实现的用# TODO注释绝不写进 example。5. 常见问题与排查技巧实录那些没写在文档里的坑5.1 “Spec 写得很清楚但 AI 生成的代码还是不对”——排查四步法这是最高频问题。别急着骂 AI先按这个顺序检查Step 1检查 Spec 是否通过 linter运行spec-lint your.spec.yaml。90% 的“AI 不听话”问题根源是 Spec 本身有语法错误或字段缺失。比如忘记写required: trueAI 默认所有字段可选生成的代码就做了空值处理type: array却没写items_typeAI 可能当成string处理constraints.regex的正则表达式少了个转义符\linter 会直接报错。提示把spec-lint加到 VS Code 的保存钩子save hook编辑器自动校验防患于未然。Step 2检查 AI 工具的 Spec 解析能力不是所有 AI 编程工具都原生支持 YAML Spec。Cursor 和 GitHub Copilot 支持较好但某些国产工具只认 Markdown 表格。解决方案用spec-gen --format markdown-table将 Spec 转成 Markdown 表格粘贴给 AI或在 prompt 开头加一句“请严格按照以下 YAML Spec 生成代码不要自行发挥yaml ...”。我们测试过加了这句生成准确率提升 40%。Step 3检查 inputs/outputs 的“隐含类型”Spec 定义type: string但实际业务中这个 string 可能是 UUID、手机号、邮箱。AI 不知道这些语义。对策在description中明确“report_id: UUID v4 格式如 123e4567-e89b-12d3-a456-426614174000”或在constraints中加format: uuid我们扩展了 Schema 支持此字段。不要指望 AI 猜Spec 要做“最笨的解释者”。Step 4检查 constraints 的“可执行性”constraints.idempotent: true很好但 AI 不知道怎么实现幂等。你需要在description或#注释中补充“使用 Redis SETNX 存储 request_id过期时间 10 分钟”。Spec 不是魔法它需要你把“怎么做”的线索以 AI 能理解的方式埋进去。5.2 “团队不愿意写 Spec觉得多此一举”——破冰三招改变习惯最难。我们用这三招让团队从抵触到依赖第一招用“救火”建立信任找一个正在线上“扑街”的功能比如某个经常超时的接口召集相关人花 20 分钟一起写它的 Spec。重点不是写多全而是把大家口头说的“应该……”“一般会……”“最好……”全写下来。写完立刻用spec-diff对比线上实际行为会发现至少 3 处不一致比如超时设置、错误码定义、日志级别。这时大家就明白了Spec 不是增加工作而是暴露盲区。第二招Spec 即文档一键生成配置spec-gen --format md每次 Spec 更新自动更新 Confluence 页面。运维同学再也不用问“这个接口的超时是多少”直接看 Spec 生成的文档。当 Spec 成为唯一真相源大家自然愿意维护它。第三招Spec 与奖金挂钩小范围试点在季度 OKR 中加入一条“关键模块 Spec 覆盖率 ≥ 90%”。不是考核“写了多少”而是考核“线上故障中有多少是因 Spec 缺失导致的”。我们试行半年因 Spec 缺失导致的 P0 故障降为 0团队自发开始写 Spec。5.3 Spec 的“灰色地带”处理那些无法结构化的部分怎么办Spec 不是万能的。有些东西确实不适合放进 Spec算法细节比如“用 Dijkstra 算最短路径”Spec 只需定义输入起点、终点、权重图和输出路径数组、总距离不规定用什么算法。这是代码层的优化空间。UI/UX 交互按钮颜色、动画时长、加载提示文案这些属于设计规范应由 Figma 设计稿或 Design System 管理Spec 只约定“点击后触发 export 事件”。基础设施配置服务器 CPU 核数、Redis 连接池大小、K8s Pod 副本数这些是运维配置用 Terraform 或 Helm 管理Spec 只约定“支持并发 100 QPS”由压测报告验证。我们的原则是Spec 只管“契约”不管“实现”只管“行为”不管“体验”只管“接口”不管“部署”。越界只会让 Spec 变重、变慢、失去焦点。5.4 Spec 的演进路线图从个人工具到团队标准Spec 的成熟度我们划分为四个阶段每个阶段都有明确标志阶段标志团队状态典型问题L1个人实验一个人用生成自己的代码骨架信 Spec但不敢推给别人“Spec 写起来比直接写代码还慢”L2模块自治一个模块如用户中心所有接口都有 SpecCI 强制校验Spec 是模块的“宪法”代码必须服从“前端说 Spec 里字段名错了该听谁的”L3跨团队契约前后端、中台与业务方用同一份 Spec 作为联调依据Spec 是合同变更需双方签字Git PR“产品临时改需求Spec 还没更新代码先上了”L4生态集成Spec 自动同步到 API 网关、监控系统、测试平台Spec 是数据源一切围绕它流动“Spec 更新了但网关没同步导致 404”我们目前在 L3 阶段。达到 L3 的关键动作是把 Spec 评审纳入需求评审会的固定议程。不是会后补而是会上对着 Spec 一行行过。当产品、前端、后端、测试都围着一份 Spec 讨论“这个 constraints 合理吗那个 example 覆盖全了吗”Spec 就真正活了。最后分享一个小技巧Spec 文件名加上日期前缀如20240520_report_export_to_csv_v2.spec.yaml。这样在 Git 历史中一眼就能看出哪个版本对应哪次需求评审。我们曾靠这个快速定位到一个线上 Bug是某次评审会后Spec 更新了但代码没跟上日期戳成了救命稻草。
阅读完成 · 觉得有帮助?
咨询建站