03 · Structured Output#

LLM 输出的是自然语言,但程序需要的是结构化数据(JSON、字典、对象)。 如何可靠地从 LLM 获取结构化输出,是生产环境中最常见的挑战之一。

本章介绍三种方案,从简单到严格:

  1. Prompt 约束法 — 用提示词要求输出 JSON

  2. JSON Mode — API 级别强制输出合法 JSON

  3. Function Calling — 定义 schema,模型按 schema 填写

每种方案都会展示如何用 Pydantic 验证输出。

import json
import litellm
import os
from dotenv import load_dotenv
from utils.llm_client import chat

load_dotenv()
True

1. Prompt 约束法#

最简单的方式:在 prompt 里要求模型输出 JSON,并给出 schema 示例。

优点: 所有模型通用,无需特殊 API 缺点: 模型可能在 JSON 前后加多余文字,需要额外解析

def extract_json(text: str) -> dict:
    """从模型输出中提取 JSON,容忍前后有多余文字。"""
    # 找第一个 { 到最后一个 }
    start = text.find("{")
    end   = text.rfind("}") + 1
    if start == -1 or end == 0:
        raise ValueError(f"No JSON found in: {text!r}")
    return json.loads(text[start:end])


review = "这款耳机音质非常好,低音有力,佩戴也很舒适,就是价格略贵,充电仓做工一般。总体推荐。"

prompt = f"""分析以下产品评论,以 JSON 格式输出,字段说明如下:
- sentiment: 情感倾向(正面/负面/中性)
- score: 评分 1-5
- pros: 优点列表
- cons: 缺点列表
- recommend: 是否推荐(true/false)

只输出 JSON,不要其他文字。

评论:{review}"""

raw = chat(prompt, temperature=0)
print("原始输出:")
print(raw.strip())

data = extract_json(raw)
print("\n解析结果:")
print(json.dumps(data, ensure_ascii=False, indent=2))
原始输出:
{
  "sentiment": "正面",
  "score": 4,
  "pros": [
    "音质非常好",
    "低音有力",
    "佩戴舒适"
  ],
  "cons": [
    "价格略贵",
    "充电仓做工一般"
  ],
  "recommend": true
}

解析结果:
{
  "sentiment": "正面",
  "score": 4,
  "pros": [
    "音质非常好",
    "低音有力",
    "佩戴舒适"
  ],
  "cons": [
    "价格略贵",
    "充电仓做工一般"
  ],
  "recommend": true
}

2. JSON Mode#

OpenAI、部分其他 provider 支持 response_format={"type": "json_object"}, 在 API 层面保证输出是合法 JSON(不会有多余文字)。

注意: JSON Mode 只保证合法 JSON,不保证字段完整——仍需 Pydantic 验证。

articles = [
    "OpenAI 于 2024 年发布了 GPT-4o,这是一个多模态模型,支持文字、图像和音频输入。",
    "苹果公司在 WWDC 2024 上发布了 Apple Intelligence,将 AI 功能集成到 iOS 18 中。",
    "谷歌 DeepMind 的 AlphaFold 3 能够预测几乎所有生物分子的结构,2024年荣获诺贝尔化学奖。",
]

system_prompt = """从新闻文章中提取结构化信息。
输出包含以下字段的 JSON:
- company: 公司名称
- product: 产品或技术名称
- year: 年份(整数)
- category: 类别(如:大语言模型/多模态/生物AI/其他)
- summary: 一句话摘要"""

for article in articles:
    response = litellm.completion(
        model=os.getenv("LLM_MODEL"),
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": article},
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )
    data = json.loads(response.choices[0].message.content)
    print(json.dumps(data, ensure_ascii=False, indent=2))
    print()
{
  "company": "OpenAI",
  "product": "GPT-4o",
  "year": 2024,
  "category": "多模态",
  "summary": "OpenAI 于 2024 年发布 GPT-4o,这是一个多模态模型,支持文字、图像和音频输入。"
}
{
  "company": "苹果公司",
  "product": "Apple Intelligence",
  "year": 2024,
  "category": "多模态",
  "summary": "苹果公司在 WWDC 2024 推出 Apple Intelligence,将多模态 AI 功能集成到 iOS 18 中。"
}
{
  "company": "谷歌 DeepMind",
  "product": "AlphaFold 3",
  "year": 2024,
  "category": "生物AI",
  "summary": "AlphaFold 3 能够预测几乎所有生物分子的结构,并在2024年因该成果荣获诺贝尔化学奖。"
}

3. Function Calling#

Function Calling(也叫 Tool Use)是目前最严格的结构化输出方式:

  • 你定义一个 JSON Schema 描述数据结构

  • 模型按 schema 填写,字段类型有保证

  • 适合需要精确控制输出格式的生产场景

# 定义 schema:从简历文本中提取结构化信息
resume_schema = {
    "type": "function",
    "function": {
        "name": "extract_resume",
        "description": "从简历文本中提取结构化信息",
        "parameters": {
            "type": "object",
            "properties": {
                "name":       {"type": "string", "description": "姓名"},
                "email":      {"type": "string", "description": "邮箱"},
                "years_exp":  {"type": "number", "description": "工作年限"},
                "skills":     {"type": "array", "items": {"type": "string"}, "description": "技能列表"},
                "education":  {"type": "string", "description": "最高学历"},
                "languages":  {"type": "array", "items": {"type": "string"}, "description": "编程语言"},
            },
            "required": ["name", "years_exp", "skills"],
        },
    },
}

resume_text = """
张伟,毕业于上海交通大学计算机系(本科),邮箱:zhangwei@example.com
5年后端开发经验,熟悉 Python、Go、Java,
擅长分布式系统、微服务架构、Kubernetes 容器编排。
有丰富的 LLM 应用开发经验,做过 RAG 系统和 AI Agent。
""".strip()

response = litellm.completion(
    model=os.getenv("LLM_MODEL"),
    messages=[{"role": "user", "content": resume_text}],
    tools=[resume_schema],
    tool_choice={"type": "function", "function": {"name": "extract_resume"}},
    temperature=0,
)

tool_call = response.choices[0].message.tool_calls[0]
extracted = json.loads(tool_call.function.arguments)
print(json.dumps(extracted, ensure_ascii=False, indent=2))
{
  "name": "张伟",
  "email": "zhangwei@example.com",
  "years_exp": 5,
  "skills": [
    "分布式系统",
    "微服务架构",
    "Kubernetes",
    "RAG 系统",
    "AI Agent",
    "LLM 应用开发",
    "Python",
    "Go",
    "Java"
  ],
  "education": "上海交通大学 计算机系 本科",
  "languages": [
    "Python",
    "Go",
    "Java"
  ]
}

4. Pydantic 验证#

无论用哪种方法获取 JSON,都应该用 Pydantic 验证:

  • 字段类型检查

  • 缺失字段检测

  • 自动类型转换

  • 清晰的错误信息

from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional

class Resume(BaseModel):
    name:       str
    email:      Optional[str] = None
    years_exp:  float = Field(ge=0, description="工作年限,必须 >= 0")
    skills:     List[str]
    education:  Optional[str] = None
    languages:  List[str] = []

# 验证 Function Calling 的输出
resume = Resume(**extracted)
print("验证通过!")
print(f"姓名:     {resume.name}")
print(f"工作年限: {resume.years_exp} 年")
print(f"技能:     {', '.join(resume.skills)}")
print(f"编程语言: {', '.join(resume.languages)}")
print(f"\nPydantic model dict:")
print(resume.model_dump_json(indent=2))
验证通过!
姓名:     张伟
工作年限: 5.0 年
技能:     分布式系统, 微服务架构, Kubernetes, RAG 系统, AI Agent, LLM 应用开发, Python, Go, Java
编程语言: Python, Go, Java

Pydantic model dict:
{
  "name": "张伟",
  "email": "zhangwei@example.com",
  "years_exp": 5.0,
  "skills": [
    "分布式系统",
    "微服务架构",
    "Kubernetes",
    "RAG 系统",
    "AI Agent",
    "LLM 应用开发",
    "Python",
    "Go",
    "Java"
  ],
  "education": "上海交通大学 计算机系 本科",
  "languages": [
    "Python",
    "Go",
    "Java"
  ]
}
# Pydantic 会捕获错误
from pydantic import ValidationError

bad_data = [
    {"name": "张伟", "years_exp": -1, "skills": []},   # years_exp < 0
    {"years_exp": 3, "skills": ["Python"]},             # 缺少 name
    {"name": "李四", "years_exp": "五年", "skills": []}, # 类型错误(字符串→数字)
]

for data in bad_data:
    try:
        Resume(**data)
    except ValidationError as e:
        errors = e.errors()
        print(f"数据: {data}")
        print(f"错误: {errors[0]['loc']} - {errors[0]['msg']}\n")
数据: {'name': '张伟', 'years_exp': -1, 'skills': []}
错误: ('years_exp',) - Input should be greater than or equal to 0

数据: {'years_exp': 3, 'skills': ['Python']}
错误: ('name',) - Field required

数据: {'name': '李四', 'years_exp': '五年', 'skills': []}
错误: ('years_exp',) - Input should be a valid number, unable to parse string as a number

5. 端到端示例:结构化信息提取流水线#

把以上所有技术组合成一个可复用的提取函数。

from pydantic import BaseModel
from typing import List, Optional, Type, TypeVar

T = TypeVar("T", bound=BaseModel)

def extract_structured(
    text: str,
    schema: Type[T],
    instructions: str = "",
) -> T:
    """
    从文本中提取结构化数据,用 Pydantic schema 验证。
    自动生成 JSON 格式要求,通过 JSON Mode 获取输出。
    """
    # 根据 Pydantic schema 生成字段说明
    fields = schema.model_fields
    field_desc = "\n".join(
        f"- {name}: {field.description or field.annotation}"
        for name, field in fields.items()
    )

    system = f"""从文本中提取信息,以 JSON 格式输出。
{instructions}

字段说明:
{field_desc}"""

    response = litellm.completion(
        model=os.getenv("LLM_MODEL"),
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": text},
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )

    raw = json.loads(response.choices[0].message.content)
    return schema(**raw)


# 示例:从新闻提取事件信息
class NewsEvent(BaseModel):
    headline:  str   = Field(description="新闻标题(一句话)")
    who:       str   = Field(description="主要人物或组织")
    what:      str   = Field(description="发生了什么")
    when:      Optional[str] = Field(None, description="时间")
    where:     Optional[str] = Field(None, description="地点")
    impact:    str   = Field(description="潜在影响")

news = """
2024年12月,谷歌 DeepMind 发布了新一代 AI 模型 Gemini 2.0,
该模型支持原生多模态输入输出,包括文字、图像、音频和视频。
这是谷歌在 AI 领域对抗 OpenAI 的最新举措,
预计将对企业 AI 采购决策产生重大影响。
""".strip()

event = extract_structured(news, NewsEvent)
print(event.model_dump_json(indent=2, ensure_ascii=False))
{
  "headline": "谷歌 DeepMind 发布支持原生多模态的 Gemini 2.0 AI 模型",
  "who": "谷歌 DeepMind(与 OpenAI 的竞争背景)",
  "what": "发布新一代 AI 模型 Gemini 2.0,支持原生多模态输入输出(文字、图像、音频、视频),作为对抗 OpenAI 的最新举措。",
  "when": "2024年12月",
  "where": "未具体说明(由谷歌 DeepMind 对外发布,面向全球)",
  "impact": "预计将显著影响企业 AI 采购决策,推动多模态应用部署并加剧谷歌与 OpenAI 之间的竞争,可能重塑 AI 产品与服务市场格局。"
}

小结#

方案

可靠性

通用性

适用场景

Prompt 约束

⭐⭐

✅ 所有模型

原型验证、简单提取

JSON Mode

⭐⭐⭐

✅ 主流模型

生产环境通用方案

Function Calling

⭐⭐⭐⭐

⚠️ 需支持 tool use

精确 schema 控制

Pydantic 验证

永远都要加,不管用哪种方案

最佳实践: JSON Mode + Pydantic 是目前生产环境最稳健的组合。


Module 2 · Prompting 完结 🎉

下一模块 → Module 3: API 实战:Streaming、Function Calling 深度使用、Vision 多模态