《AI时代的程序员修养》前四篇讲了程序、数据结构、执行模型和模块通信。从这一篇开始,进入“把功能做成能扛流量的服务”这一卷。第一件事是接口。AI 写接口很快。给它一句“实现用户查询接口”,它能马上生成路由、参数、ORM 查询和 JSON 返回。问题是,接口最重要的部分不是 URL,也不是 handler 里那几行代码,而是系统对调用方做出的承诺:字段长什么样,错误怎么算,能不能重试,权限怎么判,版本怎么演进,旧客户端会不会被新字段打坏。接口不是 URL,是契约。契约没写清楚,代码越快生成,后面越难收拾。URL 只是入口很多接口设计讨论会停在这一层:GET /api/users/{id} POST /api/orders GET /api/orders?page=1page_size=20这些当然要设计,但它们只是入口。真正的契约至少包括:请求参数和字段类型。字段是否必填,默认值是什么。返回结构是否稳定。错误码和错误语义。权限要求。幂等规则。分页和排序规则。限流和超时边界。版本兼容策略。示例参数和示例响应。可观测字段,如request_id、trace_id。AI 生成接口时,最常见的问题是“看起来能调”。能调不代表能用,更不代表能长期维护。比如一个查询工具调用历史的接口,如果只写:GET /tool-history?user_id=xxx它没有说明很多关键问题:只能查自己的历史,还是管理员可以查所有人?时间范围有没有上限?默认排序是什么?返回字段里是否包含敏感参数?分页是 page/page_size,还是 cursor?tool_id搜索是精确匹配还是模糊匹配?没有结果时返回空列表,还是 404?查询太慢时返回什么错误?这些不明确,前端、后端、测试和数据分析都会各自猜一套。先写 schema,再写 handler接口契约里最基础的是 schema。schema 不是“文档装饰”,它应该约束代码。以创建订单为例,一个随手写的请求可能是:{ "user_id": "u_123", "items": [ {"sku": "book_001", "count": 2} ], "coupon": "NEWUSER" }但契约必须更明确:{ "user_id": "string, required", "items": "array, required, 1..100", "items[].sku": "string, required, max 64", "items[].quantity": "integer, required, 1..999", "coupon_code": "string, optional, max 64", "client_order_id": "string, required, max 128" }这里有几个小变化很关键。count改成quantity,语义更清楚。items有长度上限,避免一次请求塞进几万行。quantity有范围,避免 0、负数或离谱数字。client_order_id用于幂等,避免客户端重试创建重复订单。如果用 Python,可以把 schema 直接写成模型:from pydantic import BaseModel, Field class OrderItemIn(BaseModel): sku: str = Field(min_length=1, max_length=64) quantity: int = Field(ge=1, le=999) class CreateOrderRequest(BaseModel): user_id: str = Field(min_length=1, max_length=64) items: list[OrderItemIn] = Field(min_length=1, max_length=100) coupon_code: str | None = Field(default=None, max_length=64) client_order_id: str = Field(min_length=1, max_length=128)这段代码不复杂,AI 也能写。重点是你要在 prompt 里要求它先定义契约:先不要写 handler。 请先定义请求和响应 schema,说明每个字段的类型、必填、默认值、长度范围、枚举范围和业务含义。 schema 通过后再写接口实现。没有 schema 的接口,就像没有类型的系统边界。小项目可以靠记忆,大系统只能靠约束。响应结构要稳定接口返回值最怕“今天想起来加一个字段,明天顺手改一个结构”。一个比较稳的列表响应通常长这样:{ "items": [ { "id": "order_123", "status": "paid", "total_amoun