大模型 API 400 错误:请求体校验排查清单
一次 400 并不等于「参数写错了」,它只说明服务端在把请求变成一次推理之前就拒绝了解析或校验。500 代表请求已经进入服务端内部,400 代表问题大概率在你发出去的字节里。这个区分很重要:遇到 400 时去看模型输出、调整提示词、改系统提示,基本是无效动作。
真正难的地方在于信息不足。SDK 帮你把对象序列化成请求体,返回的错误往往只有一句 invalid request 或一个笼统的错误类型;多轮对话的请求体动辄几万字符,靠肉眼对比「能通的那次」和「报错的那次」非常低效。
下面是一份分层排查清单。需要提前说明:本文不给出任何厂商的字段名、role 枚举值、参数区间或错误码对照表,因为本次没有可引用的官方接口资料。下面每一项都是需要你对着自己所用服务的最新接口文档去核对的检查点,而不是可以直接照抄的结论。
判断失败发生在哪一层
把 400 拆成四个可能的位置,能显著缩小范围:
- 客户端构造与序列化层:对象转 JSON 时字段丢失、类型改变、出现非法字面量。
- 传输层:编码、压缩、分块或代理改写,让服务端收到的字节和你不一致。
- 服务端 schema 校验层:字段名、类型、枚举值、必填项不符合接口定义。
- 服务端业务校验层:结构与类型都对,但取值或组合不被接受。
区分方法只有一个:拿到你实际发出的原始字节,以及服务端返回的原始响应体。很多 SDK 在异常里只保留了解析后的错误对象,因此第一件事是把请求日志从「打印对象」改成「打印 wire 层的原始 body 与 header」。没有这一步,后面所有推断都缺少依据。
先构造一个最小可复现请求
排查效率取决于能不能把问题收敛到一个变量。可操作的做法:
- 绕开 SDK,用 curl 或任意 HTTP 客户端直接发送原始 JSON 字符串。SDK 的便利性同时也是信息损失点。
- 起点尽量小,例如只有一条 user 消息、content 是纯字符串、不带任何可选参数。
- 确认最小请求正常后,一次只加一个变量:加一条 system 消息、加一个采样参数、把 content 换成结构化数组,逐步逼近出问题的版本。
- 每次请求都记录三样东西:原始请求体、原始响应体、服务端返回的请求标识(如果有)。
这个过程通常能在两三次迭代内锁定字段。反过来,把线上那条失败请求整段复制出来「改到能通」,会同时改变多个变量,即使成功也不知道是哪一处修好的。
JSON 序列化层最容易出现的失真
很多 400 并不是业务参数写错,而是序列化阶段就已经不是你以为的那个对象:
- 双重序列化:body 传入的是已经 stringify 过的字符串,服务端收到的是一个字符串而不是对象。
- content 类型不稳定:不同代码路径下产出了字符串与数组两种类型。
- 数字变字符串:大整数、表单式解析、环境变量注入都可能把数值参数变成字符串。
- 非法数值:NaN、Infinity、undefined 被序列化成非法字面量或 null。
- 非标准 JSON:尾随逗号、单引号、注释,手写模板拼接字符串时尤其常见。
- 控制字符与换行:用户输入里带未转义的控制字符,拼接时漏掉转义。
- 编码问题:body 不是 UTF-8,或带了 BOM。
- null 与缺失的语义差异:接口定义里「字段可选」和「字段可以是 null」通常是两回事,序列化框架的默认行为未必符合接口预期。
处理方式不是逐个记,而是在客户端加一层请求体校验:发送前用目标接口的 schema 校验一遍,把类型错误拦在本地。
messages 数组的结构性检查点
会话类接口的问题大多集中在这个数组上。需要逐项对照官方文档确认的点包括:
- role 的取值集合,以及代码里是否存在拼写或大小写差异。枚举类错误通常最容易修,也最容易漏。
- content 允许的形态:是只允许字符串,还是允许结构化分片;如果是分片,每个分片的类型字段与内容字段如何对应。
- 消息数量与顺序约束:是否允许空数组、是否允许只有系统消息、相邻同角色消息是否需要合并、系统消息是否必须位于首位。
- 多模态字段:图片等分片是走 URL 还是 base64,两种方式对应的字段名是否不同,字段是否必须成对出现。
- 工具调用相关字段:请求里声明的工具定义与消息里的调用记录必须结构一致,缺一个字段就可能导致整条请求被拒。
这些约束在不同服务商之间差异很大,不能因为你在另一家接口上这样写能通,就假定这里也能通。
参数取值与未知字段
两类容易被忽略的问题。
第一类是取值域。采样参数、最大生成长度这类数值字段都有各自的合法区间,超出范围时服务端可能直接返回参数错误,而不是自动截断。区间以官方文档为准,不要沿用其他接口的经验值。
第二类是未知字段。有的服务端对请求体采取严格模式,出现未定义字段就直接拒绝;有的服务端会忽略。如果你的代码里有一份「通用请求模板」在多个接口之间复用,这类问题会集中爆发。排查时可以把可疑字段全部删掉,再逐个加回。
用回放和字段级 diff 定位
当最小复现做不出来时,用对比法:
- 保存一次成功请求和一次失败请求的原始 body。
- 先做结构比较:字段集合是否一致、同名字段的类型是否一致、数组长度差多少。
- 再做文本比较,把已确认稳定的字段(如固定前缀提示词)从两边剔除,只保留有差异的部分。
- 对差异字段做二分:注释掉一半,看是否恢复。
日志脱敏要在这里一起设计。把用户内容原样写进日志既有合规风险,也没必要——排查 400 只需要结构信息:字段名、类型、长度、摘要哈希、是否为空。把「内容」替换成「长度加哈希」,既能定位结构问题,又不会把业务数据落到日志系统里。
请求回放时要连响应一起存,尤其是服务端返回的请求标识。有了它,无论是自查还是找服务方支持,都能把一次失败对应到一次具体调用,而不是靠时间点猜测。
有些 400 不该往请求体上找
请求体校验是高频原因,但不是唯一原因。以下情况需要先排除,判断依据以官方错误码说明为准:
- 鉴权或权限类错误被中间层统一包装成了 400。这类问题改请求体不会有效果。
- 经过网关、代理或自建转发时,body 被改写、截断或重复编码,服务端收到的和你发出的不同。验证方法是对比客户端抓包与服务端日志中的 body 长度。
- 分块传输或压缩处理不当,导致 JSON 不完整。
- 服务端侧校验规则更新,昨天能通的请求今天被拒。这类问题表现为「没改代码但开始稳定报错」,处理方式是把调用契约版本化,并保留失败请求样本,便于快速区分是自身变更还是外部变更。
把 400 变成可维护的问题
工程上比较有效的做法是把这件事前移,而不是靠线上报错驱动。
在客户端建立一层独立的请求体构造与校验逻辑,业务代码只描述意图(消息列表、模型、参数),由这一层负责序列化、类型规整、未知字段过滤和本地 schema 校验。这样字段级错误在测试环境就能暴露,而不是等到线上流量才出现。
同时给 400 一个明确的重试策略。按一般 HTTP 语义,请求体错误属于客户端问题,重试同样的请求体不会改变结果,因此 400 应当被归类为不可直接重试,进入修复或降级路径,例如把多模态请求降级为纯文本、把超出范围的参数收敛到合法值,并把原始请求体记录下来。具体哪些 400 可以重试,仍要以官方错误码说明为准。
最后一点是预期管理:请求体校验规则由服务方定义和演进,任何一份写死的「正确写法」都会过期。把校验规则做成配置、把失败样本留成可回放的数据,比记住某个字段的正确拼写更有长期价值。