当前位置:首页>排行榜>评测体系:自动化与人工

评测体系:自动化与人工

  • 更新时间 2026-10-03 09:30:12
评测体系:自动化与人工

标题:评测体系:自动化与人工

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])

人工评测和自动评测的分工:

维度
自动评测
人工评测
格式合规
✓ 精确
不需要
事实准确性(有参考答案)
✓ F1/BERTScore
不需要
事实准确性(无参考答案)
✗
✓
语义相关性
△ LLM-as-Judge 约 70% 精度
✓ 更可靠
语气/品牌风格
✗
✓
用户体验感知
✗
✓

自动评测做监控(持续运行、发现退化),人工评测做标定(建立基准、验证自动评测的可靠性)。


下一篇讲 Prompt 版本管理——评测发现质量退步时,如何回滚,如何在改 Prompt 之前就跑完评测再决定上不上线。

*《AI 应用可观测性》系列每天更新一篇,下篇预告:*

*05 — Prompt版本管理与回归测试*

#AI编程#大模型开发#LLM可观测性#评测#LLM-as-Judge#人工标注#Python#RAG

随机文章