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

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

> **本文适合谁**

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

本文适合谁

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


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

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

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

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

希望这对你有帮助!

code

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

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

---

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

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


想象两种场景:

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

结构化输出的优势

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

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


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

这段代码在做什么

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

python
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)

你应该看到什么输出

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

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


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

这段代码在做什么

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

python
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)

你应该看到什么输出

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

注意事项

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

方式三:Pydantic 验证(最可靠)

这段代码在做什么

结合 pydantic 定义期望的数据结构,在解析 JSON 时自动验证类型和字段。如果字段不对或类型错误,会直接抛出异常,而不是悄悄产生错误的数据。

python
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

你应该看到什么输出

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

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


处理 JSON 解析失败

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

python
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  # 所有方法都失败了

带重试的生产级实现

python
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

差的做法(太模糊):

code
请以 JSON 格式返回分析结果

好的做法(明确字段):

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

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

本页目录