01. 成就感背后的“生产事故”
当你第一次成功让模型返回一个完美的 JSON 结构时,那种“代码终于受控”的成就感溢于言表。但如果你直接在代码里写下 print(data['title']),那么恭喜你,你已经为未来的生产事故埋下了伏笔。
在 AI 工程中,有一个残酷的现实:模型返回 JSON,并不代表程序可以无条件信任它。
02. 模型会给 JSON 挖哪些坑?
即便你用了最强的 GPT-4 或 Claude 3,模型依然会偶尔“掉链子”。常见的异常场景包括:
1. 结构性“幻觉”
你要求返回一个数组,模型可能因为上下文太长,返回了一个被截断的字符串,或者干脆在 JSON 前后加了句“这是你要的结果:”。
- 后果:
json.loads()直接抛出ValueError或JSONDecodeError。
2. 字段“失踪案”
模型可能会漏掉你认为“必填”的字段,或者自作聪明地修改了键名(比如把 user_name 改成了 username)。
- 后果:程序访问时触发
KeyError。
3. 类型“变色龙”
最典型的例子:你期待 tags 是一个数组 ["A", "B"],模型却返回了逗号分隔的字符串 "A, B"。
- 后果:下游的
.map()或foreach逻辑直接崩溃。
03. 核心观念:AI 输出是“不可信输入”
在传统工程中,我们对用户提交的表单、第三方接口的返回都会进行严格的校验。AI 输出本质上也是一种“外部输入”,而且是比第三方接口更不稳定、更不可控的输入。
优秀的 AI 工程师不相信“概率”,只相信“防御”。
04. 实战:如何优雅地“接住”模型输出?
不要只写 json.loads。一个工业级的解析流程应该是这样的:
❌ 试玩版代码(极脆)
response = get_ai_response(prompt)
data = json.loads(response)
print(data['tags'][0]) # 万一 tags 是字符串?万一 data 是空?
✅ 工程版代码(稳健)
def safe_parse_tags(ai_output: str):
try:
data = json.loads(ai_output)
# 1. 结构校验
if not isinstance(data, dict):
return []
# 2. 字段与类型强校验
tags = data.get('tags')
if isinstance(tags, list):
return tags
elif isinstance(tags, str):
# 这里的“防御”体现为:即便模型给错了类型,我们也尝试修复它
return [t.strip() for t in tags.split(',')]
return [] # 兜底逻辑
except (json.JSONDecodeError, Exception):
return [] # 彻底失败时的安全降级(降级思维)
05. 进阶:引入 Schema 验证
在复杂的 AI 项目中,手动写 isinstance 太痛苦了。业界标准的做法是引入 Pydantic(Python)或者 Zod(TypeScript)。
from pydantic import BaseModel, Field, validator
from typing import List
class ProductSchema(BaseModel):
title: str
price: float
tags: List[str] = Field(default_factory=list)
@validator('tags', pre=True)
def handle_tags_string(cls, v):
if isinstance(v, str):
return [t.strip() for t in v.split(',')]
return v
通过 Schema,你可以把“不确定”的模型输出,强行约束为“确定”的程序对象。这就是从“调包”到“工程”的跃迁。
06. 结语:工程感的起点
很多人问:会调 API 就算 AI 工程师了吗?
我的回答是:当你开始不再迷信模型输出,而是像对待任何“不可信输入”一样去解析、校验和兜底时,你才算真正开始了 AI 工程化的修行。
一句话总结:结构化输出解决的是“模型怎么说”,而结构化消费解决的是“程序怎么活”。
讨论区:你在解析 AI 返回的 JSON 时遇到过哪些离谱的“幻觉”?你是怎么做兜底的?欢迎交流!
本文首发于掘金,记录前端开发者向 AI 工程转型的防御性编程实践。