调用 DeepSeek API 时,400 Bad Request 是最常见的客户端错误之一。它表示服务器收到了请求,但请求体没有通过字段校验。问题可能出在 JSON 格式、字段类型、必填项缺失、枚举值非法,甚至可能是消息文本结构不符合官方接口定义。对开发团队来说,真正的挑战不是“看到 400”,而是快速定位到底是哪个字段、哪一层数据导致请求被拒。

先拆解 400 错误的响应内容

收到 400 后,第一步不是改代码,而是完整记录并解析响应体。大部分 API 客户端在抛出异常时,只把错误消息展示出来,但真正有用的定位信息往往在响应体的结构化字段中。

从工程实践看,一个典型的 400 响应可能包含以下信息:

  • 错误类型(如 type 字段)
  • 错误消息(如 message 字段)
  • 触发错误的参数名(如 param 字段)
  • 请求唯一标识(如 trace_id 等)

其中 param 字段对定位最有价值。如果响应中明确指出了字段名,问题范围会被急剧缩小。例如,如果响应提示 messages 字段有问题,那就要检查消息数组的整体结构,而不是逐个猜测。

需要特别提醒的是:不要把错误消息中的提示当作唯一依据。某些 400 错误消息是通用文案,比如“invalid request”或“bad request”,此时必须依靠日志中保存的请求体原文做进一步核对。

建立拦截器:记录实际发出的请求体

很多 400 错误之所以难排查,是因为代码里的参数对象和实际发送的 JSON 并不一致。序列化过程可能修改字段名、丢失字段、或者嵌套结构被拍平。因此,在客户端层增加一个请求拦截器,记录最终序列化后的请求体,是定位字段校验失败的基础设施。

拦截器需要捕获的信息包括:

  • 完整 URL(包括 query string)
  • 请求头中的 Content-Type
  • 实际发送的 JSON 请求体
  • 响应状态码与响应体

这里有两个工程要点。

1. 日志脱敏

请求体中通常包含 API Key。在调试阶段,可以在本地环境打印完整请求体;但一旦进入共享环境或生产日志,就必须对敏感字段做掩码处理。建议默认只记录除 Authorization 之外的请求内容,或者在打日志前将 key 替换为前缀加星号。

2. 区分“代码对象”和“线上报文”

不要在日志里只打印 Python 字典或 TypeScript 对象,因为序列化器可能对非 ASCII 字符、空值、枚举类型做额外处理。务必打印 json.dumps() 之后或 JSON.stringify() 之后的实际文本。这样才能确保你看到的,就是 DeepSeek 服务器看到的。

对照官方接口定义逐字段检查

DeepSeek API 的接口定义以官方文档为准。当请求体被完整记录下来后,可以按以下顺序逐层检查。

第一层:顶层字段

检查请求体中是否出现了文档未定义的顶层字段。某些 SDK 或框架会自动附加自定义字段,例如客户端标识、追踪信息等。如果服务器对未知字段采取严格模式,这会导致 400。

第二层:messages 数组结构

对话补全请求的核心是 messages 字段。常见错误包括:

  • messages 不是数组,而是被序列化成了对象
  • 数组元素缺少 role 字段
  • role 的取值不是有效的消息角色
  • 消息内容不是合法的文本格式

其中角色取值错误需要特别注意。如果使用 openai 兼容端点,role 通常支持 systemuserassistant。如果把自定义角色名称传进去,服务器无法识别,就会拒绝请求。

第三层:content 字段格式

content 的类型错误是高频问题。在多数兼容接口中,文本消息的 content 直接使用字符串,例如:

{
  "role": "user",
  "content": "你好"
}

如果代码中把 content 设成了对象,或者塞入了某种富文本结构,服务器就可能在字段校验阶段返回 400。这里需要认真阅读所使用的 API 端点文档,确认 content 是纯文本还是支持内容块数组。不同兼容协议对该字段的定义不完全相同。

第四层:可选参数的类型与枚举值

temperaturetop_pmax_tokens 等数字类型参数,如果传入字符串,即使内容看起来像数字,也可能触发类型校验失败。

此外,如果使用 response_format 指定输出格式为 JSON,官方接口可能要求 messages 中必须包含“json”相关提示词,否则请求也会被拒绝。这是接口层面的行为约束,建议在集成时单独验证。

最小化复现:把问题隔离到单个字段

当请求体较大、消息轮次较多时,手动逐字段检查比较低效。推荐做法是构造一个最小请求,通过二分法逐步增加字段,直到 400 复现。

最小请求示例(以 openai 兼容格式为例):

{
  "model": "deepseek-chat",
  "messages": [
    {"role": "user", "content": "hi"}
  ]
}

这个请求可以作为基线。如果在本地环境中基线请求返回 200,说明服务连通性、认证、模型名都正常。接下来可以逐个添加以下维度:

  1. 增加系统提示词
  2. 增加多轮消息
  3. 增加 temperature 等推理参数
  4. 增加 response_format
  5. 增加工具调用相关字段

每次只加一个维度,直到出现 400。此时可以确认是最后增加的那个字段引发问题,再针对该字段做更细粒度的调整。

这种方法比在复杂业务代码中反复试错快得多,尤其适合多轮对话、流式输出、工具调用等组合场景。

不同 400 错误信息的处理侧重

虽然无法断言 DeepSeek API 每一种 400 错误的准确规则,但根据客户端错误的一般特征,可以区分两种排查路径。

错误信息指向具体参数

如果响应中的错误信息明确提到了某个参数,优先检查该参数的数据类型和取值范围。不要先怀疑网络代理或服务端问题。例如,信息中提到 messages 的格式不正确时,就去检查 messages 数组中每一轮的 rolecontent

错误信息是通用提示

如果错误信息比较笼统,则优先怀疑请求结构本身。此时可以抓取 HTTP 请求的原始报文,确认请求是否被代理、网关或 SDK 层改写。某些代理会自动修改 body,或者在没有配置 Content-Type 时发送错误的内容编码。

多轮对话中容易被忽略的历史消息错误

在一次多轮会话中,客户端通常需要把之前的 assistant 响应作为下一轮请求的 messages 内容继续发送。如果上一轮的响应中带有工具调用或其他结构化字段,并且客户端把这些字段原样回传,可能造成 schema 不匹配。

例如,assistant 消息中可能包含工具调用块,而某些回调逻辑没有正确剥离或转换,导致下一轮请求中的 assistant 消息结构不符合校验规则。此时 400 可能只在第三轮、第四轮出现,而不是发生在第一轮。

这种场景下,只打印“当前这一轮的请求体”还不够,应该把完整消息数组都记录下来,并逐轮核对角色、内容、工具调用字段是否与接口要求一致。

建议:把 400 定位沉淀为测试用例

对于长期维护 DeepSeek API 集成的团队,建议把每一次 400 定位过程转化为自动化测试用例,覆盖以下典型场景:

  • 合法的单轮文本请求
  • 多轮消息请求
  • response_format 的请求
  • 非法角色的请求
  • content 类型错误的请求
  • 超长消息或 Token 受限的请求

这一步的价值在于:以后任何 SDK 升级、接口参数调整或公共网关变更,都能通过回归测试提前发现请求体结构变化,而不是等到线上出现 400 再重新排查。

另外需要注意,并非所有 400 都来自字段校验。如果请求体结构完全正常,仍然返回 400,需要检查是否有上下文长度超限、频率控制或其他服务端校验逻辑。错误消息中的提示措辞是区分这些情况的重要依据。不要把所有 400 都默认归因于“参数格式不对”,也不要忽略响应中可能存在的 Token 相关提示。

排查步骤总结

面对 DeepSeek API 400 错误,推荐按以下顺序处理:

  1. 保留完整错误响应,提取错误类型、错误消息和参数提示。
  2. 在客户端增加拦截器,记录实际发送的 JSON 请求体。
  3. 对照官方接口文档,检查顶层字段、messages 结构、content 类型和可选参数格式。
  4. 构造最小请求作为基线,逐项增加参数,二分定位触发 400 的字段。
  5. 特别检查多轮对话回传历史消息时,assistant 消息结构是否被错误保留。
  6. 将 400 定位过程固化为自动化测试,防止后续回归。

这里还要强调一个工程原则:不要用“猜”的方式修改参数。每做一次修改前,先确认当前请求体的真实结构和官方接口要求,再执行最小化实验。对于错误消息中未明确指出的信息,不要自行推断平台内部校验规则。很多时候,问题只出在一个字段的类型上,而完整的请求体日志会让这个问题变得一目了然。