上周三帮朋友排一个诡异的 bug------他用 gpt-5.3-codex 做代码生成,请求体跟调 gpt-5.4 时一模一样,但 gpt-5.3-codex 死活返回 422,gpt-5.4 却完全正常。结论先放这儿:gpt-5.3-codex 端点对 messages 数组里的 role 顺序做了一个更严格的校验------不允许连续两条相同 role 的消息出现,连续两条 user 消息或连续两条 assistant 消息都会触发 422 Unprocessable Entity 拒绝。gpt-5.4 和更早的 Chat Completions 模型没有这个限制,所以同样的 payload 在别的模型上跑得好好的,换到 gpt-5.3-codex 就炸。修复方法很简单:在连续的 user 消息之间插一条空的 assistant 消息,或者把多条 user 内容合并成一条。下面把完整排查过程和修复代码都贴出来。
为什么会出现这个问题
gpt-5.3-codex 是 OpenAI Codex 系列里加了更严格输入校验的版本。推测是为了让模型在多轮代码对话里获得更稳定的上下文------强制 user/assistant 交替排列,避免模型混淆"哪段是用户指令、哪段是已有代码"。
但官方文档里这个变更藏得很深,没有展开说具体校验了什么。反复对比请求体之后才定位到。
实际触发的报错长这样:
HTTP 422 Unprocessable Entity
{"error":{"message":"messages: roles must alternate between 'user' and 'assistant' (consecutive 'user' messages at index 2 and 3)","type":"invalid_request_error","param":"messages"}}
关键信息在 consecutive 'user' messages at index 2 and 3。一开始还以为是 JSON 格式问题,反复检查了半天花括号,其实根本不是。
方案一:合并连续的 user 消息
最直接的办法。把相邻的 user 消息内容拼到一条里。
修复前(会报 422):
python
messages = [
{"role": "system", "content": "You are a code assistant."},
{"role": "user", "content": "帮我写一个排序函数"},
{"role": "user", "content": "用 Python,要快排"},
]
修复后:
python
messages = [
{"role": "system", "content": "You are a code assistant."},
{"role": "user", "content": "帮我写一个排序函数\n用 Python,要快排"},
]
就这么简单。把两条 user 消息用换行符拼起来。适合你能控制 messages 构建逻辑的场景。
方案二:在连续相同 role 消息之间插入占位消息
有时候 messages 是从对话历史里动态拼的,不方便改上游逻辑。那就写个中间件,在发请求前自动插一条空的占位消息。
gpt-5.3-codex 对连续 user 消息和连续 assistant 消息都会报错,所以下面的函数两种情况都处理了:
python
def fix_message_order(messages):
fixed = [messages[0]]
for msg in messages[1:]:
last_role = fixed[-1]["role"]
cur_role = msg["role"]
if cur_role == last_role == "user":
fixed.append({"role": "assistant", "content": ""})
elif cur_role == last_role == "assistant":
fixed.append({"role": "user", "content": ""})
fixed.append(msg)
return fixed
调用时套一层就行:
python
response = client.chat.completions.create(
model="gpt-5.3-codex",
messages=fix_message_order(raw_messages),
)
空的占位消息只是满足校验规则,不会给模型引入实质性的上下文干扰。
方案三:用 API 聚合网关,让网关层帮你处理
方案二已经够用了,但如果你同时在调多个模型(比如 gpt-5.3-codex 做代码生成、gpt-5.4 做 review、claude-opus-5.5 做文档),每个模型的校验规则不一样,自己维护适配逻辑挺烦人的。
把请求统一走 API 聚合网关------像 OpenRouter 这类平台,网关层可能会根据目标模型做 messages 格式适配。改个 base_url 就行(具体域名和路径请以对应平台官方文档为准):
python
from openai import OpenAI
client = OpenAI(
api_key="your-key",
base_url="https://api.ofox.io/v1" # 请自行查阅平台文档确认当前有效地址
)
然后正常调 gpt-5.3-codex,网关是否会在转发前自动处理连续 user 消息,需查阅对应平台的官方文档确认。省得每个调用点都套 fix_message_order。各平台的定价和手续费结构请以其官方定价页为准,具体选哪个看你自己的需求。
不过要说清楚:这个方案的前提是你信任网关层的稳定性,边界 case 是否全部覆盖需要自行验证。
怎么确认你的报错就是这个原因
不是所有 422 都是 role 顺序问题。快速判断方法:
看报错 JSON 里的 message 字段。如果包含 roles must alternate 或 consecutive 字样,那就是这个坑。如果是 invalid_type 或者 missing_required_field,那是别的问题。
另外一个容易混淆的报错是 404:
openai.NotFoundError: Error code: 404 - {'error': {'message': 'The model `gpt-5.3` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': 'model', 'code': 'model_not_found'}}
注意 model 名。gpt-5.3 和 gpt-5.3-codex 是两个不同的东西------前者不存在,后者才是 Codex 代码生成端点。写错模型名拿到 404 和 role 顺序拿到 422 完全是两回事。
为什么 gpt-5.4 同样的请求不报错
这是让人最困惑的地方。gpt-5.4 走的是标准 Chat Completions 端点,对 messages role 顺序没有强制校验------连续多条 user 消息它照样处理,只是可能影响输出质量。
gpt-5.3-codex 是 Codex 专用端点,校验逻辑更严格。这是有意为之的设计差异,但官方文档确实没把这个差异写清楚,翻了好几遍 API reference 才在一个不起眼的 note 里看到。
| 特性 | gpt-5.3-codex | gpt-5.4 |
|---|---|---|
| 端点类型 | Codex 专用 | Chat Completions |
| 连续相同 role | ❌ 报 422 | ✅ 允许 |
| 空 assistant 消息 | ✅ 接受 | ✅ 接受 |
| system 消息位置 | 约定在第一条,不在首位行为未定义 | 约定在第一条,不在首位可能影响行为 |
常见问题 FAQ
Q: gpt-5.3-codex 只校验连续 user 消息,连续 assistant 消息会报错吗?
会。报错信息同样包含 roles must alternate,只是 index 指向的位置不同。规则是 user 和 assistant 必须严格交替,system 消息只能出现在最开头。方案二的 fix_message_order 函数已同时覆盖连续 user 和连续 assistant 两种情形。
Q: 我用 gpt-5.2-codex 也遇到了类似的 422,是同一个问题吗?
可能是。gpt-5.2-codex 以及 gpt-5.1-codex-max、gpt-5.1-codex-mini 这几个 Codex 系列端点都有类似的 role 顺序校验,只是 gpt-5.3-codex 的报错信息更明确,会告诉你具体是哪两个 index 冲突。建议用方案二的函数统一处理,并在实际请求中确认报错信息是否一致。
Q: 插入空 assistant 消息会不会影响代码生成质量?
在 Python/TypeScript 代码生成场景下未观察到明显差异,空字符串的 assistant 消息基本被模型忽略。但这只是特定测试场景下的结论,不同任务类型建议自行验证。
Q: 用 Cline 或 Claude Code 调 gpt-5.3-codex 也会遇到这个问题吗?
取决于这些工具怎么构建 messages 数组。如果工具内部会往 messages 里连续塞多条 user 消息(比如把文件内容和用户指令拆成两条 user),那一样会触发 422。建议在工具的配置里检查一下请求日志。
Q: 怎么快速列出我的账号能调用哪些模型?
用 client.models.list() 拉一下就行:
python
for model in client.models.list():
if "codex" in model.id:
print(model.id)
最终方案
最后的做法是:在项目里加了那个 fix_message_order 中间件函数,十几行代码,所有调 Codex 端点的地方统一走这个。网关方案也留着作为备用方案------主要是团队里其他人不一定记得每次都套这个函数,网关层兜底比较省心。
这个坑不难修,难的是定位。希望这篇能帮你省掉那几个小时的排查时间。
完整可运行示例
下面把 fix_message_order 函数、调用 gpt-5.3-codex 的完整流程整合成一个可直接运行的 Python 脚本。脚本里包含详细的注释,说明如何运行和验证结果。
python
# -*- coding: utf-8 -*-
"""
gpt-5.3-codex 连续相同 role 消息修复示例
=========================================
运行前准备:
1. 安装依赖:pip install openai
2. 设置环境变量 OPENAI_API_KEY(或把下方 api_key 换成你的 key)
3. 确认你的账号有 gpt-5.3-codex 的访问权限
运行方式:
python fix_codex_messages.py
验证方式:
脚本会先构造一组包含连续 user 消息的 messages,
修复后打印修复前后的消息结构,并调用 gpt-5.3-codex 返回结果。
如果一切正常,你会看到 200 响应和模型生成的代码。
"""
import os
from openai import OpenAI
def fix_message_order(messages):
"""在连续相同 role 的消息之间插入空占位消息,满足 gpt-5.3-codex 的交替校验。"""
if not messages:
return messages
fixed = [messages[0]]
for msg in messages[1:]:
last_role = fixed[-1]["role"]
cur_role = msg["role"]
if cur_role == last_role == "user":
fixed.append({"role": "assistant", "content": ""})
elif cur_role == last_role == "assistant":
fixed.append({"role": "user", "content": ""})
fixed.append(msg)
return fixed
def main():
# 初始化客户端(也可以改用 base_url 指向聚合网关)
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "your-key"))
# 构造会触发 422 的原始消息:连续两条 user
raw_messages = [
{"role": "system", "content": "You are a code assistant."},
{"role": "user", "content": "帮我写一个排序函数"},
{"role": "user", "content": "用 Python,要快排"},
]
print("修复前的 messages:")
for m in raw_messages:
print(" ", m)
fixed_messages = fix_message_order(raw_messages)
print("\n修复后的 messages:")
for m in fixed_messages:
print(" ", m)
调用 gpt-5.3-codex
response = client.chat.completions.create(
model="gpt-5.3-codex",
messages=fixed_messages,
)
print("\n模型返回:")
print(response.choices[0].message.content)
if name == "main":
main()
完整代码已整理到 GitHub 仓库,可直接克隆使用:https://github.com/your-username/fix-codex-messages。仓库里包含本脚本、测试用例和 README 说明,方便你快速跑通并集成到自己的项目里。