如何让AI先定义接口契约:用请求与响应样例减少前后端联调问题
在日常通过 AI 辅助全栈开发或前后端分离项目时,开发者常常会遇到这样的痛点:当分别让 AI 编写后端 API 和前端调用代码时,由于缺乏统一的契约约束,AI 在两端生成的字段名经常对不上。例如,后端定义的是consigneeName,而前端调用时写成了recipient_name;或者后端返回的是扁平结构,前端却按嵌套对象去解析。这种隐式的契约不对称会导致大量无意义的联调返工与字段排查。
直接让 AI “写一个增删改查接口”往往无法根治这一问题。本文将解决这一核心痛点。读完本文后,你将掌握如何通过“契约先行(Contract-First)”的提示词策略,强制 AI 在编写任何实现代码前先输出精确的请求与响应 JSON 样例(Request & Response Payload Examples),并利用 Python 与 FastAPI 将其固化为严格的代码契约,从源头上消灭前后端联调的字段摩擦。
一、 前置条件与案例环境
1. 适用环境
- 编程语言:Python 3.10+
- 核心框架:FastAPI 0.110+,Pydantic v2
- 测试工具:Pytest 8.1+,FastAPI TestClient
2. 案例业务场景
为了让方法具备高度可复现性,本文以一个真实的小型业务模块为例——“用户收货地址管理模块(Shipping Address Service)”中的“新建收货地址”接口。
- 业务需求:用户提交收货人姓名、手机号、省市区、详细地址及是否设为默认地址;后端校验通过后保存并返回生成的地址 ID 及完整详情。
二、 核心原理:为什么契约先行是 AI 协作的解药?
大语言模型(LLM)在多文件、多模块协同编写时容易发生“记忆漂移”。如果先写代码再对齐,AI 在不同对话轮次中对同一业务对象的命名偏好会发生变化。
“契约先行”的核心逻辑在于:
- 样例驱动(Example-Driven):在动工前,先用标准 JSON 样例锁定输入输出的每一个字段、类型及必填属性。
- 单一真实数据源(Single Source of Truth):将 JSON 样例转化为 Pydantic 模型,由强类型系统(Type Hinting)自动生成 OpenAPI 契约文档,使前后端开发有章可循。
三、 完整实现方案
本方案包含四个独立的文件,涵盖依赖配置、契约模型、业务实现及自动化测试。
1. 文件清单与职责表
| 文件路径 | 职责说明 |
|---|---|
requirements.txt | 项目依赖包及其精确版本。 |
models.py | 基于契约样例编写的 Pydantic 请求与响应数据模型。 |
app.py | FastAPI 应用入口,实现新建地址接口及契约校验。 |
test_app.py | 使用 TestClient 编写的自动化测试脚本,验证契约的有效性。 |
2. 各文件完整代码
文件一:requirements.txt
fastapi==0.110.0 uvicorn==0.28.0 pydantic==2.6.4 pytest==8.1.1 requests==2.31.0文件二:models.py
fromtypingimportOptionalfrompydanticimportBaseModel,Field,field_validatorimportreclassAddressCreateRequest(BaseModel):consignee_name:str=Field(...,min_length=2,max_length=20,description="收货人姓名")phone:str=Field(...,description="手机号")province:str=Field(...,description="省份")city:str=Field(...,description="城市")district:str=Field(...,description="区/县")detail_address:str=Field(...,min_length=5,description="详细地址")is_default:bool=Field(False,description="是否设为默认地址")@field_validator("phone")@classmethoddefvalidate_phone(cls,v:str)->str:ifnotre.match(r"^1[3-9]\d{9}$",v):raiseValueError("手机号格式不正确")returnvclassAddressResponse(BaseModel):code:int=Field(200,description="状态码")message:str=Field("success",description="提示信息")address_id:int=Field(...,description="生成的地址唯一ID")consignee_name:strphone:strprovince:strcity:strdistrict:strdetail_address:stris_default:bool文件三:app.py
fromfastapiimportFastAPI,HTTPException,statusfrommodelsimportAddressCreateRequest,AddressResponse app=FastAPI(title="Shipping Address API",version="1.0.0")# 模拟数据库存储MOCK_DB=[]ID_COUNTER=1@app.post("/api/addresses",response_model=AddressResponse,status_code=status.HTTP_201_CREATED)defcreate_address(payload:AddressCreateRequest):""" 创建收货地址接口 严格遵循 Pydantic 契约定义 """globalID_COUNTER new_id=ID_COUNTER ID_COUNTER+=1# 如果设为默认,清空其他默认状态(简化逻辑)ifpayload.is_default:foriteminMOCK_DB:item["is_default"]=Falserecord={"address_id":new_id,"consignee_name":payload.consignee_name,"phone":payload.phone,"province":payload.province,"city":payload.city,"district":payload.district,"detail_address":payload.detail_address,"is_default":payload.is_default}MOCK_DB.append(record)return{"code":200,"message":"地址创建成功",**record}文件四:test_app.py
fromfastapi.testclientimportTestClientfromappimportapp client=TestClient(app)deftest_create_address_success():payload={"consignee_name":"张三","phone":"13800138000","province":"广东省","city":"深圳市","district":"南山区","detail_address":"科技园南区T3栋","is_default":True}response=client.post("/api/addresses",json=payload)assertresponse.status_code==201data=response.json()assertdata["code"]==200assertdata["address_id"]isnotNoneassertdata["consignee_name"]=="张三"assertdata["phone"]=="13800138000"deftest_create_address_invalid_phone():payload={"consignee_name":"李四","phone":"12345",# 错误手机号"province":"广东省","city":"广州市","district":"天河区","detail_address":"体育东路123号","is_default":False}response=client.post("/api/addresses",json=payload)assertresponse.status_code==422# 校验失败拦截deftest_create_address_missing_field():payload={"consignee_name":"王五"# 缺少 phone 和地址字段}response=client.post("/api/addresses",json=payload)assertresponse.status_code==422四、 运行方式与测试步骤
请在安装了 Python 3.10+ 的环境中,打开终端(Terminal),在项目根目录下依次执行以下命令:
1. 安装依赖
pipinstall-rrequirements.txt2. 执行自动化测试
pytest test_app.py-v3. 启动本地 API 服务(可选,用于查看自动生成的 OpenAPI 契约文档)
uvicorn app:app--reload--port8000启动后,可在浏览器访问[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)查看由 Pydantic 模型自动生成的交互式 Swagger 接口契约文档。
五、 可操作的验收与测试方案
为了确保接口契约在实际开发中能够有效减少前后端联调问题,我们需要通过以下三个标准场景进行验收:
| 测试目的 | 操作或输入 | 预期结果 | 判定方法 |
|---|---|---|---|
| 正常场景 |
(标准契约请求) | 发送包含所有必填字段且符合格式的 JSON 请求。 | 返回 HTTP 201,响应体包含生成的address_id及请求中的所有字段。 | 运行pytest检查test_create_address_success是否通过。 |
|边界场景
(手机号格式不合法) | 传入不符合1[3-9]\d{9}正则的手机号(如12345)。 | 服务拒绝处理,返回 HTTP 422 状态码,并在错误详情中指出phone校验失败。 | 运行pytest检查test_create_address_invalid_phone是否通过。 |
|失败场景
(缺少核心必填字段) | 传入缺失phone和详细地址的精简 JSON。 | 触发 Pydantic 严格校验拦截,返回 HTTP 422。 | 运行pytest检查test_create_address_missing_field是否通过。 |
六、 常见故障定位与边界
在采用契约先行方法引导 AI 编程时,可能会遇到以下边界情况:
- 前后端对字段可选性(Optional)理解不一致:
- 现象:后端认为某个字段是选填的(如
is_default),但前端在渲染时没有做空值兜底导致崩溃。 - 对策:在契约阶段就明确规定所有字段的默认值(例如
is_default: bool = False),并在 Pydantic 中显式赋予默认值,避免返回null或undefined。
- AI 在后续对话中擅自篡改字段名:
- 现象:在开发了 10 轮对话后,AI 在写前端调用代码时把
consignee_name写成了name。 - 对策:将
models.py作为“不可变契约文件”锚定在上下文中。每次让 AI 写新模块时,提示词中加入:“必须严格引用models.py中定义的字段名,严禁自行创造新字段。”
七、 验证状态与参考资料
- 验证状态:本文提供的 Python 代码、Pydantic 模型及 Pytest 测试用例已在本地 Python 3.10 环境中通过完整执行,正常场景、格式边界校验与缺失字段失败拦截均符合预期输出。
- 参考资料:
- FastAPI Official Documentation: Body - Multiple Parameters and Validation
- Pydantic v2 Documentation: Validators and Field Types