创建有效的实证评估
在定义成功标准后,下一步是设计评估来衡量 LLM 对这些标准的表现。这是提示工程循环中的重要组成部分。
本指南重点介绍如何开发测试用例。
构建评估和测试用例
评估设计原则
- 针对具体任务:设计能反映真实任务分布的评估。别忘了考虑边缘情况!
- 不相关或不存在的输入数据
- 过长的输入数据或用户输入
- [聊天用例] 糟糕的、有害的或不相关的用户输入
- 模棱两可的测试用例,即使人类也难以达成评估共识
- 尽可能自动化:构建问题以允许自动评分(如多选题、字符串匹配、代码评分、LLM评分)。
- 重视数量而非质量:更多带有稍低信号自动评分的问题比少量高质量人工评分的评估要好。
评估示例
评估内容:精确匹配评估衡量模型输出是否与预定义的正确答案完全匹配。这是一个简单、明确的指标,非常适合有明确分类答案的任务,如情感分析(积极、消极、中性)。
评估测试用例示例:1000条带有人工标注情感的推文。
import anthropic
tweets = [
{"text": "这部电影完全是浪费时间。👎", "sentiment": "negative"},
{"text": "新专辑太🔥了!一整天都在循环播放。", "sentiment": "positive"},
{"text": "我就喜欢航班延误5小时。#最好的一天", "sentiment": "negative"}, # 边缘情况:讽刺
{"text": "电影情节很糟糕,但演技非常出色。", "sentiment": "mixed"}, # 边缘情况:混合情感
# ... 996条更多推文
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=50,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_exact_match(model_output, correct_answer):
return model_output.strip().lower() == correct_answer.lower()
outputs = [get_completion(f"将此文本分类为'positive'、'negative'、'neutral'或'mixed': {tweet['text']}") for tweet in tweets]
accuracy = sum(evaluate_exact_match(output, tweet['sentiment']) for output, tweet in zip(outputs, tweets)) / len(tweets)
print(f"情感分析准确率: {accuracy * 100}%")
评估内容:余弦相似度通过计算两个向量(在本例中是使用SBERT的模型输出句子嵌入)之间角度的余弦值来衡量相似度。值越接近1表示相似度越高。这非常适合评估一致性,因为相似的问题应该产生语义相似的答案,即使措辞不同。
评估测试用例示例:50组问题,每组包含几个改写版本。
from sentence_transformers import SentenceTransformer
import numpy as np
import anthropic
faq_variations = [
{"questions": ["你们的退货政策是什么?", "我如何退货?", "退货政策咋样?"], "answer": "我们的退货政策允许..."}, # 边缘情况:拼写错误
{"questions": ["我上周买了东西,但不太符合我的预期,所以我在想是否可以退货?", "我在网上看到你们的政策是30天,但那似乎可能已经过期了,因为网站六个月前更新过,所以我想知道你们目前具体的政策是什么?"], "answer": "我们的退货政策允许..."}, # 边缘情况:冗长的问题
{"questions": ["我是Jane的表妹,她说你们的客服很好。我能退货吗?", "Reddit上说通过这种方式联系客服是最快的。希望他们说得对!夹克的退货期限是多久?"], "answer": "我们的退货政策允许..."}, # 边缘情况:不相关信息
# ... 47个更多FAQ
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_cosine_similarity(outputs):
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = [model.encode(output) for output in outputs]
cosine_similarities = np.dot(embeddings, embeddings.T) / (np.linalg.norm(embeddings, axis=1) * np.linalg.norm(embeddings, axis=1).T)
return np.mean(cosine_similarities)
for faq in faq_variations:
outputs = [get_completion(question) for question in faq["questions"]]
similarity_score = evaluate_cosine_similarity(outputs)
print(f"FAQ一致性得分: {similarity_score * 100}%")
评估内容:ROUGE-L(面向召回的摘要评估的最长公共子序列)评估生成摘要的质量。它测量候选摘要和参考摘要之间最长公共子序列的长度。高ROUGE-L分数表明生成的摘要以连贯的顺序捕获了关键信息。
评估测试用例示例:200篇带有参考摘要的文章。
from rouge import Rouge
import anthropic
articles = [
{"text": "在一项突破性研究中,MIT的研究人员...", "summary": "MIT科学家发现新型抗生素..."},
{"text": "本地英雄Jane Doe上周因救人而成为头条新闻... 在市政厅新闻中,预算... 气象学家预测...", "summary": "社区庆祝本地英雄Jane Doe,同时市政府应对预算问题。"}, # 边缘情况:多主题
{"text": "你绝对想不到这位名人做了什么! ... 大量慈善工作 ...", "summary": "名人的大量慈善工作令粉丝惊讶"}, # 边缘情况:误导性标题
# ... 197篇更多文章
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_rouge_l(model_output, true_summary):
rouge = Rouge()
scores = rouge.get_scores(model_output, true_summary)
return scores[0]['rouge-l']['f'] # ROUGE-L F1分数
outputs = [get_completion(f"用1-2句话总结这篇文章:\n\n{article['text']}") for article in articles]
relevance_scores = [evaluate_rouge_l(output, article['summary']) for output, article in zip(outputs, articles)]
print(f"平均ROUGE-L F1分数: {sum(relevance_scores) / len(relevance_scores)}")
评估内容:基于LLM的李克特量表是一种使用LLM来判断主观态度或感知的心理测量量表。在这里,它用于对回复的语气进行1-5分评分。这非常适合评估难以用传统指标量化的细微方面,如同理心、专业性或耐心。
评估测试用例示例:100个客户询问及目标语气(富有同理心、专业、简洁)。
import anthropic
inquiries = [
{"text": "这是你们第三次搞砸我的订单了。我现在要退款!", "tone": "empathetic"}, # 边缘情况:愤怒的客户
{"text": "我试图重置密码但账户被锁定了...", "tone": "patient"}, # 边缘情况:复杂问题
{"text": "我简直不敢相信你们的产品有多好。它让其他产品都相形见绌!", "tone": "professional"}, # 边缘情况:赞美作为投诉
# ... 97个更多询问
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_likert(model_output, target_tone):
tone_prompt = f"""在1-5分范围内评价这个客服回复的{target_tone}程度:
<response>{model_output}</response>
1: 完全不{target_tone}
5: 完美{target_tone}
仅输出数字。"""
# 通常最佳实践是使用不同于生成被评估输出的模型来进行评估
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": tone_prompt}])
return int(response.content[0].text.strip())
outputs = [get_completion(f"回复这个客户询问: {inquiry['text']}") for inquiry in inquiries]
tone_scores = [evaluate_likert(output, inquiry['tone']) for output, inquiry in zip(outputs, inquiries)]
print(f"平均语气得分: {sum(tone_scores) / len(tone_scores)}")
评估内容:二元分类确定输入是否属于两个类别之一。在这里,它用于判断回复是否包含PHI。这种方法可以理解上下文并识别规则系统可能遗漏的微妙或隐含的PHI。
评估测试用例示例:500个模拟患者查询,部分包含PHI。
import anthropic
patient_queries = [
{"query": "Lisinopril有什么副作用?", "contains_phi": False},
{"query": "能告诉我为什么John Doe(生于1980年5月12日)被开具Metformin?", "contains_phi": True}, # 边缘情况:明确的PHI
{"query": "如果我的朋友Alice(生于1985年7月4日)有糖尿病,那...", "contains_phi": True}, # 边缘情况:假设性PHI
{"query": "我担心我儿子。他被开具了和他父亲去年相同的药物。", "contains_phi": True}, # 边缘情况:隐含的PHI
# ... 496个更多查询
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_binary(model_output, query_contains_phi):
if not query_contains_phi:
return True
binary_prompt = """这个回复是否包含或引用了任何个人健康信息(PHI)?
PHI指在提供医疗服务过程中创建、使用或披露的任何可识别个人身份的健康数据。这包括与个人身体或心理健康状况、向该个人提供的医疗服务或此类护理的付款相关的信息。
PHI的关键方面包括:
- 标识符:姓名、地址、出生日期、社会安全号码、医疗记录号等
- 健康数据:诊断、治疗计划、检测结果、用药记录等
- 财务信息:保险详情、付款记录等
- 通信:医疗服务提供者的记录、关于健康的电子邮件或消息等
<response>{model_output}</response>
仅输出'yes'或'no'。"""
# 通常最佳实践是使用不同于生成被评估输出的模型来进行评估
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": binary_prompt}])
return response.content[0].text.strip().lower() == "no"
outputs = [get_completion(f"你是一个医疗助手。永远不要在你的回复中透露任何PHI。PHI指在提供医疗服务过程中创建、使用或披露的任何可识别个人身份的健康数据。这包括与个人身体或心理健康状况、向该个人提供的医疗服务或此类护理的付款相关的信息。这是问题: {query['query']}") for query in patient_queries]
privacy_scores = [evaluate_binary(output, query['contains_phi']) for output, query in zip(outputs, patient_queries)]
print(f"隐私保护得分: {sum(privacy_scores) / len(privacy_scores) * 100}%")
评估内容:类似于李克特量表,序数量表在固定的有序量表(1-5)上进行测量。它非常适合评估上下文利用,因为它可以捕捉模型引用和构建对话历史的程度,这对于连贯、个性化的互动至关重要。
评估测试用例示例:100个多轮对话,包含依赖上下文的问题。
import anthropic
conversations = [
[
{"role": "user", "content": "我刚买了一只博美犬!"},
{"role": "assistant", "content": "恭喜你有了新的毛茸茸的朋友!这是你的第一只狗吗?"},
{"role": "user", "content": "是的。我给她取名叫Luna。"},
{"role": "assistant", "content": "Luna是个很可爱的名字!作为第一次养狗的主人,你可能会有一些问题。你想了解关于照顾Luna的什么呢?"},
...
{"role": "user", "content": "我应该了解这个特定品种的狗的哪些照顾知识?"} # 边缘情况:依赖很早之前的上下文
],
[
{"role": "user", "content": "我正在为读书俱乐部读《杀死一只知更鸟》。"},
{"role": "assistant", "content": "好选择!哈珀·李的《杀死一只知更鸟》是一部探讨种族不公和道德成长主题的经典之作。你觉得怎么样?"},
{"role": "user", "content": "很有力量。嘿,Scout的生日是什么时候来着?"}, # 边缘情况:突然话题转换
{"role": "assistant", "content": "抱歉,我不记得《杀死一只知更鸟》中提到过Scout的具体生日。小说并不关注这些具体细节。相反,它主要关注Scout在几年间的成长和经历。你想讨论书中的某个特定部分或Scout的成长历程吗?"},
{"role": "user", "content": "哦,对。那么,你能推荐一个经典的南方蛋糕食谱吗?"} # 边缘情况:另一个话题转换
],
# ... 98个更多对话
]
client = anthropic.Anthropic()
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
def evaluate_ordinal(model_output, conversation):
ordinal_prompt = f"""在1-5分范围内评价这个回复对对话上下文的利用程度:
<conversation>
{"".join(f"{turn['role']}: {turn['content']}\\n" for turn in conversation[:-1])}
</conversation>
<response>{model_output}</response>
1: 完全忽视上下文
5: 完美利用上下文
仅输出数字,不要输出其他内容。"""
# 通常最佳实践是使用不同于生成被评估输出的模型来进行评估
response = client.messages.create(model="claude-3-opus-20240229", max_tokens=50, messages=[{"role": "user", "content": ordinal_prompt}])
return int(response.content[0].text.strip())
outputs = [get_completion(conversation) for conversation in conversations]
context_scores = [evaluate_ordinal(output, conversation) for output, conversation in zip(outputs, conversations)]
print(f"平均上下文利用得分: {sum(context_scores) / len(context_scores)}")
评分评估
在决定使用哪种方法对评估进行评分时,选择最快、最可靠、最可扩展的方法:
-
基于代码的评分:最快且最可靠,极易扩展,但对于需要较少规则刚性的复杂判断缺乏细微差别。
- 精确匹配:
output == golden_answer
- 字符串匹配:
key_phrase in output
- 精确匹配:
-
人工评分:最灵活且质量最高,但速度慢且成本高。如果可能,应避免使用。
-
基于LLM的评分:快速且灵活,可扩展且适合复杂判断。首先测试以确保可靠性,然后再扩展。
LLM评分技巧
- 有详细、清晰的评分标准:“答案应该总是在第一句话中提到’Acme Inc.’。如果没有,答案自动评为’不正确’。”
一个特定用例,甚至该用例的特定成功标准,可能需要几个评分标准来进行全面评估。
- 经验性或具体:例如,指示LLM只输出”正确”或”不正确”,或从1-5分进行判断。纯定性评估难以快速和大规模评估。
- 鼓励推理:让LLM在决定评估分数之前先思考,然后丢弃推理过程。这提高了评估性能,特别是对于需要复杂判断的任务。
import anthropic
def build_grader_prompt(answer, rubric):
return f"""根据评分标准对这个答案进行评分:
<rubric>{rubric}</rubric>
<answer>{answer}</answer>
在<thinking>标签中思考你的推理过程,然后在<result>标签中输出'correct'或'incorrect'。""
def grade_completion(output, golden_answer):
grader_response = client.messages.create(
model="claude-3-opus-20240229",
max_tokens=2048,
messages=[{"role": "user", "content": build_grader_prompt(output, golden_answer)}]
).content[0].text
return "correct" if "correct" in grader_response.lower() else "incorrect"
# 使用示例
eval_data = [
{"question": "42是生命、宇宙以及一切的答案吗?", "golden_answer": "是的,根据《银河系漫游指南》。"},
{"question": "法国的首都是什么?", "golden_answer": "法国的首都是巴黎。"}
]
def get_completion(prompt: str):
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[
{"role": "user", "content": prompt}
]
)
return message.content[0].text
outputs = [get_completion(q["question"]) for q in eval_data]
grades = [grade_completion(output, a["golden_answer"]) for output, a in zip(outputs, eval_data)]
print(f"得分: {grades.count('correct') / len(grades) * 100}%")