结构化输出

首先,是问题的本质。结构化输出是让模型返回代码可以直接读取的数据的实践——你可以对其中的字段做判断,而不必手动解析一段文本。本章讲的是如何让这一点可靠。
到目前为止,模型返回的是文本,当由人类阅读时没问题。但软件无法对段落做太多处理。要在代码中基于模型的答案采取行动,你需要的是数据:一个你可以读取的字段、一个你可以做判断的值、一条你可以保存的记录。
这个转变虽然很小,但它改变了下游的一切。一旦模型返回 { "sentiment": "negative" } 而不是关于客户似乎不满意的句子,你的代码就可以路由工单、更新仪表板或发送警报。有趣的部分是你如何让模型在只会预测文本的情况下生成干净的数据,答案揭示了一个你没有意识到的杠杆。
请求 JSON
第一直觉是在提示词中请求:"回复为 JSON",这是软件用来在花括号内用字段和值传递数据的纯文本格式。试试看,它通常可以工作,这正是陷阱所在。模型仍然只是在预测可能的文本,有时可能的文本是一个代码栏、在数据前的友好"好的,给你"或在它后面的尾部注释。任何这些都会破坏读取 JSON 的代码。友善地请求会偏向预测朝向 JSON;它不会强制实现。
所以提供商给你一个真正的杠杆。使用 response_format 打开 JSON 模式的作用不仅仅是请求:它限制了模型,使语法以有效的 JSON 形式返回。
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": '提取城市和温度。回复为 JSON: { "city": string, "tempC": number }.'},
{"role": "user", "content": "现在北京大约是 18 度。"},
],
response_format={"type": "json_object"}, # 将输出约束为有效的 JSON 语法
)
data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"]) # "北京" 18response_format={"type": "json_object"} 使输出成为有效的 JSON 语法,所以 json.loads 不会被分散的散文阻碍。你仍然需要在提示词中描述想要的字段,因为 JSON 模式承诺有效的 JSON,而不是你心目中的特定字段。它排除了"根本不是 JSON",而不是"形状错误的 JSON"。它仍然不能承诺一个:如果回复在中途被截断,你可能会收到一个半 JSON 对象,所以这是可靠的语法,而不是在每种情况下的硬保证。
response_format 设置为 JSON 模式会约束语法,使其以有效的 JSON 形式返回。它不承诺字段匹配你要求的,在中途截断的回复仍然可能作为半对象到达,所以在提示词中描述形状并检查返回的内容。 使用模式锁定形状
JSON 模式保持语法有效,但模型仍可能重命名字段、删除一个或不同地嵌套内容。要消除那个摇晃,给它一个**模式**:字段和类型的精确描述,输出必须具有的形式,模型被要求填充的表单。使用 strict: true,输出然后保证与该形式匹配。
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "从消息中提取联系人信息。"},
{"role": "user", "content": "嗨,我是李明,可以通过 liming@example.com 联系我。"},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "contact",
"strict": True, # 精确执行模式
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
},
"required": ["name", "email"],
"additionalProperties": False,
},
},
},
)
contact = json.loads(response.choices[0].message.content)
# {"name": "李明", "email": "liming@example.com"}当模型只预测令牌时,模式如何成为硬保证?因为提供商限制它被允许预测的令牌。在每一步,模型仍然对所有可能的下一个令牌进行排名,但系统会删除会破坏你的模式的令牌,模型从剩下的中选择。
如果模式说下一个字段必须是 email,会开始任何其他字段的令牌会在模型选择之前被从表上删除。不合形状的令牌被删除,所以模型不能漂离形状。
有一个值得了解的较软的效果。你选择的字段名称在模型预测值时成为它读取的内容的一部分,所以一个清晰的名称引导更好的答案。被要求填充 tempC,模型倾向于摄氏度数;被要求填充 value,它的依据远少。清楚地命名字段是部分指令,部分模式。
strict: true 它得到精确的字段和类型,因为系统删除任何会破坏形状的令牌,然后模型才选择。那是执行,而不是礼貌的请求。也清楚地命名你的字段,因为字段名称是模型在预测值时读取的上下文。我花了一段时间才相信像 tempC 这样的名称会做真实的工作。 仍然要验证
模式约束形状,但无论如何都将输出视为来自外部世界的数据。形状可以有效,而内容是错误的:提取的电子邮件实际上是一个打字错误,模型猜测的数字,字段留空因为输入不包含它。而调用仍然可以以普通方式失败,比如被 max_tokens 截断并作为截断的 JSON 到达。防御性解析:有效的形状不是正确的值。
import json
def parse_contact(raw):
try:
data = json.loads(raw)
if not data.get("name") or not data.get("email"):
return None # 存在但为空
return data
except json.JSONDecodeError:
return None # 不是有效的 JSON(例如,截断的回复)
contact = parse_contact(response.choices[0].message.content)
if not contact:
pass # 处理失败:重试、重新询问或显示友好错误这是来自大语言模型如何工作的**幻觉**课程以新的服装出现:形状正确不会使值正确。模式保证你得到一个 name 字段;它不保证名字是正确的。在解析周围加一个 try/except 加上值的检查,是针对格式良好但仍然错误的回复的便宜保险。
try/except 中并在信任之前检查值。提前决定验证失败时会发生什么,重试或友好错误,所以稍后不会很乱。 两个模式:分类和提取
大多数结构化输出工作是两个形状之一。
**分类**将输入排序到固定标签集中的一个。模式中的 enum 将输出约束到恰好那些标签,使用相同的令牌修剪技巧:在标签位置,只有允许的值可以被预测,所以模型不能发明一个不在你列表上的类别。
# 分类的模式片段
{"type": "string", "enum": ["billing", "technical", "general"]}提取从自由文本中拉出特定字段,比如前面的联系人示例:一个名字、日期、金额、产品名称列表。分类和提取一起覆盖实际 AI 特征的大部分:路由支持工单、标记内容、将电子邮件转变为记录、阅读收据。两者都将一个模糊的文本生成器转变成你的代码可以构建的可靠组件。
enum 锁定,所以模型不能发明一个不在你列表上的类别。提取从自由文本中拉出命名字段到记录。两者都将一个模糊的文本生成器转变成可靠组件,它们一起涵盖大量实际 AI 特征。 在实践中
将混乱的支持电子邮件变成结构化工单,在一个模式中组合分类和提取:
import json
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": "将支持电子邮件变成工单。"},
{"role": "user", "content": email_text},
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "ticket",
"strict": True,
"schema": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["billing", "technical", "general"]},
"urgency": {"type": "string", "enum": ["low", "medium", "high"]},
"summary": {"type": "string"},
},
"required": ["category", "urgency", "summary"],
"additionalProperties": False,
},
},
},
)
ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "上个月的订阅被重复扣费。"}一个调用以两种方式分类电子邮件并提取总结,返回你的代码可以路由和存储的记录,形状保证且值仍然值得检查。到此为止,一切都是流向文本。接下来你给模型一个完全不同的感觉:在嵌入,文本变成数字,你可以按意义比较,搜索和处理你自己文档的基础。

