关于 llm-structured-output
面向开发者的 Skill,用于从 LLM 响应中提取经过类型约束和验证的数据。它介绍各平台的结构化输出方法、Schema 验证与重试处理,并说明下游使用时的防护措施。
主要功能
- 01OpenAI JSON Schema 输出
- 02Anthropic tool_use 提取
- 03Gemini responseSchema
- 04本地约束解码
- 05Schema 验证与重试
- 06生产环境防护
适用场景
- 01将文本字段提取为 JSON
- 02把验证后的 LLM 数据传入代码
- 03修复格式错误或字段缺失的输出
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill llm-structured-output安装后在 Claude Code 中输入/llm-structured-output确认已加载。
详细介绍
大模型结构化输出
此技能的作用
从大模型 API 响应中提取经过类型校验的数据,而不是解析自由文本。本技能涵盖三种主要方法:OpenAI 的 JSON Schema response_format、Anthropic 用于结构化提取的 tool_use 块,以及 Gemini 中的 Google responseSchema。你将了解每种方法何时有效、何时会失效,以及如何围绕模式校验失败构建重试逻辑——每个生产系统都会遇到这类失败。
何时使用此技能
- 用户需要从大模型响应中提取结构化数据(JSON 对象、数组、枚举)
- 用户正在构建一个工作流,其中大模型输出会直接传入代码(数据库写入、API 调用、UI 渲染)
- 用户询问 OpenAI 中的
response_format、json_mode、json_object或json_schema - 用户询问如何使用 Anthropic 的
tool_use或tool_result块提取数据(而非实际执行工具) - 用户询问如何使用
openainpm 包中的zodResponseFormat()处理 Zod 模式 - 用户需要使用
instructor、marvin或手动校验,将大模型输出解析为 Pydantic 模型 - 用户从大模型响应中获得格式错误的 JSON、缺失字段或错误类型,需要修复方法
- 用户询问本地模型中的
controlled generation、constrained decoding或grammar-based sampling
以下情况请勿使用此技能:
- 用户想生成自由形式文本(摘要、文章、聊天)
- 用户询问用于表单校验或 API 输入校验的 Zod(请改用
zod-validation-expert) - 用户需要通过提示词工程提升文本质量(而非结构)
- 用户想调用真实的外部工具/API(本技能介绍的是将 tool_use 作为结构化输出技巧,而非实际的工具编排)
示例中使用的是历史模型 ID 和 SDK 辅助函数路径;部署前请确认其可用性,并固定使用受支持的 SDK/模型。不同提供商/API 对严格输出的支持及模式子集各不相同。符合模式的值仍是不可信数据,绝不能作为数据库写入或外部调用的授权。请将源文档中的指令与提取指令分开,并在应用代码中强制执行允许的操作。
核心工作流
-
确定目标架构。询问用户需要提取哪些字段。为每个字段定义类型、是否必填,以及适用时的有效枚举值。没有明确的架构时,不要继续。
-
选择适合相应提供方的方法:
- OpenAI(gpt-4o、gpt-4o-mini): 使用
response_format: { type: "json_schema", json_schema: { ... } }。此方法通过受限解码启用 Structured Outputs,并保证输出符合架构。 - Anthropic(Claude): 定义一个以目标架构作为
input_schema的工具,并设置tool_choice: { type: "tool", name: "extract_data" }。Claude 会在tool_use内容块中返回结构化数据。 - Google(Gemini): 使用带有 JSON Schema 对象的
generationConfig.responseSchema,并设置responseMimeType: "application/json"。 - 本地模型(llama.cpp、vLLM): 使用 GBNF 文法或
--json-schema标志,在 token 级别进行受限解码。
- OpenAI(gpt-4o、gpt-4o-mini): 使用
-
使用用户所用的语言编写架构定义。对于 Python,定义一个 Pydantic
BaseModel。对于 TypeScript,定义 Zod 架构,并通过zodResponseFormat()转换。对于原始 API 调用,直接编写 JSON Schema。 -
在架构中添加字段级描述。每个字段都应包含一个
description字符串,告诉模型应在该字段中填写什么。模型会将这些描述视为隐式提示词指令——例如,与不带上下文的sentiment: str相比,将字段描述为"The user's sentiment as positive, negative, or neutral"能得到更好的结果。 -
设置系统提示词以强化结构要求。告诉模型,它的任务是提取数据,而不是对话。示例:
"You are a data extraction system. Analyze the input and return the requested fields. Do not include explanations outside the JSON structure." -
如果使用 OpenAI 的
json_schema模式,请在架构定义中设置"strict": true。这会启用受限解码,模型只能输出符合架构的 token。没有strict: true时,模型仍可能生成无效 JSON。 -
如果使用 Anthropic 的 tool_use 方法,请在
response.content中找到type == "tool_use"的块,并读取其input字段,以提取结构化数据。不要解析文本块——结构化数据仅存在于 tool_use 块中。 -
在应用程序代码中根据架构验证响应。即使使用了受限解码,在将数据传递给下游前,也要使用 Pydantic 的
model_validate()或 Zod 的.parse()进行验证。这样可以捕获仅靠架构符合性无法避免的语义问题(例如空字符串、超出范围的数字)。 -
为验证失败构建重试循环。验证失败时,将原始输入、失败的输出和验证错误一并发送回模型,并附上类似
"Your previous output failed validation: {error}. Fix the output."的指令。最多重试 3 次。 -
记录架构和模型版本、请求 ID、验证结果以及经过脱敏的诊断样本。只有在获得授权且确有必要时,才保留原始输入/响应,并设置访问控制和保留期限;切勿在常规日志中记录密钥或敏感源文档。当结构化输出在生产环境中出错时,你需要通过这些日志判断问题是架构设计、提示词还是模型回归导致的。
示例
示例 1:使用 Pydantic 的 OpenAI Structured Outputs(Python)
from pydantic import BaseModel, Field
from openai import OpenAI
from enum import Enum
class Sentiment(str, Enum):
positive = "positive"
negative = "negative"
neutral = "neutral"
class ReviewAnalysis(BaseModel):
sentiment: Sentiment = Field(description="Overall sentiment of the review")
key_topics: list[str] = Field(max_length=5, description="Main topics mentioned, max 5")
purchase_intent: bool = Field(description="Whether the reviewer would buy again")
confidence_score: float = Field(ge=0.0, le=1.0, description="Model confidence 0-1")
client = OpenAI()
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extract structured review analysis."},
{"role": "user", "content": "This laptop is amazing. The battery lasts forever and the keyboard feels great. Definitely buying the next version."}
],
response_format=ReviewAnalysis,
)
message = response.choices[0].message
if message.refusal or message.parsed is None:
raise ValueError("Refused or incomplete extraction; do not write downstream")
result = message.parsed
# result.sentiment == Sentiment.positive
# result.key_topics == ["battery life", "keyboard"]
# result.purchase_intent == True
示例 2:用于结构化提取的 Anthropic tool_use(Python)
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="You are a data extraction system. Use the provided tool to return structured data.",
tools=[{
"name": "extract_invoice",
"description": "Extract invoice fields from text",
"input_schema": {
"type": "object",
"properties": {
"vendor_name": {"type": "string", "description": "Company that issued the invoice"},
"total_amount": {"type": "number", "description": "Total amount in USD"},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "integer"},
"unit_price": {"type": "number"}
},
"required": ["description", "quantity", "unit_price"]
}
}
},
"required": ["vendor_name", "total_amount", "line_items"]
}
}],
tool_choice={"type": "tool", "name": "extract_invoice"},
messages=[{"role": "user", "content": "Invoice from Acme Corp: 3x Widget A at $10 each, 1x Widget B at $25. Total: $55."}]
)
# Find the tool_use block — do NOT parse text blocks
blocks = [b for b in response.content if b.type == "tool_use" and b.name == "extract_invoice"]
if response.stop_reason != "tool_use" or len(blocks) != 1:
raise ValueError("Missing, ambiguous or truncated extraction")
invoice = blocks[0].input # Validate against the invoice schema before any downstream use.
# invoice["vendor_name"] == "Acme Corp"
# invoice["total_amount"] == 55.0
示例 3:使用 Zod + zodResponseFormat 的 TypeScript
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
const EventSchema = z.object({
event_name: z.string().describe("Name of the event"),
date: z.string().describe("ISO 8601 date string"),
location: z.string().describe("City and venue"),
attendee_count: z.number().int().describe("Expected number of attendees"),
is_virtual: z.boolean().describe("Whether the event is online-only"),
});
const client = new OpenAI();
const completion = await client.beta.chat.completions.parse({
model: "gpt-4o-2024-08-06",
messages: [
{ role: "system", content: "Extract event details from the text." },
{ role: "user", content: "Tech Summit 2025 in Austin at the Convention Center on March 15th. Expecting 2000 attendees, in-person only." },
],
response_format: zodResponseFormat(EventSchema, "event_extraction"),
});
const message = completion.choices[0].message;
if (message.refusal || !message.parsed) throw new Error("Refused or incomplete extraction");
const event = EventSchema.parse(message.parsed);
// event.event_name === "Tech Summit 2025"
// event.is_virtual === false
绝对不要这样做
-
绝对不要在未提供 schema 的情况下使用
response_format: { type: "json_object" }。 这是 OpenAI 的旧版 JSON 模式——它只保证 JSON 语法有效,并不保证符合 schema。当你期望{"name": str, "age": int}时,模型可能返回{"result": "hello"}。请始终改用json_schema并提供完整的 schema 定义。 -
绝对不要解析 Anthropic 的 text block 来提取结构化数据。 当使用
tool_choice强制结构化输出时,数据位于tool_usecontent block 中,而不在任何textblock 中。解析response.content[0].text要么返回空字符串,要么返回对话式的开场白——绝不会返回你需要的数据。 -
绝对不要定义没有 description 的 schema 字段。 一个名为
status且没有 description 的字段可能表示 HTTP 状态、订单状态或审核状态。模型会将字段 description 作为提取指令。省略它们等同于省略一半的提示词。 -
绝对不要在 strict 模式的 schema 中使用
additionalProperties: true。 OpenAI 的 strict 模式要求 schema 中每个对象都设置additionalProperties: false。如果你将其设为 true 或省略它,API 会在请求时以 400 错误拒绝请求,而不是在响应时——你根本得不到任何响应。 -
绝对不要把提取指令只放在用户消息中而不放在系统提示词中。 系统提示词对行为指令具有更高的注意力权重。把“提取以下字段”只放在用户消息中并与源文本放在一起,会迫使模型在指令和数据之间分散注意力。系统提示词定义行为;用户消息提供输入数据。
-
绝对不要假设结构化输出就意味着正确的输出。 约束解码保证响应符合 schema 的类型和结构。它并不保证值是正确的。如果源文本含糊不清,模型可能会对一条负面评论返回
{"sentiment": "positive"}。请务必在 schema 校验之后于应用代码中验证语义。 -
绝对不要在没有测试的情况下使用递归或深层嵌套的 schema。 递归类型(
$ref指向同一定义)以及深度超过 3 层的 schema 会显著增加解码延迟,并提高模型在完成 JSON 结构之前达到 max_tokens 的概率。请尽可能展平嵌套的 schema。
边缘情况
-
源文本过长,超出上下文窗口。 输入文本过长时,模型可能会截断读取内容,导致提取结果不完整。请将长文档拆分成多个片段,分别提取后,再在应用代码中合并结果。不要指望模型一次调用就能处理 50 页的文档。
-
模型返回
refusal,而不是结构化数据。 当模型认为请求不安全时,OpenAI 的结构化输出可能会返回refusal字段。在访问.parsed前,请检查response.choices[0].message.refusal。如果refusal不为 None,解析后的数据将为 None,访问该数据会引发错误。 -
数据存在时,数组字段却返回空值。 即使源文本中包含相关数据,如果字段描述过于含糊,模型有时仍会对数组字段返回
[]。请将描述写得更明确,例如:"文本中提及的所有产品名称列表。如果提及了任何产品,至少返回一个。"。 -
枚举值因大小写不匹配。 如果你将枚举定义为
["Active", "Inactive"],但模型返回"active",验证就会失败。请将架构中的所有枚举值改为小写,或在验证前添加规范化步骤。OpenAI 的严格模式会严格匹配大小写;Anthropic 则可能不会。 -
使用结构化输出进行流式处理。 OpenAI 支持结构化输出流式处理,JSON 片段会逐块到达。中间片段无法作为有效 JSON 解析。请使用
openaiSDK 内置的增量解析功能,或将片段缓冲至流结束。Anthropic 的工具输入会以input_json_delta片段到达。请按块索引累积partial_json,并在content_block_stop后解析;也可以使用 SDK 的最终消息累加器。
最佳实践
-
从能解决问题的最简单架构开始。 包含 3-5 个字段的扁平对象,准确率高于包含 20 多个字段的嵌套架构。如果需要处理复杂数据,可分两步提取:先提取顶层实体,再分别调用一次,为每个实体提取详细信息。
-
分类数据使用枚举,而不是自由格式字符串。
mood: str字段可以返回任意内容;mood: Literal["happy", "sad", "neutral", "angry"]则将模型的输出限制为这些确切值。这样可以将下游解析逻辑降至零。 -
在生产环境中固定模型版本。
gpt-4o是一个别名,OpenAI 发布新版本时可能会发生变化。结构化输出的行为可能因版本而异。请明确使用gpt-4o-2024-08-06,以确保架构与提示词的组合保持稳定,直到你主动升级。 -
部署前,使用 20 个以上的真实输入测试架构变更。 架构变更(添加字段、更改类型、修改描述)可能导致此前有效的输入无法提取。请使用带有预期输出的真实输入构建测试套件,并在每次架构变更时运行。这相当于结构化输出的单元测试。
-
为可选字段在 Pydantic 模型中使用
default值。 如果源文本中可能没有相关数据,请在提供方架构中使用必需的可空字段(例如str | None或.nullable()),并指示模型对未知事实返回 null。OpenAI 的严格架构要求required包含所有属性;缺失或可选的键与可空值并不相同。验证通过后,再将 null 映射为应用默认值。 -
将提取架构与应用架构分开。 LLM 提取架构应符合模型能够可靠生成的内容。应用数据库架构可能还有其他计算字段、外键或约束。请在应用代码中进行映射——不要强迫 LLM 理解你的数据库架构。
限制
- 仅当任务明确符合上述范围时,才使用此技能。
- 不要将输出视为特定环境验证、测试或专家评审的替代品。
- 如果缺少必需的输入、权限、安全边界或成功标准,请停止并请求澄清。