如何让大语言模型稳定输出 JSON 的三层防御体系

核心思想:从"灵光一闪"到"确定防线"

不能让模型靠运气输出,要用软件工程防线 包裹非确定性的生成模型,最终实现条件反射式的正确输出 。对于极致场景,可通过 SFT 监督微调让模型形成"肌肉记忆"。

三层防线详解

第一道防线:提示词控制(软约束)
  • Schema 注入 + Few-Shot:提供 JSON Schema 或 TypeScript 接口定义,并给 2-3 个范例,把字段类型约束写死。

  • 利用近因效应 :指令末尾强调"禁绝废话,仅输出 JSON",并用定界符 json ... 框定区域。

第二道防线:生成控制层(物理硬手段)
  • 闭源 API :使用 Structured Outputs 功能,预编译 Schema 进解码引擎,遵循率接近 100%。

  • 开源模型 :采用 Logit Masking(概率掩码),通过有限状态机强制将不符合语法的 token 概率设为负无穷,杜绝格式错误。

第三道防线:工程校验与自修复(闭环逻辑)
  • 强校验框架 :用 Pydantic 等工具检查字段完整性和类型正确性,杜绝脏数据。

  • 流式解析优化:对超长 JSON 边生成边检查,发现错误立即中断,节省算力。

  • 闭环自修复:将校验报错信息(如"字段缺失")反馈给模型,指导其利用 LLM 能力自我修正。

第一部分:第三道防线 ------ Pydantic 强校验与自修复

这里展示一个完整的闭环逻辑:定义结构 -> 尝试解析 -> 失败则把报错喂给模型重试。

复制代码
import json
import openai
from pydantic import BaseModel, Field, ValidationError
from typing import List, Optional

# 1. 定义期望的数据结构 (Schema)
class UserProfile(BaseModel):
    name: str = Field(description="用户姓名")
    age: int = Field(description="年龄")
    skills: List[str] = Field(description="技能列表")
    email: Optional[str] = Field(description="邮箱,可选")

# 2. 模拟大模型返回的"不干净"数据 (JSON字符串)
bad_response_from_llm = '''
{
    "name": "张三",
    "age": "二十八", 
    "skills": ["Python", "Java"],
    "extra_field": "这是多余字段会被忽略"
}
'''

def validate_and_repair(llm_output: str, max_retries: int = 2):
    """校验失败时,将报错反馈给模型进行自修复"""
    
    for attempt in range(max_retries):
        try:
            # 解析 JSON 字符串
            data = json.loads(llm_output)
            
            # === 第三道防线核心:Pydantic 校验 ===
            # 这里会自动检查 age 是否为 int,缺少字段会报错,多余字段默认忽略
            validated_data = UserProfile(**data)
            print(f"校验通过!数据: {validated_data.model_dump()}")
            return validated_data
            
        except (json.JSONDecodeError, ValidationError) as e:
            print(f"第 {attempt + 1} 次校验失败: {e}")
            
            if attempt == max_retries - 1:
                raise e
            
            # === 闭环自修复机制 ===
            # 把具体的报错信息 (如 'age: Input should be a valid integer')
            # 反馈给模型,让模型自己重写 JSON
            repair_prompt = f"""
            你之前输出的 JSON 格式有误,报错信息如下:
            {str(e)}
            
            请根据以下 Schema 定义修正错误,仅输出纯 JSON,不要解释:
            {UserProfile.model_json_schema()}
            
            原始错误输出:
            {llm_output}
            """
            
            # 这里调用 LLM (以 OpenAI 为例)
            # 注意:实际生产中这里需要调用你的模型 API
            print("正在请求模型自我修复...")
            # response = openai.ChatCompletion.create(...)
            # llm_output = response.choices[0].message.content
            
            # 模拟修复后的输出 (实际运行时会替换为模型返回)
            llm_output = '{"name": "张三", "age": 28, "skills": ["Python", "Java"], "email": null}'
            
    return None

# 执行测试
validate_and_repair(bad_response_from_llm)

第二部分:第二道防线 ------ OpenAI Structured Outputs 写法

这是 物理硬手段,在 API 请求阶段就直接约束模型生成符合 Schema 的 Token。

复制代码
from openai import OpenAI
from pydantic import BaseModel
from typing import List

client = OpenAI(api_key="your-api-key")

# 1. 定义结构 (使用 Pydantic 定义即可,OpenAI SDK 直接兼容)
class Step(BaseModel):
    explanation: str
    output: str

class MathResponse(BaseModel):
    steps: List[Step]
    final_answer: str

# 2. 调用 API,使用 parse 方法 (Structured Outputs 模式)
completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06", # 注意:需使用支持 Structured Outputs 的模型版本
    messages=[
        {"role": "system", "content": "你是一个数学老师,回答问题要分步骤。"},
        {"role": "user", "content": "解方程 2x + 5 = 15"},
    ],
    response_format=MathResponse, # 直接传入 Pydantic 类
)

# 3. 获取结果 ------ 已经是实例化好的 Pydantic 对象,100% 符合结构
math_result = completion.msg.parsed

# 4. 直接使用属性,无需担心格式错误
print(f"最终答案: {math_result.final_answer}")
for i, step in enumerate(math_result.steps):
    print(f"步骤 {i+1}: {step.explanation} -> {step.output}")

# === 如果是开源模型,这里本应是 Logit Masking 的代码 ===
# 开源侧通常使用 outlines、guidance 或 llama.cpp grammars 库
# 例如 outlines 示例 (伪代码逻辑):
# import outlines
# model = outlines.models.transformers("microsoft/Phi-3-mini-4k-instruct")

# --- 构建 Logit Masking 生成器 (第二道防线物理硬手段) ---
# 这一行代码背后:Outlines 将 Pydantic Schema 编译成了有限状态机 (FSM),
# 并在推理时自动挂载 LogitsProcessor 进行概率掩码。
# generator = outlines.generate.json(model, UserProfile.model_json_schema())
# result = generator("给我一个张三的用户档案")

总结两者的区别

维度 Pydantic 校验 (第三道防线) OpenAI Structured Outputs (第二道防线)
执行时机 模型生成之后 模型生成过程之中
类比 质检员 带卡槽的模具
适用场景 所有模型、老版本 API、极端自定义校验 闭源商业 API (OpenAI, 火山等)
优点 灵活,能捕获业务逻辑错误,能自修复 零格式错误,速度快,Token 省

一个综合运用三道防线的完整生产级示例。

场景设定为:AI 辅助医疗问诊,需要模型输出包含诊断和处方的严格 JSON

将代码分为四个逻辑块:第一道防线(Prompt构建)第二道防线(API约束尝试)第三道防线(Pydantic校验与自修复)

复制代码
import json
import openai
from pydantic import BaseModel, Field, ValidationError, field_validator
from typing import List, Optional, Literal
from openai import OpenAI

# ==================== 0. 初始化与Schema定义 ====================
client = OpenAI(api_key="your-api-key") # 请替换为真实Key

# 定义处方的药物结构
class PrescriptionDrug(BaseModel):
    name: str = Field(..., description="药物通用名")
    dosage: str = Field(..., description="单次剂量,如 '10mg'")
    frequency: str = Field(..., description="用药频率,如 '每日一次'")
    duration: str = Field(..., description="疗程,如 '7天'")

# 定义完整的诊断报告结构(Pydantic Schema,用于第一道防线的注入和第三道防线的校验)
class DiagnosisReport(BaseModel):
    patient_complaint: str = Field(..., description="主诉,总结患者问题")
    
    # 使用 Literal 限制枚举值,防止模型瞎编严重程度
    severity: Literal["轻度", "中度", "重度", "危急"] = Field(..., description="严重程度分级")
    
    diagnosis: str = Field(..., description="初步诊断名称")
    reasoning: str = Field(..., description="诊断依据,逻辑推理过程")
    
    prescriptions: List[PrescriptionDrug] = Field(..., min_length=1, description="处方列表,至少包含一种药物或处理方式")
    notes: Optional[str] = Field(None, description="医嘱备注,如饮食禁忌")

    # 第三道防线补充:自定义逻辑校验(Pydantic 的 field_validator)
    @field_validator('reasoning')
    def reasoning_must_be_meaningful(cls, v):
        if len(v) < 10:
            raise ValueError('诊断依据过于简短,需详细说明')
        return v

# ==================== 1. 第一道防线:Prompt 工程 ====================
def build_defense_prompt(user_input: str) -> str:
    """
    应用高手策略:
    1. Schema注入 + Few-Shot (给出一个范例)
    2. 近因效应 (末尾强调格式)
    """
    
    # 将 Pydantic Schema 转换为 JSON 结构描述字符串,注入给模型看
    schema_desc = DiagnosisReport.model_json_schema()
    
    system_prompt = f"""
你是一名专业的全科医生AI助手。你必须严格按照以下 JSON Schema 定义输出诊断结果。
Schema 定义如下:
{schema_desc}

【Few-Shot 范例】:
用户输入:"我头疼流鼻涕,有点发烧,38度"
正确输出:
{{
    "patient_complaint": "头痛、流涕伴发热1天",
    "severity": "中度",
    "diagnosis": "急性上呼吸道感染(感冒)",
    "reasoning": "患者有明确的鼻部卡他症状(流涕)及发热体征,符合病毒性上呼吸道感染典型表现。",
    "prescriptions": [
        {{
            "name": "对乙酰氨基酚片",
            "dosage": "500mg",
            "frequency": "必要时服用",
            "duration": "3天"
        }},
        {{
            "name": "生理性海水鼻腔喷雾",
            "dosage": "每侧2喷",
            "frequency": "每日3次",
            "duration": "5天"
        }}
    ],
    "notes": "多喝温水,注意休息,若高热不退或出现呼吸困难请及时就医。"
}}
"""

    user_prompt = f"""
患者描述:{user_input}

请给出诊断和处方。
"""

    # === 第一道防线核心:近因效应 ===
    # 将最严格的格式约束指令放在 Prompt 的最末尾
    final_instruction = """
【最高优先级指令】
1. 仅输出符合上述 JSON Schema 的纯 JSON 字符串。
2. 严禁在 JSON 之外添加任何解释、问候语、Markdown标记(如 ```json)或换行字符。
3. 确保所有字段齐全,prescriptions 必须为数组。
"""
    
    # 拼接最终消息
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt + final_instruction}
    ]
    return messages

# ==================== 2. 第二道防线:生成控制层 (可选硬手段) ====================
def call_llm_with_structured_output(messages):
    """
    尝试使用 OpenAI Structured Outputs 作为第二道防线(物理硬手段)。
    如果模型或API不支持,则降级为普通JSON模式调用。
    """
    try:
        # 使用 beta.chat.completions.parse 进行强约束生成
        completion = client.beta.chat.completions.parse(
            model="gpt-4o-2024-08-06",  # 确保是支持此功能的模型
            messages=messages,
            response_format=DiagnosisReport,
            temperature=0.1  # 降低随机性,提高格式遵循度
        )
        # 如果成功,这里返回的已经是校验通过的 Pydantic 对象
        return completion.choices[0].message.parsed, None
    except Exception as e:
        # 降级逻辑:如果不支持 Structured Outputs 或网络出错,走普通 Chat 接口
        print(f"第二道防线未启用或失败 (降级至普通生成): {e}")
        response = client.chat.completions.create(
            model="gpt-3.5-turbo", # 普通模型
            messages=messages,
            temperature=0.1
        )
        raw_content = response.choices[0].message.content
        return None, raw_content

# ==================== 3. 第三道防线:Pydantic 校验 + 闭环自修复 ====================
def robust_diagnosis_parser(raw_json_str: str, original_messages: list) -> DiagnosisReport:
    """
    尝试解析 JSON,失败则进入重试循环(自修复)。
    """
    MAX_RETRY = 2
    current_output = raw_json_str
    
    for attempt in range(MAX_RETRY + 1):
        try:
            # 1. 基础清洗:移除可能的 Markdown 代码块标记 (第一道防线失效时的补救)
            clean_str = current_output.strip()
            if clean_str.startswith("```json"):
                clean_str = clean_str[7:]
            if clean_str.startswith("```"):
                clean_str = clean_str[3:]
            if clean_str.endswith("```"):
                clean_str = clean_str[:-3]
                
            # 2. 解析 JSON
            data_dict = json.loads(clean_str)
            
            # 3. Pydantic 强校验 (字段缺失、类型错误、自定义校验器都会在此报错)
            validated_report = DiagnosisReport(**data_dict)
            
            print(f"第三道防线校验通过 (尝试次数: {attempt + 1})")
            return validated_report
            
        except (json.JSONDecodeError, ValidationError) as e:
            print(f"解析/校验失败 (Attempt {attempt + 1}): {e}")
            
            if attempt == MAX_RETRY:
                raise ValueError(f"模型经过 {MAX_RETRY} 次修复仍未通过校验,最后错误: {e}")
            
            # === 闭环自修复逻辑 ===
            # 构建修复用的消息,直接追加报错信息和错误输出
            repair_messages = original_messages.copy()
            
            # 模拟 Assistant 的错误回答
            repair_messages.append({"role": "assistant", "content": current_output})
            
            # 用户反馈错误详情 (这是最关键的反馈信号)
            user_feedback = f"""
你的上一轮回答 JSON 格式或内容校验失败,详细错误如下:
{str(e)}

请根据报错信息修正 JSON。确保:
1. severity 只能是 '轻度'、'中度'、'重度'、'危急' 之一。
2. reasoning 长度需大于10个字符。
3. 字段必须完整。

请直接输出修正后的纯 JSON,不要加任何多余字符。
"""
            repair_messages.append({"role": "user", "content": user_feedback})
            
            print("进入自修复模式,重新请求模型...")
            
            # 重新调用模型 (这里为了演示,直接走普通接口,实际可复用 call_llm_with_structured_output)
            response = client.chat.completions.create(
                model="gpt-3.5-turbo",
                messages=repair_messages,
                temperature=0.0
            )
            current_output = response.choices[0].message.content

# ==================== 4. 主流程整合 (三道防线协同) ====================
def get_medical_diagnosis(user_query: str):
    print(f"用户输入: {user_query}")
    
    # Step 1: 构建第一道防线 Prompt
    messages = build_defense_prompt(user_query)
    
    # Step 2: 尝试第二道防线 (物理硬约束)
    parsed_obj, raw_text = call_llm_with_structured_output(messages)
    
    # Step 3: 如果第二道防线直接成功了 (返回了 Pydantic 对象)
    if parsed_obj is not None:
        print("第二道防线直接命中,无需第三道防线介入。")
        return parsed_obj
    
    # Step 4: 否则进入第三道防线 (软件校验 + 自修复)
    print("启动第三道防线:工程校验与自修复...")
    final_report = robust_diagnosis_parser(raw_text, messages)
    
    return final_report

# ==================== 运行示例 ====================
if __name__ == "__main__":
    # 模拟一个可能让模型出错的模糊输入
    test_input = "嗓子疼,浑身没劲,感觉发冷,量了下38度5"
    
    try:
        result = get_medical_diagnosis(test_input)
        
        print("\n========== 最终处方单 (JSON) ==========")
        # 直接打印格式化的 JSON,完全符合应用层调用要求
        print(result.model_dump_json(indent=2, ensure_ascii=False))
        
    except Exception as e:
        print(f"系统处理异常: {e}")

三道防线在这个例子中的具体体现

防线层级 代码位置 具体作用 对应策略
第一道 build_defense_prompt 1. 注入 model_json_schema() 2. 提供 Few-Shot 范例 3. 末尾强调"仅输出 JSON" Schema注入 + 近因效应
第二道 call_llm_with_structured_output 使用 client.beta.chat.completions.parse Structured Outputs (物理硬手段)
第三道 robust_diagnosis_parser 1. Pydantic 校验字段类型、枚举值 2. field_validator 校验逻辑 3. 失败时将报错反馈给模型 强校验 + 闭环自修复

运行预期输出

当模型第一次尝试输出 "severity": "高烧" (不在枚举范围)或缺失字段时,系统会打印:

复制代码
解析/校验失败 (Attempt 1): 1 validation error for DiagnosisReport severity ...
进入自修复模式,重新请求模型...
第三道防线校验通过 (尝试次数: 2)

最终输出一定是符合 DiagnosisReport 定义的干净 JSON,可以直接被下游的业务代码(如电子病历系统、药房系统)使用。

相关推荐
weixin_156241575762 小时前
基于YOLO深度学习的运动品牌检测与识别系统
人工智能·深度学习·yolo·识别·模型、
兴趣使然黄小黄2 小时前
【AI-agent】Claude code+Minimax 2.7环境搭建
人工智能·ai编程
物联网软硬件开发-轨物科技2 小时前
【行业动态】AI发展历程通俗速览
人工智能
实在智能RPA2 小时前
Agent 在招投标场景能解决哪些问题?——2026年招投标数智化转型深度解析
人工智能·ai
软件工程师文艺2 小时前
AI 编程助手的演进:从 REPL 到智能体
人工智能
Dfreedom.2 小时前
【实战篇】神经网络在回归任务中的应用
人工智能·神经网络·算法·机器学习·回归
AI2512242 小时前
主流AI视频生成工具技术测评对比:生成质量与性能分析
人工智能·音视频
laomocoder2 小时前
AI网关设计
人工智能·rust·系统架构
爱分享的阿Q2 小时前
VSCode1114-AI全面接管编辑器
人工智能·编辑器