# 修复 API 400 角色不受支持错误，不破坏系统提示词

> 把 API 400 unsupported role 当作角色契约不匹配处理：读取 param，记录最终 JSON，按端点重映射，并用同一路由最小请求验证。

- URL: https://blog.laozhang.ai/zh/posts/api-400-role-system-is-not-supported-on-this-model
- Published: 2026-05-21
- Updated: 2026-05-21
- Author: LaoZhang AI Team (https://blog.laozhang.ai/zh/about)
- Category: API 故障排查
- Tags: API 400, OpenAI API, 系统提示词, Developer Message, Responses API

---
如果接口返回 <code>400</code>，并提示 <code>role 'system' is not supported on this model</code>，先不要换 key、加重试或怀疑余额。这个请求已经到达了某个会校验消息结构的路由；它拒绝的是你最终发出去的 <code>messages[n].role</code>，也就是角色契约不匹配。

第一步是把错误体里的 <code>param</code> 找出来，再看经过 SDK、框架、代理网关和中间件处理后的最终出站 JSON。修复时要保留原来的系统级指令意图，把它放到当前路由真正支持的最高优先级指令面里，而不是简单塞进普通 <code>user</code> 消息。

| 失败线索 | 可能归属 | 第一安全动作 | 验证方式 |
| --- | --- | --- | --- |
| <code>messages[n].role = system</code> | 端点、兼容网关、Azure 部署或适配器契约 | 保留指令文本，映射到该路由支持的指令面 | 同一路由最小请求返回 2xx，或至少不再报角色错误 |
| <code>developer</code> 被拒绝 | API 版本、兼容层或框架路由 | 确认当前 Chat Completions 路由是否支持该角色；否则转 Responses <code>instructions</code> 或路由专用映射 | 相同 payload 形状在选定路由通过 |
| <code>tool</code> 或旧 <code>function</code> 失败 | 工具调用协议不匹配 | 使用该路由要求的 tool message、call id 和结果形状 | 工具结果被接受且不再报角色错误 |
| 源码里没有这个角色 | Agent 框架、提示词包装器、中间件或 SDK 适配器 | 记录最终出站 JSON，关闭或重映射注入消息 | 最小复现和完整流程使用同一组被接受的角色 |

先停在这里。轮换密钥、调高配额、改超时或换供应商，都不会修复一个已经被路由拒绝的角色契约。真正的结束标准，是同一个 provider、base URL、endpoint、API version、model 或 deployment、SDK adapter 和 framework route 上的最小请求通过。

## 先读错误体，再改代码

这类 400 最有价值的不是英文句子，而是结构化错误体。优先保存 <code>status</code>、<code>type</code>、<code>code</code>、<code>message</code> 和 <code>param</code>。如果 <code>param</code> 指向 <code>messages[0].role</code>，就先检查数组里的第一个消息对象；如果它指向更靠后的索引，就说明框架可能在用户消息之后又追加了工具结果或历史消息。

排查时不要只看业务代码里手写的数组。很多请求在发出前会经过 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，被哪一层最终发了出去。

![定位不支持角色错误归属的角色契约决策图](https://blog.laozhang.ai/posts/zh/api-400-role-system-is-not-supported-on-this-model/img/role-contract-map.webp)

## 按端点修，不要按习惯替换角色

最危险的修法是把 <code>system</code> 一律改成 <code>developer</code>，或者把所有指令降级进 <code>user</code>。角色支持不是全局规则，而是端点、模型路由、API 版本、Azure 部署、兼容网关、SDK 和框架共同决定的契约。

截至 2026-05-21，OpenAI 的 Responses 路线提供顶层 <code>instructions</code> 用来承载开发者指令；当前官方 Chat Completions 路由也有自己的 messages 数组和指令型角色。这个事实只能约束官方 OpenAI 路由，不能自动证明 Azure 某个 <code>api-version</code>、第三方 OpenAI-compatible gateway、旧 SDK 适配器或 agent 框架也接受同样角色。

如果你走的是当前官方 Chat Completions，并且该路由接受 <code>developer</code>，修复可以是把系统级指令移动到被支持的指令角色：

    {
      "model": "your-selected-model",
      "messages": [
        { "role": "developer", "content": "Follow the support triage rules." },
        { "role": "user", "content": "Classify this API error." }
      ]
    }

如果你走的是 Responses，就把长期指令放进 <code>instructions</code>，把用户任务放到 <code>input</code>：

    {
      "model": "your-selected-model",
      "instructions": "Follow the support triage rules.",
      "input": "Classify this API error."
    }

如果兼容网关只接受 <code>user</code> 和 <code>assistant</code>，也不要马上把系统提示词当普通用户文本发送。先查这个网关有没有专用 instruction 字段、模型参数、适配器配置或可切换路由。如果没有，就要把它记录成行为变化，因为指令优先级已经变了。

![保留指令优先级的 payload 修复前后对照](https://blog.laozhang.ai/posts/zh/api-400-role-system-is-not-supported-on-this-model/img/payload-repair.webp)

## 每个角色分支的修法不同

<code>system</code> 被拒绝时，目标是保留系统指令的约束力。官方 Chat Completions 可能映射到 <code>developer</code>，Responses 通常映射到 <code>instructions</code>，兼容网关可能要求某个 route-specific 字段。只有在没有更高优先级指令面时，才考虑降级到用户消息，并且要明确它可能改变输出行为。

<code>developer</code> 被拒绝时，常见原因是兼容层仍按旧 schema 执行，或者 Azure/API version、供应商路由还没有支持这个角色。不要因为 direct OpenAI 可用，就推断你的 base URL 也可用；两个 URL 在客户端里只差一行配置，但服务端角色契约可能完全不同。

<code>tool</code> 被拒绝时，不要只看角色名。工具结果通常还需要 tool call id、与模型输出对应的 call 结构，以及当前 API 要求的结果消息形状。旧的 <code>function</code> 消息是另一条分支；把旧 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 进入生产事故。

## 框架可能在你看不到的位置注入失败角色

很多团队排查时会说：我的代码里根本没有 <code>system</code>。这句话可能是真的，但最终请求里仍然有。Agent 框架、聊天 UI 库、RAG 包装器、memory 模块、guardrail 中间件、兼容 SDK 都可能在发送前拼出完整 messages 数组。

| 层级 | 要检查什么 | 典型失败 |
| --- | --- | --- |
| Agent 框架 | 默认系统提示词、memory prompt、工具策略、guardrail message | 用户输入前出现隐藏 <code>system</code> 或 <code>developer</code> |
| SDK 适配器 | OpenAI-compatible 转换、枚举映射、旧工具协议 | 角色名被改写或序列化成服务端不认识的值 |
| 兼容网关 | provider mode、上游路由、model alias | 网关接收请求，上游模型拒绝角色契约 |
| Azure 路由 | deployment、<code>api-version</code>、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，再宣布问题解决。那样只能说明某个新组合可用，不能说明原生产路由的角色契约已经修复。

最小请求通过后，还要用打开角色日志的完整业务流程跑一次。许多框架会在最小请求之外重新注入 <code>system</code>、<code>developer</code>、<code>tool</code> 或旧 <code>function</code> 消息；完整流程复测能抓住这种回归。

## 升级支持时给证据，不给截图

如果同一路由最小请求仍然失败，再升级给供应商、Azure 管理员、网关维护者或框架作者。不要只发一张错误截图。截图通常缺少路由、索引、适配器版本和请求 ID，接收方仍然要猜。

应该提供：

- 脱敏后的请求 JSON，保留 role、message index 和请求形状
- 完整 base URL 与 path，或供应商控制台中的 route 名称
- model、deployment、region、API version
- SDK、框架、适配器和网关版本
- 响应体、状态码、<code>param</code>、<code>code</code>
- request ID、correlation ID 或 gateway trace ID
- 最小同一路由复现，以及你尝试过的一个受控修复

![不支持角色错误的同一路由升级证据清单](https://blog.laozhang.ai/posts/zh/api-400-role-system-is-not-supported-on-this-model/img/escalation-packet.webp)

不要附带 API key、Bearer token、客户原文、私有提示词、文件内容、工具输出或暴露密钥的截图。如果供应商确认当前路由不支持你需要的角色，诚实的选择只有三个：换到有正确指令面的端点，配置适配器保留指令优先级，或者明确记录备用路由会改变行为。

## 现场恢复清单

线上故障时按这个顺序处理：

1. 保存响应体和失败的 <code>param</code>。
2. 在框架与适配器转换之后记录最终出站 JSON。
3. 判断归属：direct OpenAI、Azure、兼容网关、SDK 还是框架。
4. 把指令文本移动到该路由支持的最高优先级指令面。
5. 把 <code>tool</code> 和旧 <code>function</code> 协议与指令角色分开验证。
6. 跑一个最小同一路由 smoke test。
7. 继续打开角色日志，跑完整业务流程。
8. 如果仍失败，再用脱敏证据包升级。

这个顺序能保持信号干净，也能避免最常见的坏修法：为了让 400 消失，把系统指令放进普通用户消息，最后接口不报错了，模型行为却悄悄变松。

## 常见问题

### 这个错误是不是说明 API key 错了？

通常不是。key 错误一般在鉴权阶段失败，服务端还没到校验 <code>messages[n].role</code> 的步骤。这个错误说明请求已经被某个路由解析，并且该路由拒绝了一个消息角色。先查角色契约，不要先轮换 key。

### 能不能把所有 <code>system</code> 都改成 <code>developer</code>？

不能。<code>developer</code> 只适用于支持该角色的 Chat Completions 路由。Responses 有顶层 <code>instructions</code>，Azure 和兼容网关也可能有不同契约。应该按端点映射，并在同一路由验证。

### 把系统提示词放到 <code>user</code> 里安全吗？

只有在没有更好指令面时才考虑，而且必须把它当成行为变化。普通用户消息通常没有系统、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 和框架版本、响应体、<code>param</code>、request ID，以及最小同一路由复现。先移除密钥和用户数据。
