Language:Chinese VersionEnglish Version

评估 LLM 输出不是指标问题 — 而是哲学问题

大多数构建 LLM 驱动应用的团队都低估了评估工作,直到他们将产品部署到生产环境后才发现”测试中看起来不错”并不是一种方法论。LLM 评估的难度是传统软件测试所不具备的:主观任务没有标准答案,输出是概率性和可变的,且故障模式是定性的而非二元的。本指南涵盖了团队在生产环境中实际使用的评估框架、指标和工具 — 以及那些没有清晰自动化解决方案的评估问题的概念框架。

为什么 LLM 评估确实很困难

传统软件测试有明确的结构:给定输入 X,期望输出 Y。确定性、二元性、可自动化。LLM 评估打破了这一模型的所有假设。

考虑一个客户支持机器人,应该准确且有帮助地回答有关您产品的问题。您如何测试它?”准确”需要知道正确的答案进行比较 — 但对于开放式问题,可能有多个有效答案,没有一个与您的参考答案完全匹配。”有帮助”是一种主观质量评估。而同一个提示发送给同一个模型两次可能会产生明显不同的输出。

这不是一个通过更好的工具就能解决的问题。这是一个需要通过自动化指标、人工判断和生产监控的正确组合来管理的问题 — 要认识到没有单一方法是足够的。

四种评估范式

1. 基于参考的指标

当您有标准答案 — 一组具有已知正确答案的问题时,基于参考的指标会将模型输出与参考答案进行比较。

ROUGE(面向摘要评估的召回率导向辅助工具):衡量生成文本与参考文本之间的 n-gram 重叠。ROUGE-1 比较一元语法,ROUGE-2 比较二元语法,ROUGE-L 比较长公共子序列。最初为摘要评估开发。

BLEU(双语评估辅助工具):类似的 n-gram 重叠指标,最初用于机器翻译。衡量精确度(输出中有多少出现在参考中)而非召回率。

BERTScore:使用 BERT 的上下文嵌入来衡量输出与参考答案之间的语义相似性,而不是基于表面层面的词汇重叠。在捕捉不同措辞的释义和语义等效输出方面表现更好。

from bert_score import score as bert_score
from rouge_score import rouge_scorer

# 示例:评估摘要输出
references = [
    "ACME 协议通过挑战-响应验证自动化 TLS 证书颁发。",
]
candidates = [
    "ACME 是一种通过验证域名所有权来处理自动证书管理的协议。",
]

# ROUGE 分数
scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)
scores = scorer.score(references[0], candidates[0])
print(f"ROUGE-1 F1: {scores['rouge1'].fmeasure:.3f}")
print(f"ROUGE-2 F1: {scores['rouge2'].fmeasure:.3f}")
print(f"ROUGE-L F1: {scores['rougeL'].fmeasure:.3f}")

# BERTScore — 捕捉超越词汇重叠的语义相似性
P, R, F1 = bert_score(candidates, references, lang="en")
print(f"BERTScore F1: {F1.mean():.3f}")

基于参考指标的评估方法有一个根本性局限:它们需要高质量的参考答案,并且衡量的是与这些参考答案的相似性,而非正确性或质量。一个事实正确但表述方式与参考答案不同的输出会得到低分。在你拥有高质量参考答案且输出空间受限的任务中使用这些指标(摘要、翻译、事实性问答)。

2. LLM 作为评判者

目前最广泛使用的定性评估范式是使用一个强大的大语言模型(通常是 GPT-4o 或 Claude Sonnet)来评估另一个大语言模型的输出。评判模型会收到一个评分标准并根据它来评估输出。

import anthropic

client = anthropic.Anthropic()

def evaluate_with_llm_judge(
    question: str,
    model_output: str,
    criteria: list[str]
) -> dict:
    """
    使用 Claude 作为评判者来评估大语言模型输出的质量。
    返回每个标准的分数和推理。
    """
    criteria_text = "n".join(f"{i+1}. {c}" for i, c in enumerate(criteria))
    
    prompt = f"""您正在评估一个 AI 助手回答的质量。

提问的问题: {question}

待评估的回答:
{model_output}

根据以下标准评估回答:
{criteria_text}

对于每个标准,请提供:
- 分数: 1(差),2(可接受),3(好),4(优秀)
- 简短推理(1-2句话)

以 JSON 格式回复:
{{
  "scores": {{
    "标准名称": {{"score": X, "reasoning": "..."}}
  }},
  "overall_score": X,
  "overall_assessment": "..."
}}"""

    message = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}]
    )
    
    import json
    return json.loads(message.content[0].text)

# 使用示例
result = evaluate_with_llm_judge(
    question="如何在 REST API 中实现速率限制?",
    model_output="...",
    criteria=[
        "技术准确性 — 信息是否正确?",
        "完整性 — 是否涵盖了关键方法?",
        "代码质量 — 代码示例是否正确且符合习惯?",
        "清晰度 — 解释是否易于理解?"
    ]
)
print(f"总体分数: {result['overall_score']}/4")

大语言模型作为评判者在许多任务上与人类判断有很高的相关性(LMSYS 的研究表明与人类偏好评分的一致性超过 80%),但它有文献充分记录的偏见:位置偏见(比较时偏好第一个选项)、冗长偏见(无论质量如何偏好更长的输出)和自我提升偏见(模型在作为评判者时倾向于偏好自己的输出)。

缓解这些问题的方法包括:位置交换测试(运行两次比较,交换候选对象,标记不一致之处)、使用与被评估模型不同的评判模型,以及在代表性样本上根据人类标签校准评判模型。

3. 人工评估

对于高风险任务,人工评估仍然是黄金标准。问题不在于是否使用人工评估,而在于如何使其高效和一致。

可靠人工评估的关键原则:

盲评: 评估者不应知道哪个模型或提示生成了被评分的输出。即使评估者出于好意,知道来源也会引入偏见。

清晰的评分标准与示例:“这个回答有帮助吗?”不是评分标准。”根据有用性对回答进行评分:1 = 没有回答问题,2 = 部分回答了问题,3 = 回答了问题但有遗漏或错误,4 = 完整准确地回答了问题” — 每个级别都有示例 — 可以产生一致的评分。

评分者间一致性:让多个评估者对随机样本进行评分并测量一致性(分类评分使用 Cohen’s Kappa)。低一致性表明你的评分标准不明确,而不是评估者不可靠。

4. 任务特定的自动化指标

对于结构化任务,构建自定义的自动化评估来测试特定属性:

import ast
import subprocess
import tempfile
import os

def evaluate_code_output(
    generated_code: str,
    test_cases: list[dict]
) -> dict:
    """
    通过实际运行测试用例来评估生成的代码。
    对于代码评估,比文本相似度更可靠。
    """
    results = {
        "syntax_valid": False,
        "tests_passed": 0,
        "tests_total": len(test_cases),
        "errors": []
    }
    
    # 检查语法有效性
    try:
        ast.parse(generated_code)
        results["syntax_valid"] = True
    except SyntaxError as e:
        results["errors"].append(f"语法错误: {e}")
        return results
    
    # 运行测试用例
    for i, test_case in enumerate(test_cases):
        test_code = f"""
{generated_code}

# 测试用例 {i+1}
result = {test_case['call']}
expected = {repr(test_case['expected'])}
assert result == expected, f"期望 {{expected}},实际 {{result}}"
print("PASS")
"""
        with tempfile.NamedTemporaryFile(
            mode='w', suffix='.py', delete=False
        ) as f:
            f.write(test_code)
            f.flush()
            
            proc = subprocess.run(
                ["python3", f.name],
                capture_output=True, text=True, timeout=5
            )
            os.unlink(f.name)
            
            if "PASS" in proc.stdout:
                results["tests_passed"] += 1
            else:
                results["errors"].append(
                    f"测试 {i+1} 失败: {proc.stderr[:200]}"
                )
    
    results["pass_rate"] = results["tests_passed"] / results["tests_total"]
    return results

评估框架

RAGAS: RAG 特定评估

对于检索增强生成系统,RAGAS 提供了一个测量四个维度的框架:忠实度(答案是否仅使用检索到的上下文中的信息?)、答案相关性(答案与问题的相关性如何?)、上下文召回率(检索到的上下文是否包含所需信息?)以及上下文精确度(检索到的上下文是否相关?)。

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision
)
from datasets import Dataset

# 准备评估数据集
eval_data = {
    "question": ["什么是 mTLS?", "ACME 是如何工作的?"],
    "answer": ["mTLS 要求客户端和服务器都进行身份验证...", "ACME 使用挑战-响应验证..."],
    "contexts": [
        ["mTLS 或 mutual TLS 是一种双方都进行身份验证的协议..."],
        ["ACME 协议通过验证来自动化证书颁发..."]
    ],
    "ground_truth": ["相互 TLS 对客户端和服务器都进行身份验证...", "ACME 自动化 TLS 证书颁发..."]
}

dataset = Dataset.from_dict(eval_data)
result = evaluate(dataset, metrics=[
    faithfulness, answer_relevancy, context_recall, context_precision
])
print(result)

PromptFoo: 系统化提示测试

PromptFoo 是一个用于跨模型和数据集系统化测试提示的 CLI 工具。它可以集成到 CI 管道中,并能自动对模型输出运行断言。

# promptfooconfig.yaml
prompts:
  - file://prompts/code-reviewer.txt

providers:
  - id: ollama:qwen2.5-coder:7b
  - id: openai:gpt-4o-mini

tests:
  - description: "应该识别 SQL 注入"
    vars:
      code: |
        query = f"SELECT * FROM users WHERE id = {user_input}"
        cursor.execute(query)
    assert:
      - type: contains
        value: "SQL injection"
      - type: llm-rubric
        value: "响应识别了 SQL 注入漏洞并提供了一个参数化查询修复"

  - description: "不应该标记安全代码"
    vars:
      code: |
        cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    assert:
      - type: not-contains
        value: "SQL injection"
# 运行评估
promptfoo eval

# 比较不同提供商的结果
promptfoo view

生产监控:评估不会在部署时结束

生产环境中的模型行为与评估集中的模型行为不同——有时差异很大。用户会提出您没有预料到的问题,以您没有测试过的格式,并且当您更新提示、更改模型或底层模型被提供商更新时,模型的响应质量也会发生漂移。

LLM 应用程序的生产监控需要:

  • 记录所有输入和输出:这是你进行事后分析的基准真相。存储每个请求和响应,包含时间戳、模型版本和提示模板版本。
  • 抽样人工审核:每周随机审核生产输出的1-5%。这是在用户发现问题之前发现质量下降的方法。
  • 用户反馈信号:点赞/点踩、明确纠正、后续澄清请求——这些是大规模下虽然微弱但真实的质量信号。
  • 每次部署的自动化回归测试:在更改提示模板或更新到新模型版本之前,运行完整的评估套件,并要求达到最低质量阈值。

关于LLM评估的不适真相

没有任何指标组合能明确告诉你你的LLM应用运行良好。基于参考的指标会遗漏语义正确的转述。LLM评判者存在偏见。人工评估既昂贵又缓慢。特定任务的指标只覆盖了你想到要测试的内容。

在生产环境中构建可靠LLM应用的团队会综合使用所有这些方法——并且对每种方法都保持健康的怀疑态度。他们大量投入日志记录,以便从生产数据中学习,他们有明确的质量阈值,违反时会阻止部署,他们将评估视为持续实践而非发布前检查清单。

目标不是完美的评估。而是足够好的评估,能在用户发现问题之前发现回归,并生成改进系统所需的反馈循环。

关键要点

  • 没有单一的评估指标是足够的。对于有基准真相的结构化任务,使用基于参考的指标;对于定性评估,使用LLM作为评判者;对于代码和结构化输出,使用特定任务的自动化测试。
  • LLM作为评判者与人类判断高度相关,但有记录的偏见——通过位置交换测试和与人类标签对比的评判校准来缓解。
  • RAGAS为RAG系统提供了标准化的评估维度(忠实度、相关性、召回率、精确度),这些指标难以手动测量。
  • PromptFoo将LLM评估集成到CI管道中,使用声明式测试配置和多模型比较。
  • 生产监控——日志记录、抽样、用户反馈——不是可选的。评估集无法捕获生产输入的完整分布。

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed