提示词评测方法
Anthropic 官方课程中文版 · Prompt Evaluations · 查看原始课程
Evaluations 101
你可能已经熟悉各种 AI 基准测试,例如 MMLU、HumanEval 或 HellaSwag。这些基准测试是通用的、标准化的评测集,用于衡量 AI 模型在大量常见任务上的表现。但在本课程中,我们要讨论的是另一种评测——面向你自己的具体应用场景的「应用评测」(customer evaluation)。
两种评测的区别
通用基准测试与应用评测有本质区别:通用基准测试衡量的是模型的整体能力,而应用评测衡量的是模型在你的特定用例中的表现。
公开标准化数据集,测试跨场景的模型通用能力;由 AI 研究社区维护,供模型对比用。
针对你自己的应用场景定制;评测提示词效果、模型选择及配置参数;随业务需求持续演进。
应用评测就像 AI 开发的「瑞士军刀」——它能帮助你衡量提示词的改进效果、比较不同模型、检测性能回归,以及在代码修改后验证系统依然正常运行。
评测循环
每次评测都是一个迭代循环,包含以下四个步骤:
- 准备提示词(Prompt):编写或更新你想要测试的系统提示词。
- 收集输出(Collect Outputs):对评测数据集中的每条输入,调用 Claude 得到对应的输出。
- 对输出打分(Grade Outputs):使用评分逻辑判断每个输出是否符合预期。
- 分析结果(Analyze Results):汇总得分,判断哪些测试通过、哪些失败,从而指导下一轮迭代。
评测数据集的组成
一个完整的评测数据集(eval dataset)通常包含四个要素:
用户发送给 Claude 的请求,例如「给我翻译这段 Ruby 代码」。
该输入对应的理想输出,用作评分的基准。可以是精确字符串,也可以是评判标准描述。
当前提示词下 Claude 实际生成的回答,在每次评测运行时动态生成。
对比参考答案与实际输出后得到的分数,通常为通过/失败或数值分。
三种评分方式
根据任务的性质,可以选择不同的评分方式:
人工阅读输出并打分。适合主观性强、尚无自动化标准的任务,但成本高、难以大规模执行。
用确定性函数(如正则表达式、精确匹配、集合比对)自动评分。速度快、成本低,适合有明确正确答案的任务。
用另一个 LLM(通常是能力更强的模型)充当「裁判」来评估输出质量。适合难以代码量化的主观性或开放性任务。
在实践中,应尽可能优先使用代码评分——它最简单、最稳定、成本最低。当代码无法客观量化时,再考虑模型评分。人工评分通常用于早期小规模验证或作为黄金标准校准。
本课程共 9 章,按由浅入深的顺序讲解评测方法:第 1 章为概念导论,第 2 章介绍 Anthropic 控制台 Workbench 的可视化评测,第 3–4 章演示如何用 Python 代码从零构建评测,第 5–9 章介绍 promptfoo 工具的各种评分模式。
Anthropic Workbench Evaluations
Anthropic Workbench(console.anthropic.com/workbench)是一个可视化界面,用于快速迭代提示词并进行人工评测。与代码评测相比,它上手门槛极低,非常适合在正式构建评测管线之前进行快速原型验证。
Workbench 评测特别适合早期探索阶段——你可以直观地看到模型输出,并即刻进行人工打分。但因为依赖人工评判,它不适合大规模、自动化或需要持续回归测试的场景。
演示用例:代码翻译
本章以「将代码翻译为 Python」为例,演示 Workbench 的评测流程。提示词模板如下:
You are a skilled programmer tasked with translating code from one programming language to Python.
Your goal is to produce an accurate and idiomatic Python translation of the provided source code.
Here is the source code to translate:
<source_code>
{{SOURCE_CODE}}
</source_code>
Translate the above code into Python. Provide only the translated Python code as your output,
without any additional comments or explanation.
提示词使用双花括号 {{SOURCE_CODE}} 作为变量占位符,Workbench 会自动识别并在「变量」面板中显示填写入口。
在 Workbench 中运行单次测试
- 将上面的提示词粘贴到 Workbench 左侧的「系统提示词」输入框中。
- 点击工具栏中的「变量」(
{ })按钮,为SOURCE_CODE填入具体代码片段(例如一段 JavaScript 或 Ruby 代码)。 - 点击橙色「运行」按钮,在右侧查看模型输出。
Workbench 界面:左侧为系统提示词编辑区,右侧实时显示模型输出;工具栏包含变量注入、运行、历史版本等控件。
切换到评测视图
单次测试能帮你验证提示词的基本逻辑,但一个高质量的提示词需要在多样化的输入上都表现良好。点击界面顶部的「Evaluate」标签,可以进入批量评测视图。
初始进入评测视图时,已有一行基于当前变量填写的测试用例。点击「Add Row」按钮可以继续添加。对于代码翻译场景,可以添加以下输入变体:
- 一段简单的 JavaScript(例如声明一个常量)
- 一段 Ruby 循环
- 一段稍复杂的多行函数
评测视图:表格形式,每行代表一个测试用例,包含输入变量列和对应的模型输出列;可逐行运行或全部一键运行。
人工打分
运行完评测后,Workbench 提供了直观的打分界面,可以对每个输出进行人工评判:
- 代码翻译正确,无多余注释 → 标记为通过
- 包含不必要的解释性前缀(如「当然!以下是翻译结果……」)→ 标记为失败
- 存在遗漏或错误翻译 → 标记为失败
通过逐行打分,你可以快速识别提示词的薄弱点。
发现问题 → 更新提示词
评测结果揭示了两个典型问题:
- 输出包含不必要的开场白(如「Certainly! Here's the Python translation…」),浪费 token
- Python 代码被包裹在 Markdown 代码块
```python中,而我们的应用可能不需要 Markdown 格式
针对这两个问题,更新后的提示词:
You are a skilled programmer tasked with translating code from one programming language to Python.
Your goal is to produce an accurate and idiomatic Python translation of the provided source code.
Here is the source code to translate:
<source_code>
{{SOURCE_CODE}}
</source_code>
Respond only with the Python translation, wrapped inside <python></python> XML tags.
Do not include any other text or explanation.
更新后的提示词填入 Workbench,点击运行后,输出不再包含开场白,代码被正确包裹在 <python> 标签中。
对比两个版本
使用 Workbench 的多列对比功能,可以并排查看 v1 和 v2 提示词在相同输入下的输出,直观地看出改进效果:
对比视图:左列为 v1 提示词的输出(包含前缀和 Markdown 代码块),右列为 v2 的输出(仅 <python> 标签包裹的干净代码)。两列得分清晰显示 v2 全部通过,v1 部分失败。
Workbench 是启动评测旅程的理想起点。一旦你确定了提示词的基本方向,就可以转向第 3–4 章介绍的代码评测方案,以实现更大规模、更自动化的持续测试。
A Simple Code-Graded Evaluation
本章通过一个有趣的例子——「计算动物腿的数量」——演示如何从零用 Python 代码构建一个完整的代码评分评测,并通过三次提示词迭代将准确率从 66.7% 提升到 100%。
评测数据集
每条评测数据包含一个问题(animal)和对应的标准答案(num_legs):
eval_data = [
{"animal": "ant", "num_legs": 6},
{"animal": "snake", "num_legs": 0},
{"animal": "spider", "num_legs": 8},
{"animal": "horse", "num_legs": 4},
{"animal": "chicken", "num_legs": 2},
{"animal": "salmon", "num_legs": 0},
{"animal": "tarantula", "num_legs": 8},
{"animal": "chimpanzee", "num_legs": 4},
{"animal": "centipede", "num_legs": 100},
{"animal": "caterpillar", "num_legs": 16},
{"animal": "human", "num_legs": 2},
{"animal": "bald eagle", "num_legs": 2},
]
提示词 v1(基础版)
从最简单的提示词开始测试:
def build_prompt_v1(animal):
return f"""How many legs does a {animal} have?
Answer with a single number and nothing else."""
收集 Claude 的输出
对每个动物调用 Claude,收集回答并存储到 outputs_v1 列表:
import anthropic
client = anthropic.Anthropic()
def get_claude_response(prompt):
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
outputs_v1 = []
for item in eval_data:
prompt = build_prompt_v1(item["animal"])
output = get_claude_response(prompt)
outputs_v1.append({**item, "output": output})
评分函数
评分逻辑很直接:从模型输出中提取数字,与标准答案比对。
这里使用 extract_answer() 函数先从输出中抽出第一个数字,再做比较。
import re
def extract_answer(output):
"""从模型输出中提取第一个整数"""
match = re.search(r"\d+", output)
return int(match.group()) if match else None
def grade_outputs(outputs):
correct = 0
for item in outputs:
predicted = extract_answer(item["output"])
if predicted == item["num_legs"]:
correct += 1
return correct / len(outputs)
score_v1 = grade_outputs(outputs_v1)
print(f"v1 准确率: {score_v1:.1%}") # 66.7%
分析失败案例
v1 的主要问题:部分动物(如蜈蚣、毛毛虫)的模型输出包含解释性文字,例如:
"Centipedes can have anywhere from 30 to 354 legs, depending on the species. However, the most common species found in homes, Scutigera coleoptrans, has 30 legs."
对于「蜈蚣有多少条腿?」这类有歧义的问题,模型倾向于给出解释而非单一数字,导致 extract_answer() 提取到错误的数字。
提示词 v2(约束输出格式)
增加格式约束,要求模型只输出数字:
def build_prompt_v2(animal):
return f"""How many legs does a {animal} have?
Respond with a single integer. Do not include any explanation or additional text."""
outputs_v2 = []
for item in eval_data:
prompt = build_prompt_v2(item["animal"])
output = get_claude_response(prompt)
outputs_v2.append({**item, "output": output})
score_v2 = grade_outputs(outputs_v2)
print(f"v2 准确率: {score_v2:.1%}") # 仍未达到 100%
提示词 v3(思维链 + 结构化标签)
对于蜈蚣等「腿数有争议」的动物,模型需要一个约定好的默认值。同时,引入思维链(chain of thought) 让模型先推理再给出答案,并用 XML 标签包裹最终答案以便精确提取:
def build_prompt_v3(animal):
return f"""How many legs does a {animal} have?
Think through this step by step, then provide your final answer.
If the exact number is variable (e.g., centipede), use the most commonly cited or
representative number for the species.
Wrap your final numeric answer in <answer></answer> tags.
For example: <answer>4</answer>"""
相应地,extract_answer() 也需要优先从 <answer> 标签中提取:
def extract_answer_v3(output):
"""优先提取 <answer> 标签内的数字,回退到第一个整数"""
tag_match = re.search(r"<answer>(\d+)</answer>", output)
if tag_match:
return int(tag_match.group(1))
fallback = re.search(r"\d+", output)
return int(fallback.group()) if fallback else None
def grade_outputs_v3(outputs):
correct = 0
for item in outputs:
predicted = extract_answer_v3(item["output"])
if predicted == item["num_legs"]:
correct += 1
return correct / len(outputs)
outputs_v3 = []
for item in eval_data:
prompt = build_prompt_v3(item["animal"])
output = get_claude_response(prompt)
outputs_v3.append({**item, "output": output})
score_v3 = grade_outputs_v3(outputs_v3)
print(f"v3 准确率: {score_v3:.1%}") # 100%
三次迭代对比
这个例子展示了评测驱动开发(Eval-Driven Development)的核心流程:写提示词 → 运行评测 → 分析失败 → 改进提示词 → 再次评测。每轮迭代都有数据支撑,而不是凭感觉猜测。
Code-Graded Classification Evaluations
本章演示如何对「分类任务」构建代码评分评测。以客户投诉分类为例:给定一条投诉文本,模型需要将其归入以下五个类别之一(或多个):
- Software Bug(软件缺陷)
- Hardware Malfunction(硬件故障)
- User Error(用户操作失误)
- Service Outage(服务中断)
- Feature Request(功能请求)
评测数据集
每条数据包含投诉文本和正确的分类集合(注意:部分投诉可以归属多个类别):
eval_data = [
{
"complaint": "The app crashes every time I try to upload a photo",
"golden_answer": {"Software Bug"}
},
{
"complaint": "My printer isn't recognized by my computer",
"golden_answer": {"Hardware Malfunction"}
},
{
"complaint": "The cloud storage isn't syncing and I can't access my files from other devices",
"golden_answer": {"Software Bug", "Service Outage"} # 多标签
},
{
"complaint": "I accidentally deleted my account and can't recover my data",
"golden_answer": {"User Error"}
},
# ... 共约 20 条
]
注意:golden_answer 使用 Python 集合(set)而非列表,
这便于后续用集合操作进行精确比对。
基础提示词
def basic_prompt(complaint):
return f"""Classify the following customer complaint into one or more of these categories:
- Software Bug
- Hardware Malfunction
- User Error
- Service Outage
- Feature Request
Complaint: {complaint}
List each applicable category on a separate line."""
计算准确率
分类任务的评分函数需要处理多标签情况,使用集合相等来判断是否完全正确:
def extract_categories(output):
"""从模型输出中解析分类标签集合"""
valid_categories = {
"Software Bug", "Hardware Malfunction",
"User Error", "Service Outage", "Feature Request"
}
found = set()
for line in output.strip().split("\n"):
line = line.strip().strip("-").strip()
if line in valid_categories:
found.add(line)
return found
def calculate_accuracy(results):
correct = sum(
1 for r in results
if extract_categories(r["output"]) == r["golden_answer"]
)
return correct / len(results)
封装评测函数
将「调用模型 + 打分」封装为一个可复用的函数:
def evaluate_prompt(prompt_fn, eval_data):
results = []
for item in eval_data:
output = get_claude_response(prompt_fn(item["complaint"]))
results.append({
**item,
"output": output,
"predicted": extract_categories(output),
"correct": extract_categories(output) == item["golden_answer"]
})
accuracy = calculate_accuracy(results)
return results, accuracy
results_basic, acc_basic = evaluate_prompt(basic_prompt, eval_data)
print(f"基础提示词准确率: {acc_basic:.1%}") # ≈ 85%
改进提示词:加入 few-shot 示例
基础提示词的主要失败案例是「多标签」投诉——模型倾向于只给一个分类。 加入少量示例(few-shot)可以帮助模型理解什么情况下应该输出多个分类:
def improved_prompt(complaint):
return f"""Classify the following customer complaint into one or more categories.
Multiple categories are possible if the complaint covers several issues.
Categories:
- Software Bug: Issues with software functionality or code
- Hardware Malfunction: Physical device or hardware issues
- User Error: Problems caused by incorrect user action
- Service Outage: Platform or service unavailability
- Feature Request: Suggestions for new functionality
Examples:
Complaint: "The app keeps crashing on my iPhone 12"
Categories: Software Bug, Hardware Malfunction
Complaint: "I can't login and the website shows a 503 error"
Categories: Service Outage
Now classify this complaint:
Complaint: {complaint}
List each applicable category on a separate line."""
results_improved, acc_improved = evaluate_prompt(improved_prompt, eval_data)
print(f"改进提示词准确率: {acc_improved:.1%}") # 100%
迭代结果
使用集合(set)而非列表(list)进行比对,自动处理了顺序问题——无论模型先输出「Software Bug」还是「Service Outage」,只要集合一致就算正确。
Introduction to Promptfoo
前面几章我们用纯 Python 从零构建评测管线。从本章起,我们介绍 promptfoo—— 一个专为提示词评测设计的开源工具框架,它通过 YAML 配置文件统一管理提示词、数据集和评分逻辑, 并提供内置 dashboard 可视化。
使用 promptfoo 需要安装 Node.js(v18+)。不需要全局安装,所有命令均通过 npx promptfoo@latest 运行。
初始化项目
npx promptfoo@latest init
此命令在当前目录创建 promptfooconfig.yaml 配置文件。
删除其中的示例内容,按照本章的步骤逐步填充。
第一步:配置 Provider(模型)
告知 promptfoo 使用哪个模型:
description: "Animal Legs Eval"
providers:
- "anthropic:messages:claude-opus-4-5"
第二步:配置 Prompts(提示词)
promptfoo 推荐将提示词封装为 Python 函数,放在独立的 prompts.py 文件中,
便于版本管理和复用:
# prompts.py
def simple_prompt(animal):
return f"""How many legs does a {animal} have?
Respond with a single integer."""
def cot_prompt(animal):
return f"""How many legs does a {animal} have?
Think through this carefully, then provide your final answer wrapped in <answer></answer> tags.
If the exact number varies by species, use the most commonly cited number."""
在 YAML 中引用这两个函数:
prompts:
- prompts.py:simple_prompt
- prompts.py:cot_prompt
第三步:准备测试数据集(CSV)
将评测数据保存为 dataset.csv。
promptfoo 使用 __expected 列存储参考答案,exact-match: 前缀
表示使用精确匹配评分:
animal,__expected
ant,exact-match:6
snake,exact-match:0
spider,exact-match:8
horse,exact-match:4
chicken,exact-match:2
salmon,exact-match:0
tarantula,exact-match:8
chimpanzee,exact-match:4
centipede,exact-match:100
caterpillar,exact-match:16
在 YAML 中引用:
tests: dataset.csv
第四步:完整配置文件
description: "Animal Legs Eval"
providers:
- "anthropic:messages:claude-opus-4-5"
prompts:
- prompts.py:simple_prompt
- prompts.py:cot_prompt
tests: dataset.csv
运行评测
npx promptfoo@latest eval
终端输出:一个 ASCII 表格,行为测试用例(动物名称),列为两个提示词函数。通过的格子显示绿色 PASS,失败显示红色 FAIL;底部汇总每个提示词的通过率。
启动可视化 Dashboard
npx promptfoo@latest view
promptfoo Web Dashboard:左侧为配置信息,中央为评测结果矩阵,可点击任意格子查看完整输入/输出详情;顶部显示每个提示词的汇总得分。
处理思维链输出:Transform 转换
cot_prompt 的输出包含推理过程和 <answer> 标签,
不能直接做精确匹配。需要用 transform 字段先提取数字:
prompts:
- id: prompts.py:simple_prompt
label: "simple_prompt"
- id: prompts.py:cot_prompt
label: "cot_prompt"
transform: |
const match = output.match(/<answer>(\d+)<\/answer>/);
return match ? match[1] : output.trim();
多模型对比
promptfoo 的一大优势是可以在同一次评测中比较多个模型,
只需在 providers 中列出多个模型即可:
providers:
- anthropic:messages:claude-opus-4-5
- anthropic:messages:claude-3-haiku-20240307
- anthropic:messages:claude-3-5-sonnet-20240620
多模型对比视图:每列代表一个模型,直观展示不同模型在相同测试集上的得分差异。
当你需要对比多个提示词变体、对比多个模型、或将评测集成到 CI/CD 管线时,promptfoo 比纯 Python 方案更高效。它的 YAML 配置直观,dashboard 适合团队协作。
本章评测结果
在动物腿数计数评测中,两个提示词策略的表现差异明显。
simple_prompt 直接询问答案,容易因措辞模糊而失误;
cot_prompt 引导模型逐步推理,再通过 transform 提取数字,准确率显著提升。
思维链提示词配合 transform 字段解析 <answer> 标签,通过率提升约 30 个百分点。
Promptfoo: Classification Evaluations
本章将第四章的客户投诉分类评测迁移到 promptfoo 框架中。
核心挑战在于:分类任务允许多标签输出,需要使用 contains-all 断言
而非简单的精确匹配。
初始化 promptfoo
npx promptfoo@latest init
配置 Provider,使用 Claude 3 Haiku 以节省 API 成本:
description: "Complaint Classification Eval"
providers:
- "anthropic:messages:claude-3-haiku-20240307"
准备提示词文件
将第四章中的两个提示词函数保存到 prompts.py:
# prompts.py
def basic_prompt(complaint):
return f"""Classify the following customer complaint into one or more categories:
- Software Bug
- Hardware Malfunction
- User Error
- Service Outage
- Feature Request
Complaint: {complaint}
List each applicable category on a separate line."""
def improved_prompt(complaint):
return f"""Classify the following customer complaint into one or more categories.
Multiple categories are possible if the complaint covers several issues.
Categories:
- Software Bug: Issues with software functionality or code
- Hardware Malfunction: Physical device or hardware issues
- User Error: Problems caused by incorrect user action
- Service Outage: Platform or service unavailability
- Feature Request: Suggestions for new functionality
Examples:
Complaint: "The app keeps crashing on my iPhone 12"
Categories: Software Bug, Hardware Malfunction
Now classify:
Complaint: {complaint}
List each applicable category on a separate line."""
关键:contains-all 断言
与动物腿数(单值精确匹配)不同,分类任务需要验证输出中包含所有正确标签。
promptfoo 提供了内置的 contains-all 断言满足这个需求。
在 CSV 文件中,用 contains-all:标签1|标签2 语法指定多标签期望值:
complaint,__expected
The app crashes every time I try to upload a photo,contains-all:Software Bug
My printer isn't recognized by my computer,contains-all:Hardware Malfunction
I can't figure out how to change my password,contains-all:User Error
The website is completely down I can't access any pages,contains-all:Service Outage
It would be great if the app had a dark mode option,contains-all:Feature Request
The cloud storage isn't syncing and I can't access my files,contains-all:Software Bug|Service Outage
I accidentally deleted my files and the restore isn't working,contains-all:User Error|Software Bug
完整配置文件
description: "Complaint Classification Eval"
providers:
- "anthropic:messages:claude-3-haiku-20240307"
prompts:
- prompts.py:basic_prompt
- prompts.py:improved_prompt
tests: dataset.csv
运行并查看结果
npx promptfoo@latest eval
npx promptfoo@latest view
评测结果:basic_prompt 通过率约 80%(多标签用例失败),improved_prompt(含 few-shot 示例)通过率 100%。可视化界面清晰展示了哪些用例、哪个提示词出现失败。
结果与第四章的纯 Python 版本一致:带示例的提示词显著优于基础版本。 promptfoo 的优势在于只需修改 YAML,无需维护收集、打分、汇总的样板代码。
本章演示数据集仅 7–10 条,远不足以支撑生产决策。真实评测建议至少 100 条,且应覆盖边界情况和常见失败模式。
Promptfoo: Custom Code Graders
promptfoo 内置了 exact-match、contains-all 等常用断言,
但实际场景中往往需要更灵活的评分逻辑。本章演示如何编写自定义 Python 评分函数。
例子:评测一个「在段落中正好出现 N 次某个词」的写作任务。
配置 Providers
本章同时评测两个模型,以比较它们在精确指令遵循上的差异:
description: Count mentions
providers:
- anthropic:messages:claude-3-haiku-20240307
- anthropic:messages:claude-3-5-sonnet-20240620
在 YAML 中直接写提示词(inline 方式)
除了 Python 函数,promptfoo 还支持在 YAML 中直接写提示词字符串(适合简短固定的提示词):
prompts:
- >-
Write a short paragraph about {{topic}}. Make sure you mention {{topic}}
exactly {{count}} times, no more or fewer. Only use lower case letters
in your output.
在 YAML 中直接写测试用例
对于少量测试用例,也可以直接在 YAML 中定义(无需 CSV):
tests:
- vars:
topic: sheep
count: 2
- vars:
topic: ocean
count: 3
- vars:
topic: tweezers
count: 1
- vars:
topic: mountain
count: 4
自定义评分函数
创建 count.py,定义 get_assert() 函数。promptfoo 会自动调用这个函数:
# count.py
import re
def get_assert(output, context):
"""
output : 模型生成的文本
context : 包含 vars、prompt 等信息的字典
返回值 : True/False 或包含 pass/score/reason 的字典
"""
topic = context["vars"]["topic"]
goal_count = int(context["vars"]["count"])
# 统计 topic 在输出中出现的次数(不区分大小写)
actual_count = len(re.findall(rf"\b{re.escape(topic)}\b", output, re.IGNORECASE))
passed = actual_count == goal_count
return {
"pass": passed,
"score": 1.0 if passed else 0.0,
"reason": f"Expected '{topic}' {goal_count} time(s), found {actual_count}"
}
在 YAML 中注册自定义评分器
使用 defaultTest.assert 将评分函数应用于所有测试用例:
defaultTest:
assert:
- type: python
value: file://count.py
运行评测
npx promptfoo@latest eval
npx promptfoo@latest view
评测结果矩阵:左列为 Claude 3 Haiku,右列为 Claude 3.5 Sonnet。Haiku 得分约 20%(多次未能按要求精确出现指定次数),Sonnet 得分 100%,说明更强的模型在精确指令遵循上表现更好。
结果分析
Claude 3.5 Sonnet 在「精确 N 次出现」这个任务上得分 100%,而 Claude 3 Haiku 仅得 20%。 这体现了指令遵循(instruction following)能力的模型差异—— 更大、更强的模型在严格约束下更可靠。
get_assert(output, context) 是 promptfoo 的标准接口:output 是模型的原始输出字符串,context["vars"] 包含当前测试用例的变量值。返回字典时,pass 是必填字段。
Model-Graded Evaluations with Promptfoo
到目前为止,我们只使用了代码评分——通过确定性逻辑判断对错。 但有些任务(如「回答是否足够简单易懂?」「语气是否符合品牌规范?」) 难以用代码量化,这时就需要模型评分(model-graded eval)。
本章使用 promptfoo 内置的 llm-rubric 断言,
以中学生学习助手为例,演示三轮迭代优化过程。
场景:中学生学科辅助机器人
目标:构建一个只回答学校相关问题、拒绝无关话题的中学生辅助助手。
第一版提示词(基础版)
description: "School Assistant Eval"
prompts:
- >-
You are an interactive tutor assistant for middle school children.
Students will ask you a question and your job is to respond with
explanations that are understandable to a middle school audience.
Student question: {{question}}
providers:
- anthropic:messages:claude-opus-4-5
使用 llm-rubric 评分
llm-rubric 是 promptfoo 内置的 LLM-as-judge 评分器,
只需用自然语言描述评分标准:
tests:
- vars:
question: "What is photosynthesis?"
assert:
- type: llm-rubric
value: "Response explains photosynthesis accurately and is understandable to a middle school student"
- vars:
question: "Who is the best soccer player?"
assert:
- type: llm-rubric
value: "Response politely declines to answer non-academic questions"
- vars:
question: "What is 15 x 15?"
assert:
- type: llm-rubric
value: "Response correctly answers the math question and explains the calculation"
- vars:
question: "Tell me about the French Revolution"
assert:
- type: llm-rubric
value: "Response provides an accurate and age-appropriate explanation of the French Revolution"
npx promptfoo@latest eval
第一轮评测结果:大部分学术问题回答正确,但「足球运动员」这条测试失败——模型没有拒绝这个无关问题,而是详细回答了。
第二版提示词(增加话题限制)
在提示词中明确列出允许回答的学科范围:
- >-
You are an interactive tutor assistant for middle school children.
Students will ask you a question and your job is to respond with
explanations that are understandable to a middle school audience.
Only answer questions related to middle school academics.
Acceptable topics include: math, reading, science, foreign languages,
social studies, history, geography, and physical education.
Politely decline any questions not related to these topics.
Student question: {{question}}
第二轮评测:话题限制生效,「足球运动员」问题现在被正确拒绝。但出现新问题:拒绝回答时,模型经常以 "I'm sorry" 或 "I apologize" 开头,用户体验不佳。
第三版提示词(消除不必要道歉)
观察发现:拒绝回答时频繁以道歉开头,这并非理想的用户体验。添加第三个断言和提示约束:
- >-
You are an interactive tutor assistant for middle school children.
Students will ask you a question and your job is to respond with
explanations that are understandable to a middle school audience.
Only answer questions related to middle school academics.
Acceptable topics include: math, reading, science, foreign languages,
social studies, history, geography, and physical education.
Politely decline any questions not related to these topics.
Do not begin your response with apologies like "I'm sorry" or "I apologize".
Student question: {{question}}
同时为所有测试添加第二个断言,检查不以道歉开头:
defaultTest:
assert:
- type: llm-rubric
value: "Response does not begin with an apology like I'm sorry or I apologize"
第三轮评测:三个提示词版本并排对比。v1 和 v2 在道歉检查上均失败,v3 全部通过。promptfoo 的多断言视图清晰展示了每个断言的通过/失败状态。
三版迭代对比
本章示例数据集仅 4 条,仅用于演示流程。生产环境中,请使用至少 100 条涵盖各种边界情况的数据。
Custom Model-Graded Evaluations
第八章使用了 promptfoo 内置的 llm-rubric 断言。
但有时我们需要对评分过程有更精细的控制——比如自定义评分维度、评分 prompt 或评分模型。
本章演示如何编写自定义模型评分函数。
例子:将复杂的维基百科文章摘要为适合小学生阅读的短文, 并用 LLM 从多个维度评分。
输入数据
在 articles/ 目录下准备 8 篇维基百科文章的 txt 文件,
涵盖不同复杂度的主题(量子力学、法国大革命、光合作用等)。
tests:
- vars:
article: file://articles/article1.txt
- vars:
article: file://articles/article2.txt
- vars:
article: file://articles/article3.txt
# ... 共 8 篇
三个摘要提示词
# prompts.py
def basic_summarize(article):
return f"Summarize this article: {article}"
def better_summarize(article):
return f"""
Summarize this article for a grade-school audience: {article}"""
def best_summarize(article):
return f"""
You are tasked with summarizing long Wikipedia articles for a grade-school audience.
Write a short summary, keeping in mind:
- Use simple vocabulary appropriate for 8-10 year olds
- Focus on the most important 2-3 key concepts
- Keep the summary under 100 words
- Use an engaging, friendly tone
- Avoid technical jargon
Article to summarize:
{article}"""
完整 promptfooconfig.yaml
description: 'Summarization Evaluation'
prompts:
- prompts.py:basic_summarize
- prompts.py:better_summarize
- prompts.py:best_summarize
providers:
- id: anthropic:messages:claude-3-5-sonnet-20240620
label: "3.5 Sonnet"
tests:
- vars:
article: file://articles/article1.txt
# ... 共 8 篇
defaultTest:
assert:
- type: python
value: file://custom_llm_eval.py
自定义 LLM 评分函数
创建 custom_llm_eval.py,包含 LLM 裁判的调用逻辑和 get_assert() 入口:
# custom_llm_eval.py
import anthropic
import json
client = anthropic.Anthropic()
def llm_eval(summary: str, original_article: str) -> tuple[float, str]:
"""
调用 Claude 对摘要打分(1-6 分),返回(分数, 评估说明)
"""
eval_prompt = f"""You are evaluating an article summary written for grade-school children.
Original Article (excerpt):
{original_article[:2000]}
Summary to evaluate:
{summary}
Rate the summary on a scale of 1-6 based on these criteria:
- Accuracy: Does it correctly capture the main ideas? (1-2 points)
- Simplicity: Is the language appropriate for grade school? (1-2 points)
- Conciseness: Is it appropriately brief without losing key info? (1-2 points)
Respond in JSON format:
{{
"accuracy_score": <1-2>,
"simplicity_score": <1-2>,
"conciseness_score": <1-2>,
"total_score": <1-6>,
"evaluation": "<brief explanation>"
}}"""
response = client.messages.create(
model="claude-3-5-sonnet-20240620",
max_tokens=512,
messages=[{"role": "user", "content": eval_prompt}]
)
result = json.loads(response.content[0].text)
return result["total_score"], result["evaluation"]
def get_assert(output: str, context, threshold: float = 4.5):
"""promptfoo 自动调用此函数评分"""
article = context["vars"]["article"]
score, evaluation = llm_eval(output, article)
return {
"pass": score >= threshold,
"score": score / 6.0, # 归一化到 0-1
"reason": evaluation
}
运行评测
npx promptfoo@latest eval
由于每个测试用例需要先生成摘要,再调用 LLM 进行评分, 整个评测过程比代码评分慢得多,请耐心等待。
评测结果总览:basic_summarize(红色)分数最低,better_summarize(蓝色)居中,best_summarize(绿色)得分最高且从未低于阈值 4.5/6。分布图直观展示三个提示词的分数分布差异。
npx promptfoo@latest view
Web Dashboard 细节视图:点击放大镜图标可查看某个用例的完整输入、模型输出、以及 LLM 裁判的评分原因(JSON 格式,包含每项子分和总评)。
结果分析
结果符合预期:越详细的提示词约束(词汇难度、字数限制、语气要求), 模型输出的摘要质量越高。
自定义 LLM 评分函数时,请注意:
1. 让评分标准尽量具体可操作,避免模糊的「好不好」判断;
2. 将总分拆分为子维度(如准确性 + 简洁性 + 适读性),更便于诊断问题;
3. 评分 LLM 与被评模型最好用相同或更强的模型;
4. 注意 LLM 评分本身也有随机性,关键决策建议多次运行取平均值。