AIREITER

OpenRouter Structured Output:为什么你的 Schema 会被忽略

最后更新: 2026-08-23 01:28:01

同一份 JSON Schema,在某个 OpenRouter 模型上能稳定返回干净、带类型的结果,换到下一个模型,即使请求体完全不变,也可能得到字段名不同的 JSON、空字符串,甚至直接报 400。Reddit 用户 u/MicBeckie 通过 OpenRouter 测试 Qwen 模型的结构化输出时表示,自己“10 次里有 9 次都会报错”;而在同一套配置中,OpenAI 模型却能遵守 Schema。

这并不是一个可以简单提交 Bug 解决的问题。OpenRouter 的结构化输出能力是按端点划分,而不是按模型划分;而所谓“支持”,又分为三个执行层级:最严格的是原生 Schema 强制约束,最宽松的则只是把 Schema 当作提示词。本指南会说明它的路由逻辑、实际开发中常见的六种失败方式,以及让 Schema 输出能够上线的加固措施。执行机制依据官方结构化输出文档,故障案例则来自文中链接的开发者讨论。

OpenRouter 结构化输出文档页面

OpenRouter 所说的“支持结构化输出”,到底支持什么

OpenRouter 接受 response_format 参数,其中包含 type: "json_schema"、Schema 的 name、strict 开关,以及 JSON Schema 本身。一个最小请求如下:

{
  "model": "openai/gpt-4o",
  "messages": [{ "role": "user", "content": "Extract the shipping info" }],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "shipping_info",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "tracking_number": { "type": "string", "description": "Carrier tracking ID" },
          "carrier": { "type": "string" },
          "eta_days": { "type": "number", "description": "Days until delivery" }
        },
        "required": ["tracking_number", "carrier", "eta_days"],
        "additionalProperties": false
      }
    }
  }
}

官方文档里有两个决定请求能否正常工作的关键点:

  • 能力按端点划分,不按模型划分。同一个模型由五家提供商托管时,可能只有其中两家的结构化输出可用。模型页的 Providers 区域会展示每家提供商的 structured_outputs 参数;文档也明确提醒,“端点支持情况也可能随时间变化”。
  • 覆盖范围是逐步扩大的。OpenRouter 在 2024 年 12 月 12 日宣布结构化输出功能时,仅支持 OpenAI 4o 和 Fireworks 模型。其余支持都是后来由各提供商逐步补上的,因此今天整理出的模型支持列表很快就会过时。

文档还建议为每个属性写上描述,并设置 additionalProperties: false。原因是,在较低的执行层级中,Schema 同时也会成为模型可见的提示材料。

同一个 strict: true,背后可能是三种执行方式

strict: true 的实际含义,取决于请求最终落到了哪个端点。官方指南将提供商行为分为三个层级:

层级提供商如何处理 Schema输出能否信任?
原生严格模式在解码生成阶段严格执行 Schema可以:输出从生成机制上就符合 Schema
格式转换模式将 Schema 转换为提供商特定的结构化输出格式大多数情况下可以:但仅限于该格式支持的 Schema 特性
强提示模式把 Schema 注入为对模型的引导不可以:顺利时像 Schema,不顺利时会编造字段

OpenRouter 不会在请求时标明某个端点属于哪一层;官方文档让开发者查阅各提供商自己的说明。原生严格模式通常还会限制可用的 JSON Schema 特性,因此一些较少见的关键字,可能在最严格的端点上报错,却在其他端点被当作提示接受。

Claude 有一个文档明确说明的特殊情况,见提供商路由页面:对于 response_format.type: "json_schema",OpenRouter 会自动附加 Anthropic 的 structured-outputs-2025-11-13 beta 请求头,以启用严格的、会验证 Schema 的工具参数。但如果通过 tools 发送带有 strict: true 的工具定义,则调用方必须自行显式发送该 beta 请求头。否则,OpenRouter 会移除 strict 并按非严格模式路由。这个问题最麻烦的地方在于它不会报错:工具调用不再经过 Schema 校验,但你不会收到任何异常。

同一份 Schema 最常见的六种失败方式

前两类会立即失败,官方指南已有说明;另外四类则主要出现在社区讨论中,往往最耗排查时间。

立即失败 1:端点不支持结构化输出。请求会明确报错,表示该能力不受支持。虽然烦人,但问题至少很清楚。立即失败 2:JSON Schema 本身无效。API 会拒绝请求,原因可能是 Schema 无法解析,或违反了该端点的 Schema 规则。

静默失败 1:Schema 被忽略。返回的是合法 JSON,但字段结构完全不是你定义的 Schema。在 r/LocalLLaMA 的Schema 未被遵守讨论帖中,u/DaniyarQQQ 写道:

它返回的 JSON 看起来完全不像我的 Schema。

同一讨论中,u/MicBeckie 也指出了诊断上的困难:

我看到的要么是 JSON 完全符合要求的成功结果,要么就是报错,根本没法看到 JSON 内容。

包装器失败 2:你没传 tool_choice,却收到了与它有关的 400。在链接的 LangChainJS 案例中,withStructuredOutput() 是通过强制指定 tool_choice 为一个自动生成的函数来实现“结构化输出”的。对于那些宣称支持工具调用、却不支持强制选择工具的模型,请求会以 invalid_request_error 失败;在 DeepSeek v4 的案例里,错误直接点名了模型:deepseek-reasoner does not support this tool_choice。u/shansoft 通过 LangChainJS 遇到了这个问题(讨论帖)。u/eyueldk 原本认为“既然它支持工具调用,就应该支持结构化输出”,但事实并非如此。工具调用支持与严格 Schema 支持,是两项独立能力。

静默失败 3:不报错,也没有内容。一份关于 gpt-oss-120b 的报告提到:严格 Schema 请求直连提供商时返回 400,经由 OpenRouter 却返回 200,但 message.content 为空。r/openrouter 的另一篇讨论帖显示,一个标注为“支持”的模型只返回 [1] 或 [1.1]。如果 SDK 将空字符串照常解析,故障就会被推迟到下游三层之后才暴露。

静默失败 4:端点一直挂起。u/Beneficial-Loss-1031 在讨论 DeepSeek v4 的结构化输出端点时提到(讨论帖):

deepinfra/fp4 和 akashml/fp8 都有结构化输出选项,但我分别等了 3 分钟,API 依然没有返回任何内容。

#失败形式你看到的现象典型原因
1端点不支持报错:不支持结构化输出请求被路由到不具备该能力的提供商
2Schema 无效请求直接触发 API 错误Schema 违反端点规则
3Schema 被忽略JSON 合法,但字段不对提示层级的执行方式
4tool_choice 400invalid_request_errorSDK 通过强制工具调用模拟 Schema
5内容为空200,但 message.content 为空提供商未正确处理严格模式
6请求挂起数分钟没有响应报告中尚未确认原因——fp4/fp8 端点等待了 3 分钟

先加固请求,再怀疑模型

最值得优先启用的设置,是在 provider 对象中加入 require_parameters: true。其默认值是 false,对于未知参数,OpenRouter 会将其透传给可能静默忽略它们的提供商。即使保持 false,response_format 和结构化输出也只是端点选择中的软偏好:会优先考虑,但不保证。根据提供商路由文档,将该选项设为 true 后,路由只会选择支持你所传全部参数的端点:

{
  "model": "deepseek/deepseek-chat",
  "messages": [{ "role": "user", "content": "Extract the shipping info" }],
  "response_format": { "type": "json_schema", "json_schema": { "name": "shipping_info", "strict": true, "schema": { "...": "..." } } },
  "provider": {
    "require_parameters": true,
    "order": ["fireworks"],
    "allow_fallbacks": false
  }
}

限制条件越多,可选提供商池就越小;allow_fallbacks: false 则是用可用性换取确定性。同一份路由文档说明,默认策略会按正常运行时间,以及过去 30 秒内价格倒数的平方进行负载均衡。它优化的是价格低、状态健康的端点,而不是 Schema 执行能力强的端点。将 order 固定为单一提供商并禁用回退,可以让路由行为可复现:发生故障时,请求不会悄悄漂移到另一家提供商。不过,该端点属于哪个执行层级,仍需要你自己验证。

还有两项审计习惯,能补上路由解决不了的问题:

  • 确认实际处理请求的提供商。OpenRouter 的生成元数据会记录每次生成的提供商路由信息,以及模型、延迟和 Token 数量。如果输出质量发生漂移,这些归因数据能帮你判断是模型行为变了,还是路由切换了提供商。
  • 无论如何都要在客户端验证。三个执行层级都不能替代你自己用 Pydantic 或 Zod 做解析。r/LLMDevs 测试讨论反复强调的一点是:“合法 JSON”“符合 Schema”“语义正确”是三道不同的门槛,而 API 至多也只部分负责前两项。

支持流式输出,但解析责任仍在应用侧

结构化输出可以与 stream: true 一起使用。文档定义的行为是:模型持续输出合法的部分 JSON,待流结束后,拼接完成的响应应符合 Schema。不过,这种符合程度同样继承端点的执行层级——提示层级的端点最终仍可能输出不合规对象。因此,最终对象仍需自行验证。官方文档也没有提供增量解析器;对于重视延迟的 UI,这才是真正的工程难题。r/LLMDevs 的流式输出最佳实践讨论帖中,u/am174744 写道:

最后我还是自己写了一个函数来补全 JSON。——u/am174744

“……这其实是一个状态机。”——u/ImNotLegitLol,纠正了“先修复再解析”的思路

实际可选方案包括:使用容错的流式 JSON 解析器解析片段;只渲染已经完整的字段;或者放弃增量渲染,等最终对象拼装完成前仅显示加载状态。

Response Healing 能修什么,不能修什么

OpenRouter 的 Response Healing 插件面向非流式 json_schema 请求,用于修复格式层面的不完整情况,例如被截断的 JSON、混入的 Markdown 代码围栏等。但相比它能做什么,下面两项限制更重要:

  1. 不支持流式请求。文档明确将该插件限定在非流式请求中。
  2. 不处理 Schema 违规。Healing 能让 JSON 重新可解析,却不能让一个忽略了你 Schema 的响应变得合规。上文的第 3 类失败不会因此改善。

如何挑选真正会遵守 Schema 的模型

模型支持清单会过期,但筛选标准不会。以下三个过滤条件能拦住大部分问题:

  1. 优先原生严格执行。优先选择其服务提供商在解码阶段强制执行 Schema 的模型,而不是转换或提示型实现。模型页的 Providers 表会显示哪些端点声明支持 structured_outputs;但实际执行质量取决于提供商层级。
  2. 选择可审计的单一提供商。针对一个已知可靠的端点,连续多次调用并核对提供商归因。如果路由将请求分散到不同层级的提供商,你的失败率就是一场路由彩票。固定提供商,或直接选择单一提供商托管的模型。
  3. 以你自己跑过的冒烟测试为准。社区信号无论好坏都变化很快:上文 Qwen 的报错案例,以及 DeepSeek v4 缺失支持的情况,都可能随着提供商更新端点而改变。真正有意义的可靠性数据,只能来自你的 Schema 实测结果。

OpenRouter 结构化输出常见问题

json_object 和 json_schema 有什么区别?

json_object 只要求返回语法合法的 JSON;json_schema 则提供一个响应必须遵守的 Schema。json_object 保证的是 JSON 语法,而不是字段级 Schema 的一致性;如果下游代码依赖指定字段,仍需要自行验证。

哪些 OpenRouter 模型支持结构化输出?

没有一份静态列表值得长期信任:支持能力按端点划分,会随时间变化,而且在 2024 年 12 月最初只覆盖 OpenAI 4o 和 Fireworks 模型。请查看模型页面的 Providers 区域,确认每个端点是否带有 structured_outputs 标记。

为什么模型会忽略我的 Schema?

常见原因有三类:请求被路由到提示层级或不支持该能力的端点,可通过 require_parameters: true 和固定提供商来改善;Schema 使用了端点严格模式不接受的关键字;或者 SDK 包装器在一个不支持强制工具选择的模型上,使用工具调用来模拟结构化输出。

可以将 Pydantic 或 LangChain 与 OpenRouter 结构化输出一起使用吗?

可以。官方文档说明,该请求格式兼容 OpenRouter 的 chat-completions 风格 API,因此 Pydantic 生成的 Schema 和 OpenAI SDK 都可以直接使用。LangChain 的 withStructuredOutput() 同样可用,但要确认它发送的是 response_format,而不是通过 tool_choice 进行模拟;后者正是 DeepSeek v4 出现 400 错误的原因。

结构化输出支持流式响应吗?

支持。流会输出合法的部分 JSON,但最终对象是否符合 Schema,仍取决于端点的执行层级,因此应自行验证拼装后的对象。片段的增量解析需要由应用负责,Response Healing 也不适用于流式响应。

OpenRouter 会根据我的 Schema 验证响应吗?

不能将其视为跨全部端点的保证:执行强度取决于提供商层级,而 Response Healing 只会修复格式错误的 JSON,不会处理 Schema 违规。客户端验证仍然是必需的。

10 次调用冒烟测试

任何模型在通过结构化输出进入生产环境前,都应先完成以下测试:

  1. 固定一个有代表性的 Schema:中等复杂度,设置 additionalProperties: false,并为所有属性补充描述。
  2. 使用 strict: true 和 require_parameters: true 发送 10 次完全相同的请求,并开启回退。此轮测试刻意要检验回退行为,因此不要关闭它。
  3. 从三个维度评估每条响应:JSON 能否解析?是否符合 Schema?语义是否合理?
  4. 通过生成元数据记录每条响应实际由哪家提供商处理。由四家不同提供商跑出的 10/10 通过率,仍是一场路由彩票,而不是确定性保证。
  5. 做出决策:直接上线;将 provider.order 固定为通过测试的端点后,再跑一轮固定端点的 10 次调用;或者更换模型,并加入客户端验证与重试层。

通过阈值由你自己设定,但对于固定 Schema 而言,低于 9/10 就意味着重试和验证代码不再是可选项,而是产品本身的一部分。

延伸阅读:OpenRouter 自动路由如何选择提供商、使用 OpenRouter Prompt Caching 降低成本,以及解决 OpenRouter 429 限流问题。