跳到内容

糟糕的Schema可能会破坏您的LLM结构化输出

使用错误的响应模型,您可能会损失高达60%的性能提升。响应模型对Claude和GPT-4o的模型性能影响巨大,无论您使用的是JSON模式还是工具调用。

使用正确的响应模型可以帮助确保您的模型以正确的语言响应或防止在提取视频时间戳时产生幻觉

我们决定通过在GSM8k数据集上对Claude和GPT-4o进行基准测试来调查这个问题,发现:

  1. 字段命名对性能影响巨大 - 将单个字段名从 final_choice 更改为 answer,模型准确率从4.5%提高到95%。我们在响应模型中构造和命名字段的方式可以从根本上改变模型解释和响应查询的方式。
  2. 思维链显著提升性能 - 添加 reasoning 字段使模型在GSM8k数据集上的准确率提高了60%。模型在逐步解释其逻辑时表现显著更好。
  3. JSON模式需谨慎使用 - 在重命名字段时,JSON模式的性能变化比工具调用多50%。不同的响应模型在JSON模式和工具调用之间表现出不同的性能水平,这表明JSON模式需要更仔细的优化。

我们将通过以下步骤进行:

  1. 我们将首先讨论GSM8k数据集以及我们如何将其用于基准测试
  2. 然后我们将介绍我们获得的一些结果,并讨论我们发现的一些关键要点
  3. 最后,我们将提供一些优化模型响应格式的技巧,您可以立即应用

数据集

我们使用OpenAI的GSM8k数据集来衡量模型性能。这个数据集挑战LLM模型解决涉及多步推理的简单数学问题。例如:

“Natalia在四月卖了48个夹子给朋友,五月卖了一半。Natalia总共卖了多少夹子?”

原始数据集包含推理步骤和最终答案。我们将其精简至基本要素:问题、答案和分离的推理。为此,我们使用此代码处理数据

from datasets import load_dataset, Dataset, DatasetDict

splits = ["test", "train"]


def generate_gsm8k(split):
    ds = load_dataset("gsm8k", "main", split=split, streaming=True)
    for row in ds:
        reasoning, answer = row["answer"].split("####")
        answer = int(answer.strip().replace(",", ""))
        yield {
            "question": row["question"],
            "answer": answer,
            "reasoning": reasoning,
        }


# Create the dataset for train and test splits
train_dataset = Dataset.from_generator(lambda: generate_gsm8k("train"))
test_dataset = Dataset.from_generator(lambda: generate_gsm8k("test"))

# Combine them into a DatasetDict
dataset = DatasetDict({"train": train_dataset, "test": test_dataset})

dataset.push_to_hub("567-labs/gsm8k")

这使我们能够测试响应格式、响应模型甚至所选模型本身的变化将如何影响模型的推理能力。

利用这个新数据集,我们随后使用各种不同的响应模型和响应模式(如JSON模式和工具调用)测试了Claude和GPT-4o模型。最终结果令人着迷——强调了使用良好的响应模型对于从所选模型中榨取最大性能的重要性。

基准测试

我们有两个想要回答的关键问题:

  1. 与JSON模式等其他响应模式相比,结构化抽取如何影响模型性能。
  2. 不同的响应模型对模型性能有何影响?

为了回答这些问题,我们从GSM8k数据集中抽取了前200个问题,并测试了响应模式和响应模型的不同组合。

我们的实验分为两部分:

  1. 模式与模型:我们首先探讨了不同的响应模式和模型组合如何影响GSM8k上的性能
  2. 响应模型: 然后我们考察了不同复杂度的响应模型如何影响每个模型的性能

让我们更详细地探讨每个部分。

模式与模型

在这些实验结束时,我们得到了以下结论:

  1. Claude模型擅长复杂任务:与GPT-4o变体相比,Claude模型在少样本改进方面有显著更大的提升。这意味着对于具有特定细微输出格式或指令的复杂任务,Claude模型将从少样本示例中获益更多。

  2. 结构化抽取表现不逊色:虽然JSON模式相对于函数调用有1-2%的性能差异,但当响应模型变得复杂时,使用JSON模式会很棘手。使用Haiku等较小的模型进行JSON模式工作通常需要解析控制字符并增加重试次数。这与结构化抽取的稳定性能形成对比,后者返回一致的Schema。

  3. 4o Mini应谨慎使用:我们发现4o-mini与Claude模型相比可控性要低得多,少样本示例有时会导致性能下降。

需要注意的是,这里提到的少样本示例仅在提供了答案背后的推理时才有所不同。如果没有这个推理示例,则不会观察到相同的性能提升。

以下是Claude系列模型的结果:

模型 Anthropic JSON模式 JSON (5个少样本) Anthropic 工具 工具 (5个少样本) 工具 (10个少样本) 基准测试
claude-3.5-sonnet 97.00 98.5 96.00 98.00% 98% 96.4
claude-3-haiku 87.50% 89% 87.44% 90.5% 90.5% 88.9
claude-3-sonnet 94.50% 91.5 91.00% 96.50% 91.5% 92.3
claude-3-opus 96.50% 98.50% 96.50% 97.00% 97.00% 95

以下是4o-mini的结果:

模型 gpt-4o-mini gpt-4o
结构化输出 95.5 91.5%
结构化输出 (5个少样本) 94.5 94.5%
工具调用 93.5 93.5%
工具调用 (5个少样本) 93.0 95%
JSON模式 94.5 95.5
JSON模式 (5个少样本) 95.0 97%

这里很清楚,与GPT-4o变体相比,Claude模型在使用少样本示例时持续显示出显著改进。这与4o-mini形成对比,后者在使用简单示例时,工具调用性能反而下降了。

响应模型

基于这些新结果,我们接着研究了响应模型如何影响我们的模型在函数调用方面的性能。在此过程中,我们得到了以下结论。

  1. 思维链:思维链极其重要,根据我们的基准测试,它可以在GSM8k上将模型性能提升高达60%
  2. JSON模式比工具调用敏感得多:在我们最初的基准测试中,我们发现响应模型的微小变化,例如增加额外参数,可能导致性能下降高达30%——这是工具调用不会遇到的问题。
  3. 命名非常重要:响应参数的命名极其重要。仅将potential_final_choicefinal_choice改为potential_answersfinal_answer,最终准确率就从4.5%提高到了95%。

思维链

很难低估允许模型在生成最终响应之前进行推理和规划的重要性。

在我们最初的测试中,我们使用了以下两个模型

class Answer(BaseModel):
    chain_of_thought: str
    answer: int


class OnlyAnswer(BaseModel):
    answer: int
模型 JSON模式 工具调用
Answer 92% 94%
OnlyAnswer 33% 33.5%

这些模型使用了完全相同的提示和问题进行测试。它们之间唯一的区别是增加了一个chain_of_thought响应参数,以使模型能够有效地进行推理。

我们不局限于chain_of_thought这种特定的命名约定,尽管它一直表现良好。我们可以通过查看我们测试以下响应模型时获得的结果来证明这一点。

为了验证这一点,我们从测试数据集中随机抽取了50个问题,并查看了在GSM8k上实现类似推理字段的不同响应模型的性能。

我们的结论是什么?简单地为模型添加额外的字段来推理其最终响应,就能全面提高推理能力。

class AssumptionBasedAnswer(BaseModel):
    assumptions: list[str]
    logic_flow: str
    answer: int

class ErrorAwareCalculation(BaseModel):
    key_steps: list[str]
    potential_pitfalls: list[str]
    intermediate_results: list[str]
    answer: int

 lass AnswerWithIntermediateCalculations(BaseModel):
    assumptions: list[str]
    intermediate_calculations: list[str]
    chain_of_thought: str
    final_answer: int

class AssumptionBasedAnswerWithExtraFields(BaseModel):
    assumptions: list[str]
    logic_flow: str
    important_intermediate_calculations: list[str]
    potential_answers: list[int]
    answer: int


class AnswerWithReasoningAndCalculations(BaseModel):
    chain_of_thought: str
    key_calculations: list[str]
    potential_answers: list[int]
    final_choice: int
模型 准确率
AssumptionBasedAnswer 78%
ErrorAwareCalculation 92%
Answer With Intermediate Calculation 90%
AssumptionBasedAnswerWithExtraFields 90%
AnswerWithReasoningAndCalculations 94%

因此,如果您正在生成任何类型的响应,请不要忘记添加一个简单的推理字段,以实现这种性能提升。

JSON模式极其敏感

我们很好奇这如何反映在最初抽样的200个问题上。为此,我们选取了之前实验中抽样的原始200个问题,并尝试查看JSON模式和工具调用在与gpt-4o-mini结合使用不同排列组合时的表现。

以下是我们使用的模型:

class Answer(BaseModel):
    chain_of_thought: str
    answer: int


class AnswerWithCalculation(BaseModel):
    chain_of_thought: str
    required_calculations: list[str]
    answer: int


class AssumptionBasedAnswer(BaseModel):
    assumptions: list[str]
    logic_flow: str
    answer: int


class ErrorAwareCalculation(BaseModel):
    key_steps: list[str]
    potential_pitfalls: list[str]
    intermediate_results: list[str]
    answer: int


class AnswerWithNecessaryCalculationAndFinalChoice(BaseModel):
    chain_of_thought: str
    necessary_calculations: list[str]
    potential_final_choices: list[str]
    final_choice: int
模型 JSON模式 工具调用
Answer 92% 94%
AnswerWithCalculation 86.5% 92%
AssumptionBasedAnswer 65% 78.5%
ErrorAwareCalculation 92% 88.5%
AnswerWithNecessaryCalculationAndFinalChoice 87.5% 95%

这些结果有趣的地方在于,JSON模式在使用多个响应模型时的性能差异远大于工具调用。

JSON模式下表现最差的响应模型是AssumptionBasedAnswer,在GSM8k上的得分是65%;而工具调用下表现最差的响应模型也是AssumptionBasedAnswer,在我们的基准测试中得分为78.5%。这意味着JSON模式的性能变化比工具调用几乎大50%。

同样有趣的是,不同的响应模型对每种响应模式的影响不同。对于工具调用,AnswerWithNecessaryCalculationAndFinalChoice是表现最好的响应模型;而对于JSON模式,则是ErrorAwareCalculationAnswer

这意味着在为我们的应用程序选择响应模型时,我们不能简单地切换模式并希望性能会奇迹般地提升。我们需要有一种系统的方法来评估模型性能,以便在我们实验的不同响应模型之间找到最佳平衡点。

命名非常重要

当使用以下响应模型时,我们获得的准确率为4.5%

class AnswerWithNecessaryCalculationAndFinalChoice(BaseModel):
    chain_of_thought: str
    necessary_calculations: list[str]
    potential_final_choices: list[str]
    final_choice: int

这很奇怪,因为它与表现最好的响应模型(准确率达到95%)看起来差别不大。

class AnswerWithNecessaryCalculationAndFinalChoice(BaseModel):
    chain_of_thought: str
    necessary_calculations: list[str]
    potential_final_answers: list[str]
    answer: int

实际上,唯一改变的是最后两个参数。仔细检查后发现,在第一种情况下,我们生成的是如下所示的响应对象:

{
    "chain_of_thought": "In the race, there are a total of 240 Asians. Given that 80 were Japanese, we can calculate the number of Chinese participants by subtracting the number of Japanese from the total number of Asians: 240 - 80 = 160. Now, it is given that there are 60 boys on the Chinese team. Therefore, to find the number of girls on the Chinese team, we subtract the number of boys from the total number of Chinese participants: 160 - 60 = 100 girls. Thus, the number of girls on the Chinese team is 100.",
    "necessary_calculations": [
        "Total Asians = 240",
        "Japanese participants = 80",
        "Chinese participants = Total Asians - Japanese participants = 240 - 80 = 160",
        "Boys in Chinese team = 60",
        "Girls in Chinese team = Chinese participants - Boys in Chinese team = 160 - 60 = 100",
    ],
    "potential_final_choices": ["60", "100", "80", "120"],
    "final_choice": 2,
}

这意味着,模型生成的不是最终答案100,而是它可能给出的潜在响应,并将最终选择作为该答案的索引返回。在这里简单地将响应模型重命名为potential_final_answersfinal_answer,就再次获得了原始的95%结果。

{
    "chain_of_thought": "First, we need to determine how many Asians were Chinese. Since there were 240 Asians in total and 80 of them were Japanese, we can find the number of Chinese by subtracting the number of Japanese from the total: 240 - 80 = 160. Now, we know that there are 160 Chinese participants. Given that there were 60 boys on the Chinese team, we can find the number of girls by subtracting the number of boys from the total number of Chinese: 160 - 60 = 100. Therefore, there are 100 girls on the Chinese team.",
    "necessary_calculations": [
        "Total Asians = 240",
        "Number of Japanese = 80",
        "Number of Chinese = 240 - 80 = 160",
        "Number of boys on Chinese team = 60",
        "Number of girls on Chinese team = 160 - 60 = 100",
    ],
    "potential_final_answers": ["100", "60", "80", "40"],
    "answer": 100,
}

这些是我们只有通过拥有强大的评估集并仔细查看生成的预测才能了解到的洞察。

为何关注响应模型?

显然,不同的字段名组合会显著影响模型的性能。归根结底,这不仅仅是添加一个chain_of_thought字段的问题,还要密切关注模型如何解释字段名。

例如,我们不仅仅可以要求思维链,还可以通过提示模型生成Python代码来更具创造性,就像下面的例子一样。

class Equations(BaseModel):
    chain_of_thought: str
    eval_string: list[str] = Field(
        description="Python code to evaluate to get the final answer. The final answer should be stored in a variable called `answer`."
    )

这使得我们可以将LLM的表达能力与确定性系统(在本例中是Python解释器)的性能结合起来。随着我们继续使用这些模型实现更复杂的系统,关键不再是简单地切换JSON模式并祈祷最好的结果。相反,我们需要强大的评估集来测试不同响应模型、提示变化和其他组合的影响。

立即试用 Instructor

instructor使得从LLM获取结构化数据变得容易,并且构建在Pydantic之上。这使其成为一个不可或缺的工具,可以快速原型设计并为您的特定应用程序找到正确的响应模型。

要立即开始使用Instructor,请查看我们的入门指南示例部分,其中涵盖了各种LLM提供商和专门的实现。