把 API 改动描述成 Agent 可以检查的契约
本文目录
API 改动任务说明应展示既有调用方收到什么、新调用方可以请求什么、错误如何处理,以及哪些兼容性说法仍需检查。“增加一个摘要字段”并不够:字段不存在、字段值为 null,以及必填字符串,是三份不同的契约。
本指南设计的是位于 https://api.example.invalid 的完全虚构只读卡片目录。这个主机刻意不用于真实服务;没有发送请求,也没有实现服务。契约、消息和数据均为原创教学示例。
从调用方与不变的响应开始
既有调用方发送以下请求:
GET /v1/cards?limit=2 HTTP/1.1
Host: api.example.invalid
Accept: application/json
示意响应如下:
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{"id": "c1", "title": "Review draft"},
{"id": "c2", "title": "Plan export"}
],
"next_cursor": null
}
拟议功能让新调用方用 include_summary=true 主动请求 summary 属性。省略选项或明确传入 false 时,保持既有结构。这是一项选定的迁移策略,不是“可选字段永远安全”的通用规则。
| 契约要点 | 之前 | 之后 |
|---|---|---|
| 方法与路径 | GET /v1/cards |
不变 |
limit |
可选整数,默认 20,范围 1–50 | 不变 |
| 不带摘要选项 | 每项有 id 和 title |
结构相同;summary 不存在 |
include_summary=false |
不是既有选项 | 结构与省略时相同 |
include_summary=true |
不是既有选项 | 每个返回项还包含 summary,其值为字符串或 null |
| 结果顺序与所选 ID | 既有选择行为 | 对相同数据和 limit 保持不变 |
| 访问模型 | 虚构的公开目录 | 不变;不修改授权 |
这个公开数据案例并不是要求从其他服务移除认证。调整任务说明时,应使用对应服务真实的访问与归属规则。
展示新请求及响应
GET /v1/cards?limit=2&include_summary=true HTTP/1.1
Host: api.example.invalid
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{"id": "c1", "title": "Review draft", "summary": "Review the proposed wording."},
{"id": "c2", "title": "Plan export", "summary": null}
],
"next_cursor": null
}
这里的 null 表示卡片没有可用摘要,不表示“客户端没有请求摘要”。另一种情况由 summary 不存在来表达。新客户端主动启用摘要后,必须同时处理字符串和明确的 null。
空结果应返回 items: [] 和 next_cursor: null。这时,“每个返回项”的规则适用于零项,不要求插入占位卡片。
在边界定义错误
本例中的 include_summary 只接受小写查询字面值 true 与 false。limit 是 1 到 50 的规范十进制整数:2 有效,而 02、2.0 与空值无效。重复任一已识别参数都是错误,不是要求采用第一个或最后一个值。
| 请求情况 | 选定响应 |
|---|---|
| 有效 limit,摘要选项省略或为 false | 200,既有结构 |
有效 limit,摘要选项为 true |
200,每项包含可为 null 的 summary |
limit 为 0、51、02、two 或空值 |
400,INVALID_QUERY |
摘要选项为 yes、1、False 或空值 |
400,INVALID_QUERY |
| 重复已识别参数 | 400,INVALID_QUERY |
示意错误响应体如下:
{
"code": "INVALID_QUERY",
"message": "include_summary must be true or false."
}
消息应解释具体无效输入;code 是稳定的机器可读字段。这些查询规则不会把无关服务错误重新定义为 400。既有路由、未知参数和服务器失败行为不属于这项小改动,必须按真实服务契约保留。
用 OpenAPI 描述结构,再检查条件行为
下面的教学文档使用固定版本 OpenAPI 3.1.1,不是声称它是最新版本。OpenAPI 3.1.1 规范定义了这里使用的文档、参数与 Schema Object 结构。在该版本中,类型数组可以表示字符串或 null 字段。
{
"openapi": "3.1.1",
"info": {
"title": "Fictional Card Catalog",
"version": "1.1.0-example"
},
"servers": [{"url": "https://api.example.invalid"}],
"paths": {
"/v1/cards": {
"get": {
"operationId": "listCards",
"summary": "List fictional cards, optionally including summaries",
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"description": "Canonical decimal 1-50 only; repeated values are invalid.",
"schema": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}
},
{
"name": "include_summary",
"in": "query",
"required": false,
"description": "Lowercase true/false only; repeated values are invalid. True requires summary on every returned item; otherwise summary is absent.",
"schema": {"type": "boolean", "default": false}
}
],
"responses": {
"200": {
"description": "Cards; item shape also follows the include_summary behavior contract.",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/CardList"}
}
}
},
"400": {
"description": "Invalid recognized query parameter",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/QueryError"}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Card": {
"type": "object",
"required": ["id", "title"],
"additionalProperties": false,
"properties": {
"id": {"type": "string"},
"title": {"type": "string"},
"summary": {"type": ["string", "null"]}
}
},
"CardList": {
"type": "object",
"required": ["items", "next_cursor"],
"additionalProperties": false,
"properties": {
"items": {"type": "array", "items": {"$ref": "#/components/schemas/Card"}},
"next_cursor": {"type": ["string", "null"]}
}
},
"QueryError": {
"type": "object",
"required": ["code", "message"],
"additionalProperties": false,
"properties": {
"code": {"type": "string", "const": "INVALID_QUERY"},
"message": {"type": "string"}
}
}
}
}
}
这个 schema 允许 summary 存在或不存在,不会依据查询参数强制要求其存在。同样,整数参数 schema 本身也不能确定服务器如何处理数值的文字形式或重复查询值。这些要求需要在 HTTP 边界做行为检查。
配套文档已有的检查仅是作为 JSON 解析;没有运行完整 OpenAPI 校验器、生成客户端或服务。应把它们视为另外的实现检查,而不是从 JSON 语法有效推断出来。
按调用方行为检查兼容性
| 调用方 | 在这份拟议契约下的效果 | 迁移前检查 |
|---|---|---|
| 省略选项的既有客户端 | 收到旧结构 | 对固定数据比较改动前后的响应字段 |
| 拒绝未知字段的既有严格客户端 | 默认仍收到旧结构 | 核实其请求不会意外开始启用新选项 |
| 请求摘要的新客户端 | 每项收到字符串或 null | 检查两种取值以及空列表 |
| 未知调用方 | 兼容性未知 | 识别其请求与解码假设 |
一种可能的顺序,是先部署服务端支持,再让客户端发送新参数。应先检查既有服务端如何处理未知参数,因为旧服务端可能拒绝新请求。同样,回退部署可能留下仍在调用不受支持选项的新客户端。主动启用式设计除了 schema,也需要迁移方案。
一份完整的实现请求
实现虚构 GET /v1/cards 的主动启用摘要契约。
使用附带的 OpenAPI 3.1.1 文档和两组请求/响应示例。
example.invalid 主机仅供说明,不要联系它。
保留既有方法、路径、数据选择、排序、limit 默认值 20、
limit 范围 1-50、响应封套与访问行为。
新行为:
- include_summary 省略或为 false:每个条目中 summary 不存在。
- include_summary=true:每个条目都有 summary,值为字符串或 null。
- Null 表示不可用,不要替换成空字符串。
- 空结果仍为 items: [],并且 next_cursor: null。
- include_summary 只接受小写 true/false。
- 接受规范十进制 limit 1-50;拒绝重复的已识别参数。
- 无效的已识别输入返回 400,带 code INVALID_QUERY 和有用消息。
保留无关错误行为。
在 HTTP 边界检查选项省略、false、true,null 摘要、空列表、
limit 边界、无效文字形式以及重复参数。
根据固定的改动前样例,比较 ID、标题、顺序与封套。
单独检查存在性规则,因为响应 schema 无法强制执行这些规则。
交付实现、实际检查结果、调用方影响说明,以及服务端/客户端部署顺序提案。
不要修改认证、写入接口、线上部署或未知调用方的契约。
不要把单纯 JSON 解析描述成 OpenAPI 或兼容性验证。
给评审者可观察的改动前后行为,才能判断 Agent 是否交付了所要求的 API 改动,而不把看似很小的字段增加当成普遍兼容保证。