标题:评测体系:自动化与人工
digest:自动评测的边界在哪,人工评测该补哪里
"接了 LangSmith,能看到 trace 了,但我怎么知道回答好不好?"
这是 tracing 之后必然遇到的下一个问题。Tracing 记录了"发生了什么",评测回答"做得怎么样"。两件事缺一不可,但方法论完全不同。
自动评测不是一个方法,是一类方法的集合,每种方法有不同的精度上限和适用边界。用错了,评测结果不代表真实质量,反而误导迭代方向。
5 种打分模式
模式一:规则匹配
最简单,也最可靠。适合有明确正确答案或格式要求的任务。
适用:JSON schema 验证、关键词出现/不出现、长度范围、正则匹配、数字精确对比。
边界:只能评"格式和结构",不能评"语义质量"。对语言生成任务,规则覆盖不了 90% 的情况。
import json, refrom dataclasses import dataclass@dataclassclass EvalResult: passed: bool score: float # 0.0 - 1.0 reason: strdef eval_json_schema(response: str, required_keys: list[str]) -> EvalResult: try: data = json.loads(response) except json.JSONDecodeError as e: return EvalResult(passed=False, score=0.0, reason=f"JSON parse error: {e}") missing = [k for k in required_keys if k not in data] if missing: return EvalResult(passed=False, score=0.0, reason=f"Missing keys: {missing}") return EvalResult(passed=True, score=1.0, reason="All required keys present")def eval_no_harmful_content(response: str) -> EvalResult: patterns = [r"\b(kill|harm|attack|exploit)\b"] # 示例,实际用更完整的列表 for pattern in patterns: if re.search(pattern, response, re.IGNORECASE): return EvalResult(passed=False, score=0.0, reason=f"Harmful pattern: {pattern}") return EvalResult(passed=True, score=1.0, reason="No harmful patterns found")
模式二:语义相似度
把 reference answer 和 model output 都 embed,算余弦相似度。适合有标准答案的 QA 任务。
适用:FAQ 回答质量评估、知识库覆盖度检测。
边界:embedding 相似度和人类判断的相关性约 0.6-0.7,不是 1.0。两个语义相似的句子可能一个对一个错("价格是 100 元" vs "价格不是 100 元"——相似度很高,但一真一假)。
import mathdef cosine_similarity(v1: list[float], v2: list[float]) -> float: dot = sum(a * b for a, b in zip(v1, v2)) norm1 = math.sqrt(sum(a * a for a in v1)) norm2 = math.sqrt(sum(b * b for b in v2)) if norm1 == 0 or norm2 == 0: return 0.0 return dot / (norm1 * norm2)def mock_embed(text: str) -> list[float]: """实际替换为 anthropic Embeddings 或 sentence-transformers。""" import hashlib h = int(hashlib.md5(text.encode()).hexdigest(), 16) return [(h >> i & 1) * 0.1 + 0.5 for i in range(8)]def eval_semantic_similarity(response: str, reference: str, threshold: float = 0.8) -> EvalResult: emb_response = mock_embed(response) emb_reference = mock_embed(reference) sim = cosine_similarity(emb_response, emb_reference) passed = sim >= threshold return EvalResult( passed=passed, score=round(sim, 3), reason=f"Cosine similarity: {sim:.3f} (threshold: {threshold})", )
模式三:LLM-as-Judge
用一个更强的模型(或同等模型)来评判目标模型的输出。能处理语义质量,是目前最接近人类判断的自动方法。
适用:开放式问答质量、总结完整性、推理过程合理性。
边界:
- 评判模型本身的偏见会带进来(偏好长答案、偏好自己的输出风格)
- 成本高:评测 1000 条 = 1000 次 LLM 调用
- 评分不稳定:同样的问题换个评判 prompt,分数可能差 15%
- 不要用来评"绝对分数",适合做"A 比 B 好吗"的相对比较
import anthropicimport reeval_client = anthropic.Anthropic()FAITHFULNESS_PROMPT = """\你是一个评测助手。评判下面的"模型回答"是否忠实于"参考上下文",即回答中的每一个事实断言,都能在参考上下文中找到依据。参考上下文:{context}模型回答:{response}评分标准:- 5分:回答完全基于上下文,无幻觉- 3分:大部分基于上下文,有1-2处轻微延伸- 1分:有明显幻觉或与上下文矛盾的内容只输出一个JSON:{{"score": <1-5>, "reason": "<一句话解释>"}}"""def eval_faithfulness(response: str, context: str, use_mock: bool = True) -> EvalResult: if use_mock: # 测试时用 mock,避免 API 调用 return EvalResult(passed=True, score=0.8, reason="[mock] Faithfulness eval") prompt = FAITHFULNESS_PROMPT.format(context=context, response=response) msg = eval_client.messages.create( model="claude-sonnet-4-6", max_tokens=200, messages=[{"role": "user", "content": prompt}], ) raw = msg.content[0].text.strip() # 提取 JSON,允许模型输出包含额外文本 match = re.search(r'\{"score":\s*(\d+),\s*"reason":\s*"([^"]+)"\}', raw) if not match: return EvalResult(passed=False, score=0.0, reason=f"Parse failed: {raw}") score_raw = int(match.group(1)) reason = match.group(2) score_normalized = (score_raw - 1) / 4 # 1-5 → 0.0-1.0 return EvalResult(passed=score_raw >= 3, score=round(score_normalized, 2), reason=reason)
模式四:对比评测(Pairwise)
不打绝对分,只问"A 比 B 好吗"。用于比较两个版本的 Prompt 或两个模型。
适用:Prompt 迭代时的 A/B 对比、模型升级前后的质量对比。
边界:只能给出相对排序,不能量化"好多少";当 A 和 B 质量接近时,评判不稳定。
import randomPAIRWISE_PROMPT = """\问题:{question}回答 A:{answer_a}回答 B:{answer_b}哪个回答更好?只输出 "A"、"B" 或 "Tie",不要解释。"""def eval_pairwise( question: str, answer_a: str, answer_b: str, use_mock: bool = True,) -> str: """返回 'A'、'B' 或 'Tie'。""" if use_mock: return random.choice(["A", "B", "Tie"]) prompt = PAIRWISE_PROMPT.format(question=question, answer_a=answer_a, answer_b=answer_b) msg = eval_client.messages.create( model="claude-sonnet-4-6", max_tokens=10, messages=[{"role": "user", "content": prompt}], ) result = msg.content[0].text.strip() if result not in ("A", "B", "Tie"): return "Tie" return resultdef pairwise_tournament(question: str, answer_a: str, answer_b: str, rounds: int = 5) -> dict: """ 多轮对比减少随机性。实际使用时建议 rounds=5,结果稳定性明显提升。 """ counts = {"A": 0, "B": 0, "Tie": 0} for _ in range(rounds): winner = eval_pairwise(question, answer_a, answer_b) counts[winner] += 1 return counts
模式五:基于参考的多维评分
当有标准答案(reference)时,同时计算多个维度:precision、recall、F1(可以是 token 级别的 BERTScore,也可以是关键词级别)。
适用:有人工标注 golden answers 的评测集,适合精确度要求高的场景(医疗、法律)。
边界:需要构建和维护标注集,成本高;对于开放式生成任务,reference 本身的质量决定了评测上限。
def token_f1(prediction: str, reference: str) -> tuple[float, float, float]: pred_tokens = set(prediction.lower().split()) ref_tokens = set(reference.lower().split()) common = pred_tokens & ref_tokens if not common: return 0.0, 0.0, 0.0 precision = len(common) / len(pred_tokens) recall = len(common) / len(ref_tokens) f1 = 2 * precision * recall / (precision + recall) return round(precision, 3), round(recall, 3), round(f1, 3)
完整的评测 Pipeline
把 5 种模式组合起来,针对不同任务维度分别评测:
from dataclasses import dataclass, field@dataclassclass EvalSuite: format_score: EvalResult = field(default=None) faithfulness_score: EvalResult = field(default=None) similarity_score: EvalResult = field(default=None) f1_score: tuple[float, float, float] = field(default=(0.0, 0.0, 0.0)) def overall_passed(self, require_format: bool = True) -> bool: if require_format and self.format_score and not self.format_score.passed: return False if self.faithfulness_score and not self.faithfulness_score.passed: return False return Truedef run_rag_eval( question: str, response: str, context: str, reference: str, required_keys: list[str] | None = None,) -> EvalSuite: suite = EvalSuite() if required_keys: suite.format_score = eval_json_schema(response, required_keys) suite.faithfulness_score = eval_faithfulness(response, context, use_mock=True) suite.similarity_score = eval_semantic_similarity(response, reference) suite.f1_score = token_f1(response, reference) return suiteif __name__ == "__main__": result = run_rag_eval( question="退款需要多少天?", response="根据我们的退款政策,处理时间为 7 个工作日。", context="退款政策:所有退款申请将在 7 个工作日内处理完成。", reference="退款需要 7 个工作日处理。", ) print("评测结果:") print(f" 格式检查: {result.format_score}") print(f" 忠实度: {result.faithfulness_score}") print(f" 语义相似度: {result.similarity_score}") print(f" Token F1: P={result.f1_score[0]}, R={result.f1_score[1]}, F1={result.f1_score[2]}") print(f" 综合通过: {result.overall_passed()}")
不要把这些指标当 KPI 追
自动评测指标是诊断工具,不是目标函数。针对指标优化,往往会让模型"学会"通过评测而不是真正变好——更长的回答拿到更高的 recall,更像 reference 的表达拿到更高的 similarity,但实际用户体验可能变差了。
自动评测的正确用法是:发现退化(上次改动让哪些维度变差了),而不是证明优秀。
六、人工评测:三个失败原因和一张分工表
自动评测有一个系统性的盲区:它只能测可以用规则或模型定义清楚的维度。格式合规、Token F1、有参考答案时的事实准确性——这些能自动打分。但"回答有没有业务感"、"措辞是否符合品牌风格"——这些没有参考答案,必须人工介入。
问题不在于需不需要人工评测,而在于做不好的人工评测比没有更糟——结论不一致,误导迭代方向。
失败原因一:任务描述模糊
"这个回答好不好"没有意义。每个评测维度要独立打分,且有清晰的 1-3 分标准(不用 5 分,中间值让标注者倾向于全打 3):
维度:事实准确性1 分 — 有明显事实错误2 分 — 基本准确但有小瑕疵3 分 — 完全准确维度:语气风格1 分 — 过于生硬或过于随意,不符合品牌调性2 分 — 语气一般,无明显问题但也无亮点3 分 — 专业、简洁、让用户感到被理解
失败原因二:没有检验标注者一致性
两个标注者对同一条 case 打分相差 2 分,他们在评同一件事吗?一致率低于 70% 的维度,要重新讨论标注标准,不要强行汇总。
失败原因三:样本选取偏向正常 case
随机抽 100 条,大概率大多数是"正常回答"——问题 case 出现率本来就低。要用分层采样,确保低质量 case 有代表:
import randomdef stratified_sample( candidates: list[dict], n: int, strata_key: str = "source",) -> list[dict]: """ 按来源分层采样。candidates 里每个 dict 需要 strata_key 字段, 取值如 "low_auto_score" / "user_retry" / "random"。 """ strata: dict[str, list] = {} for c in candidates: strata.setdefault(c.get(strata_key, "unknown"), []).append(c) result, remaining = [], n items = list(strata.items()) for i, (_, group) in enumerate(items): quota = remaining if i == len(items) - 1 else max(1, round(n * len(group) / len(candidates))) take = min(quota, len(group)) result.extend(random.sample(group, take)) remaining -= take return result[:n]# 示例candidates = [ {"query": "退款多久?", "source": "low_auto_score"}, {"query": "支付方式?", "source": "random"}, {"query": "地址修改?", "source": "user_retry"}, {"query": "优惠券?", "source": "negative_feedback"},]sampled = stratified_sample(candidates, n=3)print([c["source"] for c in sampled])
人工评测和自动评测的分工:
自动评测做监控(持续运行、发现退化),人工评测做标定(建立基准、验证自动评测的可靠性)。
下一篇讲 Prompt 版本管理——评测发现质量退步时,如何回滚,如何在改 Prompt 之前就跑完评测再决定上不上线。
*《AI 应用可观测性》系列每天更新一篇,下篇预告:*
*05 — Prompt版本管理与回归测试*
#AI编程#大模型开发#LLM可观测性#评测#LLM-as-Judge#人工标注#Python#RAG