SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

如何自托管 AI Agent 评测并跟踪通过率

用真实追踪记录构建基准集,先运行低成本确定性检查,再用 LLM 评审,并按提交记录通过率,例如从 60 个用例通过 58 个降至 51 个。

什么是自托管的 AI agent 评测

自托管的 AI agent 评测包含 4 项内容,全部保存在您自己的代码仓库中:保存测试用例的文件、运行 agent 的脚本、为每个答案评分的一组检查项,以及可查询的结果表。这些内容都不需要依赖供应商。整个流程只需几百行 Python 代码和一个 SQLite 文件。

演示中的 agent 能正常工作,是因为您亲自选择了 5 个输入。到了第 2 周,它可能因为提示词中的一行发生变化、模型发生变化,或工具描述发生变化而失败,而原有测量没有覆盖这些变化。评测流程可以将“感觉现在更差了”转化为“在提交 4f1c9ab 上,通过率从 60 个用例中的 58 个降至 60 个中的 51 个”。

该流程包含 4 个步骤,本指南每个步骤对应一个章节:收集真实运行轨迹,将有价值的轨迹整理为测试用例,在每次变更后对所有用例进行评分,并将通过率与生成该结果的提交一起保存。无论 agent 运行在哪种环境中,同一流程都适用;值得运行的自托管 agent 框架主要区别在于它们会免费提供多少运行轨迹信息。

代理为什么在第二周失效

代理由提示词、模型、工具定义以及运行时检索到的上下文组成。这四项都可以在不修改应用代码的情况下发生变化,因此常规代码审查通常看不出问题。

最常见的原因是修改了提示词。你增加一句话来避免回复无礼。这句话会改变此前未重新测试的输入的行为,跟踪记录会清楚显示这一点:上周针对同一问题的跟踪记录包含 create_refund 工具调用,本周则没有,回复也变成了礼貌的道歉。整个过程没有报错,因此不会触发告警。

第二个原因是模型。每次运行都记录发送的完整模型字符串,claude-haiku-4-5-20251001 不要只记录你凭记忆使用的简写,因为只有在模型名称位于对应记录中时,才能诊断模型切换当天通过率下降的问题。

第三个原因是工具。重新措辞工具描述会改变模型决定调用工具的时机。如果工具通过 在 VPS 上运行的 MCP 服务器提供,架构定义位于另一个进程中,因此它可能在你不知情的情况下发生变化,而你的代码仓库中完全没有差异。第四个原因是检索:同一个问题命中了夜间重建的索引,答案随之采用了新文档。

根据已经收集的追踪记录构建基准集

不要凭空设计评估用例。直接从网络流量中提取。如果您已经为代理运行 自托管的 Langfuse 追踪,每个请求都会保存输入、工具调用和输出,这正是用例所需的原始材料。

通过公共 API 导出一段时间内的 root 观测记录。该 API 使用基本身份验证,以 public key 作为用户名,以 secret key 作为密码。

export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
  "$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
  | jq '.data[0]'

在编写任何解析代码前,先读取一条记录。数据行位于 data 下,但保存问题和回复的字段名取决于代理如何为 span 添加检测信息。因此,应根据实际返回内容进行映射,不要按照预期字段名映射。然后手动编写用例,每行一个 JSON 对象,保存到 evals/cases.jsonl:

{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}

以下 5 条规则可以确保该集合具有运行价值:

  • 一开始准备 40 到 80 个用例即可。少于 20 个时,一个不稳定的用例就会使通过率变化 5 个百分点;无故波动的数字最终会被忽略。
  • 每个修复的生产环境缺陷,都应在修复当天转化为一个用例。坚持这样做,集合才能沿着正确方向增长。
  • 每个用例只验证一种行为。同时检查退款金额和语气的用例失败时,您无法判断具体问题。
  • id 永远不变,因为系统通过该 id 对比本次运行与上个月的运行结果。
  • 提交前先进行脱敏。该文件会提交到 git,因此应删除客户姓名以及不属于您的订单号。

优先使用确定性检查进行评分,因为它们无需成本

凡是存在明确正确答案的内容,都直接使用断言。无需调用模型、无需成本,也不存在歧义。确定性检查可以捕获结构性回归,而这类问题会破坏代理周围的系统:JSON 无法解析、工具根本未被调用、被禁止的短语再次出现,或答案没有引用任何来源。

只有一个函数需要了解您的代理。测试框架中的其他部分都应保持通用。

import json, os, urllib.request

def run_agent(case):
    req = urllib.request.Request(
        os.environ["AGENT_URL"],
        data=json.dumps({"input": case["input"]}).encode(),
        headers={"content-type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.load(resp)


def deterministic(case, result):
    text = result.get("output", "")
    called = [c["name"] for c in result.get("tool_calls", [])]
    failures = []
    for tool in case.get("must_call", []):
        if tool not in called:
            failures.append(f"tool not called: {tool}")
    for phrase in case.get("must_not_include", []):
        if phrase.lower() in text.lower():
            failures.append(f"forbidden phrase: {phrase}")
    if len(called) > case.get("max_tool_calls", 12):
        failures.append(f"too many tool calls: {len(called)}")
    return failures

将工具调用预算保留在该列表中。代理今天通过 3 次调用解决问题,明天却需要 11 次调用,这就是回归,即使最终答案正确,因为每次调用都需要付费。

LLM 作为评审,以及它出错的 4 种方式

通过断言检查的结果仍需要一个能够阅读内容的评分器。LLM 评审就是再次调用模型:它接收问题、代理的回答和一项标准,然后返回判定。这是评估“回答是否回应了用户的问题”唯一实用的方法。

以下 4 条规则可以让评审真正可用:

  • 只返回二元判定,不要使用 1 到 10 的评分。评分制会让几乎所有结果都落在 7 或 8,分数不会变化,因此你无法从中获得信息。
  • 每次调用只使用一项标准。只询问退款金额或语气,不要同时询问两者。
  • 如果案例有预期答案,就始终提供给评审。根据参考答案评分,比抽象地评分容易得多。
  • 强制规定输出格式,并严格解析结果。
from anthropic import Anthropic

client = Anthropic()  # reads ANTHROPIC_API_KEY from the environment


def judge_prompt(case, output):
    return (
        "You grade one answer against one criterion.\n"
        "Reply with JSON only, in this exact shape:\n"
        '{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
        f"Criterion: {case['rubric']}\n"
        f"Question: {case['input']}\n"
        f"Answer: {output}\n"
        "Length is not a criterion. Judge only the criterion above."
    )


def judge(case, output, model):
    msg = client.messages.create(
        model=model,
        max_tokens=200,
        messages=[{"role": "user", "content": judge_prompt(case, output)}],
    )
    return json.loads(msg.content[0].text)

下面介绍失败模式。每种模式都有一个你今天下午就能运行的测试。运行这些测试很重要,因为未经检查的评审会产生看似精确、实际毫无意义的数字。

长度偏差。 较长的回答更容易通过测试。测试方法:选出 10 个被评审判定为失败的回答,分别加入两段自信但没有新增事实的填充内容,然后再次评分。如果某个判定因此从失败变为通过,就是长度偏差,需要修正评分标准。

自我偏好。 评审通常会更宽松地评价来自同一模型系列的输出,而不是来自其他模型系列的输出。测试方法:使用两个不同模型系列的评审,对相同的 30 个回答进行评分,然后逐个比较判定结果。对于不一致的案例,直接人工阅读。

位置偏差。 如果使用评审比较两个回答 A 和 B,请交换它们的顺序后再次运行。如果判定结果因交换顺序而改变,说明这种成对比较尚不能安全用于该评分标准。

评分标准漂移。 模糊的标准会产生迎合性的评审。“回答是否有帮助”几乎什么都能通过。“回答是否以美元说明退款金额”只会让符合要求的内容通过。重写每项标准,直到它明确指出要检查的事实。

有一项防护措施可以覆盖全部 4 种问题。保留 30 个由人工标注的案例。每次更换评审模型或评审提示词时,都将评审结果与这些人工标签进行比较。如果它在 10 个案例中有超过 1 个案例与你的判断不一致,请先修正评分标准,再信任它生成的任何通过率。评审属于代码,因此应像代码一样进行版本控制和评审。

低价评测,必要时升级到前沿模型

在每次提交中都用最昂贵的模型评测每个案例,会让评测费用超过被测试的 agent。按价格排列评测器,只要结果已经明确就立即停止。

图表Cost to judge 1,000 eval cases, list prices, October 2026
此图表背后的数据
[
  {
    "label": "Haiku 4.5, Batch API",
    "usd_per_1000_judge_calls": "0.90"
  },
  {
    "label": "Haiku 4.5",
    "usd_per_1000_judge_calls": "1.80"
  },
  {
    "label": "Sonnet 5",
    "usd_per_1000_judge_calls": "3.60"
  },
  {
    "label": "Opus 5",
    "usd_per_1000_judge_calls": "9.00"
  }
]

这些数字假设每次评测调用包含约 1,200 个输入 token 和 120 个输出 token。对于一个问题、一个答案和一个评测标准,这是合理的规模。使用 Claude Haiku 4.5 评测 1,000 个案例的费用为 1.80 美元,使用 Claude Opus 5 的费用为 9.00 美元。单次差额看起来很小,但累计后就不同了。一个包含 60 个案例的集合,每次提交都进行评测,每周提交 40 次,那么在任何人运行 nightly job 之前,每周就会产生 2,400 次评测调用。

评测工作可以直接使用一项折扣。评测运行不是交互式任务,因此 Batch API 通过异步交付将输入和输出价格都减半,这也是图表中的第一行。Prompt caching 对这种规模的评测器没有帮助。它的倍率确实有效(5 分钟缓存写入费用是基础输入价格的 1.25 倍,缓存读取费用是基础输入价格的十分之一),但提示中要缓存的部分必须达到最低长度:Claude Haiku 4.5 为 4,096 个 token,Claude Sonnet 5 为 1,024 个 token,Claude Opus 5 为 512 个 token。一次包含 1,200 个 token 的评测调用远低于 Haiku 的最低要求。API 会在不使用缓存的情况下处理更短的提示,也不会返回错误,因此不会发生缓存写入、缓存命中或费用节省。只有在 Opus 5 中,共享前缀至少达到 512 个 token 才符合要求;而 judge_prompt 中的固定指令远短于此长度,因为评测标准会随每个案例变化。以上是 Anthropic 截至 2026 年 10 月的公开价格。

评测阶梯如下:

  • 每个案例都运行确定性检查。不产生任何 API 费用。
  • 对通过这些检查的案例使用小型模型评测器。
  • 仅在小型模型判定失败,或判定通过但置信度较低时,使用前沿模型评测器。
  • 每周对少量样本进行人工复核。
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"


def grade(case, result):
    hard = deterministic(case, result)
    if hard:
        return False, "deterministic", "; ".join(hard)
    first = judge(case, result["output"], CHEAP)
    if first["verdict"] == "pass" and first["confidence"] == "high":
        return True, CHEAP, first["reason"]
    second = judge(case, result["output"], STRICT)
    return second["verdict"] == "pass", STRICT, second["reason"]

截至 2026 年 10 月,Anthropic 将这两个评测器都列为仍可用的 legacy 模型。其弃用页面显示,Claude Haiku 4.5 的退役日期不早于 2026 年 10 月 15 日。因此,长期依赖低价评测器前应先查看该页面,并将评测模型替换视为普通变更:先重新运行 30 个手工标注的案例。

这种做法会以部分评测准确率换取成本降低,因此应测量这种取舍,而不是凭假设判断。每月使用严格评测器评测整个集合,并比较两列结果。如果两者在超过少数几个案例上不一致,说明评测标准对小型模型来说过于宽松,此时应修正评测标准。控制 agent 自身的费用是另一项工作,详见 VPS 上 AI agent 的费用控制。

持续跟踪自有系统中的通过率

无法与某个提交关联的通过率只是主观感受。每次运行中的每个用例存储一行,并将提交和模型写入该行。

CREATE TABLE IF NOT EXISTS results (
  run_id      TEXT NOT NULL,
  ran_at      TEXT NOT NULL,
  git_sha     TEXT NOT NULL,
  agent_model TEXT NOT NULL,
  case_id     TEXT NOT NULL,
  passed      INTEGER NOT NULL,
  graded_by   TEXT NOT NULL,
  reason      TEXT
);
SELECT run_id, git_sha, agent_model,
       count(*) AS cases,
       round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;

使用 sqlite3 evals/results.db < evals/schema.sql 加载架构,然后使用 sqlite3 -box evals/results.db < evals/passrate.sql 读取趋势。按每天运行一次、每次运行 60 个用例计算,一年的数据约为 22,000 行,因此存储不会发展成独立项目。在 VPS 上将 SQLite 用于生产环境介绍了在此文件需要在多台机器之间共享时才开始重要的设置。

运行器会为人员打印相同的信息:

run 2026-08-05T09:14:22Z  sha 4f1c9ab  model claude-sonnet-5  58/60 pass (96.7%)
FAIL refund-double-charge  deterministic: tool not called: create_refund
FAIL pto-policy-question   judge(opus): reply gives no dollar amount

针对可能导致 agent 出现问题的变更运行测试套件,也就是提示词编辑、模型变更和工具变更,而不是仓库中的每次提交。使用 pre-push hook 可以覆盖快速子集:

cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-push

完整运行速度较慢,应按计划执行。VPS 上的每夜 systemd 服务和计时器会针对已部署的提示词运行完整测试集。这样可以捕获仓库外部产生的变更,例如托管工具的行为发生变化。

人工审核,抽样而非穷举

评审器需要根据人工标注进行校准,因此必须有人生成这些标注。每周读取一批样本:包括评审器判定失败的所有案例,再加上随机抽取的 10 个通过案例。随机通过案例是其中更重要的一半,因为如果评审器已经悄悄开始放过错误答案,那么在基于自身判定结果构建的任何仪表板上,它看起来都会表现完美。

每周审核 15 个案例,每个案例用时 3 分钟,共需 45 分钟。这样可以在您与评审器意见不一致时修正评分标准,并为此前未考虑过的失败类型补充新案例。将人工判定结果写入同一个表,并将 graded_by 设置为 human,这样评审器与人工之间的一致性就可以通过查询获取,而不必依赖记忆。

评估测试框架本身会出现的问题

首次完整运行中的 anthropic.RateLimitError。 六十个用例同时展开会超过您所在层级的请求数或令牌限制。将并发数限制为 4 个工作进程,并将夜间运行迁移到 Batch API。

来自评判器的 json.JSONDecodeError: Expecting value: line 1 column 1 (char 0)。 模型返回了散文格式的内容,或将 JSON 包在代码围栏中。重试 1 次,然后将该用例记录为错误。解析失败绝不能算作通过,因为会将错误转换为通过的测试套件,会在代理质量下降的同时逐渐逼近 100%。

不稳定的用例。 同一输入在一次运行中通过、下一次运行中失败,是因为代理会对输出进行采样。将不稳定用例运行 3 次,并记录通过比例,而不是删除该用例。一个用例在 3 次运行中通过 2 次,仍然是实际的稳健性缺陷,客户也会发现它。

基准集腐化。 有人修改预期答案,以便让测试套件变绿。应像仔细审查代理代码的差异一样,仔细审查指向 evals/cases.jsonl 的差异,因为该文件是您对“正确”的书面定义。

永不失败的测试套件。 如果通过率连续 1 个月保持在 100%,说明该集合已停止反映产品状态。提取最近的 10 条跟踪记录,找出代理处理不当的记录,并将其加入测试集。然后故意破坏某项功能,确认运行结果变为红色。这就是 变异测试适用于测试套件 的检查,也是确认测试集仍然有效的唯一方法。

FAQ

AI agent 的评测集合需要多少个案例?

先从 40 到 80 个案例开始,再根据真实故障逐步扩充。少于约 20 个案例时,一个不稳定的结果就会让通过率波动 5 个百分点,因此该数字不再具有足够信息量。超过几百个案例后,每次运行都会消耗实际的费用和时间,而新增案例带来的覆盖率很低。重要的不是案例数量,而是已知的生产环境故障类型中,有多少至少在集合中出现过一次。

我可以相信 LLM 评测器来评测我的 agent 吗?

只有在根据你自己的标注完成测量后才可以。保留 30 个由你手工评测的案例,并在更换评测模型或评测提示后,用这些案例评估评测器。评测器存在长度偏差:填充内容更多的答案更容易通过;也存在自偏好:来自其自身模型家族的输出通常会得到更宽松的评分。这两种偏差都可以测试:给失败答案增加填充内容后重新评测,或者使用其他模型家族的评测器评测相同答案。如果评测器与人工标注在十分之一以上的案例中不一致,说明评测标准过于模糊,无法使用。

应该使用哪个模型评测评测集?

先使用低价模型评测,必要时再升级。确定性断言不产生费用,因此先对每个案例运行。小型模型负责处理明确通过的案例。只有失败案例和低置信度判定才交给前沿模型。在 2026 年 10 月的公开价格下,使用 Claude Haiku 4.5 评测 1,000 个案例的费用约为 1.80 美元,使用 Claude Opus 5 的费用约为 9.00 美元;由于评测运行是异步的,Batch API 会将这两项费用分别减半。

评测能替代生产监控吗?

不能,因为两者回答的问题不同。评测套件告诉你,即将发布的变更是否让一组固定案例变好或变差。Tracing 和监控告诉你真实用户当前遇到的问题,包括评测案例未覆盖的输入。两者会相互提供信息:traces 提供新的案例,评测套件则判断修复是否真正生效。

这篇文章对您有帮助吗?