建立強大的實證評估
在定義成功標準後,下一步是設計評估來衡量 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-20240620",
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"將此分類為 '正面'、'負面'、'中性' 或 '混合':{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-20240620",
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": "在一項突破性研究中,麻省理工學院的研究人員...", "summary": "麻省理工學院科學家發現新型抗生素..."},
{"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-20240620",
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-20240620",
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-20240620",
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>
只輸出 '是' 或 '否'。"""
# 通常最佳做法是使用不同於生成被評估輸出的模型來進行評估
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() == "否"
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": "很好的選擇!Harper Lee 的《殺死一隻知更鳥》是一部探討種族不公和道德成長主題的經典之作。到目前為止你覺得怎麼樣?"},
{"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-20240620",
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> 標籤中輸出 '正確' 或 '不正確'。"""
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-20240620",
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}%")