引言
2023 年的一个深夜,一家 AI 创业公司的值班工程师被 PagerDuty 的警报吵醒。他们的核心管线——一个用 ChatGPT 分析新闻情感并自动交易股票的系统——崩溃了。
排查日志,错误堆栈指向一行代码:json.loads(model_output)。
模型的输出是这样的:
json
{
"sentiment": "positive",
"confidence": 0.85,
"summary": "科技公司业绩超预期"
}看起来没问题?仔细看——summary 后面多了一个逗号。合法的 JSON 不能在最后一个字段后面有逗号。json.loads() 解析失败,整条管线崩了。
一个多余的逗号,让公司损失了一整晚的交易时间。
这是大模型应用开发中最普遍的痛点:模型输出的是自然语言,而你的代码需要的是结构化数据。这两者之间的鸿沟,是一切痛苦的根源。
问题:模型输出不可预测
你说「输出 JSON」,模型理解成「尽量输出 JSON」
你说:输出 JSON 格式
模型理解:大概输出一个像 JSON 的东西
实际可能出现的问题:
1. 输出 "Here is the JSON:\n{...}"(多了前缀文字)
2. 输出 {key: "value"}(key 没有引号,不是合法 JSON)
3. 输出 {...} 然后加一段 "以上是分析结果"(多了后缀)
4. 输出 {...,"items": [1,2,3],}(多了尾逗号)
5. 输出 {...} 这里其实想输出字符串,但 JSON 里嵌套了新 JSON
6. 什么格式都对,但字段名和之前不一样(不稳定)早期解决方案:手工修补
python
import json
import re
def extract_json_from_llm(text: str) -> dict:
"""从模型输出中提取 JSON 的各种黑科技"""
# 方法 1:直接解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 方法 2:找第一个 { 和最后一个 }
try:
start = text.index("{")
end = text.rindex("}") + 1
return json.loads(text[start:end])
except (ValueError, json.JSONDecodeError):
pass
# 方法 3:用正则提取
json_pattern = r'```(?:json)?\s*([\s\S]*?)\s*```'
match = re.search(json_pattern, text)
if match:
try:
return json.loads(match.group(1))
except json.JSONDecodeError:
pass
# 方法 4:尝试修复常见问题
text = text.rstrip(",") # 去掉尾逗号
text = re.sub(r',\s*}', '}', text) # 去掉 } 前的逗号
try:
return json.loads(text)
except json.JSONDecodeError:
pass
raise ValueError(f"无法从模型输出中提取 JSON: {text[:100]}...")这种「打补丁」的方式:
- 脆弱:模型换个花样输出就又崩了
- 复杂:代码里一堆 try-except 和正则
- 不可靠:总有新的边界情况
JSON Mode:API 级别的解决方案
GPT-4 的 json_object 模式
2023 年下半年,OpenAI 在 API 中加入了 JSON Mode:
python
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "你是一个数据分析助手。始终以 JSON 格式输出。"},
{"role": "user", "content": "分析以下评论的情感:这家餐厅太好吃了!"}
],
response_format={"type": "json_object"} # ← 关键参数
)
result = json.loads(response.choices[0].message.content)
# 保证是合法 JSON!JSON Mode 的工作原理
普通模式:
模型生成 → 任意文本 → 你来解析(可能失败)
JSON Mode:
模型生成 → 约束为合法 JSON → 保证可以解析
技术原理(简化):
在 token 生成时,模型只选择符合 JSON 语法的 token
例如:
生成 "{" 之后,下一个 token 只能是字符串 key 或 "}"
生成 "key": 之后,下一个 token 必须是值
生成字符串值之后,只允许 "," 或 "}"JSON Mode 的注意事项
必须做的事:
✓ 在 system 或 user 消息中明确指示「输出 JSON」
(虽然 response_format 已经设定了,但不说的话模型可能输出 {"message": "好的"})
✓ 用 json.loads() 解析结果(保证是合法 JSON)
不能做的事:
✗ 不能保证 JSON 的 schema(字段名、类型)
JSON Mode 只保证语法合法,不保证结构正确
✗ 不能保证特定字段一定存在
✗ 输出可能是一个完全空的对象 {}JSON Mode 的局限
python
# 你想要的结构:
{
"sentiment": "positive",
"confidence": 0.85,
"keywords": ["餐厅", "好吃"]
}
# JSON Mode 可能输出的结构:
{"sentiment": "正面", "score": "高"} # 字段名不同
{"result": "好评"} # 完全不同的结构
{"sentiment": "positive", "confidence": "high"} # 类型不对(字符串而非数字)
这些都是合法 JSON,但不等于你想要的 JSON。错误处理模式
生产级 JSON 解析
python
from pydantic import BaseModel, ValidationError
from typing import List
class SentimentResult(BaseModel):
sentiment: str
confidence: float
keywords: List[str]
def analyze_sentiment(text: str, max_retries: int = 3) -> SentimentResult:
"""带重试和验证的情感分析"""
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": """分析文本情感。
输出 JSON,包含以下字段:
- sentiment: "positive" 或 "negative" 或 "neutral"
- confidence: 0.0 到 1.0 的浮点数
- keywords: 关键词列表"""},
{"role": "user", "content": text}
],
response_format={"type": "json_object"}
)
data = json.loads(response.choices[0].message.content)
return SentimentResult(**data) # Pydantic 验证
except (json.JSONDecodeError, ValidationError) as e:
if attempt == max_retries - 1:
raise
continue
raise RuntimeError("重试次数用尽")格式约束的进化
从最原始到最可靠:
Level 0: 只在 prompt 里说「输出 JSON」
→ 约束力:❌ 可能输出任何东西
Level 1: prompt + 正则提取
→ 约束力:⚠️ 脆弱,总有新问题
Level 2: JSON Mode(response_format: json_object)
→ 约束力:✅ 保证合法 JSON,但不保证结构
Level 3: Structured Outputs(下一节课)
→ 约束力:✅ 保证合法 JSON + 指定 schema
每一级都在减少不确定性,把控制权从「提示词」转移到「API 约束」。本节小结
| 概念 | 要点 |
|---|---|
| 核心痛点 | 模型输出自然语言,代码需要结构化数据——格式不可靠 |
| 早期方案 | 正则提取、JSON 修复——脆弱且复杂 |
| JSON Mode | API 层面保证输出合法 JSON |
| JSON Mode 局限 | 只保证语法合法,不保证 schema(字段名、类型) |
| 进化方向 | 从 prompt 约束 → API 约束 → Schema 约束 |
思考题
- 为什么 JSON Mode 不能保证字段名和值的类型?想想模型在生成 token 时能控制什么。
- 在 JSON Mode 出现之前,开发者是如何解决格式问题的?这些方法今天还有用吗?
- 为什么说「格式约束从提示词转移到 API」是一个重要的趋势?这和传统软件工程中「契约优于约定」有什么相似之处?