建立強大的實證評估
在定義成功標準後,下一步是設計評估來衡量 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"Classify this as 'positive', 'negative', 'neutral', or '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": "好選擇!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-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}%")