为什么结构化输出如此重要

LLM的默认输出是自由文本,但实际工程中我们几乎总是需要结构化数据——API调用需要JSON参数、数据抽取需要表格、Agent决策需要指令。结构化输出的可靠性直接决定了系统能否自动化运行。

目前主流的三种结构化输出方案各有优劣,选对场景才能发挥最大价值。

方案一:JSON Mode

原理与用法

JSON Mode是OpenAI率先引入的功能,强制模型输出合法JSON。它的原理是在解码阶段约束token采样,确保输出符合JSON语法。

from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "你是数据抽取助手,输出JSON格式。"},
        {"role": "user", "content": "抽取以下文本的人名和职位:张三是CEO,李四是CTO。"}
    ],
    response_format={"type": "json_object"}
)

import json
result = json.loads(response.choices[0].message.content)
# {"people": [{"name": "张三", "title": "CEO"}, {"name": "李四", "title": "CTO"}]}

局限性

JSON Mode只保证语法合法,不保证结构符合预期。它无法约束字段名、类型和嵌套层级。你需要额外验证:

from pydantic import BaseModel, ValidationError

class PersonInfo(BasestModel):
    name: str
    title: str

class ExtractionResult(BaseModel):
    people: list[PersonInfo]

def safe_parse(raw_json: str) -> dict | None:
    try:
        data = json.loads(raw_json)
        return ExtractionResult(**data).model_dump()
    except (json.JSONDecodeError, ValidationError) as e:
        # 记录错误,触发修复流程
        logger.warning(f"Schema validation failed: {e}")
        return None

方案二:Function Calling

设计理念

Function Calling让模型"知道"有哪些可用函数及其参数schema,模型负责生成符合schema的调用参数。这是一种约束生成方式,比JSON Mode更严格。

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_database",
            "description": "在产品数据库中搜索",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"},
                    "category": {
                        "type": "string",
                        "enum": ["电子产品", "服装", "食品"],
                        "description": "限定品类"
                    },
                    "limit": {"type": "integer", "minimum": 1, "maximum": 50}
                },
                "required": ["query"]
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "帮我找3个电子产品类的手机"}],
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "search_database"}}
)

# 模型生成结构化参数
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
# {"query": "手机", "category": "电子产品", "limit": 3}

多函数编排

Function Calling的真正威力在于多函数编排和模型自主决策调用链:

class FunctionOrchestrator:
    def __init__(self, functions: dict):
        self.functions = functions  # name -> callable

    async def run(self, query: str, max_rounds: int = 5):
        messages = [{"role": "user", "content": query}]
        tools = self._build_tool_schemas()

        for round_idx in range(max_rounds):
            resp = await client.chat.completions.create(
                model="gpt-4o", messages=messages, tools=tools
            )
            msg = resp.choices[0].message
            messages.append(msg)

            if not msg.tool_calls:
                return msg.content  # 模型决定直接回复

            for tc in msg.tool_calls:
                fn = self.functions.get(tc.function.name)
                if fn:
                    result = await fn(**json.loads(tc.function.arguments))
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": str(result)
                    })

        return "达到最大轮次限制"

方案三:Schema约束生成

约束解码

这是最严格的方法——在解码阶段直接约束token采样,确保输出100%符合JSON Schema。outlinesguidance是代表性框架:

from pydantic import BaseModel
from outlines import models, generate

class PersonExtraction(BaseModel):
    name: str
    age: int
    occupation: str
    skills: list[str]

model = models.transformers("Qwen/Qwen2.5-7B-Instruct")
generator = generate.json(model, PersonExtraction)

# 输出100%符合schema,无需重试
result = generator("张三,30岁,软件工程师,擅长Python和Go")
# PersonExtraction(name="张三", age=30, occupation="软件工程师", skills=["Python", "Go"])

约束解码原理

约束解码的核心是在每一步token采样时,计算当前允许的token集合

# 简化示意:FSM状态机驱动的token mask
class JSONSchemaFSM:
    def __init__(self, schema: dict):
        self.states = self._build_states(schema)
        self.current_state = 0

    def allowed_next_tokens(self, tokenizer) -> set[int]:
        """返回当前状态下合法的token ID集合"""
        legal = set()
        for transition in self.states[self.current_state]:
            for token_str in transition.accepts:
                legal.update(tokenizer.encode(token_str))
        return legal

    def transition(self, token_str: str):
        """根据生成的token推进状态机"""
        for transition in self.states[self.current_state]:
            if token_str in transition.accepts:
                self.current_state = transition.target
                return

三方案对比

维度 JSON Mode Function Calling Schema约束
保证级别 语法合法 参数结构合规 100% Schema合规
模型支持 广泛 主流闭源模型 开源模型为主
灵活性 低(需预定义schema)
延迟开销 几乎无 轻微 有(约束解码)
重试率 中等
适用场景 简单抽取 Agent工具调用 严格数据生成

实践建议

  1. 简单抽取任务:用JSON Mode + Pydantic验证,失败重试最多3次
  2. Agent工具调用:用Function Calling,配合tool_choice控制调用时机
  3. 离线批量处理:用Schema约束生成,零重试零浪费
  4. 混合策略:在线服务用Function Calling保证体验,离线验证用Schema约束兜底
# 混合策略示例
async def robust_extract(text: str) -> dict:
    # 第一轮:快速JSON Mode尝试
    result = await try_json_mode(text)
    if result and validate(result):
        return result

    # 第二轮:降级到Function Calling
    result = await try_function_call(text)
    if result:
        return result

    # 第三轮:Schema约束兜底
    return await try_constrained(text)

总结

结构化输出不是一个简单的"让模型输出JSON"的问题,而是约束强度与灵活性之间的工程权衡。理解三种方案的原理和边界,才能在不同场景下做出正确选择。核心原则:约束越强,可靠性越高,但灵活性越低。根据业务对可靠性的要求选择合适的约束级别。