DeepSeek V3 升级后请求 500?两个常见请求格式问题排查指南
最近把项目里的 DeepSeek V3 升级到新版本,结果同一套代码开始出现 500 错误。错误信息只有一句冷冰冰的 Internal server error,没有任何有用的字段提示。排查下来,主要集中在两类请求格式问题:① system message 里混入了 Unicode 控制字符;② tools 参数传了空数组 []。下面展开讲怎么排查和修。
说明:本文基于实际排查经验整理,部分涉及模型内部行为的描述(如版本间容错差异)为推测,DeepSeek 官方未公开相关实现细节,仅供参考。
为什么会出现这个问题
先看收到的报错长什么样:
openai.InternalServerError: Error code: 500 - {'error': {'message': 'Internal server error', 'type': 'server_error', 'param': None, 'code': 'internal_error'}}
注意 param 是 None——它不会告诉你哪个字段有问题。这是最难受的地方。
500 错误可能来自多种原因:请求格式问题、服务端过载,或其他内部异常。由于错误信息统一返回 internal_error,无法从响应体直接区分,只能通过逐步缩小范围来排查。
graph TD
A[你的请求] --> B{服务端处理}
B -->|system msg 含控制字符| C[处理异常 → 500]
B -->|tools 传空数组| D[处理异常 → 500]
B -->|服务端过载| E[500 overloaded]
B -->|请求正常| F[正常响应 200]
C --> G[错误信息: Internal server error]
D --> G
E --> G
三种情况返回的错误信息可能相同,这就是为什么很多人以为是服务端问题一直在重试。
方案一:排查 system message 里的 Unicode 控制字符
这是第一个常见坑。system prompt 如果从 Notion、Word 或网页复制粘贴,肉眼看完全正常,但实际上可能藏有零宽空格(\u200b)、BOM 头(\ufeff)等不可见字符。
验证方法,跑一下这个:
system_msg = your_system_prompt
dirty = [
c for c in system_msg
if ord(c) in range(0x00, 0x09) # 0x00-0x08:不可见控制字符
or ord(c) in range(0x0e, 0x20) # 0x0e-0x1f:不可见控制字符
or ord(c) in (0x200b, 0xfeff, 0x200c, 0x200d) # 零宽字符、BOM
]
print(f"找到 {len(dirty)} 个可疑字符: {[hex(ord(c)) for c in dirty]}")
注意:
\n(0x0a)、\t(0x09)、\r(0x0d)是合法的文本字符,上面的检测逻辑已将其排除,不会误报。
如果找到了可疑字符,用下面的正则清洗(清洗范围与检测范围保持一致,同样保留 \n、\t、\r):
import re
clean_msg = re.sub(r'[\x00-\x08\x0e-\x1f\ufeff\u200b-\u200d]', '', system_msg)
清掉这些字符后,部分原本 500 的请求会恢复正常。
方案二:检查 tools 参数是否传了空数组
第二个常见问题。项目里如果有通用的请求封装函数,大概长这样:
payload = {
"model": "deepseek-chat",
"messages": messages,
"tools": get_available_tools() # 某些场景返回 []
}
当 get_available_tools() 返回空列表 [] 时,部分模型或版本可能对此报错(具体行为取决于服务端实现,官方未公开相关 schema 约束细节)。更稳妥的做法是只在有实际工具时才传该字段:
tools = get_available_tools()
payload = {"model": "deepseek-chat", "messages": messages}
if tools: # 只有非空才传
payload["tools"] = tools
这也是 OpenAI 兼容接口的通行做法:可选字段有值才传,避免传空值引发不必要的问题。
方案三:在业务层统一做请求预处理
如果项目同时调多个模型,在业务代码里逐个适配比较繁琐。可以封装一个请求预处理函数,在发送前统一做两件事:strip 控制字符 + 删除空值可选字段。
import re
def sanitize_payload(payload: dict) -> dict:
"""发送前清洗请求体:移除控制字符、删除空值可选字段"""
# 清洗所有 message content 中的控制字符
for msg in payload.get("messages", []):
if isinstance(msg.get("content"), str):
msg["content"] = re.sub(
r'[\x00-\x08\x0e-\x1f\ufeff\u200b-\u200d]',
'',
msg["content"]
)
# 删除空值可选字段
for key in ("tools", "tool_choice", "stop"):
if key in payload and not payload[key]:
del payload[key]
return payload
关于聚合 API 网关:OpenRouter 等聚合网关确实可以作为过渡方案,但在选择第三方服务时,建议自行验证其 API 端点可用性、服务条款及数据处理方式,再决定是否接入。依赖网关兜底不是长久之计,业务代码里做好预处理更可靠。
完整排查流程
遇到 500 时,按这个顺序来:
第一步:用最简请求验证服务是否可用
# 用最简单的请求测试,排除服务端过载
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Hi"}]
)
如果这个请求也 500,大概率是服务端问题,可以稍后重试。如果这个正常、你的复杂请求才 500,说明是请求格式的问题,继续往下排查。
注意:此步骤只能区分"服务端是否可用",无法区分 model ID 是否正确等其他问题,排查时需结合错误信息综合判断。
第二步:如果简单请求正常,检查 system message
把 system prompt 打印成 repr() 看有没有不可见字符,或用方案一的检测代码扫一遍。
第三步:检查请求体里有没有传空数组或空值
重点看 tools、tool_choice、stop 这几个可选字段,传了空值就删掉。
第四步:加指数退避重试兜底
import time
import openai
for i in range(3):
try:
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Hello"}]
)
break
except openai.InternalServerError:
if i < 2:
time.sleep(2 ** i)
常见问题 FAQ
Q: DeepSeek 的 model 参数填什么?
调用 DeepSeek 官方 API 时,model ID 填 deepseek-chat(对应 DeepSeek-V3 系列)。具体路由到哪个版本取决于后端,建议以 DeepSeek 官方文档 为准。
Q: 怎么区分是服务端过载还是请求格式有问题?
用 {"role": "user", "content": "Hello"} 单条消息做最简测试。如果简单请求正常、复杂请求才 500,基本可以判断是请求格式问题。服务端过载时,响应体中有时会包含 overloaded 相关提示,但不同情况下返回内容可能有差异,不能完全依赖此判断。
Q: max_tokens 设多少合适?
根据 DeepSeek 官方文档,deepseek-chat 最大输出为 8192 tokens。超出限制通常会返回 400 错误(参数校验失败),而非 500。如果遇到 500,一般不是 max_tokens 的问题,但保守起见可以设在 4096 以内。
Q: 我用 Cursor / Cline 调 DeepSeek 也报 500 怎么办?
这些工具底层也是拼 HTTP 请求,检查它们的 system prompt 模板有没有控制字符。如果自定义了 Rules 文件,从其他地方粘贴过来的内容可能带不可见字符,用方案一的检测代码扫一遍。
Q: 重试几次合适?间隔多少?
指数退避是通行做法:建议最多 3 次,间隔 1s → 2s → 4s。如果 3 次都 500,大概率不是过载问题,继续排查请求格式。具体重试策略可参考 DeepSeek 官方文档的建议。
小结
这类问题最难受的不是修起来难,而是定位花时间——错误信息什么都不说,只能一个字段一个字段排除。在业务代码里加一个统一的请求预处理步骤(strip 控制字符 + 删除空值可选字段),可以避免大部分此类问题。希望 DeepSeek 后续能把 validation error 的具体原因写进 response body 里,别再统一返回一个 internal_error 了。
更多推荐


所有评论(0)