课程0基础Agent开发课 / API入门与模型调用 / 结构化输出与预填充技巧
— 11 min read

结构化输出与预填充技巧

LLM 的默认输出是自由文本。但在实际应用中,经常需要模型输出 JSON(一种常见的数据格式,用大括号和键值对组织数据,例如 `{"name": "张三", "age": 25}`)、特定格式的数据,或者以特定开头开始回答。这篇讲几种让模型输出可预测格式的技术:Prompt 约束、JSON 模式(response_format)、以及 pydantic 验证。

结构化输出与 JSON 模式

LLM 的默认输出是自由文本。但在实际应用中,经常需要模型输出 JSON(一种常见的数据格式,用大括号和键值对组织数据,例如 {"name": "张三", "age": 25})、特定格式的数据,或者以特定开头开始回答。这篇讲几种让模型输出可预测格式的技术:Prompt 约束、JSON 模式(response_format)、以及 pydantic 验证。

为什么需要这一篇:当 LLM 的输出需要被程序处理时,自由文本是个障碍——你必须解析它,而解析自由文本既麻烦又脆弱。结构化输出让 LLM 成为可靠的数据处理工具,直接 json.loads() 就完事了。

为什么需要结构化输出

结构化输出方式对比图
JSON Mode、Function Calling、预填充三种结构化输出方式的可靠性与适用场景对比

python
# 这样的输出没法直接用
response = "好的,根据你的描述,这个产品的评分是 4.5 分,主要优点是性价比高,缺点是发货慢。"

# 这样的输出可以直接解析
response = '{"score": 4.5, "pros": ["性价比高"], "cons": ["发货慢"]}'

当 LLM 的输出需要被程序处理(存入数据库、传给下一个步骤、渲染成特定格式)时,自由文本是个障碍。结构化输出让 LLM 成为可靠的数据处理工具。

方法一:Prompt 约束

最简单的方法:在 Prompt 中明确要求 JSON 格式,并给出示例。

python
import os
import json
from openai import OpenAI

client = OpenAI(
    base_url="https://api.deepseek.com",
    api_key=os.environ.get("DEEPSEEK_API_KEY")
)

def extract_product_info(review_text: str) -> dict:
    """从评论文本中提取结构化信息"""
    response = client.chat.completions.create(
        model="deepseek-chat",
        max_tokens=512,
        messages=[
            {
                "role": "system",
                "content": "你是一个数据提取助手。从用户提供的产品评论中提取信息,只返回 JSON,不要有任何其他文字。"
            },
            {
                "role": "user",
                "content": f"""从以下评论中提取信息,返回 JSON 格式:
{{
  "score": 评分(1-5的数字),
  "pros": [优点列表],
  "cons": [缺点列表],
  "sentiment": "positive/negative/neutral"
}}

评论:{review_text}"""
            }
        ]
    )

    text = response.choices[0].message.content.strip()
    # 清理可能的 markdown 代码块
    if text.startswith("```"):
        text = text.split("```")[1]
        if text.startswith("json"):
            text = text[4:]
    return json.loads(text)

result = extract_product_info("这款耳机音质很好,但充电盒做工一般,总体给4分。")
print(result)
# {'score': 4, 'pros': ['音质很好'], 'cons': ['充电盒做工一般'], 'sentiment': 'positive'}

局限:模型有时会在 JSON 前后加解释文字,需要做清理。

方法二:JSON 模式(response_format)

DeepSeek 支持 OpenAI 兼容的 response_format 参数,设置 type: "json_object" 后模型会强制输出合法 JSON,不会混入其他文字,比 Prompt 约束更可靠。

python
import os
import json
from openai import OpenAI

client = OpenAI(
    base_url="https://api.deepseek.com",
    api_key=os.environ.get("DEEPSEEK_API_KEY")
)

def extract_product_info_json_mode(review_text: str) -> dict:
    """使用 JSON 模式强制输出结构化数据"""
    response = client.chat.completions.create(
        model="deepseek-chat",
        max_tokens=512,
        response_format={"type": "json_object"},  # 开启 JSON 模式
        messages=[
            {
                "role": "system",
                "content": "你是一个数据提取助手,只返回 JSON。"
            },
            {
                "role": "user",
                "content": f"""从以下评论中提取信息,返回包含 score(1-5数字)、pros(优点列表)、cons(缺点列表)、sentiment(positive/negative/neutral) 的 JSON:

评论:{review_text}"""
            }
        ]
    )

    # JSON 模式下无需清理,直接解析
    return json.loads(response.choices[0].message.content)

result = extract_product_info_json_mode("这款耳机音质很好,但充电盒做工一般,总体给4分。")
print(result)
# {'score': 4, 'pros': ['音质很好'], 'cons': ['充电盒做工一般'], 'sentiment': 'positive'}

注意事项

  • 开启 JSON 模式时,System Prompt 或 User Prompt 中必须提及"JSON",否则部分模型会报错
  • JSON 模式只保证输出是合法 JSON,不保证字段结构与你预期一致,仍需在 Prompt 中描述清楚结构
  • 适合需要严格格式的生产场景

方法三:使用 pydantic 做输出验证

结合 pydantic(一个 Python 数据验证库,可以定义数据结构并自动检查类型是否正确),可以在解析 JSON 时自动验证结构:

python
import os
import json
from openai import OpenAI
from pydantic import BaseModel
from typing import List

client = OpenAI(
    base_url="https://api.deepseek.com",
    api_key=os.environ.get("DEEPSEEK_API_KEY")
)

class ProductReview(BaseModel):
    score: float
    pros: List[str]
    cons: List[str]
    sentiment: str

def extract_review(text: str) -> ProductReview:
    response = client.chat.completions.create(
        model="deepseek-chat",
        max_tokens=512,
        response_format={"type": "json_object"},
        messages=[
            {
                "role": "system",
                "content": "只返回 JSON,不要其他文字。"
            },
            {
                "role": "user",
                "content": f"从评论中提取:score(1-5), pros(列表), cons(列表), sentiment(positive/negative/neutral)\n评论:{text}"
            }
        ]
    )

    raw = response.choices[0].message.content
    data = json.loads(raw)
    return ProductReview(**data)  # pydantic 自动验证类型

review = extract_review("音质不错,但价格偏贵,给3分")
print(review.score)    # 3.0
print(review.pros)     # ['音质不错']

处理 JSON 解析失败

模型输出的 JSON 有时格式不完全正确,需要做容错处理:

python
import json
import re

def safe_parse_json(text: str) -> dict | None:
    """尝试从模型输出中解析 JSON"""
    # 清理 markdown 代码块
    text = text.strip()
    if "```json" in text:
        text = text.split("```json")[1].split("```")[0].strip()
    elif "```" in text:
        text = text.split("```")[1].split("```")[0].strip()

    # 尝试直接解析
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # 尝试提取第一个 JSON 对象
    match = re.search(r'\{.*\}', text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group())
        except json.JSONDecodeError:
            pass

    return None

使用 XML 标签的结构化输出

对于不需要程序解析的结构化输出,XML 标签比 JSON 更可靠(不需要担心引号转义等问题):

python
import os
from openai import OpenAI
import xml.etree.ElementTree as ET

client = OpenAI(
    base_url="https://api.deepseek.com",
    api_key=os.environ.get("DEEPSEEK_API_KEY")
)

response = client.chat.completions.create(
    model="deepseek-chat",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": """分析这段代码,用以下 XML 格式输出:
<analysis>
  <complexity>时间复杂度</complexity>
  <issues>潜在问题</issues>
  <suggestions>改进建议</suggestions>
</analysis>

代码:
def find_duplicates(lst):
    result = []
    for i in range(len(lst)):
        for j in range(i+1, len(lst)):
            if lst[i] == lst[j] and lst[i] not in result:
                result.append(lst[i])
    return result"""
    }]
)

xml_text = response.choices[0].message.content
# 提取 XML 内容
if "<analysis>" in xml_text:
    xml_content = xml_text[xml_text.index("<analysis>"):xml_text.index("</analysis>")+11]
    root = ET.fromstring(xml_content)
    print("复杂度:", root.find("complexity").text)
    print("问题:", root.find("issues").text)

选择哪种方法

方法 可靠性 适用场景
Prompt 约束 简单结构,对偶发错误容忍
JSON 模式(response_format) 需要严格 JSON 格式的生产场景
XML 标签 不需要程序解析,只需人工阅读
pydantic 验证 高(有校验) 需要类型安全的数据提取

在实际项目中,JSON 模式(response_format={"type": "json_object"})+ pydantic 验证的组合是最可靠的方案。如果解析失败,可以加一次重试逻辑。


常见错误和解决方法

错误现象 原因 解决方法
JSON 解析失败(json.JSONDecodeError 模型在 JSON 前后加了解释文字 使用 JSON 模式(response_format
JSON 模式下模型报错 Prompt 中没有提到 "JSON" 这个词 在 System Prompt 或 User Prompt 中明确说"以 JSON 格式输出"
pydantic 验证失败 模型输出了预期外的字段名或类型 在 Prompt 中明确描述每个字段的名称和类型
模型输出了正确 JSON 但字段名不对 Prompt 描述不够精确 给出完整的字段名示例,或者直接给 JSON schema
本页目录