deepseek-v4-pro 识图接口报 400 怎么办?两种 error message 不同但都是 400,排查方向完全不一样

上周三我在给一个电商客户做商品图自动标注的功能,用 deepseek-v4-pro 调多模态接口。纯文本聊天一切正常,Key 没问题,额度也够——但一传图片就 400。折腾了大半天才搞明白:同样是 HTTP 400,视觉模式有两处强制校验,error type 字段都是 invalid_request_error,但 error message 文本不同,修法也完全不一样。一个是 base64 图片缺 data URI 前缀,另一个是 messages 数组里混了纯文本 role。

下面直接给排查路径,5 分钟能定位问题。

flowchart TD
 A[调用 deepseek-v4-pro 识图] --> B{返回 400?}
 B -->|是| C{看 error message}
 C -->|"image_url is not supported\n且模型名已确认正确"| D[检查 base64 前缀]
 C -->|"image_url is not supported\n且模型名有误或不支持视觉"| F[更换为支持视觉的模型]
 C -->|content must be a string| E[检查 messages 结构]
 B -->|否,返回 200| G[正常]
 D --> H[补上 data:image/jpeg;base64, 前缀]
 E --> I[system/assistant role 的 content 改回纯字符串]

为什么会出现这个问题

deepseek-v4-pro 的视觉模式对请求体的校验比纯文本接口严格很多。之前你 content 字段随便传个字符串就行,视觉模式要求 content 必须是数组格式,而且数组里每个元素的 type 和结构都有硬性约束。

关键点:纯文本调用同一个 Key 完全正常,不代表你的请求格式对多模态也合法。这俩是不同的校验逻辑。

报错场景一:base64 图片缺 data URI 前缀

这是我最先踩到的坑。完整报错长这样:

openai.BadRequestError: Error code: 400 - {
 'error': {'message': 'Invalid content type. image_url is not supported for this model.',
 'type': 'invalid_request_error'}
}

注意:这个 message 有一定误导性——如果你模型名写的是 deepseek-v4-pro,问题大概率不是模型不支持视觉,而是 image_url 字段里的 url 值格式不对。我一开始是这么写的:

import base64
with open('product.jpg', 'rb') as f:
    b64 = base64.b64encode(f.read()).decode()

然后直接把 b64 塞进了 url 字段:

{"type": "image_url", "image_url": {"url": b64}}

这就炸了。DeepSeek 的视觉接口要求 base64 图片必须带完整的 data URI 前缀,格式是 data:image/jpeg;base64,{实际base64内容}。你光传一串 base64 字符,服务端不知道这是什么格式,直接拒绝。

正确写法:

url_val = f"data:image/jpeg;base64,{b64}"
{"type": "image_url", "image_url": {"url": url_val}}

注意 MIME type 要跟实际图片格式对应——PNG 就写 image/png,WebP 就写 image/webp。写错了也会 400。单张图片大小和格式限制请以官方文档为准。

报错场景二:messages 里混入纯文本 role 触发格式冲突

这个更隐蔽。修完 base64 前缀之后,还是 400,但 error message 变了:

openai.BadRequestError: Error code: 400 - {
 'error': {'message': 'content must be a string when using this model.',
 'type': 'invalid_request_error'}
}

乍一看很困惑——明明用的是多模态模型,为什么要求 content 是 string?

查了半天发现问题在 messages 数组里。我的 system prompt 也用了数组格式的 content:

messages = [
    {"role": "system", "content": [{"type": "text", "text": "你是图片分析助手"}]},
    {"role": "user", "content": [...]},
]

坑在这里:deepseek-v4-pro 的视觉模式下,systemassistant 角色的 content 必须保持纯字符串,只有 user 角色的 content 才能是数组格式。你一旦给 system role 也传了数组,服务端就会报 content must be a string——它校验的是非 user 角色的 content 类型。请注意这是 DeepSeek 接口的特有约束,与 OpenAI 原生 Vision API 的行为不同,建议以 DeepSeek 官方文档为准。

正确写法:

messages = [
    {"role": "system", "content": "你是图片分析助手"},
    {"role": "user", "content": [
        {"type": "image_url", "image_url": {"url": url_val}},
        {"type": "text", "text": "描述这张商品图"}
    ]},
]

只有 user 的 content 用数组,其他 role 老老实实写字符串。

方案一:逐步排查法(推荐新手)

先跑通纯文本,确认基础链路没问题,再加图片测试。将以下代码合并为一个完整块直接运行:

from openai import OpenAI

client = OpenAI(api_key="YOUR_KEY", base_url="https://api.deepseek.com")

# 第一步:先跑纯文本
resp = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "你好"}]
)
print(resp.choices[0].message.content)

纯文本通了,再加图片。图片先用公网 URL 测(排除 base64 编码问题),URL 通了再换 base64。

注:base_url 此处使用 https://api.deepseek.com,与 DeepSeek 官方文档一致;OpenAI SDK 会自动拼接路径,无需手动添加 /v1

方案二:写个校验函数提前拦截

每次构造 messages 前自动检查格式,本地就能拦住这两类错误,不用等服务端返回 400 再猜:

def validate_messages(messages):
    for msg in messages:
        if msg["role"] != "user" and not isinstance(msg["content"], str):
            raise ValueError(f"{msg['role']} content 必须是字符串")
        if msg["role"] == "user" and isinstance(msg["content"], list):
            for item in msg["content"]:
                if not isinstance(item, dict):
                    raise TypeError(f"content 数组元素必须是 dict,实际是 {type(item)}")
                if "type" not in item:
                    raise ValueError("content 数组元素缺少 'type' 字段")
                if item.get("type") == "image_url":
                    url = item["image_url"]["url"]
                    if not url.startswith(("http://", "https://", "data:")):
                        raise ValueError("image_url 缺少合法前缀(http://、https:// 或 data:)")

client.chat.completions.create 之前调一下 validate_messages(messages) 即可。

方案三:用聚合 API 网关统一管理调用

如果你同时在用多个模型(比如 deepseek-v4-pro 做识图、claude-sonnet-5 做长文本),每家的格式要求都不一样,挺烦人的。可以把调用统一切到聚合网关,像 ofox.io、OpenRouter 这类第三方聚合服务商,网关层通常会在格式不合规时给出更明确的错误提示,方便定位问题。

ofox.io 为例,base_url 替换后模型名填写方式与 OpenRouter(deepseek/deepseek-v4-pro)类似,其他代码结构不变:

client = OpenAI(
    api_key="your-gateway-key",
    base_url="https://openrouter.ai/api/v1"  # 或替换为 ofox.io 对应端点
)
# 模型名示例(OpenRouter 格式):
model = "deepseek-v4-pro"

需要注意:网关能提供更清晰的错误提示,但 messages 结构里 system content 必须是字符串这个约束,还是得你自己在代码层面保证。网关不会帮你改 messages 结构。

两种 400 报错的快速对照表

维度 报错一 报错二
error message Invalid content type. image_url is not supported content must be a string when using this model
error type invalid_request_error invalid_request_error
根本原因 base64 缺 data:image/xxx;base64, 前缀 system/assistant 的 content 用了数组格式
触发角色 user system 或 assistant
修复方法 补全 data URI 前缀 非 user 角色 content 改回纯字符串
排查耗时(实测) 约 40 分钟 约 2 小时(因为 error message 有误导)

常见问题 FAQ

Q: deepseek-v4-pro 纯文本正常但传图就 400,是不是 Key 权限不够?

不是 Key 的问题。同一个 Key 对文本和视觉接口都有效。400 说明请求到了服务端但格式校验没过,重点排查 content 数组结构和图片 URL 格式。

Q: base64 图片的 data URI 前缀写错了会怎样?

比如图片是 PNG 但你写了 data:image/jpeg;base64,...,根据实测 DeepSeek 会尝试解码但可能返回识别结果异常或直接 400。建议用 filetypepython-magic 库先检测实际格式再拼前缀(imghdr 模块自 Python 3.11 起标记为废弃,并于 Python 3.13 正式移除,不建议使用)。

Q: 多轮对话里 assistant 的回复要带图片怎么办?

根据实测,assistant role 的 content 只支持字符串。如果你需要在上下文里引用之前的图片分析结果,把结果文字放 assistant content 就行,不要把图片 URL 塞进去。

Q: 图片超过大小限制怎么处理?

压缩。用 Pillow 降分辨率或调 JPEG quality 到 85 基本都能大幅缩小体积。实测 1024×1024 的 JPEG 一般在 200KB-1MB。具体大小上限以官方文档为准。

Q: 能不能用聚合网关中转来辅助排查格式问题?

聚合网关(如 OpenRouter、ofox.io)能在格式出错时给出更明确的提示,方便排查。两者都兼容 OpenAI SDK 的调用方式,切换时只需替换 base_url 和对应的 api_keymessages 结构校验逻辑仍需在本地代码层面自行保证,网关不会帮你改 messages 结构。

最终方案

现在的做法是:本地加一个 validate_messages 函数做前置校验,线上通过聚合网关调用(方便切模型和看调用日志)。双重保障之后,这类 400 基本没再出现过。

如果你也遇到了类似的 400,先看 error message 里是 image_url is not supported 还是 content must be a string——这俩修法完全不同,别搞混了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐