如果接口返回 400,并提示 role 'system' is not supported on this model,先不要换 key、加重试或怀疑余额。这个请求已经到达了某个会校验消息结构的路由;它拒绝的是你最终发出去的 messages[n].role,也就是角色契约不匹配。
第一步是把错误体里的 param 找出来,再看经过 SDK、框架、代理网关和中间件处理后的最终出站 JSON。修复时要保留原来的系统级指令意图,把它放到当前路由真正支持的最高优先级指令面里,而不是简单塞进普通 user 消息。
| 失败线索 | 可能归属 | 第一安全动作 | 验证方式 |
|---|---|---|---|
messages[n].role = system | 端点、兼容网关、Azure 部署或适配器契约 | 保留指令文本,映射到该路由支持的指令面 | 同一路由最小请求返回 2xx,或至少不再报角色错误 |
developer 被拒绝 | API 版本、兼容层或框架路由 | 确认当前 Chat Completions 路由是否支持该角色;否则转 Responses instructions 或路由专用映射 | 相同 payload 形状在选定路由通过 |
tool 或旧 function 失败 | 工具调用协议不匹配 | 使用该路由要求的 tool message、call id 和结果形状 | 工具结果被接受且不再报角色错误 |
| 源码里没有这个角色 | Agent 框架、提示词包装器、中间件或 SDK 适配器 | 记录最终出站 JSON,关闭或重映射注入消息 | 最小复现和完整流程使用同一组被接受的角色 |
先停在这里。轮换密钥、调高配额、改超时或换供应商,都不会修复一个已经被路由拒绝的角色契约。真正的结束标准,是同一个 provider、base URL、endpoint、API version、model 或 deployment、SDK adapter 和 framework route 上的最小请求通过。
先读错误体,再改代码
这类 400 最有价值的不是英文句子,而是结构化错误体。优先保存 status、type、code、message 和 param。如果 param 指向 messages[0].role,就先检查数组里的第一个消息对象;如果它指向更靠后的索引,就说明框架可能在用户消息之后又追加了工具结果或历史消息。
排查时不要只看业务代码里手写的数组。很多请求在发出前会经过 memory prompt、system prompt wrapper、工具适配器、重试中间件、兼容网关转换,最后到 HTTP 边界时已经不是你在源码里看到的对象。应该在最靠近 HTTP 请求的位置记录一个脱敏后的最终 payload。
{
"endpoint": "POST /v1/chat/completions",
"model": "example-model",
"messages": [
{ "role": "system", "content": "[redacted instruction]" },
{ "role": "user", "content": "[redacted user task]" }
]
}
日志只需要证明结构,不需要暴露内容。不要记录 API key、Bearer token、cookie、客户文本、私有系统提示词、文件正文或工具输出。你要证明的是:哪个 endpoint、哪个 model 或 deployment、哪个 message index、哪个 role,被哪一层最终发了出去。

按端点修,不要按习惯替换角色
最危险的修法是把 system 一律改成 developer,或者把所有指令降级进 user。角色支持不是全局规则,而是端点、模型路由、API 版本、Azure 部署、兼容网关、SDK 和框架共同决定的契约。
截至 2026-05-21,OpenAI 的 Responses 路线提供顶层 instructions 用来承载开发者指令;当前官方 Chat Completions 路由也有自己的 messages 数组和指令型角色。这个事实只能约束官方 OpenAI 路由,不能自动证明 Azure 某个 api-version、第三方 OpenAI-compatible gateway、旧 SDK 适配器或 agent 框架也接受同样角色。
如果你走的是当前官方 Chat Completions,并且该路由接受 developer,修复可以是把系统级指令移动到被支持的指令角色:
{
"model": "your-selected-model",
"messages": [
{ "role": "developer", "content": "Follow the support triage rules." },
{ "role": "user", "content": "Classify this API error." }
]
}
如果你走的是 Responses,就把长期指令放进 instructions,把用户任务放到 input:
{
"model": "your-selected-model",
"instructions": "Follow the support triage rules.",
"input": "Classify this API error."
}
如果兼容网关只接受 user 和 assistant,也不要马上把系统提示词当普通用户文本发送。先查这个网关有没有专用 instruction 字段、模型参数、适配器配置或可切换路由。如果没有,就要把它记录成行为变化,因为指令优先级已经变了。

每个角色分支的修法不同
system 被拒绝时,目标是保留系统指令的约束力。官方 Chat Completions 可能映射到 developer,Responses 通常映射到 instructions,兼容网关可能要求某个 route-specific 字段。只有在没有更高优先级指令面时,才考虑降级到用户消息,并且要明确它可能改变输出行为。
developer 被拒绝时,常见原因是兼容层仍按旧 schema 执行,或者 Azure/API version、供应商路由还没有支持这个角色。不要因为 direct OpenAI 可用,就推断你的 base URL 也可用;两个 URL 在客户端里只差一行配置,但服务端角色契约可能完全不同。
tool 被拒绝时,不要只看角色名。工具结果通常还需要 tool call id、与模型输出对应的 call 结构,以及当前 API 要求的结果消息形状。旧的 function 消息是另一条分支;把旧 function role 和新 tool-call response 混在一起,很容易变成角色错误。
角色为空、大小写错误、枚举序列化错误或字段丢失时,问题在客户端构造层。可以在发送前加本地校验:
const allowedRoles = new Set(["developer", "user", "assistant", "tool"]);
for (const [index, message] of messages.entries()) {
if (!allowedRoles.has(message.role)) {
throw new Error("Unsupported outbound role at messages[" + index + "].role");
}
}
本地校验不能替代官方文档和供应商合同,但它能阻止 malformed request 进入生产事故。
框架可能在你看不到的位置注入失败角色
很多团队排查时会说:我的代码里根本没有 system。这句话可能是真的,但最终请求里仍然有。Agent 框架、聊天 UI 库、RAG 包装器、memory 模块、guardrail 中间件、兼容 SDK 都可能在发送前拼出完整 messages 数组。
| 层级 | 要检查什么 | 典型失败 |
|---|---|---|
| Agent 框架 | 默认系统提示词、memory prompt、工具策略、guardrail message | 用户输入前出现隐藏 system 或 developer |
| SDK 适配器 | OpenAI-compatible 转换、枚举映射、旧工具协议 | 角色名被改写或序列化成服务端不认识的值 |
| 兼容网关 | provider mode、上游路由、model alias | 网关接收请求,上游模型拒绝角色契约 |
| Azure 路由 | deployment、api-version、region、model family | 同一 payload 在一个部署可用,在另一个部署失败 |
| 中间件 | 重试器、请求装饰器、日志包装器 | 每次调用都追加一条指令消息 |
因此,排查边界要放在最终 HTTP 请求附近。如果你只能看到框架前的对象,就还没有看到真正被服务器校验的 payload。把最终出站 JSON 脱敏记录下来,才能知道该修业务代码、框架配置、SDK 适配器,还是供应商路由。
用同一路由最小请求验证
另一个端点跑通,不代表当前端点已经修好。同一路由的意思是 provider、base URL、endpoint、API version、model 或 deployment alias、SDK adapter、framework settings 和 tool protocol 都不变,只改变导致角色错误的那一个变量。
可以用最小请求验证修复后的角色面:
curl "$BASE_URL/v1/chat/completions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "your-selected-model",
"messages": [
{"role": "developer", "content": "Answer in one sentence."},
{"role": "user", "content": "Say ready."}
]
}'
一次只改一个变量。不要同时换 Azure 到 direct OpenAI、换 API version、换 model alias、移除框架、简化 prompt,再宣布问题解决。那样只能说明某个新组合可用,不能说明原生产路由的角色契约已经修复。
最小请求通过后,还要用打开角色日志的完整业务流程跑一次。许多框架会在最小请求之外重新注入 system、developer、tool 或旧 function 消息;完整流程复测能抓住这种回归。
升级支持时给证据,不给截图
如果同一路由最小请求仍然失败,再升级给供应商、Azure 管理员、网关维护者或框架作者。不要只发一张错误截图。截图通常缺少路由、索引、适配器版本和请求 ID,接收方仍然要猜。
应该提供:
- 脱敏后的请求 JSON,保留 role、message index 和请求形状
- 完整 base URL 与 path,或供应商控制台中的 route 名称
- model、deployment、region、API version
- SDK、框架、适配器和网关版本
- 响应体、状态码、
param、code - request ID、correlation ID 或 gateway trace ID
- 最小同一路由复现,以及你尝试过的一个受控修复

不要附带 API key、Bearer token、客户原文、私有提示词、文件内容、工具输出或暴露密钥的截图。如果供应商确认当前路由不支持你需要的角色,诚实的选择只有三个:换到有正确指令面的端点,配置适配器保留指令优先级,或者明确记录备用路由会改变行为。
现场恢复清单
线上故障时按这个顺序处理:
- 保存响应体和失败的
param。 - 在框架与适配器转换之后记录最终出站 JSON。
- 判断归属:direct OpenAI、Azure、兼容网关、SDK 还是框架。
- 把指令文本移动到该路由支持的最高优先级指令面。
- 把
tool和旧function协议与指令角色分开验证。 - 跑一个最小同一路由 smoke test。
- 继续打开角色日志,跑完整业务流程。
- 如果仍失败,再用脱敏证据包升级。
这个顺序能保持信号干净,也能避免最常见的坏修法:为了让 400 消失,把系统指令放进普通用户消息,最后接口不报错了,模型行为却悄悄变松。
常见问题
这个错误是不是说明 API key 错了?
通常不是。key 错误一般在鉴权阶段失败,服务端还没到校验 messages[n].role 的步骤。这个错误说明请求已经被某个路由解析,并且该路由拒绝了一个消息角色。先查角色契约,不要先轮换 key。
能不能把所有 system 都改成 developer?
不能。developer 只适用于支持该角色的 Chat Completions 路由。Responses 有顶层 instructions,Azure 和兼容网关也可能有不同契约。应该按端点映射,并在同一路由验证。
把系统提示词放到 user 里安全吗?
只有在没有更好指令面时才考虑,而且必须把它当成行为变化。普通用户消息通常没有系统、developer 或顶层 instructions 的优先级。它可能让 400 消失,但也可能让约束被用户输入覆盖。
为什么框架升级后才出现?
框架可能改了默认 role mapping,新增隐藏 instruction message,或者把旧 function 协议换成新 tool 协议。排查时看最终 HTTP payload,不要只看应用代码里传给框架的消息数组。
direct OpenAI 可用,但兼容供应商失败怎么办?
那就是供应商路由的契约差异。保存同一 payload、endpoint、model alias 和 provider trace,询问该路由支持哪些角色和 tool message 形状。官方 OpenAI 可用不能证明兼容网关也支持同样角色。
支持工单里应该放什么?
放脱敏请求 JSON、精确端点、model 或 deployment、API version、SDK 和框架版本、响应体、param、request ID,以及最小同一路由复现。先移除密钥和用户数据。
