Skip to content
This page has been auto-translated and may contain errors.View in English

评估

docs.scrimba.com

你改了个提示词,试了一次,答案看起来更好了,然后你发布了。一周后用户撞上了它悄悄变糟的情况。评估(evaluation 的简称)就是解决方案:对你的 AI 功能是否有效的可重复测试,一组输入配上好的输出应该是什么样子,每次都用同样的方式评分,这样你就能知道一个改变是否真的有帮助。模型是一个概率黑箱,每次运行时对同一个问题的回答都不一样,所以测量是唯一的办法,这种纪律感是把演示和产品区分开来的很大一部分。

为什么凭眼感判断失败

手工检查你的 AI 的答案一开始感觉还不错。你运行一个提示词,读取回复,看起来不错,然后继续。问题在于有三件事悄悄地对你不利,而你已经经历过所有这些。

这三件事是:

  • 输出会变化。 同一个提示词可以在每次运行时给出不同的答案,所以一个好的结果不能保证下一次也会好。
  • 改变会导致回退。 一个修复一个情况的提示词编辑经常会破坏你没有看到的另一个情况。
  • 它无法扩展。 你可以读五个答案。但每次你改一行时,你无法读五百个。

这些都不意味着你做错了什么。这意味着凭眼感判断是一个这么大的任务的错误的工具,有一个更平静的方式来做这件事。

Juno为什么凭眼感判断失败 手工读答案有三个原因会崩溃,所有这些都扎根于模型的工作方式:输出在运行间变化,编辑悄悄破坏你没有看的情况,而且一旦有数百个答案,你无法靠手工跟上。这不是一个纪律问题,这是这个工作量的错误工具。好消息是修复很友好,而且接下来就来了。

凭眼感判断一个回复几乎什么都告诉不了你,原因在于模型的工作方式。因为输出是非决定性的(同一个提示词可以在每次运行时给出不同的答案),单个回复是大小为一的样本。它无法告诉你**通过率**,即有多少情况结果正确,因为你需要在同样的情况上进行许多运行才能估计它实际上多久才能正常工作。

回退隐藏的原因是相关的。手工修复一个情况,你就没有了其他情况的记录,所以下一个编辑可以破坏你停止看的东西。看到这一点的唯一方式是一个固定的情况集合,每次都用同样的方式评分,所以下降会显示为一个数字而不是生产中的惊喜。

在数学之上还有一个人的问题。手工评审在人们之间和心情之间漂移:同样的答案在周一读起来不错,在周五读起来就弱。所以这步动作是把"看起来不错"转换成在固定情况集合上测量的通过率。把一个感觉转变成同样情况上的一个数字。

Juno为什么凭眼感判断失败 一个回复是大小为一的样本,所以它告诉你模型工作过一次,而不是它多久工作一次。要看到真实的通过率你要运行许多情况;要抓住回退你要修复那个情况集合并每次都用同样的方式评分。手工评审也随评审者而漂移,所以这项工作是用测量的数字替换"看起来不错"。

抽查失败不是因为你粗心。它失败是因为它内置了选择偏差:你看你期望工作的情况,所以你从不检查的行为就是在用户面前回退的那个。你不跟踪的指标正好是移动的那个。

没有基准分数,每个质量改变都是你对自己讲的故事。你无法把一个移动归因到一个特定的编辑,而且你无法对一组改变进行二分以找到哪个掉了**通过率**(你的情况中有多少结果正确的份额),因为你从不测量过之前的。"感觉更好了"活着是因为没有什么与它相悖。

基于感觉的迭代也过拟合。你不停地重新输入同样的几个例子,调整直到那些看起来对,并悄悄地训练你的提示词到那个微小的私有集合,同时真实流量在别的地方漂移。手工评审无法看到那个分布移位,因为新的输入是你不在输入的那些。一个版本化的评估集合加上跟踪的分数是说一个改变有帮助而不是感觉更好的唯一方式,它是迭代和猜测之间的区别。

Juno为什么凭眼感判断失败 我在感觉上发布过,然后看着我从不抽查的那条路在错误日志中点亮,那就是选择偏差在做它的工作。没有基准意味着没有归因和没有二分:如果你从不有一个数字,你无法证明哪个编辑移动了这个数字。保持一个版本化的集合和一个跟踪的分数,或者你在过拟合你不停重新输入的五个例子,而真实流量在你无法看到的地方走开了。

构建一个评估集

一个评估集是一个测试用例的列表。每个用例有一个输入和对它什么是好输出的说明。从小开始。十到二十个覆盖你的常见情况加上几个诡异边界情况的用例比没有远有用得多,而且你可以在发现野外的新失败时扩展这个集合。

python
# 评估集:输入和预期的标签
eval_set = [
    {"input": "食物很冷,送到得很晚。", "expected": "NEGATIVE"},
    {"input": "我人生中最好的一顿饭!", "expected": "POSITIVE"},
    {"input": "还不错,没什么特别的。", "expected": "NEUTRAL"},
    # ...随着你发现新的边界情况添加真实情况和边界情况
]

当用户报告一个坏答案时,用它应该给出的输出将其添加到集合中。你的评估集然后成为每个你永远不想再犯的错误的不断成长的记忆,就像你修复的一个 bug 变成你保持的一个测试的方式一样。

Juno构建一个评估集 一个评估集是一个测试用例的列表:一个输入,和一个好答案看起来是什么样子。从十到二十个覆盖你的日常情况和几个诡异的那些开始。每次用户撞上一个坏答案,用它应该给出的答案添加它,而你的集合就悄悄记住你永远不想重复的每个错误。

一个**评估集**是你的测试用例的集合,每一个是一个输入配上你会接受为好的输出。重要的数字不是大小,而是覆盖。二十个精心选择的用例每次都击败五十个临时的。

目标是覆盖三件事:你的用户实际上撞到的通常路径、两个标签坐在一起的决策边界(歧义的情况正好在线上),和你已经看到过的已知失败模式。精心挑选的友好输入让你的分数看起来很好并告诉你什么都没有,所以保持集合代表真实流量。

用户报告的每个 bug 变成集合中的一个永久用例。这把它变成一个回退套件:一个固定的 bug 保持固定是因为暴露它的用例现在每次都运行。当你的预期输出是结构化的而不是自由文本时,像一个标签或一个 JSON 字段,你可以用精确匹配对比来评分,这很干净和明确,看结构化输出

保持一个单独的持留切片,你从不针对它调整。如果你调整提示词直到你通过你正在看的精确用例,你在评分自己的你已经研究的答案,分数停止意味着任何东西。

Juno构建一个评估集 覆盖击败原始大小:撞上通常路径、标签之间的歧义边界和你已经看到的失败。把每个报告的 bug 变成一个永久的用例,这样集合就像一个回退套件一样工作。保持一个你在调整时从不看的持留切片,或者你在评分自己的你研究过的答案。

把**评估集**作为一个资产对待,你像对提示词一样把它版本化在一起,因为这两个一起移动,一个提示词改变可以悄悄地改变什么算一个好答案。集合的标签质量是你的指标的天花板:一个错的"expected"值限制了你的分数,并悄悄地教给你错的教训,所以仔细策划标签一样重要于收集用例。

防守对抗测试泄漏。如果你调整直到你通过你报告的精确集合,这个数字是戏剧。在迭代时持留一个切片你不看,并在一段时间内旋转它,这样它不会慢慢地泄漏到你的调整循环中。

看阶级平衡。一个九十百分比清楚-切的积极的集合会报告一个奉承的数字,同时隐藏每个实际上重要的十百分比上的失败,所以取样这样硬的和罕见的情况携带真实的权重。从生产日志而不是想象中拉用例,因为真实流量会表面你永远不会在你的办公桌上发明的措辞。

接受这个集合腐烂。当产品转移时,老用例停止反映用户发送的内容,而新的失败模式出现,集合从不看过。策划它是持续的工作,不是一次性的构建。

Juno构建一个评估集 这个集合是一个资产,你像对提示词一样把它版本化在一起,而它的标签质量是你的指标的硬上限,一个坏的预期值限制了整个分数。持留一个切片并旋转它,或者你报告的数字是戏剧。看阶级平衡,从日志中取样真实用例,并假定这个集合当产品移动时腐烂,所以策划它从不真的结束。

如何评分

不同的任务需要不同的评分,所以第一步是匹配检查到你期望的答案类型。

  • 精确匹配在有一个正确答案时有效:一个分类标签、一个提取的字段、一个是或否。你直接比较输出到预期值。
  • 基于准则的评分在没有单一正确答案时有效,像一个摘要或一个回复。你检查输出是否满足条件:摘要是否提到了关键事实?它是否在 50 字以下?语气是否礼貌?
  • LLM-as-judge 使用第二个模型调用来对你的准则针对输出评分。它扩展到简单检查无法捕获的模糊质量问题,但法官本身是一个模型,所以它可以是错的,而且你现在和然后对抗人的判断检查它。
python
# 在评估集上精确匹配评分
def run_evals(eval_set, classify):
    # 通过的测试数
    passed = 0
    for test in eval_set:
        output = classify(test["input"])
        if output == test["expected"]:
            passed += 1
        else:
            print(f'失败: "{test["input"]}" -> 得到 {output},期望 {test["expected"]}')
    print(f"{passed}/{len(eval_set)} 通过了")

精确匹配是最友好的开始地方,它适合前面章节的分类和提取工作完美。LLM-as-judge 的想法有效是因为发现一个答案是否满足一个标准通常是一个比生产答案更简单的预测,就像评阅一个文章比写一个更简单。但法官是另一个有同样缺陷的预测器,所以把它的分数作为一个有用的估计,给它一个清晰的标准,并现在和然后对你自己的样本检查它的等级。

Juno如何评分 挑选适应答案的检查:精确匹配当有一个正确答案时,标准当有几个好的时,和一个第二个模型作为法官当模糊质量时。精确匹配是最温柔的开始,它整洁地对齐了你已经练习的分类和提取。但法官也是一个模型,所以现在和然后自己看一下它的等级。

匹配评分器到任务,然后伸向能廉价捕获你关心什么的那个。一个**评分器**是决定一个输出是否通过的代码或模型调用,而错误的选择要么错过真实失败要么标记答案本来是好的。

精确匹配是针对标签和提取字段:你比较输出到预期值,而结果是决定性的,所以你可以信任它。它自然地配对结构化输出,因为返回一个固定模式的模型给你一些干净的东西来比较而不是自由形式的散文。

程序化准则廉价地覆盖中间地:检查长度,确认一个必需字符串存在,验证 JSON 解析,运行一个正则表达式。这些在毫秒内运行,没有模型调用,所以在你花钱一个法官之前对任何半结构化使用它们。

LLM-as-judge 是针对开放式质量,其中摘要或回复有许多好的形式,没有精确的目标。它需要一个写好的标准这样法官应用一个一致的标准,加上一个校准检查,其中你比较它的等级到你自己的标签在样本上。记住一个评分器是你也必须信任的代码:一个不稳定的评分器递给你一个自信错误的分数。

Juno如何评分 使用能廉价抓住重要东西的评分器:精确匹配针对标签和字段,程序化检查针对长度或有效 JSON 或必需字符串,以及一个模型法官仅针对开放式质量。精确匹配很好地与结构化输出配对,因为一个固定模式给你一些干净的东西来比较。无论你挑选什么,评分器也是你信任的代码,所以一个不稳定的那个递给你一个自信错误的分数。

LLM-as-judge 是以最有趣的方式破裂的评分器,所以在你靠它之前计划它的偏差。一个**法官**倾向更长的答案,当比较一对时挑选第一个选项,并奖励读起来像它自己风格的文本,而且它的绝对分数很差地校准,所以一个 8 从一个运行不是一个 8 从下一个。

把法官作为一个你自己在评估的模型对待。钉住和版本化法官模型,因为它在它更新时漂移,而你的分数与它一起移动。给它一个紧的标准加上几个锚点例子,然后在一个标签样本上测量法官对人的一致性,这样你知道有多远信任它。

你能在哪里,要求一个成对比较("A 或 B 更好?")而不是绝对 1-to-10 分数;相对判断通常比要求一个模型撞到一个校准的数字更可靠。钉住法官,标准紧,对人检查它。

留意循环性和账单。同样的模型族评分它自己的输出共享它的盲点,所以生成器造成的缺陷是法官可能错过的缺陷,而大规模评分每个用例是一个第二个模型账单在第一个的顶上。

Juno如何评分 法官有你没有要求的意见:它喜欢长答案、第一个选项和读起来像它自己的散文,而且它的绝对分数在运行之间漂移。钉住和版本化它,递给它一个紧的标准加上锚,并跟踪它多久与人一致,因为你现在在评分评分器。倾向于相对判断而不是一个孤独的 1-to-10,并保持意识一个模型评分它自己的族共享它的盲点并把账单翻倍。

在每次改变时运行它们

一个评估集只在你实际运行它时有用。像对待代码一样对待你的提示词:在你发布对提示词、模型或你的检索的改变之前,运行评估并比较分数到它之前是什么。如果数字下降了,你导致了回退,即使你的一个手工测试碰巧看起来不错。

这是把提示词工程从猜测变成你可以有信心迭代的东西。改变什么,测量它,保持得分更高的,丢弃得分更低的。这是让普通软件可靠的同样循环,现在应用到 AI 功能的模糊部分。

如果分数下降了,你导致了回退。一个幸运的手工测试不会推翻集合,所以每次让数字做平局决定者。

Juno在每次改变时运行它们 一个评估集只在你运行它时有帮助,所以让它成为一个习惯:在任何改变发布之前,运行评估并检查分数对上一次。如果它下降了,那是一个要修复的回退,不是一个要挥手的巧合。改变,测量,保持得分更高的,你在迭代而不是猜测。

将评估运行连接到你的开发循环中,这样它就像测试套件一样阻止改变合并。对提示词、模型或你的检索(拉进支持文档的步骤)的改变直到你运行集合并比较分数到上一个运行之前不发布。数字,而不是你在一个手工测试后的直觉,决定改变是否是一个改进。

一次改变一个变量。如果你在同样的通道中编辑提示词并交换模型,分数中的移动有两个可能的原因而且你无法把它归因于任何一个。隔离改变,运行集合,读取结果,然后移动到下一个。

读取每用例的 diff,而不仅仅是**聚合分数**(跨每个用例平均的单个数字)。一个稳定的总数可以隐藏一个用例被固定和一个被破裂同时,这网络到零,同时你的行为悄悄地移位。看哪个用例翻转告诉你你的改变实际上做了什么。

跟踪分数随时间,这样改进是可见的,回退站出来。一个记录的历史把"这感觉更好"变成一条你可以指着的线,而且它让一个突然下降快速追溯到导致它的改变。

Juno在每次改变时运行它们 像你在测试上阻止合并一样在分数上阻止改变:没有运行,没有发布。一次改变一个变量一个通道这样分数移动有一个原因,并读取每用例的 diff,因为一个平坦的总数可以隐藏一个用例固定和一个破裂。登录分数随时间这样赢是可见的,回退站出来。

让它成为一个真实的门,而不是一个仪式。在 CI 中放一个分数阈值这样一个低于酒吧的改变使构建失败而不是靠某人记住看。而且在每个模型版本碰撞时重新运行集合,因为同样的提示词在新的权重上着陆有所不同;钉住你测试对抗的模型版本并在你采用一个新的之前重新评估。

持有两个陷阱在心。首先,评估集(你固定的测试用例集合加上预期结果)是一个代理,不是地面真理,所以对它调整硬过拟合:你爬分数而真实行为停止。保持扩展和旋转集合这样你测量任务,而不是记忆测试。

第二,离线评估不是全图。把它们与在线信号配对,取样生产流量,用户反馈,抱怨率,并把真实失败反馈到集合中作为新用例。

因为输出是非决定性的,同样的输入可以跨运行不同地评分。对每个用例运行足够多的样本并对抗容差而不是追逐单运行噪音评分;一个一点的摇晃在运行之间是测量呼吸,不是回退。在一个容差之上活的一个水平设置酒吧,或者你会花你的周调查鬼。我已经做过,不止一次。

Juno在每次改变时运行它们 在 CI 中放一个阈值这样一个坏改变使构建失败,而且在每个模型版本碰撞时重新评估,因为新权重读同样的提示词有所不同。集合是一个代理,所以保持旋转它并与在线信号配对,或者你会过拟合到你自己的测试。而且因为输出是非决定性的,对抗容差而不是调查鬼评分,这是的,我已经做过。

实际中

上一个部分的 run_evals 函数是一个完整的,如果小的话,评估工具。你把它指向你的分类器和你的评估集,它告诉你有多少用例通过并准确显示哪些失败:

python
run_evals(eval_set, classify_review)
# 失败: "还不错,没什么特别的。" -> 得到 POSITIVE,期望 NEUTRAL
# 2/3 通过了

那一个失败的线值一打成功的手工测试,因为它指向一个真实的弱点你现在可以修复并重新测量。最后一章,安全和限制,在你把这些中的任何一个放在真实用户面前之前覆盖风险设计对抗。

Juno实际中 一个像 run_evals 的小工具已经赚回它的价值:它给你一个分数和失败的精确用例。修复一个,再运行它,看数字移动。接下来,Safety and limits 覆盖在真实用户到达之前要防守对抗什么。

一个小**评估工具**,运行你的评估集中的每个用例并对你期望的东西评分输出的那位代码,是把"似乎很好"变成一个你可以移动的数字的循环。你运行集合,你得到一个分数加上一个失败列表,你修复最廉价的真实失败,你再运行它。

每个失败用例是一个清晰完成定义的工作单位:用例通过。那是一个比"让提示词更好"更锋利的目标,因为你可以确切地看到你何时击中它,确切地看到你何时回退。

在这些中的任何一个与真实用户满足之前,下一章,安全和限制,覆盖你事前设计对抗的风险。

Juno实际中 这个工具把感觉变成一个你可以移动的数字:运行集合,读分数和失败,修复最廉价的真实的,再运行。每个失败是一个清晰完成定义的任务,这是一个比"让它更好"更锋利的目标。然后在真实用户看它之前 Safety and limits。

在生产中那个小工具停止成为一个你手工运行的脚本,并变成系统的一部分。它长大到一个 CI 门,当分数下降到你设置的线下时阻止释放的检查,加上一个仪表板你在发行间观察以抓住一个单运行从不显示的缓慢漂移。

离线评估,你在发布之前评分的固定集合,和在线监控,你从真实流量后收集的信号,彼此反馈:生产失败变成新的离线用例,离线集合告诉你你的修复是否被持有。在这里赢的团队不是有最聪明提示词的那些。他们是那些在每个人其他人凭眼感判断时测量的那些。

那是纪律。下一章,安全和限制,覆盖在这任何东西到达真实用户之前要设计对抗的风险。

Juno实际中 这个和脚本变成一个 CI 门加上一个仪表板,离线评估和在线监控彼此反馈。我在"看起来对我很好"上发布过,而且稍后为它付出,所以从我这里拿它:测量的团队在休息的人凭眼感判断时赢。在真实用户到达之前读 Safety and limits。