课程 › 0基础Agent开发课 / Prompt工程 / 结构化输出-让LLM返回可靠的JSON
— 14 min read

结构化输出-让LLM返回可靠的JSON

> **本文适合谁**

结构化输出:让 LLM 返回可靠的 JSON

本文适合谁

需要让 LLM 的输出被程序处理(存入数据库、传给下一步骤、解析成对象)的开发者。从最简单的 Prompt 约束到最可靠的 JSON 模式 + pydantic 验证,本文给出渐进式的解决方案。


LLM 返回的永远是字符串。这句话写出来很简单,但真正理解它的含义需要从实际问题出发。

在 Prompt 里加上"请以 JSON 格式返回",模型的输出可能是这样的:

当然,这是你要的 JSON 数据:

```json
{"title": "...", "summary": "..."}

希望这对你有帮助!


然后 `json.loads()` 直接抛异常了。

这不是偶发问题。LLM 天生是文本生成模型,它不理解"返回 JSON"和"回复一段包含 JSON 的文字"有什么区别。结构化输出就是为了从根本上解决这个问题。

---

## 为什么需要结构化输出:先建立直觉

<pre class="mermaid" data-mermaid-index="0"></pre>
*结构化输出完整流程——从 Prompt 设计到 Pydantic 验证,每步都有明确职责*


想象两种场景:

**自由文本的麻烦**:
```python
# 这样的输出没法直接用
response = "好的,根据你的描述,这个产品的评分是 4.5 分,主要优点是性价比高,缺点是发货慢。"
# 你需要写正则表达式或者更复杂的解析逻辑才能提取出 4.5、优点列表、缺点列表

结构化输出的优势:

# 这样的输出可以直接解析
response = '{"score": 4.5, "pros": ["性价比高"], "cons": ["发货慢"]}'
import json
data = json.loads(response)  # 直接完成,不需要任何解析逻辑

当 LLM 的输出需要被程序处理时,结构化输出是工程化的基础。


方式一:Prompt 约束(最简单,但不可靠)

这段代码在做什么

在 Prompt 中明确要求 JSON 格式,并给出示例结构。最简单,但模型可能在 JSON 前后加解释文字。

import os
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

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

def extract_product_info(review_text: str) -> dict | None:
    """从评论文本中提取结构化信息(Prompt约束方式)"""
    response = client.chat.completions.create(
        model="deepseek-chat",
        max_tokens=512,
        temperature=0,
        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:]
    try:
        return json.loads(text.strip())
    except json.JSONDecodeError:
        return None

result = extract_product_info("这款耳机音质很好,但充电盒做工一般,总体给4分。")
print(result)

你应该看到什么输出

{'score': 4, 'pros': ['音质很好'], 'cons': ['充电盒做工一般'], 'sentiment': 'positive'}

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


方式二:JSON 模式(`response_format`,推荐)

这段代码在做什么

使用 response_format={"type": "json_object"} 参数,强制模型输出合法 JSON,不会混入其他文字。比 Prompt 约束更可靠。

import os
import json
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

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

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

评论:{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 定义期望的数据结构,在解析 JSON 时自动验证类型和字段。如果字段不对或类型错误,会直接抛出异常,而不是悄悄产生错误的数据。

import os
import json
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Literal
from dotenv import load_dotenv

load_dotenv()

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

# 定义期望的输出结构
class ProductReview(BaseModel):
    score: float = Field(description="评分,1-5之间的数字", ge=1, le=5)
    pros: List[str] = Field(description="优点列表")
    cons: List[str] = Field(description="缺点列表")
    sentiment: Literal["positive", "negative", "neutral"] = Field(
        description="整体情感倾向"
    )

def extract_review(text: str) -> ProductReview:
    """提取评论信息,用 pydantic 做类型验证"""
    response = client.chat.completions.create(
        model="deepseek-chat",
        max_tokens=512,
        temperature=0,
        response_format={"type": "json_object"},
        messages=[
            {
                "role": "system",
                "content": "只返回 JSON,不要其他文字。"
            },
            {
                "role": "user",
                "content": (
                    f"从评论中提取:score(1-5数字), pros(列表), cons(列表), "
                    f"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(f"评分: {review.score}")    # 3.0(float类型,自动转换)
print(f"优点: {review.pros}")     # ['音质不错']
print(f"情感: {review.sentiment}") # 只能是 positive/negative/neutral

你应该看到什么输出

评分: 3.0
优点: ['音质不错']
情感: negative

为什么用 pydantic:pydantic 不只是做 JSON 解析,它会验证类型、验证字段范围(ge=1, le=5 确保评分在 1-5 之间)、验证枚举值是否合法。如果模型输出了 "sentiment": "好" 而不是合法的枚举值,pydantic 会立即报错,而不是让错误数据悄悄流入后续逻辑。


处理 JSON 解析失败

即使使用 JSON 模式,也可能偶发解析失败。加入容错处理:

import json
import re

def safe_parse_json(text: str) -> dict | None:
    """
    这段代码在做什么:
    多种策略尝试从模型输出中解析 JSON
    先直接解析,失败了再尝试提取代码块内的 JSON
    """
    text = text.strip()
    # 先尝试直接解析
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # 清理 markdown 代码块
    if "```json" in text:
        text = text.split("```json")[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  # 所有方法都失败了

带重试的生产级实现

import os
import json
import time
from openai import OpenAI
from pydantic import BaseModel
from dotenv import load_dotenv

load_dotenv()

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

def extract_with_retry(
    messages: list,
    model_class: type[BaseModel],
    max_retries: int = 3
) -> BaseModel | None:
    """
    这段代码在做什么:
    带重试逻辑的结构化提取
    解析失败时自动重试,最多重试3次
    """
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="deepseek-chat",
                max_tokens=1024,
                temperature=0,
                response_format={"type": "json_object"},
                messages=messages
            )
            raw = response.choices[0].message.content
            data = json.loads(raw)
            return model_class(**data)
        except (json.JSONDecodeError, Exception) as e:
            if attempt < max_retries - 1:
                print(f"第 {attempt + 1} 次尝试失败({e}),重试中...")
                time.sleep(1)
            else:
                print(f"所有重试均失败:{e}")
                return None

选择哪种方法

方法 可靠性 代码复杂度 适用场景
Prompt 约束 中 简单 简单结构,对偶发错误容忍
JSON 模式(response_format) 高 中等 需要严格 JSON 格式的生产场景
pydantic 验证 高(有校验) 中等 需要类型安全的数据提取
带重试 最高 较复杂 高可靠性要求的生产场景

推荐组合:JSON 模式 + pydantic 验证 + 重试逻辑,是生产环境中最可靠的方案。


常见错误和解决方法

错误现象 原因 解决方法
json.JSONDecodeError 模型输出了非 JSON 内容 使用 JSON 模式
JSON 模式下模型报错 Prompt 中没有提到 "JSON" 在 System Prompt 中明确说"以 JSON 格式输出"
pydantic ValidationError 模型输出了错误的字段名或类型 在 Prompt 中明确描述字段名和类型
字段名与预期不符 Prompt 描述不够精确 给出完整的字段名示例或 JSON schema

本篇速查卡片

好的结构化 Prompt vs 差的结构化 Prompt:

差的做法(太模糊):

请以 JSON 格式返回分析结果

好的做法(明确字段):

以 JSON 格式返回,包含以下字段:
- score: 数字(1-5)
- pros: 字符串数组(优点)
- cons: 字符串数组(缺点)
- sentiment: 字符串(positive/negative/neutral)

选择原则:原型阶段用 Prompt 约束;生产环境用 JSON 模式 + pydantic;高可靠性场景加重试逻辑。

目录