如果你的日志里这段时间不断刷出 invalid_structured_output,先别急着换模型。近 7 天这个报错在 CCSub 平台上出现了 49 次,仅次于 upstream_auth_or_policy_error,属于高频但基本可控的一类问题。它几乎都发生在同一类场景:你要求模型输出结构化数据(JSON 对象、函数调用参数),但返回结果没能通过校验。
这篇文章按“成因 → 复现 → 修复 → 重试 → 省钱”的顺序讲清楚,让你能直接照着改。
先确认:报错到底出在链路的哪一环
结构化输出失败通常只有四种可能,先定位再动手改参数:
- 模型确实返回了非法 JSON:例如多了一个逗号、用了单引号、把布尔值写成
True。 - 返回被截断:输出 token 上限太低,JSON 只到一半就停了。
- 没有启用严格 JSON 模式:你只在提示词里写了“请输出 JSON”,模型有概率还是会带解释文字。
- 流式拼接破坏了结构:你自己按块拼接流式返回,拼错了位置或漏了块。
另外,函数调用(tools / function calling)的参数其实也是一段 JSON,校验失败会以同类报错返回,所以排查思路完全一致。
第一步:确认接入点与模型可用性
在 CCSub 上,先确认你请求的地址和模型名是对的,很多“看起来像结构化输出失败”的问题其实是请求打到了错误路径。
- OpenAI 兼容调用:
OPENAI_BASE_URL=https://ccsub.xyz/v1,聊天端点https://ccsub.xyz/v1/chat/completions。 - Codex 原生调用:
https://ccsub.xyz/openai,wire_api = responses,Codex 会自动请求/responses端点。 - Claude Code 原生调用:
ANTHROPIC_BASE_URL=https://ccsub.xyz/api,客户端会自动请求/v1/messages。
可用模型列表请用 GET https://ccsub.xyz/v1/models 查询,不要凭记忆写模型名。模型名不存在时,部分客户端会把错误包装成结构化输出异常,容易误判。
curl https://ccsub.xyz/v1/models \
-H "Authorization: Bearer sk-你的CCSub密钥"第二步:用最小可复现请求定位
把业务提示词砍到最短,只保留 schema 和一个样例输入,能稳定复现后再逐步加回去。
curl https://ccsub.xyz/v1/chat/completions \
-H "Authorization: Bearer sk-你的CCSub密钥" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-sol",
"messages": [
{"role": "system", "content": "你只输出 JSON,不要输出任何解释。"},
{"role": "user", "content": "提取:张三,28岁,上海"}
],
"response_format": {"type": "json_object"},
"max_tokens": 512
}'如果这个请求能稳定返回合法 JSON,说明问题在业务提示词或流式处理;如果它也失败,问题在参数或模型能力。
第三步:逐个修掉四类成因
1. 严格 JSON 模式没开
只在提示词里写“请输出 JSON”不够。OpenAI 兼容接口支持 response_format,请显式声明:
{
"response_format": { "type": "json_object" }
}注意:开这个模式时,提示词里必须出现 “JSON” 字样,否则部分接口会直接拒绝请求。如果你需要按 Schema 强约束,可以在工具里把 strict 置为 true 并要求模型调用该函数,把结构化输出交给函数参数承载。
2. 输出被截断
这是最常见的隐形原因。max_tokens 设得太小,JSON 在最后一个字段前就断了,解析器自然报错。判断方法很简单:看返回里 finish_reason 是不是 length。如果业务需要长列表,把 max_tokens 调到 2048 以上,或者分批请求。
3. 模型“话太多”
很多失败其实是返回里带了 好的,以下是结果: 这样的前缀。处理方法:
- 系统提示词明确“只输出 JSON,禁止任何前后缀”。
- 解析前做一次裁剪,定位第一个
{和最后一个},截取中间部分再解析。 - 确实无法约束时,用函数调用承载结构,而不是让模型自由生成。
4. 流式拼接破坏了结构
流式返回里,JSON 是分块到达的。如果你在每块上单独做 JSON.parse,必然失败。正确做法是把 delta 里的文本片段全部追加到缓冲区,等流结束后一次性解析。下面是一段可用的 Node.js 写法:
let buffer = '';
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) buffer += delta;
}
const data = JSON.parse(buffer);如果必须边流边用,先做增量解析库,而不是用正则凑。另外注意:部分客户端在流式模式下会忽略 response_format,结构化任务建议直接关流式。
第四步:重试策略怎么写才不浪费钱
结构化输出失败通常是偶发的,重试能解决大部分问题,但方式要对:
- 只对校验失败做重试,不要对所有错误都重试。
- 最多重试 2 次,指数退避(例如 1 秒、3 秒)。
- 重试时把上一次的报错信息作为附加提示塞进对话,例如“上次输出不是合法 JSON,请只输出 JSON 对象”。
- 重试仍失败就降级到更简单的 schema,或换一个模型。
提醒一句:每次重试都会计入 token 消耗。如果一条业务请求要重试 3 次以上,先回去修 schema 和参数,这比多充钱更划算。CCSub 按量计费,重试成本会真实体现在账单里,充值前先看一眼用量明细。
Claude 与 GPT 系列在结构化输出上的差异
两边都能做结构化输出,但习惯不同,参数也不通用:
- GPT 系列:习惯把结构化约束放在
response_format和工具的strict上,配合/v1/chat/completions使用最顺。对max_tokens截断比较敏感,长 schema 尤其要留足空间。 - Claude 系列:走的是
https://ccsub.xyz/api的/v1/messages,结构化输出通常靠提示词加工具定义来约束,倾向于把结果放在内容块或工具参数里。解析时要按内容块取,而不是直接当纯字符串。
实操建议:同一条结构化任务,先用 claude-sonnet-4-6 这类通用模型验证 schema 是否合理,再用 gpt-5.6-sol 跑量。如果某个模型持续失败,不要硬扛,换模型验证能快速区分“schema 问题”还是“模型问题”。
判断技巧:如果同一个 schema 在多个模型上都失败,问题在 schema;如果只在某一个模型上失败,优先怀疑该模型的输出风格或参数支持范围。
客户端侧的两个小坑
- Claude Code、Cursor、VS Code、Windsurf、CherryStudio 等工具对
response_format的支持程度不同,遇到报错时先在命令行用 curl 复现,再回客户端调参。 - Codex 走
/responses端点,请求体和 Chat 接口不一样,不要直接把 Chat 的 JSON 参数照搬过去。
什么时候该怀疑不是你的问题
如果你已经开了严格 JSON 模式、max_tokens 充足、关了流式、schema 也简单,仍然稳定失败,那再去看服务侧。可以先访问 故障排查文档,或用 GET /v1/models 确认模型当前可用。这类情况更接近服务可用性问题,而不是结构化输出本身。
下一步怎么做
- 用本文的最小请求跑一遍,确认能复现。
- 按四类成因逐条排,先修
max_tokens和response_format,这两个改起来最快。 - 加上最多 2 次的重试与退避,观察 7 天内的失败率是否下降。
- 如果需要更大额度跑批量任务,到 注册与充值 页面按需充值,先小额验证稳定性再加量。
结构化输出不是玄学,它就是一个“约束 + 校验 + 重试”的工程问题。把参数配对、把截断留足、把流式拼对,绝大多数 invalid_structured_output 都会消失。
CCSub API 调用方法
如果你使用 OpenAI 兼容客户端,把 Base URL 设置为 https://ccsub.xyz/v1,API Key 使用控制台创建的 sk-你的CCSub密钥。
export OPENAI_BASE_URL=https://ccsub.xyz/v1
export OPENAI_API_KEY=sk-你的CCSub密钥
Codex 请使用原生 Responses 专用地址。先设置密钥:
export CCSUB_API_KEY=sk-你的CCSub密钥
再在 ~/.codex/config.toml 配置自定义 provider:
model = "gpt-5.6-sol"
model_provider = "ccsub"
[model_providers.ccsub]
name = "CCSub"
base_url = "https://ccsub.xyz/openai"
env_key = "CCSUB_API_KEY"
wire_api = "responses"
Claude Code 使用专用 Messages Base URL:
export ANTHROPIC_BASE_URL=https://ccsub.xyz/api
export ANTHROPIC_AUTH_TOKEN=sk-你的CCSub密钥
需要确认当前可用模型时,可以请求 GET https://ccsub.xyz/v1/models。正式长任务前,建议先用短请求验证 Key、余额和模型是否正常。