如何为 AI Agent 构建自托管评估流程
用真实追踪记录构建黄金测试集,先运行便宜的确定性检查,再用 LLM 评审,并按提交记录通过率,几百行 Python 和一个 SQLite 文件即可完成。
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 导出一段时间内的根观察记录。该 API 使用基本身份验证:公钥作为用户名,密钥作为密码。
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,因此应删除客户姓名以及不属于您的订单号。
先执行确定性检查,因为它们无需成本
对于存在明确正确答案的内容,直接使用断言。无需调用模型,也没有成本或歧义。确定性检查可以捕获结构性回归,而结构性回归会破坏 agent 周围的系统:JSON 无法解析、工具从未被调用、禁用短语再次出现,或答案没有引用任何来源。
只有一个函数需要了解 agent。测试工具中的其他内容都应保持通用。
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将工具调用预算保留在该列表中。某个 agent 今天用 3 次调用解决问题,明天却需要 11 次调用,即使最终答案正确,也说明它发生了回归,因为每次调用都会产生费用。
LLM 作为评判器,以及它出错的四种方式
通过断言的结果还需要一个能够阅读答案的评分器。LLM 评判器会发起第二次模型调用:它接收问题、代理的回答和一项标准,然后返回判定。这是评估“回答是否回应了用户的问题”的唯一实用方法。
以下四条规则可确保评判器可用:
- 使用二元判定,不要使用 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,请交换两者顺序后再次运行。如果交换顺序后判定发生变化,说明对于该评分标准,成对比较还不可靠。
评分标准漂移。模糊的标准会产生迎合性的评判器。“回答是否有帮助”几乎什么都能判定为通过。“回答是否以美元说明退款金额”则只会让符合预期的内容通过。逐一重写每项标准,直到标准明确指出要检查的事实。
有一项措施可以应对这四种问题。保留 30 个由人工标注的案例,并在每次更换评判器模型或评判器提示词后,都将评判结果与人工标签进行比较。如果评判器在 10 个案例中有超过 1 个与你的判断不一致,请先修正规则,再信任它生成的任何通过率。评判器也是代码,因此应像代码一样进行版本管理和审查。
低成本评估,必要时升级到前沿模型
每次提交都用最昂贵的模型评估所有案例,会让评估费用超过被测试的代理。按价格排列评估器,答案明确后立即停止。
The data behind this chart
[
{
"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 可将输入和输出价格都降低一半,以换取异步交付。这对应图表中的第一行。每次调用中的 rubric 和指令逐字节相同,因此适合使用 prompt caching:缓存读取费用为基础输入价格的十分之一,5 分钟缓存写入费用为基础输入价格的 1.25 倍,因此缓存只需命中一次就能收回成本。这些是截至 August 2026 的 Anthropic 标价。Sonnet 5 的 introductory pricing 持续到 31 August 2026,因此第三根柱状条会在该日期后升高。
评估梯度如下:
- 对每个案例执行确定性检查。不产生 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"]这种方法会用部分评估准确性换取成本降低,因此应测量这种取舍,而不是直接假设结果。每月一次,使用严格评估器评估整个测试集,并比较两列结果。如果两者在超过少数几个案例上不一致,说明 rubric 对小型模型不够明确。应修改的是 rubric。控制代理自身的开支是另一项工作,详见 VPS 上 AI 代理的成本控制。
长期跟踪自有系统中的通过率
无法关联到某个提交的通过率,只是一种感觉。每次运行中的每个用例保存一行,并将提交和模型写入该行。
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针对可能导致代理出错的变更运行测试套件,包括提示词编辑、模型变更和工具变更,而不是仓库中的每次提交。使用 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 服务和计时器 会针对已部署的提示词运行完整测试集,从而捕获来自仓库外部的变更,例如托管工具的行为发生变化。
人工抽查,而不是穷尽审查
评审器需要根据人工标注进行校准,因此必须有人生成这些标注。每周阅读一组样本:包括评审器判定失败的所有案例,再加上随机抽取的十个通过案例。随机抽取的通过案例更重要,因为如果评审器已经悄悄开始放过错误答案,那么仅根据自身判定结果构建的任何仪表板都会显示它表现完美。
每周审核十五个案例,每个案例用时三分钟,总计45分钟。这样既能为您和评审器意见不一致的情况修正规则,也能为此前无人想到的失败类型增加新案例。将人工判定写入同一张表,并将 graded_by 设置为 human,这样评审器与人工之间的一致性就能通过查询获得,而不必依赖记忆。
评测框架本身会出现哪些问题
首次完整运行中的 anthropic.RateLimitError。 一次性并发运行 60 个用例会超出您所在层级的请求数或 token 限制。将并发数限制为 4 个 worker,并将夜间运行迁移到 Batch API。
来自评判模型的 json.JSONDecodeError: Expecting value: line 1 column 1 (char 0)。 模型返回了说明性文本,或将 JSON 包在代码块中。重试 1 次,然后将该用例记录为错误。解析失败绝不能算作通过,因为将错误转换为通过的测试套件会逐渐接近 100%,而 agent 的表现却会变差。
不稳定用例。 相同输入在一次运行中通过,在下一次运行中失败,因为 agent 会对输出进行采样。将不稳定用例运行 3 次,并记录通过比例,不要删除该用例。一个用例在 3 次运行中通过 2 次,说明存在真实的稳健性问题,客户也会发现这个问题。
基准集逐渐失真。 有人修改预期答案,只为让测试套件变绿。审查指向 evals/cases.jsonl 的差异时,应像审查 agent 的差异一样仔细,因为该文件是您对正确结果的书面定义。
永远不会失败的测试套件。 通过率连续 1 个月保持 100%,说明该测试集已停止反映产品情况。提取最近的 10 条跟踪记录,找出 agent 处理不当的记录,并将其加入测试集。
FAQ
AI agent 评估集需要多少个案例?
从 40 到 80 个案例开始,并根据真实故障逐步扩充。少于约 20 个案例时,一次不稳定的结果就会让通过率波动 5 个百分点,因此通过率不再具有参考价值。超过几百个案例后,每次运行都会消耗实际的资金和时间,而新增案例带来的覆盖范围很小。真正重要的不是案例数量,而是已知生产环境故障类型中,至少在评估集中出现过一次的比例。
可以信任 LLM 评估器为我的 agent 评分吗?
只有在使用您自己的标注对其进行测量后才可以。保留 30 个由人工评分的案例,并在更换评估模型或评估提示词时,使用这些案例对评估器重新评分。评估器可能存在长度偏差:填充内容后的答案更容易通过;也可能存在自偏好:来自其自身模型系列的输出会获得更宽松的评分。这两种偏差都可以测试:为失败答案添加填充内容后重新评估,或使用其他模型系列的评估器为相同答案评分。如果评估器与您的标注在 10 个案例中有超过 1 个不一致,说明评分标准过于模糊,无法使用。
应该使用哪个模型为评估评分?
先使用低成本模型,必要时再升级。确定性断言不产生费用,因此应先对每个案例运行。小型模型处理明确通过的案例。只有失败案例和低置信度判定才交给前沿模型。在 2026 年 8 月的公开价格下,使用 Claude Haiku 4.5 为 1,000 个案例评分的成本约为 1.80 美元,使用 Claude Opus 5 的成本约为 9.00 美元。由于评估运行是异步的,Batch API 会将这两项成本分别降低一半。
评估能取代生产监控吗?
不能,因为它们回答的问题不同。评估套件用于判断即将发布的变更是否让一组固定案例变好或变差。跟踪和监控用于了解真实用户当前遇到的问题,其中也包括评估集中没有覆盖的输入。两者相互补充:跟踪数据提供新案例,评估套件则判断修复是否确实生效。