一个 400 引出的协议转换问题:只有 system 消息时 input 为空
我维护着一个本地模型网关的分支——下游工具用 OpenAI 的 Chat 协议或 Responses 协议接进来,网关转换后发给不同上游。上一篇提到的多智能体循环第二轮的 400,根因在这个网关里。
现象
网关日志里,同一轮里前四个请求 200,第五个 400,上游报:One of "input" or "previous_response_id" ... must be provided。
对比请求体:失败的那个只有一条 system 消息,没有 user 消息。
成因
网关把 Chat 协议转成 Responses 协议时,system 消息映射成 instructions 字段,user 和 assistant 消息才进 input 数组。只有 system 时,input 就是空数组,上游拒收。
Chat 协议里只有 system 消息的请求是合法的——上游的 Chat 接口能正常回复。转换之后变成非法,问题在转换层。
谁会发这种请求
用编排框架、又配置了”不带对话历史”的智能体。它们把每轮的内容都写在系统提示词里,用户消息可有可无。第二轮如果前一个智能体只调了工具没产出文本,就没有用户消息可注入。
不是边缘情况,是这类编排的常态。
修法
思路:input 为空且有 instructions 时,把 instructions 的文本作为一条 input 项带上,并且不再重复写 instructions(否则提示词发两遍)。
问题是这条 input 项用什么角色。Responses 协议的 input 项支持 system、developer、user 等角色,但上游不一定都接受。直接打了三发:
role: system HTTP 400
role: developer HTTP 200
role: user HTTP 200
system 被这家上游拒收。developer 是 Responses 协议里 system 的正式对应角色,语义保真,选它。
改动十几行,加两个单元测试:纯 system 请求必须产出非空 input 且不带 instructions;带 user 消息的请求仍走原来的 instructions 路径。
顺手验证的两件事
- 上游项目的新版本有没有修:看了标签,同一段代码一字未改,这个补丁下次同步不会冲突。
- 另一个转换方向(Anthropic 协议 → Responses)有没有同样的洞:那边 system 是顶层字段、messages 不允许为空,构造不出空 input,不用改。
教训
协议转换的正确性不能只看”字段对应关系”,要看两边合法请求的集合是否一致。A 协议合法的请求,转换后在 B 协议下必须也合法。这次的洞就是一个 A 合法、B 非法的例子,字段映射本身没错。
另外,修法里”用哪个角色”这种事,别查文档猜,直接打请求试。三次调用不到十秒,比翻文档快,而且是真相。