有一个底层的问题,总会在真正开始“切换模型”时瞬间暴露:

GPT-4 返回的 JSON 结构,和 Claude 返回的,长得不一样。

更麻烦的是,同一个模型,同样的 Prompt,跑两遍都可能给你不同的字段名。

这不是玄学,这是今天要解决的工程问题。


一、问题的本质:三个模型,三种“方言”

AI 开发中有一个核心矛盾:业务代码需要稳定的数据结构,但 LLM 的生成本质上是概率性的。

把这个问题拆开看,分为两层:

1.1 语法层:JSON 是否合法?

模型结构化输出方案特点
OpenAIStructured Outputs API原生支持 Pydantic 数据模型,直接在解码层约束输出格式
Claude (Anthropic)Tool Use(函数调用)需要“曲线救国”——通过 mock 工具调用来强制结构化输出
Gemini (Google)genai.protos.SchemaAPI 较为笨重,文档不够清晰,且难以通过字段描述引导输出

关键差异:OpenAI 的 Structured Outputs 是在解码层直接约束 Token 空间,而 Claude 和 Gemini 更多是在提示层引导。前者是“物理屏障”,后者是“交通规则”——一个靠强制,一个靠自觉。

1.2 语义层:结构对了,内容对吗?

即使 JSON 语法完全合法,模型也可能:

  • 多出你没定义的字段
  • 漏掉你要求必填的字段
  • 把 enum 值写成了同义词
  • 在字段命名上自由发挥(user_name → userName → username)

用专业术语说,这叫 Schema Drift(模式漂移)。


二、实战:让三个模型输出同样的 JSON

2.1 目标 Schema 定义

先定一个目标 JSON Schema:

{
  "sentiment": "positive | negative | neutral",
  "key_issues": ["问题1", "问题2"],
  "action_items": [
    {"team": "Engineering", "task": "具体任务描述"}
  ]
}

2.2 OpenAI:最顺滑的体验

OpenAI 的 Structured Outputs 是当前业界最成熟方案。直接集成 Pydantic 模型:

from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List

client = OpenAI()

class ActionItem(BaseModel):
    team: str = Field(description="责任团队")
    task: str = Field(description="具体任务")

class EvalResponse(BaseModel):
    sentiment: str = Field(description="情感倾向:positive/negative/neutral")
    key_issues: List[str] = Field(description="识别出的关键问题列表")
    action_items: List[ActionItem] = Field(description="改进措施列表")

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "分析对话并输出结构化结果。"},
        {"role": "user", "content": dialog_text}
    ],
    response_format=EvalResponse,  # ← 关键:直接指定 Pydantic 模型
    temperature=0
)

result = response.choices[0].message.parsed  # 自动解析为 Pydantic 对象

优势:Schema 验证在解码层完成,不依赖 Prompt 的“自觉性”。

2.3 Claude:用“工具调用”来“骗”出 JSON

Claude 原生没有类似 response_format 的直接 JSON 约束。但有一个成熟的替代方案:把 JSON Schema 伪装成一个工具(Tool)的入参定义。

from anthropic import Anthropic
from pydantic import BaseModel, Field

client = Anthropic()

# 复用上面的 Pydantic 模型
class EvalResponse(BaseModel):
    sentiment: str
    key_issues: List[str]
    action_items: List[ActionItem]

PROMPT = """
你是一个对话分析专家,请分析对话并将结果通过 send_to_manager 工具发送。
"""

response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    system=PROMPT,
    tools=[{
        "name": "send_to_manager",
        "description": "发送对话分析结果",
        "input_schema": EvalResponse.model_json_schema(),  # ← 核心:Schema 作为工具参数
    }],
    tool_choice={"type": "tool", "name": "send_to_manager"},  # 强制使用该工具
    messages=[{"role": "user", "content": dialog_text}]
)

# 从工具调用参数中提取 JSON
tool_use = next(block for block in response.content if block.type == "tool_use")
result = tool_use.input  # 已经是 dict 格式

为什么有效? 工具调用的 input_schema 对 Claude 来说是“强制性”的——它必须生成符合该 Schema 的参数才能完成函数调用。这比在 System Prompt 里写“请输出 JSON”要可靠得多。

2.4 Gemini:最“娇气”的选手

Gemini 的 JSON 约束需要通过 genai.protos.Schema 类实现,且对字段描述的支持有限。

import google.generativeai as genai

# 需要通过 protobuf 定义 Schema,较为繁琐
# 示例省略,核心思路与前两者类似:在请求中显式声明 response_mime_type="application/json"
# 并配合 response_schema 参数

⚠️ 注意:Gemini 的结构化输出在字段描述(Field Description)的引导能力上相对较弱,需要更严格的 Schema 定义和额外的后处理校验。

2.5 实战数据:各方案失败率对比

根据生产环境实测数据:

方法模型Schema 违规率说明
JSON Mode(无 Schema 定义)GPT-4o9.3%仅保证语法合法,不保证结构匹配
JSON Mode(无 Schema 定义)Claude Sonnet11.7%违规率更高,主要是额外字段
Function CallingGPT-4o1.2%违规集中在 enum 类型匹配上
Function CallingClaude Sonnet2.1%嵌套 Schema 时违规率上升
Structured Outputs (strict)GPT-4o0.2%当前最可靠的方案
Tool Use + additionalProperties: falseClaude Sonnet0.3%与 GPT-4o strict 非常接近

结论:Schema 定义 + 工具调用(或 Structured Outputs)是必须的,只靠 Prompt 里“请输出 JSON”的叮嘱,10% 左右的请求会给你“惊喜”。


三、进阶:应对 Schema Drift(模式漂移)

即使所有响应都通过了 Schema 校验,你仍然可能收到“语义上错误”的数据。

3.1 三种常见漂移模式

类型示例风险等级
键名同义化"user_name" vs "userName" vs "username"低(可映射)
枚举值同义化"high" vs "high_risk" vs "critical"中(需校验)
语义值错误中等风险被判为高风险,Schema 依然合法高(无报错)

前两种可以通过字段名映射和后处理校验解决。第三种最隐蔽——没有 Parse Error,Pydantic 验证通过,但下游拿到了错误数据。

3.2 解法:Chain of Thought + 强制解释

在 JSON 中增加一个 reasoning(推理过程)字段,强制模型在输出结论前先展示思考过程,可以显著提升准确性。

不加 reasoning 的模型表现:准确率 33%,即便 JSON 格式完全正确。

增加 reasoning 字段后:准确率跃升至 92%~95%。

class EvalResponse(BaseModel):
    reasoning: str = Field(description="分析推理过程")  # ← 这个字段是性能关键
    sentiment: str
    key_issues: List[str]
    action_items: List[ActionItem]

为什么有效? 当模型需要“先思考再输出”时,思维链(Chain of Thought)帮助它在 Token 空间里把推理过程展开,最终决策的可靠性大幅提升。

3.3 高级技巧:Schema 抽象层

如果业务需要同时对接 OpenAI、Anthropic、Google 的 Structured Outputs,可以借助 Schema 抽象层——把一份“通用 Schema”自动转换成各厂商的方言格式:

# 示例:LangSchema(Elixir 生态)
abstract_schema = %{
  "sentiment" => %{type: :string, enum: ["positive", "negative", "neutral"]},
  "key_issues" => %{type: :array, items: %{type: :string}},
  "action_items" => %{type: :array, items: %{team: :string, task: :string}}
}

# 自动转换为各厂商格式
openai_schema = LangSchema.Converter.OpenAI.convert(abstract_schema)
claude_schema = LangSchema.Converter.Anthropic.convert(abstract_schema)
gemini_schema = LangSchema.Converter.Gemini.convert(abstract_schema)

这样,业务代码只依赖一份抽象 Schema,切换模型时无需修改数据结构定义。


四、错误修复循环:最后的防线

即使做了上述所有措施,结构化输出仍可能在极少数 Edge Cases 下“破防”。生产级系统需要一个 错误修复循环(Repair Loop):

Prompt → Generate → Validate → Repair → Parse
import json
import re
from openai import OpenAI

def only_json(s: str) -> str:
    """提取字符串中的 JSON 部分"""
    m = re.search(r"\{.*\}", s, flags=re.S)
    return m.group(0) if m else s

def repair(bad: str) -> str:
    """调用模型修复非法 JSON"""
    fix = f"Fix this so it is STRICT valid JSON with no comments or text:\n{bad}"
    out = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": fix}],
        temperature=0
    )
    return only_json(out.choices[0].message.content)

raw = generate_once(SCHEMA_PROMPT)
for _ in range(2):  # 最多重试 2 次
    try:
        data = json.loads(raw)
        doc = EvalResponse(**data)
        break
    except (json.JSONDecodeError, ValidationError):
        raw = repair(raw)

这个模式的核心思想是:将“生成器”和“修复器”分离。第一次调用专注于内容质量,第二次调用只做机械性的语法清理,不添加新内容。


五、最佳实践总结

场景推荐方案核心要点
只用 OpenAIStructured Outputs + Pydantic最省心,失败率 < 0.5%
多厂商混用Tool/Function Calling + 抽象 Schema 层用工具调用代替“请输出 JSON”
Claude 用户Tool Use + additionalProperties: false实测失败率可压至 0.3%
语义准确性要求高增加 reasoning 字段准确率提升 60%+
高可用要求接入 Repair Loop将 Edge Cases 从“系统崩溃”降级为“自动修复”
字段命名一致性在 Gateway 层做字段映射统一 snake_case 或 camelCase

一句话总结:

用 Schema 约束代替“叮嘱”,用 Tool Call 代替“请输出 JSON”,用 Chain of Thought 代替“直接给答案”。

跨模型切换这件事,不是让所有模型“变聪明”,而是让它们都接受同一套契约。Gateway 在这中间扮演的角色不是中转,而是翻译官和守门员——把各家的结构化输出方言,统一翻译成业务代码能理解的标准语言。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐