<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>JSON on Chico's Tech Blog</title><link>https://realtime-ai.chat/tags/json/</link><description>Recent content in JSON on Chico's Tech Blog</description><image><title>Chico's Tech Blog</title><url>https://github.com/chicogong.png</url><link>https://github.com/chicogong.png</link></image><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 06 May 2026 11:00:00 +0800</lastBuildDate><atom:link href="https://realtime-ai.chat/tags/json/index.xml" rel="self" type="application/rss+xml"/><item><title>让 LLM 输出可靠的结构化数据</title><link>https://realtime-ai.chat/posts/structured-output/</link><pubDate>Wed, 06 May 2026 11:00:00 +0800</pubDate><guid>https://realtime-ai.chat/posts/structured-output/</guid><description>LLM 接进系统最常踩的坑,是它返回的 JSON 时好时坏。从 prompt 约束到约束解码,逐个拆解几种方案的真实代价、schema 设计与流式场景的兜底。</description><content:encoded><![CDATA[<p>你写了个 prompt,让 LLM 把一段用户评论解析成 JSON:情感、评分、关键词。本地跑了二十次,完美。上线。</p>
<p>三天后告警响了。某条响应里,LLM 在 JSON 后面多写了一句&quot;希望这个分析对你有帮助!&quot;。你的 <code>json.loads()</code> 当场抛异常,整条链路挂掉。</p>
<p>这不是小概率事件,是<strong>结构性问题</strong>。只要你还在用&quot;自由文本里夹一段 JSON&quot;的方式跟 LLM 要数据,这种崩溃就是迟早的——区别只是它发生在测试环境还是生产环境。</p>
<p>这篇讲清楚:为什么自由文本提 JSON 天生不可靠,2026 年有哪几种正经方案、各自的代价是什么,schema 怎么设计才不坑自己,以及最难的那块——流式场景下怎么拿到结构化数据。</p>
<h2 id="为什么让它输出-json本身就是错的">为什么&quot;让它输出 JSON&quot;本身就是错的</h2>
<p>先理解 LLM 在干什么。它做的是一件事:根据前面所有 token,预测下一个 token 的概率分布,然后采样。它<strong>没有</strong>&ldquo;我现在要写一个合法 JSON&quot;这种全局意识。</p>
<p>所以当你在 prompt 里写&quot;请只返回 JSON,不要有多余文字&rdquo;,你是在用一句话,对抗模型训练数据里成千上万条&quot;先解释再给结果&quot;的对话样本。大多数时候它听话,因为你的指令把概率压过去了。但只要某次采样,在该写 <code>}</code> 的位置,&ldquo;希望&quot;这个 token 的概率偶然爬到了第一,它就会写下去——而且一旦写下去,后面就会顺着&quot;希望这个分析对你有帮助&quot;这条最自然的路径滑下去。</p>
<p>常见的失败长这样:</p>
<ul>
<li>JSON 外面套了 <code>```json</code> 代码块,或者前后有一段自然语言</li>
<li>字符串里有没转义的换行、引号</li>
<li>该是数字的字段写成了 <code>&quot;4.5&quot;</code>(带引号),或者写成 <code>4.5分</code></li>
<li>嵌套对象少了一个括号,尤其是输出很长的时候</li>
<li>枚举字段返回了你没定义的值——你要 <code>positive/negative/neutral</code>,它给你个 <code>mixed</code></li>
</ul>
<p>这些都不是模型&quot;笨&rdquo;,是概率采样的必然结果。<strong>你不可能靠把 prompt 写得更恳切来根治它</strong>,你只能降低概率,没法归零。要归零,得换思路:不是请求它输出合法结构,而是从机制上让它<strong>没法输出</strong>不合法的结构。</p>
<h2 id="五种方案以及它们各自的代价">五种方案,以及它们各自的代价</h2>
<p>2026 年,从最弱到最强,实际可用的方案是这五种。关键不是&quot;哪个最好&quot;,是搞清楚每个的边界。</p>
<pre class="mermaid">flowchart TB
  A["LLM 要输出结构化数据"] --> B{"模型在哪?"}
  B -->|"闭源 API"| C{"要不要 100% 保证 schema?"}
  B -->|"自己部署的开源模型"| D["约束解码<br/>xgrammar / llguidance"]
  C -->|"要"| E["Structured Outputs / strict 工具"]
  C -->|"差不多就行"| F["JSON Mode（已是 legacy）"]
  E --> G["拿到必定合法的 JSON"]
  D --> G
  F --> H["只保证语法合法，schema 靠运气"]
  style E fill:#fde7c2,stroke:#e8b23c
  style D fill:#fde7c2,stroke:#e8b23c
</pre><p><strong>1. 纯 prompt 约束。</strong> 就是开头那种&quot;请只返回 JSON&quot;。它的唯一价值是当作其他方案的补充——把字段含义、示例写清楚能提升内容质量。但<strong>别拿它当结构保证</strong>。如果你现在生产环境还在裸用这个,这篇文章后面的部分就是为你写的。</p>
<p><strong>2. JSON Mode。</strong> OpenAI 最早的尝试,<code>response_format: {&quot;type&quot;: &quot;json_object&quot;}</code>。它保证一件事:输出是<strong>语法合法</strong>的 JSON——不会有代码块包裹,不会有多余文字,括号配对。但它<strong>不管 schema</strong>:字段名、字段类型、枚举值、必填项,一概不保证。所以你还是得做完整校验。2026 年它基本算 legacy 了,OpenAI 自己的文档也把它标成旧特性,纯 JSON Mode 在生产里早就没人用了。</p>
<p><strong>3. 约束解码 / Grammar(constrained decoding)。</strong> 这是真正解决问题的机制,也是后面几种方案底层共用的东西。原理:在每一步采样时,根据&quot;目前已经生成的部分 + 目标 schema&quot;,算出<strong>下一个 token 哪些是合法的</strong>,把所有非法 token 的概率直接屏蔽(mask 成负无穷),只在合法集合里采样。</p>
<p>举例:已经生成到 <code>{&quot;rating&quot;:</code>,那么下一个 token 只能是数字、<code>-</code>、空格——模型这一步<strong>根本采样不到</strong> <code>&quot;</code> 或者字母。它不是&quot;被劝住了&quot;,是那条路被物理封死了。</p>
<p>开源世界里这块 2026 年很成熟。<code>xgrammar</code> 是目前 vLLM、SGLang、TensorRT-LLM 的默认结构化生成后端,支持完整的上下文无关文法(JSON、正则、自定义 CFG),每 token 开销做到了 40 微秒以下,几乎不影响吞吐;<code>llguidance</code> 是另一个主力,OpenAI 2025 年公开说过自家实现的底层借鉴了它。早期的 <code>outlines</code> 用有限状态机思路,开了这个方向,但碰到递归 schema(比如树形结构、嵌套评论)会很吃力,编译能慢到几十秒甚至几分钟,递归类结构现在更推荐用 xgrammar、llguidance 这种 CFG 引擎。</p>
<p><strong>4. Structured Outputs API。</strong> 这是闭源厂商把约束解码包装成的产品功能。OpenAI 的 <code>response_format: {&quot;type&quot;: &quot;json_schema&quot;, strict: true}</code> 就是它——你传一个 JSON Schema,模型底层用约束解码,<strong>输出必定符合 schema</strong>:每个必填字段都在、类型都对、枚举值都合法。可用模型是 gpt-4o-2024-08-06 之后的版本、GPT-4.1 全系、GPT-5 和 o 系列。2026 年它是数据抽取、Agent 场景的生产默认。</p>
<p><strong>5. Function Calling / Tool Use。</strong> 你定义工具,带 input schema,模型返回一个符合 schema 的工具调用。本质上跟 Structured Outputs 是同一套约束解码机制,只是包装成了&quot;调用工具&quot;的语义。它适合两类场景:一是模型真的要去调外部 API;二是你给了多个工具让模型自己选(多 Agent、路由)。Anthropic 的 Claude 走的就是 tool use 这条路,且复杂嵌套 schema 下也很稳;Gemini 这边把 JSON 模式和结构化输出合并成了一个特性。</p>
<p>一句话总结取舍:</p>
<table>
  <thead>
      <tr>
          <th>方案</th>
          <th>保证语法合法</th>
          <th>保证 schema</th>
          <th>适用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>纯 prompt 约束</td>
          <td>否</td>
          <td>否</td>
          <td>只用来补充内容质量,别单用</td>
      </tr>
      <tr>
          <td>JSON Mode</td>
          <td>是</td>
          <td>否</td>
          <td>已 legacy,不推荐新项目</td>
      </tr>
      <tr>
          <td>约束解码(xgrammar 等)</td>
          <td>是</td>
          <td>是</td>
          <td>自己部署开源模型</td>
      </tr>
      <tr>
          <td>Structured Outputs API</td>
          <td>是</td>
          <td>是</td>
          <td>闭源 API,要纯数据返回</td>
      </tr>
      <tr>
          <td>Function Calling</td>
          <td>是</td>
          <td>是</td>
          <td>要调外部工具,或多工具选择</td>
      </tr>
  </tbody>
</table>
<p><strong>选型其实很简单</strong>:自己部署模型,上 xgrammar;用闭源 API 且只想要一段数据,用 Structured Outputs;用闭源 API 且模型要决策调哪个工具,用 Function Calling。剩下两个,知道它们存在就行。</p>
<h2 id="schema-设计决定成败的地方往往不是代码">schema 设计:决定成败的地方往往不是代码</h2>
<p>很多人以为开了 Structured Outputs 就万事大吉了。不是。约束解码保证模型输出<strong>符合</strong>你的 schema,但如果你的 schema 设计得烂,模型会在&quot;合法&quot;的范围内给你垃圾。</p>
<p>几条我踩过坑后总结的硬规则:</p>
<p><strong>用枚举,别用开放字符串。</strong> 情感字段写成 <code>&quot;sentiment&quot;: string</code> 模型可能给你 <code>非常正面</code>、<code>positive</code>、<code>POSITIVE</code> 三种花样。写成 <code>enum: [&quot;positive&quot;, &quot;negative&quot;, &quot;neutral&quot;]</code>,约束解码会保证它只能落在这三个里。能枚举的一律枚举。</p>
<p><strong>给每个字段写 description。</strong> schema 里的 <code>description</code> 不是注释,模型会读。<code>&quot;score&quot;</code> 含糊,<code>&quot;score: 1-5 整数,5 表示强烈推荐,严格保守打分&quot;</code> 就清楚得多。约束解码管类型,不管&quot;打分准不准&quot;,后者靠 description。</p>
<p><strong>注意 strict 模式的限制。</strong> OpenAI 的 strict 模式有几条硬约束容易绊人:所有字段都必须列进 <code>required</code>(想要可选字段,得把类型写成联合类型带 <code>null</code>);不支持任意的 <code>dict[str, Any]</code>,key 不确定的字典它接不了;日期时间得用 ISO 字符串表示。设计前先翻一遍文档的限制清单,别等运行时报错。</p>
<p><strong>给模型一条&quot;我不知道&quot;的出路。</strong> 这条最容易被忽略。如果信息缺失,你又强迫模型必须填某个字段,约束解码会逼它<strong>编一个</strong>——它在合法 token 里硬凑,于是你拿到一个格式完美的幻觉。正确做法是显式留口子:加 <code>confidence</code> 字段,或者让关键字段可空,或者加一个 <code>&quot;status&quot;: [&quot;ok&quot;, &quot;insufficient_info&quot;]</code>。<strong>结构合法不等于内容可信</strong>,这是约束解码救不了你的部分。</p>
<p><strong>别一次榨太多。</strong> 一个 schema 里塞二十个字段,还层层嵌套,模型质量会肉眼可见地掉。能拆成两次调用就拆。</p>
<h2 id="出错了怎么兜底">出错了怎么兜底</h2>
<p>上了 Structured Outputs,JSON 解析层面的错确实没了。但还有别的会出问题,得有兜底。</p>
<p>第一类,<strong>API 层面的失败</strong>:超时、限流、网络抖动。这跟结构化无关,但既然你依赖一个必定返回结构的接口,它一旦不返回,你的下游就断了。退避重试,该做做。</p>
<p>第二类,<strong>约束解码碰上 token 上限</strong>。约束解码保证&quot;如果生成完成,结构一定合法&quot;,但它<strong>不保证一定能生成完成</strong>。如果 <code>max_tokens</code> 设小了,模型在一个深层嵌套里被强行截断,你拿到的是一段合法但<strong>不完整</strong>的 JSON。对策:嵌套深、字段多的 schema,把 <code>max_tokens</code> 给足;并且检查 finish reason 是不是 <code>length</code>,是的话当失败处理。</p>
<p>第三类,<strong>内容兜底</strong>——前面说的幻觉。schema 里留了 <code>confidence</code> 或 <code>status</code>,这里就要用上:低于阈值的结果不直接进库,转人工或走降级逻辑。</p>
<p>一个实战习惯:<strong>就算用了 Structured Outputs,落库前也做一次业务校验。</strong> 不是不信约束解码,是 schema 只能表达&quot;类型和结构&quot;,表达不了&quot;评分必须在 1 到 5 之间且这条订单的金额不能是负数&quot;这种业务约束。两层防线:schema 管结构,代码管语义。</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># 闭源 API:Structured Outputs 保证结构,代码补业务校验</span>
</span></span><span class="line"><span class="cl"><span class="n">resp</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">chat</span><span class="o">.</span><span class="n">completions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">messages</span><span class="o">=</span><span class="p">[</span><span class="o">...</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">response_format</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;json_schema&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;json_schema&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;review&#34;</span><span class="p">,</span> <span class="s2">&#34;strict&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&#34;schema&#34;</span><span class="p">:</span> <span class="n">SCHEMA</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="n">max_tokens</span><span class="o">=</span><span class="mi">800</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">resp</span><span class="o">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">finish_reason</span> <span class="o">==</span> <span class="s2">&#34;length&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="n">TruncatedError</span><span class="p">(</span><span class="s2">&#34;被 token 上限截断,当失败重试&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">content</span><span class="p">)</span>  <span class="c1"># 这里几乎不会再抛</span>
</span></span><span class="line"><span class="cl"><span class="n">validate_business_rules</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>  <span class="c1"># 评分范围、字段间一致性等,schema 管不到</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="流式场景最难啃的一块">流式场景:最难啃的一块</h2>
<p>到这里都还好。真正难的是这个:你要<strong>流式</strong>输出,<strong>同时</strong>要结构化数据。</p>
<p>比如一个 UI,要边生成边把解析结果填进表单——名字一出来就显示名字,地址一出来就显示地址。但 LLM 是一个 token 一个 token 吐的,而 JSON 的<strong>任何中间状态都是语法非法的</strong>:</p>
<pre class="mermaid">flowchart LR
  T1["{"] --> T2["{\"name\""] --> T3["{\"name\":\"张"] --> T4["{\"name\":\"张三\"}"]
  T1 -.->|"json.loads"| X1["报错"]
  T2 -.->|"json.loads"| X2["报错"]
  T3 -.->|"json.loads"| X3["报错"]
  T4 -.->|"json.loads"| OK["成功"]
  style X1 fill:#f8d7da,stroke:#c33
  style X2 fill:#f8d7da,stroke:#c33
  style X3 fill:#f8d7da,stroke:#c33
  style OK fill:#d4edda,stroke:#3a3
</pre><p>你不能等整段 JSON 攒齐——那就退化成非流式,流式的意义没了。也不能拿 <code>json.loads()</code> 去解每个中间态——它每次都抛异常。</p>
<p>可行的有两条路:</p>
<p><strong>一是分隔符切块。</strong> 别要一个大 JSON,让模型按 <code>JSON Lines</code> 输出——每行一个独立的小对象,中间用换行分隔。每收到一个完整换行,就解析这一行。这等于把&quot;一个大结构&quot;拆成&quot;很多个小结构&quot;,每个小结构一旦完整就立刻可用。简单、稳,适合&quot;一批结果&quot;型的输出。</p>
<p><strong>二是容错增量解析。</strong> 用一个能处理<strong>残缺 JSON</strong> 的解析器,把每个中间态尽力补全成一个带类型的部分对象——<code>{&quot;name&quot;:&quot;张</code> 直接解析成 <code>{name: &quot;张&quot;}</code>,字段还没出现的就当缺失。这条路上 2026 年比较成熟的是 <code>BAML</code> 这类工具,它内置了一个容错 parser,专门把破碎的部分 JSON 实时转成带类型的对象,既保住流式的体验,又拿到渐进的结构化数据。</p>
<p>选哪条:输出是&quot;一组同类项&quot;,用 JSON Lines;输出是&quot;一个有很多字段的大对象&quot;,且 UI 要逐字段渐进填充,用容错增量解析。</p>
<p>还有个常被忽略的点:<strong>流式 + 约束解码可以同时用</strong>。约束解码是逐 token 工作的,本来就和流式天然兼容——vLLM 这类引擎流式吐 token 的同时,xgrammar 在每一步做 mask。所以&quot;用了约束解码就不能流式&quot;是个误解,两者是正交的。难的从来不是生成端,是<strong>消费端</strong>怎么解析这些中间态。</p>
<h2 id="最后把它当工程问题别当-prompt-问题">最后:把它当工程问题,别当 prompt 问题</h2>
<p>如果只留一句话:<strong>结构化输出的可靠性,不该靠 prompt 写得好,该靠机制保证。</strong></p>
<p>很多团队卡在&quot;再调调 prompt 让它别输出多余文字&quot;。这是把一个工程问题误当成了文案问题。prompt 能把失败率从 5% 压到 0.5%,但压不到 0;而约束解码这类机制能压到 0。你的优先级应该是:</p>
<ol>
<li><strong>先换机制</strong>——自部署上 xgrammar,用 API 上 Structured Outputs 或 Function Calling。这一步把&quot;JSON 语法错&quot;和&quot;schema 不符&quot;两类问题直接归零,收益最大。</li>
<li><strong>再认真设计 schema</strong>——枚举、description、给&quot;不知道&quot;留出路。约束解码管不到的内容质量,靠这一步。</li>
<li><strong>最后补业务校验和流式兜底</strong>——schema 管结构,代码管语义;流式场景按数据形态选 JSON Lines 或容错解析。</li>
</ol>
<p>机制定了下限,prompt 和 schema 决定上限。顺序别搞反。</p>
<hr>
<p>参考:
<a href="https://platform.openai.com/docs/guides/structured-outputs">OpenAI Structured Outputs 文档</a>、
<a href="https://openai.com/index/introducing-structured-outputs-in-the-api/">Introducing Structured Outputs in the API</a>、
<a href="https://github.com/mlc-ai/xgrammar">xgrammar</a>、
<a href="https://github.com/guidance-ai/llguidance">llguidance</a>、
<a href="https://developers.redhat.com/articles/2025/06/03/structured-outputs-vllm-guiding-ai-responses">Structured Outputs in vLLM</a>、
<a href="https://nitishagar.medium.com/streaming-structured-data-from-llms-is-harder-than-you-think-6f2ee976fe5f">Streaming structured data from LLMs is harder than you think</a>、
<a href="https://www.vellum.ai/blog/when-should-i-use-function-calling-structured-outputs-or-json-mode">When should I use function calling, structured outputs or JSON mode</a></p>
]]></content:encoded></item></channel></rss>