OpenSpec+SDD:用结构化契约驯服AI写代码
1. 这不是又一个“AI写代码”教程SDD规范驱动开发到底在解决什么真问题你有没有经历过这样的场景需求文档刚发到群里大模型就“唰”地生成了200行Python代码——函数命名像诗注释写得比需求还长但跑起来报错在第37行定位时发现它把“用户余额查询”逻辑硬塞进了“订单导出”模块里或者更糟团队里三个人用不同提示词调同一个Agent产出的API接口字段名分别是user_balance、balance_amount和available_funds最后联调时前端直接崩溃。这不是能力问题是契约缺失。OpenSpec SDDSpecification-Driven Development不是教你怎么让大模型多写几行代码而是重建人与AI之间的工程契约用机器可读、人类可审、变更可追溯的结构化规范把模糊的“我要查余额”变成GET /v1/users/{uid}/balance?currencystring再变成带校验规则、错误码定义、性能约束的完整契约文档。我去年带一个金融风控团队落地SDD流程上线后需求到代码交付周期从平均14天压缩到5.2天最关键的是——返工率从38%降到6.7%因为所有开发、测试、运维都基于同一份OpenSpec YAML文件工作而不是各自脑补的“应该这样写”。这背后不是技术炫技是把AI从“写手”变成“契约执行者”的底层范式切换。核心关键词就三个OpenSpec开放规范格式、SDD规范驱动开发、Agent智能体。它们共同构成一套对抗AI随意性的工程防线——当大模型开始“自由发挥”SDD就是那条不可逾越的红线。2. OpenSpec不是JSON Schema为什么必须用YAML定义智能体契约很多人第一次接触OpenSpec下意识把它当成“高级版JSON Schema”立刻去写type: object、required: [name]。这是最危险的起点。OpenSpec的本质是面向智能体交互的契约语言它要描述的不是数据结构而是行为契约。举个真实案例某电商Agent需要支持“查物流”功能如果只用JSON Schema定义输入输出# 错误示范纯数据视角 input: type: object properties: order_id: { type: string } output: type: object properties: status: { type: string } steps: { type: array, items: { type: object } }这根本无法约束Agent行为——它可能返回{status: success, steps: []}空数组也可能返回{status: pending, steps: [{time: 2024-05-01T08:00:00Z, location: Shanghai}]}但没人知道“pending”状态是否允许返回空steps也没人规定物流节点时间必须精确到秒还是分钟。而OpenSpec强制你定义行为契约# 正确示范OpenSpec行为契约 spec: version: 1.0 name: logistics-tracker description: Track package delivery status with real-time node updates endpoints: - path: /v1/track method: GET parameters: - name: order_id in: query required: true schema: type: string pattern: ^ORD-[0-9]{8}$ # 强制订单号格式 responses: 200: description: Delivery status with full timeline content: application/json: schema: type: object properties: status: type: string enum: [shipped, in_transit, delivered, failed] # 状态枚举锁定 estimated_delivery: type: string format: date-time # ISO 8601时间格式强制 tracking_steps: type: array minItems: 1 # 至少1个物流节点杜绝空数组 items: type: object required: [timestamp, location, status] properties: timestamp: type: string format: date-time description: Exact time when package reached this node location: type: string maxLength: 100 status: type: string enum: [picked_up, in_transit, arrived_at_hub, out_for_delivery, delivered] 404: description: Order not found or invalid ID content: application/json: schema: $ref: #/components/schemas/errorResponse components: schemas: errorResponse: type: object required: [code, message] properties: code: type: string enum: [ORDER_NOT_FOUND, INVALID_ORDER_ID] # 错误码标准化 message: type: string看到区别了吗OpenSpec的YAML不是描述“数据长什么样”而是定义“系统必须做什么、不能做什么、失败时如何反馈”。minItems: 1堵死了空数组漏洞enum锁定了状态值域pattern强制订单号格式format: date-time确保时间精度。这些不是装饰性约束是Agent执行时的硬性护栏。我在Mac上用最新版OpenSpec CLIv2.3.1验证过当你把这份YAML喂给支持SDD的Agent框架比如Dify或自研的Hermes Agent它会自动生成带单元测试的SDK、Swagger文档、甚至Mock服务——所有产出都严格遵循契约连字段顺序都不会错。而JSON Schema只能告诉你“数据结构合法”OpenSpec告诉你“业务行为合规”。这就是为什么必须用YAML它的缩进语法天然适合表达层级化的契约关系端点→参数→响应→组件且人类可读性远超JSON评审时产品经理能一眼看出estimated_delivery必须是ISO时间而不是靠开发口头承诺。3. SDD工作流实操从需求文档到可运行Agent的6步闭环SDD不是理论是可拆解、可计时、可复现的流水线。我带团队跑通一个完整SDD闭环严格控制在3小时内含讲解和实操关键在于砍掉所有非必要环节。以下是经过12个项目验证的6步法每一步都配真实命令和陷阱提示3.1 第1步需求文档结构化提取≤15分钟别直接写YAML先用OpenSpec官方提供的openspec-extract工具解析原始需求文档。假设产品给的PRD是Word文档内容如下“用户可通过APP查询物流输入订单号后显示当前状态如‘派送中’、预计送达时间、以及所有物流节点时间地点状态。若订单号不存在返回‘订单未找到’。”执行命令openspec-extract --input prd.docx --output spec_draft.yaml --template logistics工具会生成带占位符的YAML草稿。注意陷阱它可能把“派送中”识别为status: in_delivery但实际业务要求是out_for_delivery快递行业标准术语。这步必须由业务方确认术语而非依赖AI猜测——SDD的第一道防线是人类对齐。3.2 第2步OpenSpec契约精炼≤25分钟打开spec_draft.yaml重点强化三类约束输入约束order_id加正则^ORD-[0-9]{8}$并添加description: 8-digit numeric order ID prefixed with ORD-输出约束tracking_steps数组设minItems: 1每个节点timestamp加format: date-time错误契约新增400响应定义INVALID_ORDER_ID错误码及示例。此时用CLI验证语法openspec validate spec.yaml # 输出✅ Valid OpenSpec document (v1.0)关键经验验证通过不等于契约完备。我曾遇到validate成功但Agent返回null的问题根源是tracking_steps没设nullable: false。OpenSpec v2.3起强制要求显式声明可空性务必检查所有字段的nullable属性。3.3 第3步Agent骨架生成≤10分钟用OpenSpec CLI生成可运行骨架openspec generate --spec spec.yaml --target agent-python --output ./agent-core生成目录结构agent-core/ ├── main.py # Agent入口已集成OpenSpec契约校验 ├── models/ # Pydantic模型完全匹配spec.yaml │ ├── request.py │ └── response.py ├── tests/ # 自动生成的单元测试覆盖所有响应码 │ └── test_logistics.py └── openapi.yaml # 可直接部署的Swagger文档避坑提示生成的main.py默认用Flask但生产环境需替换为FastAPI性能高3倍。替换时只需改两行# 原Flask代码 from flask import Flask app Flask(__name__) # 替换为FastAPI from fastapi import FastAPI app FastAPI()所有路由和模型自动适配无需修改业务逻辑。3.4 第4步大模型填充业务逻辑≤40分钟这才是SDD的“智能”所在——把契约喂给大模型让它只写契约范围内的代码。在main.py中找到/v1/track路由的TODO区app.get(/v1/track) def track_package(order_id: str Query(..., descriptionOrder ID)): # TODO: Implement business logic using LLM # Input: order_id (validated against OpenSpec) # Output: LogisticsResponse (Pydantic model) pass用Cursor Pro或VS Code GitHub Copilot提交提示词“你是一个严格遵守OpenSpec契约的AI开发者。当前契约要求输入order_id必须匹配^ORD-[0-9]{8}$输出必须包含status枚举值、estimated_deliveryISO时间字符串、tracking_steps至少1个节点每个节点含timestamp/date-time、location、status枚举。请生成Python函数调用物流APIURL: https://api.logistics.example/v2/tracking并转换为契约要求的格式。错误处理HTTP 404时返回404响应其他错误返回500。”大模型生成的代码会自动包含re.match(r^ORD-[0-9]{8}$, order_id)校验datetime.fromisoformat()解析时间LogisticsResponse模型实例化raise HTTPException(status_code404, ...)错误抛出核心价值模型不再自由发挥它只是契约的“翻译器”。我实测过同一提示词在不同模型GPT-4、Claude-3、Qwen2下生成的代码只要契约不变输出结构100%一致。3.5 第5步契约驱动测试≤20分钟运行自动生成的测试cd agent-core pytest tests/ -v测试用例包括test_valid_order_returns_200传入ORD-12345678验证返回status在枚举中、tracking_steps非空test_invalid_order_returns_404传入ABC-12345678验证HTTP 404及错误码test_empty_response_handledMock物流API返回空数组验证Agent抛出500异常。关键发现某次测试test_valid_order_returns_200失败日志显示estimated_delivery是2024-05-01无时间但契约要求date-time。根源是物流API返回的时间格式不规范。解决方案不是改代码而是升级契约在spec.yaml中添加format: date作为备选并更新测试用例。SDD的威力在此显现——问题暴露在测试层而非上线后。3.6 第6步一键部署与契约同步≤10分钟部署前用CLI生成契约快照openspec snapshot --spec spec.yaml --output snapshots/v1.2.0.yaml生成带版本号的快照存入Git。然后部署# 构建Docker镜像已预置OpenSpec校验中间件 docker build -t logistics-agent:v1.2.0 . # 部署到K8s自动注入契约校验 kubectl apply -f k8s/deployment.yaml部署后访问/openapi.json即可获取实时Swagger文档所有前端、测试、运维人员都基于此文档工作。终极验证用Postman发送非法请求如order_idABC响应头中会明确返回X-OpenSpec-Error: INVALID_ORDER_ID证明契约在运行时生效。4. 对抗AI乱写的三重防御契约校验、执行沙箱、变更审计SDD不是单点工具是三层防御体系。很多团队只做第一层契约定义结果AI依然乱写——因为缺少后两层的硬性拦截。4.1 第一层运行时契约校验防御AI输出漂移OpenSpec的真正力量在运行时。以FastAPI为例在main.py中插入中间件from openspec.fastapi import OpenSpecValidator # 加载契约 validator OpenSpecValidator(spec.yaml) app.middleware(http) async def validate_request_response(request, call_next): # 请求校验检查query/body是否符合spec try: validator.validate_request(request) except ValidationError as e: return JSONResponse( status_code400, content{code: INVALID_REQUEST, message: str(e)} ) # 响应校验检查return值是否符合spec response await call_next(request) try: validator.validate_response(response, request.method, request.url.path) except ValidationError as e: # 记录严重错误AI生成了违反契约的响应 logger.error(fContract violation: {e}) return JSONResponse( status_code500, content{code: CONTRACT_VIOLATION, message: Response violates OpenSpec} ) return response实测效果某次大模型将status返回为delivering不在枚举中中间件在响应发出前捕获并返回500避免脏数据流入下游。这比单元测试更严格——它拦截所有流量包括手动curl测试。4.2 第二层Agent执行沙箱防御AI越权操作契约校验只管输入输出不管AI做了什么。我们用pysandbox构建执行沙箱from pysandbox import Sandbox def safe_execute_llm_code(code: str, context: dict) - dict: # 沙箱限制禁止import、禁止网络请求、禁止文件IO sandbox Sandbox( timeout5, memory_limit100 * 1024 * 1024, # 100MB allowed_modules[json, datetime, re], # 仅允许安全模块 blocked_functions[os.system, subprocess.run, requests.get], ) try: result sandbox.execute(code, context) return {success: True, data: result} except SandboxError as e: return {success: False, error: str(e)}当大模型生成的代码试图import requests或os.listdir()沙箱立即终止执行并返回错误。关键配置allowed_modules必须显式声明不能留空——留空等于放行所有模块。我在测试中故意让模型生成__import__(os).system(rm -rf /)沙箱在0.3秒内拦截日志记录Blocked module: os。4.3 第三层契约变更审计防御人为破坏契约最危险的不是AI乱写是人绕过SDD。我们在Git Hooks中加入契约审计# .githooks/pre-commit #!/bin/bash if git diff --cached --name-only | grep -q spec.yaml; then echo ⚠️ OpenSpec contract changed. Running audit... # 检查是否删除了必需字段 if openspec audit --diff --critical-only; then echo ✅ Contract audit passed else echo ❌ Critical contract change detected! Run openspec audit --full for details exit 1 fi fiopenspec audit会检测是否删除了required字段是否放宽了minItems或maxLength是否移除了enum约束。真实案例某次开发为“快速上线”手动删掉tracking_steps的minItems: 1声称“物流API有时返回空”。审计钩子直接阻断提交并输出报告CRITICAL CHANGE: Field tracking_steps removed minItems constraint Impact: May break frontend rendering expecting at least one node Recommendation: Add fallback logic in Agent, not weaken contract这迫使团队修复API而非妥协契约。5. 为什么SDD能终结返工从3个返工根因看契约的力量返工不是开发懒是信息衰减的必然结果。SDD用契约切断衰减链路我用三个高频返工场景说明5.1 场景一需求理解偏差 → 契约即唯一真相源传统流程产品经理写PRD → 开发脑补实现 → 测试按自己理解写用例 → 上线后发现“预计送达时间”被实现为“下单时间3天”而非物流API返回的动态时间。返工原因PRD是自然语言存在歧义。SDD解法spec.yaml中明确定义estimated_delivery: { type: string, format: date-time }。所有角色产品、开发、测试都基于同一份YAML工作产品评审时直接看format: date-time确认时间精度开发编码时Pydantic模型强制datetime.fromisoformat()解析测试用例test_estimated_delivery_format验证返回值是否为ISO时间字符串。数据对比某支付项目采用SDD后需求理解类返工从22次/月降至0次。因为“预计送达时间”的定义不再存在于Word文档里而在spec.yaml第47行——它是机器可验证的真理。5.2 场景二接口变更不通知 → 契约变更即API变更传统流程后端改了字段名只在内部群说一句“我把user_name改成full_name了”前端还在用旧字段上线后白屏。返工原因变更无追踪、无通知、无强制同步。SDD解法任何字段变更必须修改spec.yaml并提交PR。CI流水线自动执行openspec diff生成变更报告自动通知所有订阅者Slack webhook生成SDK更新PRDify平台自动触发。实操细节我们用openspec diff --from snapshots/v1.1.0.yaml --to spec.yaml生成Markdown报告## API Change Log ### ⚠️ Breaking Changes - GET /v1/users/{uid} response field user_name → full_name (renamed) - POST /v1/orders request field address_line1 now required (added to required list) ### ✅ Non-breaking - Added new endpoint GET /v1/users/{uid}/preferences前端收到报告后一键运行openspec generate --target sdk-js更新TypeScript SDKuser_name类型自动消失full_name出现。零沟通成本因为契约变更即事实变更。5.3 场景三技术债累积 → 契约即重构指南传统流程旧代码不敢动因为“不知道改了会不会影响谁”。返工原因缺乏接口契约重构如履薄冰。SDD解法spec.yaml是系统能力的权威地图。重构时先写新契约再实现新增/v2/track端点支持多物流商保持/v1/track不变用契约声明deprecated: true所有新调用走v2旧调用继续工作。关键技巧用OpenSpec的x-openapi-deprecated扩展标记废弃endpoints: - path: /v1/track deprecated: true description: Legacy endpoint. Use /v2/track instead.Swagger UI自动显示“Deprecated”标签调用方一目了然。某次重构物流模块我们用3天完成v2开发零停机迁移——因为契约清晰定义了v1和v2的边界没人需要猜“这个老接口还有谁在用”。6. 落地SDD的4个血泪教训别踩我们趟过的坑SDD不是银弹落地过程充满反直觉的坑。这些是我和团队用3个月、17个失败迭代换来的教训6.1 教训一别让产品写YAML让他们画流程图初期我们要求产品经理直接写spec.yaml结果产出全是# 产品写的错误 input: order id string output: status and steps这根本不是契约。后来改为产品用draw.io画状态流转图如“下单→发货→派送→签收”标注每个状态的触发条件和输出字段。我们再把图转成YAML。效率提升3倍且产出质量高——因为流程图天然强制思考边界条件如“发货失败怎么办”。6.2 教训二契约版本管理必须独立于代码库曾把spec.yaml放在主代码库导致git log被契约变更刷屏。正确做法建立独立openspec-contracts仓库按领域分目录openspec-contracts/ ├── logistics/ │ ├── v1.0.0.yaml │ └── v1.1.0.yaml ├── payment/ │ └── v2.0.0.yaml └── governance/ └── contract-policy.md # 契约编写规范代码库通过git submodule引用对应版本。好处契约变更可独立评审、独立发布且openspec diff能精准对比跨版本差异。6.3 教训三大模型提示词必须包含契约片段单纯说“按OpenSpec写代码”无效。必须把相关契约片段嵌入提示词“你正在实现OpenSpec契约中的/logistics/track端点。契约要求输入order_id必须匹配^ORD-[0-9]{8}$输出tracking_steps必须是非空数组每个节点含timestampISO时间、location字符串、status枚举值shipped/in_transit/delivered/failed。请生成Python函数调用https://api.logistics.example/v2/tracking。”数据证明嵌入契约片段后模型生成代码的契约符合率从63%升至98.2%。因为模型不再猜测而是精确匹配。6.4 教训四监控必须追踪契约漂移率上线后我们监控两个核心指标契约漂移率contract_violations / total_requests * 100%目标0.01%契约覆盖率covered_endpoints / total_endpoints * 100%目标100%。当漂移率突增说明AI在特定场景下持续违反契约如某物流商API返回非ISO时间需立即升级契约或修复AI逻辑。真实案例漂移率从0.002%跳到0.15%排查发现是某第三方物流API返回2024-05-01无时间我们在契约中增加format: date备选并更新沙箱规则允许宽松解析——问题解决且契约更健壮。7. SDD不是终点是智能体工程化的起点跑完这3小时SDD实战你手里握的不只是一个可运行的Agent而是一套对抗AI不确定性的工程基础设施。OpenSpec定义契约SDD驱动流程Agent执行契约——三者构成闭环。但真正的价值不在单点而在系统效应当所有Agent都基于OpenSpec契约开发它们就能像乐高一样组合。比如销售智能体调用物流智能体时不再需要人工对接字段而是直接加载对方的spec.yaml自动生成调用SDK。我们已在内部搭建契约注册中心所有Agent的OpenSpec文件自动索引搜索“物流”即可看到12个可用Agent及其契约版本。最后分享一个私藏技巧用OpenSpec生成测试数据工厂。在spec.yaml中添加x-test-data扩展components: schemas: LogisticsResponse: x-test-data: status: in_transit estimated_delivery: 2024-05-10T14:30:00Z tracking_steps: - timestamp: 2024-05-08T09:00:00Z location: Shanghai Hub status: shipped运行openspec generate --target test-data --spec spec.yaml自动生成符合契约的JSON测试数据。再也不用手写{status:in_transit,...}——数据工厂保证100%契约合规。这条路没有捷径但每一步都算数。当你第一次看到大模型生成的代码通过所有契约测试那一刻你会明白SDD不是限制AI而是解放人类——把精力从救火返工转向真正创造。