暗无天日

=============>DarkSun的个人博客

AI 工程中最该投资的一件事:评估管道

原文链接:AI Engineering for Developers

为什么评估被跳过

先对齐术语:eval 是 evaluation(评估)的缩写。eval 管道是一套自动化流程,用来检测你的 AI 系统(LLM、RAG、Agent 等)的输出质量有没有退化。eval 数据集就是喂给这条管道的测试输入,类似于单元测试的 fixture。

传统软件工程有类型系统、单元测试、集成测试帮你守住质量底线。但在AI 工程里这些手段大部分都会失效:你能测试 prompt 模板渲染是否正确,能测试 API 调用是否返回 200,但无法用确定性手段判断"这个摘要好不好"或者"这个 SQL 生成对不对"。

结果就是评估成了 AI 工程中被跳过最多的步骤。原因不难理解:

  1. 搭 eval 管道不像写单元测试那样有即时反馈。你今天加了 eval,下个月模型升级时它才会帮你挡住回归
  2. eval 数据集难做。需要50 到 500 个有代表性的输入,每个案例还要有标准答案或评分量规,这比写 test case 麻烦得多
  3. "我的 demo 看起来效果不错"的直觉会给你虚假的信心

但跳过 eval 的代价是:每次模型变更、每次 prompt 变更、每次检索调整,你都在发布回归。你可能三个月后才发现用户已经放弃了你的产品。

最小可行 eval pipeline

一个能用的 eval 管道只需要四个组件:

  1. 数据集 :50 到 500 个有代表性的输入。覆盖你的真实使用场景,不只是容易的 case
  2. 标准 :每个输入要么有标准答案(ground truth),要么有评分量规(rubric)
  3. 执行器 :一个函数,接收输入,端到端运行你的系统,产出输出
  4. 打分器 :程序化指标(精确率、召回率、编辑距离)或 LLM-as-judge

加上 CI 集成:每个 PR 跑一遍 eval,报告指标变化。指标退化超过阈值就阻止合并。

用 DeepEval、RAGAS、Braintrust、LangSmith 或者自己写代码,一个下午能搭好评估管道的代码框架。但难的不是管道代码,是数据集。

eval 数据集的坑

一个常见错误是把开发过程中用过的 prompt 直接当 eval 数据。这些 prompt 往往偏向容易的 case,因为你下意识地在"能跑通"的例子上反复迭代。真实的用户输入比你想象的更狂野。

另一个坑是不做版本管理。eval 数据集应该和代码一样在 git 里有版本,每次变更走 PR 审查。把 eval 数据放在数据库里"方便热更新"是个反模式,你会失去可追溯性。

LLM-as-judge 实操

当你没有标准答案时(摘要质量好不好、回答有没有帮助),LLM-as-judge 是目前最实用的方案:用一个强大的模型按评分量规给另一个模型的输出打分。

下面的代码演示了完整的 eval 循环:用 DeepSeek 生成回答,用 GLM(智谱)做裁判评分。两个模型来自不同家族,避免自偏好偏见。你可以在本地跑这个例子,亲身感受 LLM-as-judge 的效果。

import json
import re
from openai import OpenAI

# 生成模型:DeepSeek
generator = OpenAI(
    api_key="your-deepseek-api-key",
    base_url="https://api.deepseek.com"
)

# 裁判模型:GLM(智谱),用不同模型族避免自偏好偏见
judge_client = OpenAI(
    api_key="your-zhipuai-api-key",
    base_url="https://open.bigmodel.cn/api/paas/v4"
)

QUESTION = "用一句话解释什么是 RAG(检索增强生成)。"
RUBRIC = """
按以下三个维度评分,每项 1-5 分:
- 准确性:技术描述是否正确
- 简洁性:是否真的做到了"一句话"
- 可理解性:没有 AI 背景的程序员能否看懂
只返回 JSON,不要加 markdown 代码块:{"accuracy": x, "conciseness": x, "clarity": x, "reason": "..."}
"""

def generate_answer(question: str) -> str:
    """让 DeepSeek 生成回答"""
    resp = generator.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content": question}],
        temperature=0.3,
    )
    return resp.choices[0].message.content

def judge_answer(question: str, answer: str) -> dict:
    """用 GLM 做裁判,给回答打分"""
    resp = judge_client.chat.completions.create(
        model="glm-4-flash",
        messages=[
            {"role": "system", "content": f"你是一个严格的评分裁判。{RUBRIC}"},
            {"role": "user", "content": f"问题:{question}\n\n回答:{answer}\n\n请评分。"},
        ],
        temperature=0.0,
    )
    raw = resp.choices[0].message.content
    # LLM 可能返回 markdown 代码块包裹的 JSON,需要提取
    match = re.search(r"\{.*\}", raw, re.DOTALL)
    return json.loads(match.group()) if match else json.loads(raw)

# 运行一次 eval
answer = generate_answer(QUESTION)
print(f"回答:{answer}\n")

score = judge_answer(QUESTION, answer)
print(f"评分:{json.dumps(score, ensure_ascii=False, indent=2)}")

运行一次的输出大概是这样(每次会有差异):

回答:RAG(检索增强生成)是一种让大语言模型在生成回答前,先从外部知识库中检索相关信息作为参考,从而提升答案准确性和时效性的技术方法。

评分:
{
  "accuracy": 4,
  "conciseness": 4,
  "clarity": 4,
  "reason": "回答准确描述了RAG的基本概念,但可以进一步精简。"
}

对比一下:如果让 DeepSeek 自己当裁判评自己的回答,同样的输出会拿到 5/5/5 满分。用不同模型族的 GLM 当裁判,分数更客观。这就是前面说的自偏好偏见。

LLM-as-judge 的偏见

LLM-as-judge 出奇地好用,但有已知的系统性偏见。裁判模型有几个倾向需要你知道:

  1. 自偏好 :裁判倾向于给自己同族的模型打更高分。就像上面说的,用 DeepSeek 当裁判评 DeepSeek 的输出,分数会偏高。缓解方法是:裁判和生成器用不同模型族
  2. 长度偏见 :裁判倾向于给更长的回答打更高分。一个冗长但信息密度低的回答可能比一个精炼准确的回答得分更高
  3. 格式偏见 :结构化输出(有标题、有列表)比纯文本更容易得高分,即使内容质量一样

缓解方式是使用结构化评分量规:把评分拆成多个具体维度(准确性、简洁性、完整性等),每个维度有独立的 1-5 分标准,而不是笼统地问"给 1 到 5 分"。并且定期抽样 1 到 5% 的裁判决策做人工审查。

比绝对评分更靠谱的方法:配对比较

让裁判在两个输出中选更好的,比让裁判给绝对分数更可靠。原因不难理解:比较两个东西的难度远低于从零开始打分。配对胜负可以转化为 Elo 排名(国际象棋用的积分制:赢涨分、输降分,对手越强涨分越多),这就是 Chatbot Arena 的运作方式。

如果你的场景是比较不同 prompt 版本或不同模型的表现,优先用配对比较而非绝对评分。

评估什么:优先级排序

原文给出了一个评估标准的优先级,从高到低:

  1. 领域能力 :模型知道你的领域吗?
  2. 生成质量 :输出正确、流畅、格式好吗?
  3. 指令遵循 :它做了你要求的事吗?
  4. 成本和延迟 :每请求多少秒、多少钱

这个顺序反映的是生产中实际出问题的模式:

  • 不了解你领域的模型会产生 自信地错误的答案 ,不管格式多漂亮
  • 有领域知识但生成差的模型产生 用户无法提取的知识
  • 忽略指令的模型不管其他品质如何都 不可靠
  • 便宜的错误答案 毫无意义

指令遵循被严重低估

很多团队在模型选型时只看领域 benchmark 分数,上线后才发现模型不听话:你让它不要加评论它偏要加,你让它用 JSON 格式它中途换成纯文本,你限制 500 字它写了 2000 字。然后花几周时间在 prompt 里各种加约束条件来弥补。指令遵循需要显式测试:给模型清晰的格式指令,在各种难度的输入上检查它是否遵守,而不只是那些简单的、模型天然就能处理好的输入。

成本和延迟要端到端测

一个便宜的模型如果需要重试两次才能得到正确结果,总成本可能比一个一次就对的贵模型更高。所以测量成本时不能只看模型标价(比如"gpt-4o 每百万 token $5"),要把重试次数也算进去,在真实的用户请求模式下端到端地算总账。

函数调用需要独立的测试套件

函数调用(function calling / tool calling)是 AI 工程中最常见的结构化输出场景。大多数团队只检查"模型调了哪个函数",不检查"参数传得对不对"。比如模型调了 search_user 函数,这只说明它选对了函数;但如果你要的是整数 ID,它传了字符串 ="abc"=,这就是错的,而你的测试可能仍然通过。

函数调用更像一个结构化输出问题而非语言问题:模型需要产生一个 JSON 对象,有正确的函数名、正确类型的参数、正确的值。常见的失败模式有四种,每种需要不同的测试用例:

  1. 参数名错误 :拼写错误或语义错误(比如把 user_id 写成 userId
  2. 参数类型错误 :传了字符串但 API 要整数
  3. 幻觉可选参数 :模型编造了 API 文档中不存在的可选参数
  4. 调用时机错误 :该调用时不调用,不该调用时调用了

一个只检查"它调了什么"的 eval 会遗漏这些。你的函数调用测试套件需要:

  • 正确调用的正面案例
  • 每种参数错误模式的负面案例
  • 应该调用但不调用的遗漏案例
  • 不应该调用但可能被触发的误调用案例

Agent 的评估维度

  1. 任务完成率 :Agent 完成了用户要求的事吗?这是最基础但也最难衡量的
  2. 工具调用准确率 :它选了正确的工具吗?参数对吗?这可以复用上一节的函数调用测试方法
  3. 轨迹质量 :它走的路径合理吗?有没有绕路、循环、无效的中间步骤?一个 3 步能完成的任务走了 15 步,即使最终结果正确,轨迹质量也很差
  4. 每解决任务的成本 :Token 花费、工具调用花费、端到端延迟。一个能解决问题但每次花 5 美元的 agent 在很多场景下不可用

Agent 评估为什么比普通 LLM 评估更难?普通 LLM 的失败是结构性的:输出格式错了、JSON 解析失败、返回了空字符串,这些都有明确的错误信号。Agent 的失败是行为性的:每一步的输出看起来都合理,但整个执行路径是错的。它不会抛异常,而是产生看起来合理但完全错误的结果,或者烧光 token 预算后什么都不返回,或者静默循环直到超时。

没有追踪(trace),你几乎无法调试 agent 失败。LangSmith、Arize、Langfuse 都支持 agent 追踪,能记录每一步的输入输出。在开发中追踪每次运行,在生产中采样追踪。

红队测试是持续过程不是一次性检查

很多团队把红队测试(red teaming)当作上线前的一次性检查。这不对。用户的创造力超出你的想象:一旦 LLM 上线,用户会不断尝试以你没预料到的方式使用它,攻击面会持续扩大。更麻烦的是,每次模型更新都可能让之前已经堵住的攻击路径重新打开(攻击向量:即攻击者利用的具体手段,比如通过 prompt 注入绕过安全限制、通过特殊编码逃逸过滤等)。

红队测试应该是一个持续流程:

  1. 在 CI 中维护一组对抗性测试用例(jailbreak 尝试、prompt 注入、边界值)
  2. 每次有东西穿透了生产防线就把它加进 CI 集
  3. 定期运行新的红队 prompt,覆盖最新的攻击手法

总结:什么时候你的 eval 算够用

作者提供了一个自检清单:

  • [ ] 有一个 50+ 条的 eval 数据集,版本管理在 git 里
  • [ ] 每次 PR 会自动跑 eval 并报告指标变化
  • [ ] 用了 LLM-as-judge 且裁判和生成器不同族
  • [ ] 函数调用有独立测试套件,覆盖四种失败模式
  • [ ] Agent 有追踪,开发环境全覆盖
  • [ ] 红队测试在 CI 中,定期更新

如果以上有超过两项是"没有",你的 AI 功能大概率在不知不觉中变差(发布回归:新版本上线后,某些原来能正常工作的场景反而坏了,而你因为没有评估管道所以没发现)。好消息是,从零搭一个最小可行的 eval 管道只需要一个下午。先从 50 条数据集和一个简单的 LLM-as-judge 打分器开始,以后再逐步完善。

AI : LLM : evaluation : eval-pipeline : LLM-as-judge