第七章:生产环境 — 01 LLM 评估#
如何衡量 LLM 输出质量?自动评估让我们可以持续监测模型表现,快速发现退化。
人工评估虽然准确,但成本高、速度慢,无法在每次代码变更后都运行。自动评估体系是 LLM 工程化的核心基础设施之一。
本章目标:
掌握基础指标:精确匹配、关键词覆盖、长度合规
实现 LLM-as-Judge:用模型评估模型
实现 A/B 对比评估
实现 RAG 专用评估指标
构建评估套件,用于回归测试
import json
import os
import re
import litellm
litellm.drop_params = True
from dotenv import load_dotenv
load_dotenv()
MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")
# 关闭 litellm 的详细日志
litellm.set_verbose = False
print(f"使用模型: {MODEL}")
print("LLM 评估框架初始化完成")
# gpt-5/o系列不支持自定义temperature值,统一用安全wrapper
def _c(**kw):
_m = kw.get('model', MODEL)
if any(_m.startswith(p) for p in ('openai/gpt-5','openai/o1','openai/o3','openai/o4')):
kw.pop('temperature', None)
return litellm.completion(**kw)
使用模型: openai/gpt-5-mini
LLM 评估框架初始化完成
Section 1:基础指标#
三种不需要 LLM 的轻量级评估指标,适合有明确正确答案的场景。
def eval_exact_match(pred: str, gold: str, case_sensitive: bool = False) -> dict:
"""
精确匹配评估
适用:分类任务、是/否问题、固定格式输出
"""
if not case_sensitive:
pred = pred.strip().lower()
gold = gold.strip().lower()
match = pred == gold
return {
"metric": "exact_match",
"score": 1.0 if match else 0.0,
"pass": match,
"pred": pred,
"gold": gold
}
def eval_contains(pred: str, keywords: list, require_all: bool = True) -> dict:
"""
关键词覆盖评估
适用:检查回答是否包含必要信息点
Args:
pred: 模型输出
keywords: 必须包含的关键词列表
require_all: True 要求全部包含,False 要求至少一个
"""
pred_lower = pred.lower()
found = [kw for kw in keywords if kw.lower() in pred_lower]
missing = [kw for kw in keywords if kw.lower() not in pred_lower]
coverage = len(found) / len(keywords) if keywords else 1.0
passed = len(missing) == 0 if require_all else len(found) > 0
return {
"metric": "keyword_coverage",
"score": coverage,
"pass": passed,
"found": found,
"missing": missing,
"coverage": f"{len(found)}/{len(keywords)}"
}
def eval_length_compliance(pred: str, min_len: int = None, max_len: int = None,
unit: str = "chars") -> dict:
"""
长度合规评估
适用:检查输出是否符合长度要求
Args:
unit: "chars" 或 "words"
"""
if unit == "words":
length = len(pred.split())
unit_name = "词"
else:
length = len(pred)
unit_name = "字符"
too_short = min_len is not None and length < min_len
too_long = max_len is not None and length > max_len
passed = not too_short and not too_long
# 计算归一化分数(超出范围时按比例扣分)
if passed:
score = 1.0
elif too_short:
score = length / min_len
else: # too_long
score = max_len / length
return {
"metric": "length_compliance",
"score": round(score, 3),
"pass": passed,
"length": length,
"unit": unit_name,
"min": min_len,
"max": max_len,
"status": "✅ 合规" if passed else ("❌ 过短" if too_short else "❌ 过长")
}
# 演示
print("=" * 60)
print("基础指标演示")
print("=" * 60)
# 精确匹配
test_cases_exact = [
("正面", "正面", "完全匹配"),
("正面情绪", "正面", "不完全匹配"),
("POSITIVE", "positive", "大小写不敏感"),
]
print("\n[1] 精确匹配:")
for pred, gold, note in test_cases_exact:
result = eval_exact_match(pred, gold)
status = "✅" if result["pass"] else "❌"
print(f" {status} {note}: pred='{pred}', gold='{gold}', score={result['score']}")
# 关键词覆盖
answer = "退款会在3-5个工作日内原路退回到您的支付宝账户"
keywords = ["退款", "工作日", "支付宝"]
result = eval_contains(answer, keywords)
print(f"\n[2] 关键词覆盖:")
print(f" 回答: {answer}")
print(f" 关键词: {keywords}")
print(f" 覆盖率: {result['coverage']},找到: {result['found']},缺失: {result['missing']}")
# 长度合规
test_answers = [
("好", "太短的回答"),
("退款需要 3-5 个工作日,原路退回原支付方式。", "合适长度"),
("关于您的退款问题,我需要详细解释一下整个退款流程,包括审核时间、到账时间、不同支付方式的差异,以及如果遇到问题如何联系我们,这可能需要一些时间来说明所有细节。", "太长"),
]
print(f"\n[3] 长度合规(要求 15-80 字符):")
for text, note in test_answers:
result = eval_length_compliance(text, min_len=15, max_len=80)
print(f" {result['status']} {note}: {result['length']} 字符,分数={result['score']}")
============================================================
基础指标演示
============================================================
[1] 精确匹配:
✅ 完全匹配: pred='正面', gold='正面', score=1.0
❌ 不完全匹配: pred='正面情绪', gold='正面', score=0.0
✅ 大小写不敏感: pred='POSITIVE', gold='positive', score=1.0
[2] 关键词覆盖:
回答: 退款会在3-5个工作日内原路退回到您的支付宝账户
关键词: ['退款', '工作日', '支付宝']
覆盖率: 3/3,找到: ['退款', '工作日', '支付宝'],缺失: []
[3] 长度合规(要求 15-80 字符):
❌ 过短 太短的回答: 1 字符,分数=0.067
✅ 合规 合适长度: 24 字符,分数=1.0
✅ 合规 太长: 79 字符,分数=1.0
Section 2:LLM-as-Judge#
用 LLM 自身来评估 LLM 的输出。这是目前最主流的 LLM 评估方法,尤其适合开放式问题。
关键设计原则:
评估标准必须明确(什么叫 5 分?什么叫 1 分?)
要求输出结构化结果(JSON)
同一答案运行多次取平均(减少随机性)
def llm_judge(question: str, answer: str, criteria: str, model: str = MODEL) -> dict:
"""
LLM-as-Judge:用 LLM 评估答案质量
Args:
question: 原始问题
answer: 待评估的回答
criteria: 评估标准("factual_accuracy" | "helpfulness" | "conciseness" | 自定义)
Returns:
dict with score (1-5) and reason
"""
criteria_prompts = {
"factual_accuracy": "事实准确性:回答是否包含正确的事实信息?是否有明显错误?",
"helpfulness": "有用性:回答是否真正解决了用户的问题?是否提供了可操作的信息?",
"conciseness": "简洁性:回答是否简洁直接?是否有冗余信息?",
}
criteria_desc = criteria_prompts.get(criteria, criteria)
prompt = f"""你是一位严格、公正的评估员。请根据以下标准评估回答质量。
评估标准:{criteria_desc}
评分规则:
- 5分:完全满足标准,无可挑剔
- 4分:基本满足标准,有小瑕疵
- 3分:部分满足标准,有明显不足
- 2分:大部分不满足标准
- 1分:完全不满足标准
问题:{question}
回答:{answer}
请严格以 JSON 格式输出,不要有额外文字:
{{"score": 1-5的整数, "reason": "评分理由(50字以内)"}}"""
response = _c(
model=model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0
)
result = json.loads(response.choices[0].message.content)
result["criteria"] = criteria
result["question"] = question[:50] + "..." if len(question) > 50 else question
return result
# 演示:同一个问题的三种不同质量答案
question = "Python 的列表和元组有什么区别?"
answers = [
{
"label": "差答案",
"text": "列表和元组都是 Python 的数据结构,它们有些不同。"
},
{
"label": "中等答案",
"text": "列表是可变的(可以增删改),用方括号 []。元组是不可变的,用圆括号 ()。这是主要区别。"
},
{
"label": "好答案",
"text": "核心区别是**可变性**:\n- 列表 `[]`:可变,支持增删改(append/remove/pop)\n- 元组 `()`:不可变,创建后不能修改\n\n实际影响:\n1. 元组更快(内存效率高)\n2. 元组可以作为字典的 key,列表不行\n3. 元组传达'这组数据不应被修改'的语义\n\n经验法则:坐标、配置等固定数据用元组;需要动态修改的用列表。"
},
]
print("LLM-as-Judge 演示")
print("=" * 70)
print(f"问题:{question}")
for criteria in ["helpfulness", "conciseness"]:
print(f"\n评估维度:{criteria}")
print("-" * 70)
for ans in answers:
result = llm_judge(question, ans["text"], criteria)
stars = "★" * result["score"] + "☆" * (5 - result["score"])
print(f" [{ans['label']}] {stars} ({result['score']}/5)")
print(f" 理由: {result['reason']}")
print()
LLM-as-Judge 演示
======================================================================
问题:Python 的列表和元组有什么区别?
评估维度:helpfulness
----------------------------------------------------------------------
[差答案] ★☆☆☆☆ (1/5)
理由: 过于笼统,未说明可变性、语法、性能与用途等关键区别
[中等答案] ★★★★☆ (4/5)
理由: 正确但过于简略,缺乏细节与示例
[好答案] ★★★★★ (5/5)
理由: 准确、简明,涵盖可变性、性能及使用建议
评估维度:conciseness
----------------------------------------------------------------------
[差答案] ★★★★★ (5/5)
理由: 回答非常简洁,直接无冗余
[中等答案] ★★★★★ (5/5)
理由: 简洁直接,无冗余,准确指出主要区别
[好答案] ★★★★★ (5/5)
理由: 条理清晰、简明扼要,覆盖要点无冗余
Section 3:对比评估(A/B Test)#
让 LLM 直接比较两个答案的优劣,适用于选择最佳提示词或模型版本。
def compare_answers(question: str, answer_a: str, answer_b: str,
model: str = MODEL) -> str:
"""
A/B 对比评估:让 LLM 选出更好的答案
Returns: "A" | "B" | "tie"
"""
prompt = f"""你是一位公正的评估员。比较以下两个回答,选出更好的那个。
评估维度:准确性、有用性、清晰度。
问题:{question}
回答A:
{answer_a}
回答B:
{answer_b}
严格以 JSON 格式输出:
{{"winner": "A" 或 "B" 或 "tie", "reason": "理由(30字以内)"}}"""
response = _c(
model=model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0
)
result = json.loads(response.choices[0].message.content)
return result["winner"], result.get("reason", "")
# 5 个问题对比测试
# 场景:旧版提示词(A) vs 新版提示词(B)
test_pairs = [
{
"question": "如何提高 Python 代码执行速度?",
"answer_a": "可以用一些方法让 Python 代码更快,比如用更好的算法。",
"answer_b": "3个核心方法:1) 用列表推导式替代循环 2) 使用 numpy 处理数值计算 3) 用 functools.lru_cache 缓存重复计算。性能关键路径可考虑 Cython 或 PyPy。"
},
{
"question": "什么是 Docker?",
"answer_a": "Docker 是一个容器化平台,让你可以把应用和它的依赖打包在一起,确保在任何环境都能一致运行。类比:Docker 容器就像标准化的货运集装箱。",
"answer_b": "Docker 是容器技术。"
},
{
"question": "Git rebase 和 merge 的区别?",
"answer_a": "merge 保留完整历史,rebase 让历史更线性干净。团队协作一般用 merge,个人分支整理用 rebase。",
"answer_b": "它们都可以合并分支,但方式不同,各有优缺点。"
},
{
"question": "解释 REST API 是什么?",
"answer_a": "REST API 是一种基于 HTTP 的接口规范,就像网站的后门——通过 GET/POST/PUT/DELETE 操作资源。例如 GET /users/123 获取用户信息。",
"answer_b": "REST API 是应用程序接口,使用 HTTP 协议,是现代应用程序之间通信的标准方式,具有无状态性等特点。"
},
{
"question": "什么是机器学习过拟合?",
"answer_a": "过拟合指模型在训练数据上表现很好,但在新数据上表现差。原因是模型'记住'了训练数据,而不是学到了规律。解决方法:正则化、Dropout、更多数据。",
"answer_b": "过拟合是机器学习中的一个问题,需要通过多种技术手段来避免。"
},
]
print("A/B 对比评估(5个问题)")
print("=" * 70)
wins = {"A": 0, "B": 0, "tie": 0}
for i, pair in enumerate(test_pairs, 1):
winner, reason = compare_answers(
pair["question"], pair["answer_a"], pair["answer_b"]
)
wins[winner] = wins.get(winner, 0) + 1
print(f"\n[Q{i}] {pair['question']}")
print(f" 胜者: {'🏆 ' + winner} | 理由: {reason}")
total = len(test_pairs)
print("\n" + "=" * 70)
print("评估结果汇总:")
print(f" A 胜: {wins.get('A', 0)}/{total} ({wins.get('A', 0)/total:.0%})")
print(f" B 胜: {wins.get('B', 0)}/{total} ({wins.get('B', 0)/total:.0%})")
print(f" 平局: {wins.get('tie', 0)}/{total}")
print(f"\n结论: {'B 版本更好,建议切换' if wins.get('B', 0) > wins.get('A', 0) else 'A 版本更好'}")
A/B 对比评估(5个问题)
======================================================================
[Q1] 如何提高 Python 代码执行速度?
胜者: 🏆 B | 理由: 具体可行,给出常用优化方法
[Q2] 什么是 Docker?
胜者: 🏆 A | 理由: 描述完整、准确且比喻清晰
[Q3] Git rebase 和 merge 的区别?
胜者: 🏆 A | 理由: A 更具体,说明保留历史与线性及适用场景
[Q4] 解释 REST API 是什么?
胜者: 🏆 B | 理由: 更准确,提到无状态等核心特性
[Q5] 什么是机器学习过拟合?
胜者: 🏆 A | 理由: 定义明确,说明原因并给出解决方法
======================================================================
评估结果汇总:
A 胜: 3/5 (60%)
B 胜: 2/5 (40%)
平局: 0/5
结论: A 版本更好
Section 4:RAG 评估指标#
RAG 系统需要评估两个维度:
Context Recall(上下文召回):答案是否被检索到的上下文所支持?
Answer Faithfulness(答案忠实性):答案是否与上下文一致,有无幻觉?
def eval_context_recall(question: str, context: str, answer: str,
model: str = MODEL) -> dict:
"""
Context Recall:检索到的上下文是否支持了答案?
分数高 = 上下文包含了回答所需信息
"""
prompt = f"""评估检索到的上下文是否包含回答问题所需的关键信息。
问题:{question}
检索到的上下文:
{context}
模型给出的答案:
{answer}
评估:答案中的关键信息点,是否都能在上下文中找到依据?
输出 JSON:
{{"score": 0.0到1.0, "supported_points": ["上下文支持的点"], "unsupported_points": ["上下文未提及的点"], "verdict": "pass/partial/fail"}}"""
response = _c(
model=model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0
)
result = json.loads(response.choices[0].message.content)
result["metric"] = "context_recall"
return result
def eval_answer_faithfulness(context: str, answer: str,
model: str = MODEL) -> dict:
"""
Answer Faithfulness:答案是否忠实于上下文(无幻觉)?
分数高 = 答案没有捏造上下文未提及的信息
"""
prompt = f"""检查答案是否与提供的上下文一致,有无捏造信息(幻觉)。
上下文:
{context}
答案:
{answer}
判断标准:答案中的每一个具体声明,是否都能在上下文中找到支持?
输出 JSON:
{{"faithfulness_score": 0.0到1.0, "hallucinations": ["捏造的信息"], "faithful_claims": ["与上下文一致的信息"], "verdict": "faithful/partially_faithful/hallucinated"}}"""
response = _c(
model=model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
temperature=0
)
result = json.loads(response.choices[0].message.content)
result["metric"] = "answer_faithfulness"
return result
# 3个测试案例
rag_test_cases = [
{
"label": "案例1:忠实且有支持",
"question": "退款需要多少天?",
"context": "根据本公司退款政策,所有订单的退款申请审核后,将在3-5个工作日内原路退回至支付账户。",
"answer": "退款需要3-5个工作日,原路退回至您的支付账户。"
},
{
"label": "案例2:答案包含幻觉",
"question": "退款需要多少天?",
"context": "根据本公司退款政策,所有订单的退款申请审核后,将在3-5个工作日内原路退回至支付账户。",
"answer": "退款需要3-5个工作日,您还可以选择加急退款,24小时内到账,需要支付5元手续费。" # 加急选项是幻觉
},
{
"label": "案例3:上下文不足",
"question": "可以申请部分退款吗?",
"context": "根据本公司退款政策,所有订单的退款申请审核后,将在3-5个工作日内原路退回至支付账户。",
"answer": "关于部分退款,需要联系客服具体咨询。" # 上下文未提及
},
]
print("RAG 评估指标演示")
print("=" * 70)
for case in rag_test_cases:
print(f"\n{case['label']}")
print(f" 问题: {case['question']}")
print(f" 答案: {case['answer']}")
recall = eval_context_recall(case["question"], case["context"], case["answer"])
faithful = eval_answer_faithfulness(case["context"], case["answer"])
print(f" Context Recall: {recall['score']:.2f} ({recall['verdict']})")
if recall.get("unsupported_points"):
print(f" 缺少支持: {recall['unsupported_points']}")
print(f" Answer Faithfulness: {faithful['faithfulness_score']:.2f} ({faithful['verdict']})")
if faithful.get("hallucinations"):
print(f" 幻觉内容: {faithful['hallucinations']}")
RAG 评估指标演示
======================================================================
案例1:忠实且有支持
问题: 退款需要多少天?
答案: 退款需要3-5个工作日,原路退回至您的支付账户。
Context Recall: 1.00 (pass)
Answer Faithfulness: 1.00 (faithful)
案例2:答案包含幻觉
问题: 退款需要多少天?
答案: 退款需要3-5个工作日,您还可以选择加急退款,24小时内到账,需要支付5元手续费。
Context Recall: 0.50 (partial)
缺少支持: ['可以选择加急退款', '24小时内到账', '需要支付5元手续费']
Answer Faithfulness: 0.50 (partially_faithful)
幻觉内容: ['您还可以选择加急退款,24小时内到账,需要支付5元手续费。']
案例3:上下文不足
问题: 可以申请部分退款吗?
答案: 关于部分退款,需要联系客服具体咨询。
Context Recall: 0.00 (fail)
缺少支持: ['关于部分退款需要联系客服具体咨询——上下文未提及是否可申请部分退款或需联系客服。']
Answer Faithfulness: 0.00 (hallucinated)
幻觉内容: ['关于部分退款,需要联系客服具体咨询。']
Section 5:评估集与回归测试#
将评估函数组织成测试套件,在每次模型/提示词更新后自动运行,防止退化。
class EvalSuite:
"""
LLM 评估套件:管理和运行回归测试
用法:
suite = EvalSuite(name="客服机器人评估")
suite.add_test(question, expected_keywords, min_score=3)
results = suite.run(model=MODEL)
suite.report(results)
"""
def __init__(self, name: str, system_prompt: str = ""):
self.name = name
self.system_prompt = system_prompt
self.test_cases = []
def add_test(self, question: str, expected_keywords: list = None,
min_score: int = 3, max_length: int = None,
tag: str = ""):
"""添加一个测试用例"""
self.test_cases.append({
"question": question,
"expected_keywords": expected_keywords or [],
"min_score": min_score,
"max_length": max_length,
"tag": tag
})
def _get_answer(self, question: str, model: str) -> str:
"""调用模型获取答案"""
messages = []
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
messages.append({"role": "user", "content": question})
response = _c(
model=model,
messages=messages,
temperature=0,
max_tokens=300
)
return response.choices[0].message.content
def run(self, model: str = None) -> list:
"""运行所有测试用例"""
model = model or MODEL
results = []
print(f"运行评估套件: {self.name}")
print(f"模型: {model}")
print(f"测试用例数: {len(self.test_cases)}")
print("-" * 50)
for i, tc in enumerate(self.test_cases, 1):
print(f"[{i}/{len(self.test_cases)}] {tc['question'][:40]}...", end=" ")
answer = self._get_answer(tc["question"], model)
# 运行各项评估
checks = []
# 关键词检查
if tc["expected_keywords"]:
kw_result = eval_contains(answer, tc["expected_keywords"])
checks.append(("keywords", kw_result["pass"], kw_result["score"]))
# 长度检查
if tc["max_length"]:
len_result = eval_length_compliance(answer, max_len=tc["max_length"])
checks.append(("length", len_result["pass"], len_result["score"]))
# LLM 质量评分
judge_result = llm_judge(tc["question"], answer, "helpfulness", model)
score_pass = judge_result["score"] >= tc["min_score"]
checks.append(("quality", score_pass, judge_result["score"] / 5.0))
overall_pass = all(p for _, p, _ in checks)
avg_score = sum(s for _, _, s in checks) / len(checks) if checks else 0
status = "✅ PASS" if overall_pass else "❌ FAIL"
print(f"{status} (score={avg_score:.2f})")
results.append({
"question": tc["question"],
"answer": answer,
"tag": tc["tag"],
"checks": checks,
"overall_pass": overall_pass,
"avg_score": avg_score,
"judge_score": judge_result["score"],
"judge_reason": judge_result["reason"]
})
return results
def report(self, results: list):
"""打印评估报告"""
total = len(results)
passed = sum(1 for r in results if r["overall_pass"])
avg_score = sum(r["avg_score"] for r in results) / total if total else 0
avg_judge = sum(r["judge_score"] for r in results) / total if total else 0
print("\n" + "=" * 60)
print(f"评估报告:{self.name}")
print("=" * 60)
print(f"通过率: {passed}/{total} ({passed/total:.0%})")
print(f"平均分: {avg_score:.3f}")
print(f"LLM评分: {avg_judge:.1f}/5.0")
# 按 tag 分组报告
tags = set(r["tag"] for r in results if r["tag"])
if tags:
print("\n按类别:")
for tag in sorted(tags):
tag_results = [r for r in results if r["tag"] == tag]
tag_pass = sum(1 for r in tag_results if r["overall_pass"])
print(f" {tag}: {tag_pass}/{len(tag_results)} 通过")
# 失败案例
failed = [r for r in results if not r["overall_pass"]]
if failed:
print("\n失败案例:")
for r in failed:
print(f" ❌ {r['question'][:40]}...")
print(f" 原因: {r['judge_reason']}")
verdict = "✅ 通过" if passed == total else ("⚠️ 部分通过" if passed > 0 else "❌ 全部失败")
print(f"\n总结: {verdict}")
return {"pass_rate": passed/total, "avg_score": avg_score}
# 创建并运行评估套件
suite = EvalSuite(
name="客服机器人回归测试",
system_prompt="你是一位专业的电商客服,用友好简洁的语气回复,每次回复不超过150字。"
)
suite.add_test(
"我的订单什么时候发货?",
expected_keywords=["工作日", "发货"],
min_score=3, tag="物流"
)
suite.add_test(
"我要退款,商品有质量问题",
expected_keywords=["退款", "售后"],
min_score=3, tag="售后"
)
suite.add_test(
"你们支持哪些支付方式?",
expected_keywords=["支付"],
max_length=200, min_score=3, tag="支付"
)
results = suite.run(model=MODEL)
suite.report(results)
运行评估套件: 客服机器人回归测试
模型: openai/gpt-5-mini
测试用例数: 3
--------------------------------------------------
[1/3] 我的订单什么时候发货?...
❌ FAIL (score=0.10)
[2/3] 我要退款,商品有质量问题...
❌ FAIL (score=0.10)
[3/3] 你们支持哪些支付方式?...
❌ FAIL (score=0.40)
============================================================
评估报告:客服机器人回归测试
============================================================
通过率: 0/3 (0%)
平均分: 0.200
LLM评分: 1.0/5.0
按类别:
售后: 0/1 通过
支付: 0/1 通过
物流: 0/1 通过
失败案例:
❌ 我的订单什么时候发货?...
原因: 未提供任何回答,未解决用户问题
❌ 我要退款,商品有质量问题...
原因: 未提供任何信息或操作指引,无法处理退款
❌ 你们支持哪些支付方式?...
原因: 未提供任何回答,未说明支持的支付方式,无法使用。
总结: ❌ 全部失败
{'pass_rate': 0.0, 'avg_score': 0.19999999999999998}
Section 6:常见评估陷阱#
LLM-as-Judge 存在一些系统性偏差,了解它们并采取措施是可靠评估的关键。
1. 位置偏差(Position Bias)#
问题:LLM 倾向于认为第一个呈现的答案更好(先入为主效应)。 缓解:交换 A/B 顺序运行两次,取共识结果。只有两次结果一致才确定胜者。
winner1, _ = compare_answers(q, answer_a, answer_b) # A在前
winner2, _ = compare_answers(q, answer_b, answer_a) # B在前(顺序翻转)
# 翻转映射:若winner2=A,实际是B赢
if winner1 == winner2:
final = winner1 # 一致,可信
else:
final = "tie" # 不一致,判平局
2. 冗长偏差(Verbosity Bias)#
问题:LLM 倾向于认为更长的答案更好,即使长答案包含废话。 缓解:评估标准中明确包含「简洁性」维度;同时评估多个维度,不只看整体。
# 多维度评估,而非单一综合分
scores = {
"accuracy": llm_judge(q, a, "factual_accuracy")["score"],
"helpfulness": llm_judge(q, a, "helpfulness")["score"],
"conciseness": llm_judge(q, a, "conciseness")["score"], # 明确评估简洁性
}
3. 自我评估偏差(Self-Evaluation Bias)#
问题:用模型评估自己的输出,可能偏向自己的风格和观点。 缓解:使用不同厂商的模型作为 Judge(如用 Claude 评估 GPT 输出)。
# 用 GPT 生成,用 Claude 评估
answer = call_model("gpt-4o", question)
score = llm_judge(question, answer, "helpfulness", model="claude-3-5-sonnet")
其他注意事项#
Temperature = 0:评估时用 temperature=0 确保可重复性
多次运行取平均:单次 LLM 评估有随机性,重要决策跑 3-5 次
人工抽验:定期抽取 5-10% 的样本人工复核 LLM 评估是否合理
# 演示:位置偏差检测
def robust_compare(question: str, answer_a: str, answer_b: str,
model: str = MODEL) -> dict:
"""抗位置偏差的 A/B 比较:交换顺序运行两次"""
# 正向比较
winner_fwd, reason_fwd = compare_answers(question, answer_a, answer_b, model)
# 反向比较(B 放前面)
winner_rev_raw, reason_rev = compare_answers(question, answer_b, answer_a, model)
# 翻转映射
flip = {"A": "B", "B": "A", "tie": "tie"}
winner_rev = flip.get(winner_rev_raw, winner_rev_raw)
# 判断一致性
if winner_fwd == winner_rev:
final = winner_fwd
consistent = True
else:
final = "tie" # 结果不一致,判平局
consistent = False
return {
"winner": final,
"consistent": consistent,
"forward_winner": winner_fwd,
"reverse_winner": winner_rev,
"position_bias_detected": not consistent
}
# 测试
question = "如何学好英语?"
answer_a = "每天听英语歌曲。" # 简短
answer_b = "学好英语需要坚持,建议每天阅读英文材料、看英文视频、与外国朋友交流,同时系统学习语法。" # 详细
print("位置偏差检测演示")
print("=" * 60)
print(f"问题: {question}")
print(f"答案A(简短): {answer_a}")
print(f"答案B(详细): {answer_b}")
result = robust_compare(question, answer_a, answer_b)
print(f"\n正向比较胜者: {result['forward_winner']}")
print(f"反向比较胜者: {result['reverse_winner']}")
print(f"结果一致: {result['consistent']}")
print(f"最终裁定: {result['winner']}")
if result['position_bias_detected']:
print("⚠️ 检测到位置偏差!两次比较结果不一致,判为平局。")
else:
print("✅ 无位置偏差,结果可信。")
位置偏差检测演示
============================================================
问题: 如何学好英语?
答案A(简短): 每天听英语歌曲。
答案B(详细): 学好英语需要坚持,建议每天阅读英文材料、看英文视频、与外国朋友交流,同时系统学习语法。
正向比较胜者: B
反向比较胜者: B
结果一致: True
最终裁定: B
✅ 无位置偏差,结果可信。
Section 7:高级评估技术#
# G-Eval:基于评估步骤的连贯性评分
# 让 LLM 先生成评估步骤,再按步骤打分
def g_eval(question: str, answer: str, aspect: str = "overall") -> dict:
"""
G-Eval:更可靠的 LLM 评估方法
先让 LLM 生成评估步骤,再逐步打分
"""
# Step 1:生成评估步骤
steps_prompt = f"""为评估以下内容,列出具体的评估步骤(3-5步):
评估维度:{aspect}
问题类型:回答用户关于 {question[:30]} 的问题
以 JSON 输出步骤列表:{{"steps": ["步骤1", "步骤2", ...]}}"""
steps_resp = _c(
model=MODEL,
messages=[{"role": "user", "content": steps_prompt}],
response_format={"type": "json_object"},
temperature=0
)
steps_data = json.loads(steps_resp.choices[0].message.content)
steps = steps_data.get("steps", [])
# Step 2:按步骤评估
steps_text = "\n".join(f"{i+1}. {s}" for i, s in enumerate(steps))
eval_prompt = f"""按照以下步骤评估回答,然后给出1-5分:
评估步骤:
{steps_text}
问题:{question}
回答:{answer}
按步骤思考,最后输出 JSON:
{{"step_scores": [每步得分0-1], "final_score": 1-5整数, "summary": "总结"}}"""
eval_resp = _c(
model=MODEL,
messages=[{"role": "user", "content": eval_prompt}],
response_format={"type": "json_object"},
temperature=0
)
eval_data = json.loads(eval_resp.choices[0].message.content)
return {
"method": "G-Eval",
"aspect": aspect,
"eval_steps": steps,
"final_score": eval_data.get("final_score", 0),
"step_scores": eval_data.get("step_scores", []),
"summary": eval_data.get("summary", "")
}
# 演示
q = "如何处理 Python 中的异常?"
a = """用 try-except 块处理异常:
```python
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"除零错误: {e}")
except Exception as e:
print(f"其他错误: {e}")
finally:
print("无论如何都执行")
```
要点:捕获具体异常(不要只用 Exception),finally 用于清理资源。"""
print("G-Eval 评估演示")
print("=" * 60)
result = g_eval(q, a, "教学质量")
print(f"问题: {q}")
print(f"\n生成的评估步骤:")
for i, step in enumerate(result['eval_steps'], 1):
print(f" {i}. {step}")
print(f"\n最终评分: {result['final_score']}/5")
print(f"总结: {result['summary']}")
G-Eval 评估演示
============================================================
问题: 如何处理 Python 中的异常?
生成的评估步骤:
1. 步骤1: 检查技术准确性——核对回答是否正确且完整地介绍了 try/except/else/finally、raise 与自定义异常、异常链(__cause__/__context__)、常见内置异常(如 ValueError、TypeError、OSError)及与上下文管理器(with)相关的异常处理,且语法示例无误。
2. 步骤2: 评估示例质量与可运行性——确认示例简洁、带注释、可直接运行,覆盖常见场景与边界情况,示范捕获具体异常(避免裸 except)、使用异常对象(as e)与抛出异常的正确用法。
3. 步骤3: 评估教学清晰度与结构——判断内容是否按逻辑分层(概念解释→语法→示例→总结/常见错误),语言是否通俗、术语解释清楚、适配目标读者的理解水平。
4. 步骤4: 评估最佳实践与实用建议——查看是否提供实际建议(如使用 logging、资源清理、何时定义自定义异常、避免用异常做流程控制、捕获范围尽量窄),并指出安全或性能相关注意事项。
5. 步骤5: 评估学习支持与反馈机制——确认是否提供练习题或小练习、测试用例、常见错误解析、调试技巧与参考资料或扩展阅读,以及鼓励读者提问或给出改进建议。
最终评分: 1/5
总结: 该回答在示例语法上基本可运行且正确演示了 try/except/finally 并展示了捕获特定异常(ZeroDivisionError)和使用异常对象(as e),但总体非常不完整。主要缺陷:未介绍 try/except/else 的用途、未说明 raise 与如何定义/使用自定义异常、未提及异常链 (__cause__/__context__)、未列举或说明常见内置异常(如 ValueError/TypeError/OSError 等)、未讨论 with/上下文管理器 与 异常处理 的关系,也缺乏关于何时重抛、如何记录异常 (logging)、资源清理的实用建议。示例缺少注释、未展示抛出异常或自定义异常、未覆盖边界情况,也出现了 catch Exception 的一般性做法而没有说明其风险。改进建议:补充概念→语法→示例→总结的结构;增加带注释、可运行的示例,包括 else、raise、自定义异常、异常链和 with 的用法;强调最佳实践(捕获尽量具体、避免用异常做流程控制、记录与清理资源、何时重抛);提供练习题、单元测试示例和调试/阅读参考。
总结:评估策略建议#
分层评估策略#
第1层:基础指标(无成本)
→ 精确匹配、关键词覆盖、长度合规
→ 适合:有明确标准答案的任务
第2层:LLM-as-Judge(低成本)
→ 单维度评分 + 多维度评分
→ 适合:开放式问答、生成类任务
第3层:A/B 对比评估(中等成本)
→ 比较两个版本,选出更好的
→ 适合:提示词迭代、模型选型
第4层:人工评估(高成本)
→ 定期抽样,校准自动评估
→ 适合:重要节点、异常情况复核
评估实施检查清单#
[ ] 定义明确的评估维度(不要只有”总体质量”)
[ ] 使用 temperature=0 确保可重复性
[ ] 评估数据集覆盖多样场景(正常、边界、错误输入)
[ ] 定期更新评估集(避免过拟合评估集)
[ ] 对比评估时交换顺序(防止位置偏差)
[ ] 评估 Judge 模型与目标模型不同厂商(防自我偏差)
[ ] 设置评估基准(baseline),每次迭代与基准对比
[ ] 失败案例人工分析,找到系统性问题
# 快速参考:各评估函数一览
print("=" * 70)
print("本章实现的评估函数快速参考")
print("=" * 70)
functions_ref = [
{
"name": "eval_exact_match(pred, gold)",
"用途": "精确匹配评估",
"返回": "score: 0/1, pass: bool",
"适用": "分类、固定答案任务"
},
{
"name": "eval_contains(pred, keywords)",
"用途": "关键词覆盖检查",
"返回": "coverage: 0-1, missing: list",
"适用": "检查必要信息点"
},
{
"name": "eval_length_compliance(pred, min, max)",
"用途": "长度合规检查",
"返回": "score: 0-1, status: str",
"适用": "有长度要求的任务"
},
{
"name": "llm_judge(question, answer, criteria)",
"用途": "LLM 质量评分",
"返回": "score: 1-5, reason: str",
"适用": "开放式评估"
},
{
"name": "compare_answers(q, answer_a, answer_b)",
"用途": "A/B 对比评估",
"返回": "winner: A/B/tie",
"适用": "版本对比、模型选型"
},
{
"name": "eval_context_recall(q, ctx, answer)",
"用途": "RAG 上下文召回",
"返回": "score: 0-1, verdict",
"适用": "RAG 系统评估"
},
{
"name": "eval_answer_faithfulness(ctx, answer)",
"用途": "RAG 答案忠实性",
"返回": "faithfulness_score, hallucinations",
"适用": "幻觉检测"
},
{
"name": "EvalSuite.run() + .report()",
"用途": "批量回归测试",
"返回": "pass_rate, avg_score, 失败列表",
"适用": "CI/CD 集成测试"
},
]
for f in functions_ref:
print(f"\n{'函数:':<6} {f['name']}")
print(f" 用途: {f['用途']}")
print(f" 返回: {f['返回']}")
print(f" 适用: {f['适用']}")
======================================================================
本章实现的评估函数快速参考
======================================================================
函数: eval_exact_match(pred, gold)
用途: 精确匹配评估
返回: score: 0/1, pass: bool
适用: 分类、固定答案任务
函数: eval_contains(pred, keywords)
用途: 关键词覆盖检查
返回: coverage: 0-1, missing: list
适用: 检查必要信息点
函数: eval_length_compliance(pred, min, max)
用途: 长度合规检查
返回: score: 0-1, status: str
适用: 有长度要求的任务
函数: llm_judge(question, answer, criteria)
用途: LLM 质量评分
返回: score: 1-5, reason: str
适用: 开放式评估
函数: compare_answers(q, answer_a, answer_b)
用途: A/B 对比评估
返回: winner: A/B/tie
适用: 版本对比、模型选型
函数: eval_context_recall(q, ctx, answer)
用途: RAG 上下文召回
返回: score: 0-1, verdict
适用: RAG 系统评估
函数: eval_answer_faithfulness(ctx, answer)
用途: RAG 答案忠实性
返回: faithfulness_score, hallucinations
适用: 幻觉检测
函数: EvalSuite.run() + .report()
用途: 批量回归测试
返回: pass_rate, avg_score, 失败列表
适用: CI/CD 集成测试