
接口调通的那一刻通常很让人放松。
控制台打印出 200 OK,响应体里也有一段文字。开发环境跑了三次都没报错,于是任务被标记为“接入完成”。
真正接进业务以后,问题才陆续出现。
- 客服机器人返回了半句话;
- 文档摘要看起来完整,最后两页却没有处理;
- 模型说自己已经创建工单,工单系统里什么也没有;
- 后端要求 JSON,偶尔收到一段带代码围栏的 Markdown;
- 流式输出显示了大半段,连接中断后前端仍然当成成功;
- 请求发生了三次自动重试,用户只看到一个结果,账单却记录了三次调用。
这些情况都可能伴随 HTTP 200。
HTTP 200 能证明服务器接收并处理了这次 HTTP 请求。它无法替你证明回答符合业务要求,更无法证明工具执行、数据写入和后续流程已经完成。
一套可上线的验收标准,至少要回答七个问题。
- 请求是否到达了正确的接口和模型?
- 模型返回的是可用答案、拒答,还是一个待执行动作?
- 输出有没有被截断或提前结束?
- 返回格式能不能被程序稳定解析?
- 工具调用是否形成完整闭环?
- 流式传输是否完整结束?
- 出问题后能不能通过日志找到这一次请求?
本文把这七项拆成可以执行的测试。你可以拿它验收 Claude API、OpenAI 兼容接口,也可以用于 Dify、n8n、Claude Code、自建 Agent 和客服机器人的上线评审。
为什么 HTTP 成功不等于业务成功
一次大模型调用通常经过四层。
| 层级 | 它回答的问题 | 常见成功信号 | 可能遗漏的问题 |
|---|---|---|---|
| 网络层 | 请求有没有送达 | DNS、TLS、连接正常 | 网络中途断开、代理缓存异常 |
| HTTP 层 | 服务是否接受请求 | HTTP 200 | 响应体可能拒答、截断或为空 |
| 模型层 | 模型有没有完成生成 | 有 content、stop reason | 内容不符合格式或任务目标 |
| 业务层 | 用户的任务有没有完成 | 工单创建、数据落库、页面更新 | 工具没执行、写入失败、状态没同步 |
很多测试只覆盖了前两层。
例如下面这段响应,在 HTTP 层没有问题,但它并不是一份完成的业务答案。
{
"id": "msg_xxx",
"content": [],
"stop_reason": "refusal",
"usage": {
"input_tokens": 1210,
"output_tokens": 18
}
}
{
"id": "msg_xxx",
"content": [],
"stop_reason": "refusal",
"usage": {
"input_tokens": 1210,
"output_tokens": 18
}
}
拒答、达到输出上限、需要调用工具,都可以通过正常响应返回。Anthropic 文档要求应用根据 stop_reason 分别处理 end_turn、max_tokens、tool_use 和 refusal 等状态。OpenAI 的 Responses API 也区分 completed、incomplete、refusal、工具调用和错误事件。
所以验收不能停在 response.status_code == 200。

验收一:确认接口、模型和响应身份
第一项先排除“请求确实成功了,但成功在错误的地方”。
常见情况包括:
- Base URL 指向测试环境,业务以为自己在测生产环境;
- 配置的模型别名被更新,返回模型和预期模型不一致;
- fallback 生效后由备用模型完成请求,但应用没有记录;
- SDK 自动拼接
/v1,最终地址多了一层或少了一层; - 多套 Key 混用,费用记在了错误的项目或账号下。
最小验收记录应该包括:
environment: staging
base_url: https://gw.apito.ai/v1
requested_model: <控制台当前可见模型 ID>
returned_model: <响应中的模型标识>
request_id: <响应头或响应体中的请求 ID>
api_key_alias: content-team-staging
test_case_id: basic-text-001
environment: staging
base_url: https://gw.apito.ai/v1
requested_model: <控制台当前可见模型 ID>
returned_model: <响应中的模型标识>
request_id: <响应头或响应体中的请求 ID>
api_key_alias: content-team-staging
test_case_id: basic-text-001
不要把完整 API Key 写进日志。保留用途别名和末四位已经足够定位配置。
通过 apito.ai 接入时,模型 ID 以账号模型页面真实可见内容为准。已经配置过旧域名的程序,可把请求地址中的 claudeapi.com 更新为 apito.ai,再执行本文的整套回归测试。地址换完后能收到 200,只能算迁移测试的第一步。
通过标准
- 请求环境、Base URL 和 Key 用途明确;
- 请求模型与预期一致,发生 fallback 时有记录;
- 每次测试都能取得可供客服或开发定位的 request ID;
- 日志中不保存明文 Key。
验收二:区分答案、拒答和待执行动作
模型的返回内容不一定是普通文本。
应用至少要识别三类结果。
普通答案
模型已经完成本轮生成,可以继续进入格式校验和业务校验。
拒答或安全回退
接口可能返回成功状态,但正文是拒答信息,或者响应里出现 refusal。这种结果不能当成空字符串重试,否则容易制造重复请求和无意义账单。
工具调用
模型没有准备直接回答,而是要求应用执行一个函数,例如查询订单、创建工单或读取数据库。此时“有响应”只代表规划出了下一步,业务动作还没发生。
可以先在适配层统一成自己的状态枚举。
from enum import Enum
class ResultType(str, Enum):
ANSWER = "answer"
REFUSAL = "refusal"
TOOL_CALL = "tool_call"
INCOMPLETE = "incomplete"
ERROR = "error"
from enum import Enum
class ResultType(str, Enum):
ANSWER = "answer"
REFUSAL = "refusal"
TOOL_CALL = "tool_call"
INCOMPLETE = "incomplete"
ERROR = "error"
不要让不同 Provider 的字段名直接散落在业务代码里。适配层负责把 stop_reason、finish_reason、status 和输出项转换成统一状态,业务层只处理上面的五类结果。
通过标准
- 拒答能被单独识别并展示适当提示;
- 工具调用不会被误当成最终文本;
- 空内容会结合状态字段判断,不会直接无限重试;
- 未知状态进入告警或人工检查,而不是默认成功。
验收三:检查回答是否完整结束
一段语法通顺的文字也可能被截断。
最常见的信号是:
- Anthropic 格式出现
stop_reason: max_tokens; - OpenAI 兼容格式出现
finish_reason: length; - Responses API 的状态为
incomplete,并在incomplete_details中说明原因; - 预期 10 项,实际只返回 7 项;
- JSON 最后缺少右括号;
- 长代码停在函数或 Markdown 代码块中间。
只检查字符数量不够。不同任务需要不同的完整性契约。
| 任务 | 建议完整性条件 |
|---|---|
| 分类 | 标签必须来自允许集合,且只有一个结果 |
| 摘要 | 必须包含结论、风险、下一步三个字段 |
| 列表 | 数量满足约定,或明确说明不足原因 |
| JSON | Schema 校验通过,必填字段齐全 |
| 代码 | 语法检查通过,必要测试通过 |
| 长文 | 章节数量、收尾标记或字数范围符合约定 |
一个简单的检查函数可以先挡住明显截断。
def assert_completion(finish_reason: str | None, text: str) -> None:
incomplete_reasons = {"length", "max_tokens", "incomplete"}
if finish_reason in incomplete_reasons:
raise ValueError(f"模型输出未完成: {finish_reason}")
if not text or not text.strip():
raise ValueError("模型返回空内容")
def assert_completion(finish_reason: str | None, text: str) -> None:
incomplete_reasons = {"length", "max_tokens", "incomplete"}
if finish_reason in incomplete_reasons:
raise ValueError(f"模型输出未完成: {finish_reason}")
if not text or not text.strip():
raise ValueError("模型返回空内容")
这只是底线。生产环境还要叠加任务自己的结构检查。
通过标准
- 截断状态不会进入正常业务流程;
- 应用能告诉用户“输出未完成”,而不是展示半段结果;
- 补全策略有次数上限,并保留前一次 request ID;
- 长任务有针对任务结构的完整性检查。
验收四:验证格式,而不是相信提示词
提示词写了“只返回 JSON”,并不能替代 JSON 校验。
模型可能返回:
下面是你需要的 JSON:
```json
{"priority":"high","reason":"客户要求退款"}
```
下面是你需要的 JSON:
```json
{"priority":"high","reason":"客户要求退款"}
```
人看没有障碍,json.loads() 会直接失败。另一些回答可以被解析,却缺少必填字段,或者把数字写成字符串。
建议把校验分成三步。
- 语法校验:是不是合法 JSON;
- Schema 校验:字段、类型、枚举和必填项是否符合约定;
- 业务校验:金额不能为负、订单号必须存在、分类必须来自当前业务词表。
import json
from jsonschema import validate
TICKET_SCHEMA = {
"type": "object",
"required": ["category", "priority", "summary"],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
},
"summary": {"type": "string", "minLength": 1}
},
"additionalProperties": False
}
def parse_ticket(raw_text: str) -> dict:
data = json.loads(raw_text)
validate(instance=data, schema=TICKET_SCHEMA)
return data
import json
from jsonschema import validate
TICKET_SCHEMA = {
"type": "object",
"required": ["category", "priority", "summary"],
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account", "other"]
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
},
"summary": {"type": "string", "minLength": 1}
},
"additionalProperties": False
}
def parse_ticket(raw_text: str) -> dict:
data = json.loads(raw_text)
validate(instance=data, schema=TICKET_SCHEMA)
return data
如果接口支持结构化输出或 JSON Schema,应优先使用对应能力;应用侧校验仍然要保留,因为网络、适配层和后续代码都可能引入异常。
通过标准
- 100 次测试中格式成功率达到项目设定门槛;
- 必填字段、枚举和类型均由程序检查;
- 校验失败不会把原始模型文本直接写入数据库;
- 修复或重试有上限,并记录失败样本。

验收五:工具调用必须走完闭环
Agent 场景里,最危险的误判是把“模型提出了工具调用”当成“工具已经执行成功”。
一次完整工具调用至少有五步。
- 模型返回工具名称和参数;
- 应用验证工具是否在允许列表;
- 应用校验参数并执行工具;
- 工具返回结果,应用把结果交回模型;
- 模型基于工具结果给出最终回答,业务再核对系统状态。
任何一步失败,HTTP 请求仍可能是 200。
例如模型生成了:
{
"name": "create_refund_ticket",
"arguments": {
"order_id": "A1024",
"reason": "duplicate_charge"
}
}
{
"name": "create_refund_ticket",
"arguments": {
"order_id": "A1024",
"reason": "duplicate_charge"
}
}
这段 JSON 只能证明模型希望调用 create_refund_ticket。工单是否创建,要看工具执行结果和工单系统里的真实记录。
工具验收要覆盖下面几组失败样本。
- 不存在的工具名;
- 缺少必填参数;
- 参数类型错误;
- 工具超时;
- 工具返回 403、404、409 或 500;
- 同一个工具调用被重复执行;
- 工具成功但最终回答描述错误;
- 高风险动作没有经过人工确认。
对于写入、退款、发消息、删除文件等动作,应使用幂等键或业务唯一键,避免网络重试造成重复执行。
通过标准
- 工具名称和参数都有白名单与 Schema 校验;
- 工具执行结果和模型最终回答能通过同一 trace 关联;
- 写操作具备幂等保护;
- 高风险动作有人工确认或明确权限边界;
- 工具失败不会被模型包装成“已经完成”。
验收六:流式输出必须收到完整结束信号
流式接口最容易制造一种假象:屏幕上已经出现很多字,看起来任务成功了。
SSE 连接中途断开时,前端可能保留已经收到的片段。如果应用没有等待完成事件,也没有记录结束原因,这段半成品就会被保存或展示。
流式验收至少要验证:
- 是否收到明确的完成事件;
- chunk 顺序是否正确;
- UTF-8 中文是否出现断字或乱码;
- 工具参数分片能否正确拼接;
- 客户端主动取消后,服务端和账单状态如何记录;
- 中途断线时是否把结果标记为 incomplete;
- 最终 usage 是否收到,没收到时怎样记账。
OpenAI 文档特别提醒,流式连接中断后,客户端可能收不到包含最终 token 用量的尾部数据。Anthropic 的流式响应也会在不同事件阶段逐步提供消息、内容块和停止原因。因此,不能把“收到第一个 chunk”当作成功,也不能把“连接关闭”一律当作正常完成。
建议在前端状态里显式区分:
idle -> connecting -> streaming -> completed
|-> incomplete
|-> cancelled
|-> failed
idle -> connecting -> streaming -> completed
|-> incomplete
|-> cancelled
|-> failed
通过标准
- 只有收到完成事件才把结果标为 completed;
- 断线、取消和错误有不同状态;
- 工具参数分片能够重组并通过 JSON 校验;
- 未取得最终 usage 时有明确标记,不伪造 token 数据;
- 用户可以重试,但系统不会把旧半成品和新结果拼在一起。
验收七:日志、用量和错误必须能对上
前六项解决“结果能不能用”,第七项解决“坏了以后能不能查”。
一条有排障价值的调用记录,至少包含:
| 字段 | 用途 |
|---|---|
trace_id |
串起用户请求、模型调用和工具执行 |
request_id |
与 API 服务侧记录对应 |
test_case_id |
知道是哪条验收用例失败 |
requested_model |
记录应用请求的模型 |
served_model/provider |
记录实际服务路径,若接口提供 |
started_at/completed_at |
计算总耗时与超时位置 |
first_token_at |
计算首 Token 延迟 |
finish_reason/status |
区分正常、截断、拒答和工具调用 |
input/output tokens |
核对成本与异常输出 |
retry_count |
识别一次任务背后的重复调用 |
tool_call_count |
识别 Agent 循环与工具成本 |
validation_result |
记录格式和业务校验结果 |
日志不要保存完整 Key、身份证号、手机号、客户原文和内部凭证。需要复现输入时,可以保存脱敏副本、内容哈希或经过权限控制的短期样本。
如果团队通过 apito.ai 统一管理模型调用,可以把控制台的用量和调用记录与自己的 trace_id、业务状态结合起来。平台日志回答“这次模型请求发生了什么”,业务日志回答“这次请求最后有没有创建工单、写入数据或交付给用户”。两边能对上,排障才不会停在猜测。
通过标准
- 任意一条用户反馈都能定位到模型 request ID;
- 请求次数、重试次数和账单用量可以核对;
- 模型调用、工具调用和业务写入共用 trace;
- 日志已脱敏,并有保存周期和访问权限;
- 告警按失败类型分类,不把所有问题都写成“模型异常”。

一张可以直接复制的上线验收表
下面这张表适合放进 PRD、测试用例或上线评审单。

# 大模型 API 接入验收单
项目:
环境:开发 / 测试 / 生产
负责人:
验收日期:
Base URL:
请求模型:
测试用例版本:
## 1. 接口与身份
- [ ] Base URL 和环境正确
- [ ] 请求模型与返回模型已记录
- [ ] Key 使用别名管理,日志无明文 Key
- [ ] 每次请求可以取得 request ID
## 2. 响应语义
- [ ] 普通答案可识别
- [ ] refusal 可识别并给出用户提示
- [ ] tool call 不会被当成最终答案
- [ ] 未知状态进入告警
## 3. 输出完整性
- [ ] length / max_tokens / incomplete 会被拦截
- [ ] 空输出不会进入业务流程
- [ ] 列表、章节或代码有任务级完整性检查
- [ ] 补全与重试次数有上限
## 4. 格式校验
- [ ] JSON 语法校验通过
- [ ] Schema 校验通过
- [ ] 业务字段校验通过
- [ ] 失败样本已归档
## 5. 工具调用
- [ ] 工具名和参数有白名单
- [ ] 写操作有幂等保护
- [ ] 工具结果与最终回答可以关联
- [ ] 高风险动作需要确认
## 6. 流式输出
- [ ] 收到完成事件才标记成功
- [ ] 断线、取消、失败分别处理
- [ ] 中文和工具参数分片可以正确重组
- [ ] 缺失最终 usage 时有明确标记
## 7. 日志与成本
- [ ] trace_id 串起模型、工具和业务动作
- [ ] request ID 可用于排障
- [ ] retry_count 和 tool_call_count 已记录
- [ ] Token 用量与账单可抽样核对
- [ ] 日志脱敏、权限和保存周期已确认
# 大模型 API 接入验收单
项目:
环境:开发 / 测试 / 生产
负责人:
验收日期:
Base URL:
请求模型:
测试用例版本:
## 1. 接口与身份
- [ ] Base URL 和环境正确
- [ ] 请求模型与返回模型已记录
- [ ] Key 使用别名管理,日志无明文 Key
- [ ] 每次请求可以取得 request ID
## 2. 响应语义
- [ ] 普通答案可识别
- [ ] refusal 可识别并给出用户提示
- [ ] tool call 不会被当成最终答案
- [ ] 未知状态进入告警
## 3. 输出完整性
- [ ] length / max_tokens / incomplete 会被拦截
- [ ] 空输出不会进入业务流程
- [ ] 列表、章节或代码有任务级完整性检查
- [ ] 补全与重试次数有上限
## 4. 格式校验
- [ ] JSON 语法校验通过
- [ ] Schema 校验通过
- [ ] 业务字段校验通过
- [ ] 失败样本已归档
## 5. 工具调用
- [ ] 工具名和参数有白名单
- [ ] 写操作有幂等保护
- [ ] 工具结果与最终回答可以关联
- [ ] 高风险动作需要确认
## 6. 流式输出
- [ ] 收到完成事件才标记成功
- [ ] 断线、取消、失败分别处理
- [ ] 中文和工具参数分片可以正确重组
- [ ] 缺失最终 usage 时有明确标记
## 7. 日志与成本
- [ ] trace_id 串起模型、工具和业务动作
- [ ] request ID 可用于排障
- [ ] retry_count 和 tool_call_count 已记录
- [ ] Token 用量与账单可抽样核对
- [ ] 日志脱敏、权限和保存周期已确认
测试时不要只用“你好”
一句“你好”适合验证连接,不适合验收业务。
至少准备下面七类样本,每类保留预期结果。
| 测试组 | 示例 | 主要验证项 |
|---|---|---|
| 最短正常请求 | 把一句话归类 | 基础响应与模型身份 |
| 超长输入 | 接近项目允许的上下文上限 | 截断、超时和成本 |
| 超长输出 | 生成多章节报告 | max_tokens、流式完整性 |
| 格式任务 | 严格 JSON、固定枚举 | Schema 稳定性 |
| 工具成功 | 查询一条测试订单 | 工具闭环 |
| 工具失败 | 查询不存在订单、模拟超时 | 错误传递与重试 |
| 边界请求 | 可能触发拒答的合规测试语句 | refusal 处理 |
每条用例不要只跑一次。温度、路由、负载和模型更新都可能带来波动。核心用例建议重复执行,并记录成功率、P50/P95 延迟和格式通过率。
建议采用分层上线,而不是一次切满流量
验收通过后,也不要立即让全部真实请求进入新链路。
比较稳妥的上线顺序是:
- 影子测试:复制脱敏请求到新链路,不把结果返回用户;
- 内部灰度:只对员工或测试账号开放;
- 小流量灰度:按用户、项目或百分比分流;
- 扩大流量:观察错误率、格式失败率、工具失败率和成本;
- 保留回退:达到预设阈值时切回旧链路。
回退条件也要写成数字。例如格式通过率低于 99%、工具调用成功率低于 98%、P95 延迟超过业务阈值,或单位任务成本超过预算时停止扩量。具体阈值由业务风险决定,不能从别人的文章里照抄。
结语
HTTP 200 是一张网络层回执。
用户要的是完整答案、正确数据和已经执行的业务动作。两者之间隔着状态识别、完整性检查、格式验证、工具执行、流式结束和日志追踪。
团队把这七项写进验收单以后,很多“偶尔失败”“模型又抽风了”“明明调用成功”的模糊问题,会变成一条可以定位、复现和修复的测试记录。
如果你正在迁移模型接口或统一团队的模型入口,可以在 apito.ai 查看账号当前可用模型与接入配置,并用本文清单完成上线前回归。模型名称、参数和能力以控制台实际展示为准。
FAQ
1. HTTP 200 到底代表什么?
它代表这次 HTTP 请求被服务端成功接收并返回响应。它不保证模型答案完整、格式合规、工具已经执行,也不保证业务数据库已经写入成功。
2. API 返回 200,但 content 是空的,应该直接重试吗?
不要立即重试。先检查 stop_reason、finish_reason、status 和输出项。空内容可能对应拒答、工具调用、输出不完整或接口错误。只有明确判断为可重试故障后再重试,并设置次数上限。
3. finish_reason: length 或 stop_reason: max_tokens 算成功吗?
网络请求成功,但输出没有完整生成。应用应将其标为 incomplete,再根据任务决定提高输出上限、压缩输入、继续生成或提示用户重新执行。
4. 提示词要求只返回 JSON,还需要 Schema 校验吗?
需要。提示词是一项生成约束,Schema 是程序验收。模型仍可能添加解释文字、漏字段或返回错误类型;进入数据库或触发工具前必须校验。
5. 模型返回了 tool call,能不能认为任务完成?
不能。tool call 只是模型提出的动作。应用还要校验参数、执行工具、处理工具结果,并核对真实业务状态。涉及写入和付款等动作时还要加入幂等和人工确认。
6. 流式输出已经显示了一大段,为什么还要等待完成事件?
因为连接可能在任何一个 chunk 后中断。没有完成事件和停止原因,应用无法判断收到的是完整答案还是半成品,也可能拿不到最终用量数据。
7. 接入 apito.ai 后,应该先测什么?
先确认 Base URL、Key 和账号可见模型,再依次测试普通回答、截断、JSON、工具调用、流式中断和日志追踪。已经配置过旧域名的程序,把请求地址从 claudeapi.com 更新为 apito.ai 后,也应该重新执行这套回归清单。



