mcpField
L
llm-structured-output

llm-structured-output

github.com/sickn33开发48.9k75 次安装今天更新
代码仓库

关于 llm-structured-output

面向开发者的 Skill,用于从 LLM 响应中提取经过类型约束和验证的数据。它介绍各平台的结构化输出方法、Schema 验证与重试处理,并说明下游使用时的防护措施。

主要功能

  1. 01OpenAI JSON Schema 输出
  2. 02Anthropic tool_use 提取
  3. 03Gemini responseSchema
  4. 04本地约束解码
  5. 05Schema 验证与重试
  6. 06生产环境防护

适用场景

  1. 01将文本字段提取为 JSON
  2. 02把验证后的 LLM 数据传入代码
  3. 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 块提取数据(而非实际执行工具)
  • 用户询问如何使用 openai npm 包中的 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 对严格输出的支持及模式子集各不相同。符合模式的值仍是不可信数据,绝不能作为数据库写入或外部调用的授权。请将源文档中的指令与提取指令分开,并在应用代码中强制执行允许的操作。

核心工作流

  1. 确定目标架构。询问用户需要提取哪些字段。为每个字段定义类型、是否必填,以及适用时的有效枚举值。没有明确的架构时,不要继续。

  2. 选择适合相应提供方的方法:

    • 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 级别进行受限解码。
  3. 使用用户所用的语言编写架构定义。对于 Python,定义一个 Pydantic BaseModel。对于 TypeScript,定义 Zod 架构,并通过 zodResponseFormat() 转换。对于原始 API 调用,直接编写 JSON Schema。

  4. 在架构中添加字段级描述。每个字段都应包含一个 description 字符串,告诉模型应在该字段中填写什么。模型会将这些描述视为隐式提示词指令——例如,与不带上下文的 sentiment: str 相比,将字段描述为 "The user's sentiment as positive, negative, or neutral" 能得到更好的结果。

  5. 设置系统提示词以强化结构要求。告诉模型,它的任务是提取数据,而不是对话。示例:"You are a data extraction system. Analyze the input and return the requested fields. Do not include explanations outside the JSON structure."

  6. 如果使用 OpenAI 的 json_schema 模式,请在架构定义中设置 "strict": true。这会启用受限解码,模型只能输出符合架构的 token。没有 strict: true 时,模型仍可能生成无效 JSON。

  7. 如果使用 Anthropic 的 tool_use 方法,请在 response.content 中找到 type == "tool_use" 的块,并读取其 input 字段,以提取结构化数据。不要解析文本块——结构化数据仅存在于 tool_use 块中。

  8. 在应用程序代码中根据架构验证响应。即使使用了受限解码,在将数据传递给下游前,也要使用 Pydantic 的 model_validate() 或 Zod 的 .parse() 进行验证。这样可以捕获仅靠架构符合性无法避免的语义问题(例如空字符串、超出范围的数字)。

  9. 为验证失败构建重试循环。验证失败时,将原始输入、失败的输出和验证错误一并发送回模型,并附上类似 "Your previous output failed validation: {error}. Fix the output." 的指令。最多重试 3 次。

  10. 记录架构和模型版本、请求 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

绝对不要这样做

  1. 绝对不要在未提供 schema 的情况下使用 response_format: { type: "json_object" }。 这是 OpenAI 的旧版 JSON 模式——它只保证 JSON 语法有效,并不保证符合 schema。当你期望 {"name": str, "age": int} 时,模型可能返回 {"result": "hello"}。请始终改用 json_schema 并提供完整的 schema 定义。

  2. 绝对不要解析 Anthropic 的 text block 来提取结构化数据。 当使用 tool_choice 强制结构化输出时,数据位于 tool_use content block 中,而不在任何 text block 中。解析 response.content[0].text 要么返回空字符串,要么返回对话式的开场白——绝不会返回你需要的数据。

  3. 绝对不要定义没有 description 的 schema 字段。 一个名为 status 且没有 description 的字段可能表示 HTTP 状态、订单状态或审核状态。模型会将字段 description 作为提取指令。省略它们等同于省略一半的提示词。

  4. 绝对不要在 strict 模式的 schema 中使用 additionalProperties: true。 OpenAI 的 strict 模式要求 schema 中每个对象都设置 additionalProperties: false。如果你将其设为 true 或省略它,API 会在请求时以 400 错误拒绝请求,而不是在响应时——你根本得不到任何响应。

  5. 绝对不要把提取指令只放在用户消息中而不放在系统提示词中。 系统提示词对行为指令具有更高的注意力权重。把“提取以下字段”只放在用户消息中并与源文本放在一起,会迫使模型在指令和数据之间分散注意力。系统提示词定义行为;用户消息提供输入数据。

  6. 绝对不要假设结构化输出就意味着正确的输出。 约束解码保证响应符合 schema 的类型和结构。它并不保证值是正确的。如果源文本含糊不清,模型可能会对一条负面评论返回 {"sentiment": "positive"}。请务必在 schema 校验之后于应用代码中验证语义。

  7. 绝对不要在没有测试的情况下使用递归或深层嵌套的 schema。 递归类型($ref 指向同一定义)以及深度超过 3 层的 schema 会显著增加解码延迟,并提高模型在完成 JSON 结构之前达到 max_tokens 的概率。请尽可能展平嵌套的 schema。

边缘情况

  1. 源文本过长,超出上下文窗口。 输入文本过长时,模型可能会截断读取内容,导致提取结果不完整。请将长文档拆分成多个片段,分别提取后,再在应用代码中合并结果。不要指望模型一次调用就能处理 50 页的文档。

  2. 模型返回 refusal,而不是结构化数据。 当模型认为请求不安全时,OpenAI 的结构化输出可能会返回 refusal 字段。在访问 .parsed 前,请检查 response.choices[0].message.refusal。如果 refusal 不为 None,解析后的数据将为 None,访问该数据会引发错误。

  3. 数据存在时,数组字段却返回空值。 即使源文本中包含相关数据,如果字段描述过于含糊,模型有时仍会对数组字段返回 []。请将描述写得更明确,例如:"文本中提及的所有产品名称列表。如果提及了任何产品,至少返回一个。"。

  4. 枚举值因大小写不匹配。 如果你将枚举定义为 ["Active", "Inactive"],但模型返回 "active",验证就会失败。请将架构中的所有枚举值改为小写,或在验证前添加规范化步骤。OpenAI 的严格模式会严格匹配大小写;Anthropic 则可能不会。

  5. 使用结构化输出进行流式处理。 OpenAI 支持结构化输出流式处理,JSON 片段会逐块到达。中间片段无法作为有效 JSON 解析。请使用 openai SDK 内置的增量解析功能,或将片段缓冲至流结束。Anthropic 的工具输入会以 input_json_delta 片段到达。请按块索引累积 partial_json,并在 content_block_stop 后解析;也可以使用 SDK 的最终消息累加器。

最佳实践

  1. 从能解决问题的最简单架构开始。 包含 3-5 个字段的扁平对象,准确率高于包含 20 多个字段的嵌套架构。如果需要处理复杂数据,可分两步提取:先提取顶层实体,再分别调用一次,为每个实体提取详细信息。

  2. 分类数据使用枚举,而不是自由格式字符串。 mood: str 字段可以返回任意内容;mood: Literal["happy", "sad", "neutral", "angry"] 则将模型的输出限制为这些确切值。这样可以将下游解析逻辑降至零。

  3. 在生产环境中固定模型版本。 gpt-4o 是一个别名,OpenAI 发布新版本时可能会发生变化。结构化输出的行为可能因版本而异。请明确使用 gpt-4o-2024-08-06,以确保架构与提示词的组合保持稳定,直到你主动升级。

  4. 部署前,使用 20 个以上的真实输入测试架构变更。 架构变更(添加字段、更改类型、修改描述)可能导致此前有效的输入无法提取。请使用带有预期输出的真实输入构建测试套件,并在每次架构变更时运行。这相当于结构化输出的单元测试。

  5. 为可选字段在 Pydantic 模型中使用 default 值。 如果源文本中可能没有相关数据,请在提供方架构中使用必需的可空字段(例如 str | None 或 .nullable()),并指示模型对未知事实返回 null。OpenAI 的严格架构要求 required 包含所有属性;缺失或可选的键与可空值并不相同。验证通过后,再将 null 映射为应用默认值。

  6. 将提取架构与应用架构分开。 LLM 提取架构应符合模型能够可靠生成的内容。应用数据库架构可能还有其他计算字段、外键或约束。请在应用代码中进行映射——不要强迫 LLM 理解你的数据库架构。

限制

  • 仅当任务明确符合上述范围时,才使用此技能。
  • 不要将输出视为特定环境验证、测试或专家评审的替代品。
  • 如果缺少必需的输入、权限、安全边界或成功标准,请停止并请求澄清。

评论

支持 Markdown