跳转到主内容
本站为独立第三方技术服务商,Claude™ 与 Anthropic® 为 Anthropic, PBC 的商标,本站与 Anthropic 无任何关联、授权或合作关系。

API 返回 200,为什么业务还是失败?7 项大模型接入验收标准

大模型 API 返回 HTTP 200 只代表请求被成功处理,不代表回答完整、JSON 可解析、工具执行成功或业务结果可用。本文提供 7 项可复制的大模型 API 接入验收标准、Python 检查代码和上线清单。

开发指南大模型 APIAPI 接入验收Claude APIOpenAI 兼容接口Tool Use预计阅读15分钟
2026.08.13 发表
llm-api-200-business-failure-7-acceptance-checks-2026

接口调通的那一刻通常很让人放松。

控制台打印出 200 OK,响应体里也有一段文字。开发环境跑了三次都没报错,于是任务被标记为“接入完成”。

真正接进业务以后,问题才陆续出现。

  • 客服机器人返回了半句话;
  • 文档摘要看起来完整,最后两页却没有处理;
  • 模型说自己已经创建工单,工单系统里什么也没有;
  • 后端要求 JSON,偶尔收到一段带代码围栏的 Markdown;
  • 流式输出显示了大半段,连接中断后前端仍然当成成功;
  • 请求发生了三次自动重试,用户只看到一个结果,账单却记录了三次调用。

这些情况都可能伴随 HTTP 200。

HTTP 200 能证明服务器接收并处理了这次 HTTP 请求。它无法替你证明回答符合业务要求,更无法证明工具执行、数据写入和后续流程已经完成。

一套可上线的验收标准,至少要回答七个问题。

  1. 请求是否到达了正确的接口和模型?
  2. 模型返回的是可用答案、拒答,还是一个待执行动作?
  3. 输出有没有被截断或提前结束?
  4. 返回格式能不能被程序稳定解析?
  5. 工具调用是否形成完整闭环?
  6. 流式传输是否完整结束?
  7. 出问题后能不能通过日志找到这一次请求?

本文把这七项拆成可以执行的测试。你可以拿它验收 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_turnmax_tokenstool_userefusal 等状态。OpenAI 的 Responses API 也区分 completedincompleterefusal、工具调用和错误事件。

所以验收不能停在 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_reasonfinish_reasonstatus 和输出项转换成统一状态,业务层只处理上面的五类结果。

通过标准

  • 拒答能被单独识别并展示适当提示;
  • 工具调用不会被误当成最终文本;
  • 空内容会结合状态字段判断,不会直接无限重试;
  • 未知状态进入告警或人工检查,而不是默认成功。

验收三:检查回答是否完整结束

一段语法通顺的文字也可能被截断。

最常见的信号是:

  • 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() 会直接失败。另一些回答可以被解析,却缺少必填字段,或者把数字写成字符串。

建议把校验分成三步。

  1. 语法校验:是不是合法 JSON;
  2. Schema 校验:字段、类型、枚举和必填项是否符合约定;
  3. 业务校验:金额不能为负、订单号必须存在、分类必须来自当前业务词表。
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 场景里,最危险的误判是把“模型提出了工具调用”当成“工具已经执行成功”。

一次完整工具调用至少有五步。

  1. 模型返回工具名称和参数;
  2. 应用验证工具是否在允许列表;
  3. 应用校验参数并执行工具;
  4. 工具返回结果,应用把结果交回模型;
  5. 模型基于工具结果给出最终回答,业务再核对系统状态。

任何一步失败,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 延迟和格式通过率。

建议采用分层上线,而不是一次切满流量

验收通过后,也不要立即让全部真实请求进入新链路。

比较稳妥的上线顺序是:

  1. 影子测试:复制脱敏请求到新链路,不把结果返回用户;
  2. 内部灰度:只对员工或测试账号开放;
  3. 小流量灰度:按用户、项目或百分比分流;
  4. 扩大流量:观察错误率、格式失败率、工具失败率和成本;
  5. 保留回退:达到预设阈值时切回旧链路。

回退条件也要写成数字。例如格式通过率低于 99%、工具调用成功率低于 98%、P95 延迟超过业务阈值,或单位任务成本超过预算时停止扩量。具体阈值由业务风险决定,不能从别人的文章里照抄。

结语

HTTP 200 是一张网络层回执。

用户要的是完整答案、正确数据和已经执行的业务动作。两者之间隔着状态识别、完整性检查、格式验证、工具执行、流式结束和日志追踪。

团队把这七项写进验收单以后,很多“偶尔失败”“模型又抽风了”“明明调用成功”的模糊问题,会变成一条可以定位、复现和修复的测试记录。

如果你正在迁移模型接口或统一团队的模型入口,可以在 apito.ai 查看账号当前可用模型与接入配置,并用本文清单完成上线前回归。模型名称、参数和能力以控制台实际展示为准。

FAQ

1. HTTP 200 到底代表什么?

它代表这次 HTTP 请求被服务端成功接收并返回响应。它不保证模型答案完整、格式合规、工具已经执行,也不保证业务数据库已经写入成功。

2. API 返回 200,但 content 是空的,应该直接重试吗?

不要立即重试。先检查 stop_reasonfinish_reasonstatus 和输出项。空内容可能对应拒答、工具调用、输出不完整或接口错误。只有明确判断为可重试故障后再重试,并设置次数上限。

3. finish_reason: lengthstop_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 后,也应该重新执行这套回归清单。

参考资料

相关文章