Products
GG网络技术分享 2026-04-16 12:42 0
坦白讲... 哎,说真的,搞大模型开发的人谁没被JSON输出气死过?你明明跟它说“请输出JSON”, 它非给你来一段“好的,这是你要的JSON: json ... ”,然后你正则表达式写了一堆,后来啊它又给你加个注释,或者字段名少个字母,直接报错!简直是崩溃!不过最近,闭源大模型们好像终于听到了我们的哀嚎,开始搞那个所谓的“结构化输出”了。这玩意儿到底是个啥?真的能拯救我们这些苦逼的程序员吗?今天我们就来扒一扒这个LLM结构化输出的代码示例和原理,顺便吐槽一下。
绝绝子! 先说说 我们要搞清楚,这里说的不是OpenAI很早就支持的Json Mode,那个老版本的Json Mode其实挺鸡肋的。它只保证模型输出一个合法的、 可以解析的json而已,至于json里面的字段对不对,字段类型是不是你想要的,取值范围有没有约束,它是一概不管的!这就是所谓的“弱约束”。但是!现在的Structure Output,这是JSON Mode的升级版啊!虽然只对gpt-4o-mini-2024-07-18和gpt-4o-2024-08-06之后的模型版本支持,但这真的是质的飞跃。

简单说Structure Output会进一步对JSON里面的具体字段和类型进行约束。这就好比以前你让模型去菜市场买菜, 它可能给你买回一堆烂叶子;现在你给了它一个严格的清单,必须买什么、买多少、什么成色,它就得照单全收。这多爽?我们举个例子, 比如我们要从基金季报中抽取基金经理对市场不同行业的观点,对观点进行情绪分类,并关联相关的申万一级行业。这任务要是放在以前,非得把人累死,现在有了结构化输出,感觉世界都清净了。
图啥呢? 这里提供两种不同的实现方案, 一种是基于条件解码的强约束方案,和基于指令的弱约束方案,并且会给出不同方案对模型推理效果的影响。大家看好了这可是干货。
我们先来看看怎么用代码实现这个。针对不满足openai版本条件的老模型,我们可以使用instructor来实现结构化输出。Instructor这个库,真的是个神器,虽然它本质上是在打猴子补丁,但是打得好啊,我不敢苟同...!
from openai import AzureOpenAI
import instructor
client = AzureOpenAI(
api_key ='...',
api_version= "2024-08-01-preview",
azure_endpoint = "..."
)
client = instructor.from_openai
resp = client.chat.completions.create(
model='gpt-4o',
response_model= ViewExtraction,
messages=,
)
看到没?就这么简单!在上面使用`instructor.from_openai`时 Instructor会打猴子补丁,在常规openai的接口上,增加`response_model`的预处理,和对输出的retry机制。这简直就是懒人福音!你不需要自己去写那些繁琐的解析逻辑,Instructor都帮你搞定了。不过这里我们定义的`ViewExtraction`是个啥?别急,我们后面会讲。
为了让大家更直观地理解, 我特意搞了个表格,对比一下现在市面上常见的几种结构化输出方案。虽然有点乱,但是凑合看吧,反正能说明问题。
| 方案名称 | 类型 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| OpenAI Structured Output | 强约束 | 基于Constrained Decoding, 直接在推理层限制Token输出 | 极其稳定,类型平安,几乎不产生幻觉 | 只支持特定新模型,枚举值有限制 | 对数据格式要求极高的生产环境 |
| Instructor | 强/弱约束混合 | Pydantic模型定义 + API层面的适配和重试 | 代码写起来爽,支持多种模型接口 | 依赖模型本身的能力,老模型效果打折 | 快速开发,兼容多种API |
| Outlines | 强约束 | 正则表达式引导的生成,FSM状态机 | 开源,可控性极强,支持本地部署 | 需要自己部署模型,技术门槛稍高 | 本地推理,需要精细控制输出格式 |
| FRI | 弱约束 | 纯Prompt Engineering,告诉模型怎么输出 | 通用性强,什么模型都能试 | 极其不稳定,经常需要正则二次清洗 | 模型太老,没别的办法的时候 |
看完了表格,是不是心里有数了?如果你有钱, 直接上OpenAI最新的Structured Output;如果你要省钱或者用本地模型, 太顶了。 Outlines是个好选择;如果你像我一样懒,Instructor绝对是首选。
那instructor,openai这些结构化输出能力都是如何实现的呢?下面我们来看几种约束模型给出结构化输出的方案。其实核心原理就是“条件解码”。这听起来很高大上, 其实说白了就是在每一步解码时都对输出词表进行MASK,只允许模型对当前位置符合输出格式的Token进行预测,把原始基于完整词表的softmax,转换成对于局部掩码词表的softmax。
尊嘟假嘟? 那问题其实就简化成了在每一步推理时如何选择该进行掩码的Token呢?毕竟GPT预测是自左向右,无法获得完整Token序列。论文把基于输出格式掩码的问题,转换成了基于有限状态机的状态转移问题。简单解释下FSM其实就是由一组状态和状态之间的转移过程组成, 词表中的字符满足条件的可以匹配到FSM的某个或某几个状态,从而在碰到字符A后就可以确认几种满足条件的状态转移路径,从而根据后面的路径确认掩码词表。
最终的最终。 主要原因是词表中的每个字符究竟满足哪些状态, 每个状态后有哪些可能的转移状态这些都是预先计算好的,所以呢并不需要在推理中动态计算,相反可以预先构建好每个词表到状态,再到后续转移状态的mapping。在解码过程中只需要根据解码字符读取mapping,对下一个字符进行对应的掩码即可。所以呢算法的时间复杂度是O,空间复杂度是O。这效率,杠杠的!
我们来看一段基于已经构建好的FSM进行解码的步骤代码,这段代码是Outlines里面的实现逻辑:,操作一波。
def sequence_generator:
while True:
# 1. 获取模型输出的logits
logits, kv_cache = model
# 2. 获取FSM允许的下一个token
allowed_tokens = get_allowed_tokens
# 3. 基于allowed_tokens对logits进行mask,不允许的token均为-inf
biased_logits = bias_logits
# 4. 采样下一个token
next_token_ids, ancestors, sequence_weights = sampler
# 5. 更新FSM状态
fsm_states = get_next_fsm_states
# 6. 检查是否生成完成
is_finished = is_generation_finished
看懂了吗?没看懂也没关系, 反正核心就是那个`bias_logits`,把不符合条件的Token概率变成负无穷大, 掉链子。 让模型根本选不到它们。这就是强约束的威力!
当冤大头了。 针对上述的两种结构化解码方案,对比常规的自然语言推理对模型效果的影响几何?我先是读到的第一篇论文,核心结论其实是结构化输出会影响模型的推理效果。这篇论文叫《Let Me Speak Freely》,听起来就很委屈,好像模型被束缚了手脚一样。作者认为,强制模型输出特定的格式,会占用模型的“认知资源”,导致推理能力下降。
勇敢一点... 但是!接着Outlines的作者们就发了一篇博客指出了论文的几个核心问题。双方各自站的立场不同,但逻辑上个博客指出的几个论文的核心问题确实很有说服力。博客给出的到头来结论是在GSM8k, Last Letter,Shuffled Object这三个任务上结构化输出相比NL输出都有提升。并且直接给出了基于Outlines的后来啊复现代码github repo。
但是吸取前面盲目偏信前一篇论文的教训, 其实在平时的任务尝试上,个人感觉结构化输出的效果和具体任务,Prompt质量,模型本身的指令能力强相关。所以呢还是倾向于在应用时充分对比NL和Structure的效果后再做应用。 躺平。 在大模型时代很多结论都有领域和模型局限性, 大家需要在自己的场景上审慎判断哈哈~
我可是吃过亏的。 说了这么多理论,还是得回到代码。先说说我们先定义抽取任务的结构体, 申万一级行业的枚举值和情绪的枚举值,这里结构化输出都是使用pydantic定义的。通过枚举值定义我们可以约束模型输出的取值范围,而通过抽取结构定义我们可以约束模型输出的结构。不过这里对Enum的取值数量有限制一次输出的枚举值总量不能超过500, 毕竟是直接作为模型上文,枚举值太多一是慢二是贵,三是不稳定。
from enum import Enum
from typing import List
from pydantic import BaseModel, Field
class SWIndustry:
AGRICULTURE = "农林牧渔"
MINING = "采掘"
CHEMICALS = "化工"
STEEL = "钢铁"
# ... 这里省略一堆行业, 反正就是枚举嘛
COMPREHENSIVE = "综合"
class ViewAspect:
POSITIVE = '正面'
NETURAL = '中性'
NEGATIVE = '负面'
class View:
extract_view: str = Field
extract_view_entities: List = Field
related_industry: list = Field
view_aspect: ViewAspect = Field
class ViewExtraction:
views: list = Field(
...,
description="每个观点都应该是一个单独的对象,包含原文中表达观点的句子,观点主体,观点情绪分类和关联的申万一级行业",
)
然后只需要把以上的结构体作为`response_format`的参数输入openai即可。这里我们还是举论文中的例子。我们的输出要求是满足浮点数“?.?0-9”。这个输出约束可以被转换成FSM中的4种不同状态,每个状态有不同的转移状态,又爱又恨。。
假设我们的词表只有5个字符{"A", ".", "42", ".2", "1"},那整个FSM掩码过程如下。这过程虽然复杂,但对于计算机来说就是查表而已,快得很,我当场石化。。
说到这里我突然想起来Context Cache的使用几乎已经是行业共识,目标是优化大模型首Token的推理延时,在多轮对话,超长System Prompt,超长结构化JSON和Few-shot等应用场景,是不可或缺的。这一章我们主要从原理、一些论文提出的cache优化项和VLLM开源项目入手,分析下context Cache的实现和适合场景。
重... 只所以对self-attention的KV进行缓存,主要原因是在Transformer的众多计算单元中只有self-attention是上下文依赖的,也就是在计算第k个token的输出时,需要使用第k个token和前面K-1个token的进行内机计算,使得self-attention的计算复杂度随序列长度平方增长。而如果对历史序列中的KV... 哎呀, 扯远了这个跟结构化输出关系不大,但是既然想到了就顺便提一下毕竟做系统优化的时候,这两个经常是一起出现的。
再举一个function calling的例子, 假设我们有两个工具一个Bing搜索,一个是基金信息查询工具,模型需要的指令被转换成了以下函数调用的指令格式,又爱又恨。。
from typing import Literal, Union, Optional
class BingSearch:
query: str = Field
class FundInfo:
"""
可以通过基金代码或基金名称, 查询基金基础信息
"""
fund_code_or_name: Optional = Field
lookup_field: Literal
class Task:
name: str = Field
tool: Union = Field
class TaskSequence:
reason: str = Field
task_actions: List = Field
completion = client.chat.completions.create(
model='gpt-4o',
messages=,
response_format= TaskSequence
)
这里我们就能得到结构化的输出如下。看到那个JSON了吗?这就是我们要的!干净、利落、没有废话,试试水。!
{
'name': 'TaskSequence',
'description': 'Correctly extracted `TaskSequence` with all required parameters with correct types',
'parameters': {
'$defs': {
'BingSearch': {
'properties': {
'query': {
'description': '网页搜索query',
'title': 'Query',
'type': 'string'
}
},
'required': ,
'title': 'BingSearch',
'type': 'object'
},
# ... 这里省略了FundInfo和Task的定义, 反正就是一大堆JSON Schema
},
'properties': {
'reason': {
'description': '先逐步思考要解决用户的问题需要哪些步骤',
'title': 'Reason',
'type': 'string'
},
'task_actions': {
'description': '任务列表,按施行顺序依次排列',
'items': {
'$ref': '#/$defs/Task'
},
'title': 'Task Actions',
'type': 'array'
}
},
'required': ,
'type': 'object'
}
}
写到这里我都不知道自己写了些啥了。反正结构化输出这东西,对于咱们开发者绝对是利大于弊的。虽然有些论文说它会影响模型智商, 但我觉得吧,只要能稳定输出我要的JSON,智商低一点点就低一点点吧,反正我可以在Prompt里多喂点Few-shot给它补补脑,换个角度。。
开源项目Outlines的两位作者Brandon T. Willard和R´emi Louf是比较早提出大模型可控生成方案的大佬。如果你用API调模型那Instructor更合适, 如果你自己部署模型调用那Outlines更合适,vllm这些推理框架最新的版本也已经融入了Outlines。这里我们就选Instructor进行介绍。还是上面的例子, 输出格式的定义相同,针对不满足openai版本条件的老模型,我们可以使用instructor来实现结构化输出,我们都曾是...。
FRI缺少严格约束, 所以只能依赖模型的指令遵从能力,有一定概率输出后来啊会无法还原成原始的的Pydantic类型。下面我们看另一种强约束的方案。条件解码方案其实就是在每一步解码时都对输出词表进行MASK, YYDS... 只允许模型对当前位置符合输出格式的Token进行预测,把原始基于完整词表的softmax,转换成对于局部掩码词表的softmax。
再说说 祝大家在新的一年里代码如诗行云流水,bug如朝露见光即散;创意如泉涌,论文如宝藏,实验如神助,成功率百分百! 躺赢。 科研路上,你我皆是‘码’到成功的幸运儿!🎉 哎,不说了继续去调我的Prompt了这模型怎么又不输出JSON了...
Demand feedback