评测¶
单元测试可以确认代码是否正确接通,却很难衡量 Agent 的实际表现:回答是否正确、工具是否
选对、表达是否简洁。模型行为本就存在波动,这类问题尤其难测。借助 lovia.eval,你可以用
Case 定义输入和验收条件;验收条件既可以是普通函数,也可以由大模型充当评审。
evaluate() 会执行全部用例,并返回一个可打印、可断言、也可与基线比较的 Report。
from lovia.eval import Case, contains, evaluate, llm_judge, tool_called
cases = [
Case("法国首都是哪里?", checks=[contains("巴黎")]),
Case("23.4 * 91 等于多少?", checks=[tool_called("calculator")]),
Case(
"为一家书店写一句雨天文案",
checks=[llm_judge("不超过 30 个汉字,包含雨天和阅读意象,不使用感叹号")],
samples=4, # 把不确定性量出来,而不是靠重试掩盖
pass_threshold=0.75, # 4 个 sample 至少 3 个通过即通过
),
]
report = await evaluate(agent, cases)
print(report)
assert report.passed
eval: 2/3 cases passed (67%) · 6 samples · 4,812 tokens · 21.4s
✓ 法国首都是哪里? 1/1
✓ 23.4 * 91 等于多少? 1/1
✗ 为一家书店写一句雨天文案 2/4 llm_judge (score 0.55) — 缺少阅读意象
定义评测用例¶
Case 字段 |
默认值 | 含义 |
|---|---|---|
input |
必填 | 字符串,或 list[Message] |
checks |
() |
通过标准(一个 sample 必须全部满足) |
name |
从 input 派生 | 报告标签,也是 compare() 的匹配键 |
samples |
1 |
同一个 case 运行 N 次;把不确定性量出来 |
pass_threshold |
1.0 |
case 判定通过所需的 sample 通过率 |
context |
None |
作为运行 deps 转发 |
output_type / max_turns |
agent 的配置 / 50 |
每个 case 的运行设置 |
model |
Agent 的配置 | 使用另一个模型派生 Agent,并运行当前用例 |
timeout |
None |
每个 sample 的实际耗时上限;timeout 会记为失败 sample,不会让整个套件中断 |
metadata |
{} |
原样携带到结果 |
Case(model=...) 很常用:在线评测可以把某个 case 固定到不同模型;离线评测可以给每个 case
自己的 scripted transcript:
from lovia.testing import ScriptedProvider, call, text
Case(
"2 + 3 等于多少?",
checks=[contains("5"), tool_called("add")],
model=ScriptedProvider([call("add", {"a": 2, "b": 3}), text("2 + 3 = 5")]),
)
设置 model= 时,agent 会按 sample clone。一次性 ScriptedProvider 如果要配合
samples > 1 使用,就只能通过工厂传入(见下)。
编写验收条件¶
任何 (RunResult) -> CheckResult | bool callable 都可以,同步或异步都行。内置匹配器、
LLM judge 和你自己的函数遵循同一套接口:
抛异常的 check 只会让自己失败(异常会作为原因记录),不会让整个套件中断。run_check 会把所有结果
规范化成 CheckResult(name=..., passed=..., score=..., reason=...);需要分数型结果时,也可以自己返回一个。
内置检查:contains(value, ignore_case=False) / not_contains、regex(pattern)、
equals(value)、matches(spec)(结构化输出的递归子集匹配:忽略额外字段,列表长度必须精确;也可传谓词)、
tool_called(name) / tool_not_called(name)、max_turns(n)、max_tokens(n)、no_error()
(运行中没有失败工具调用)。可用 all_of(...)、any_of(...) 和
weighted({check: weight, ...}, threshold=0.7) 组合;weighted 会把子 score(或通过/失败)合成一个
带分数的判断结果。
使用大模型评审¶
llm_judge(rubric, *, model=None, threshold=0.7) 可以评估匹配器难以表达的语义:
从实现上看,它仍是一项验收检查:内部会运行另一个 Agent(output_type=Verdict{score, reasoning},
temperature 0)。裁判模型来自 model= 或 $LOVIA_EVAL_JUDGE_MODEL,不会自动使用被测 agent。
passed = score >= threshold。把 ScriptedProvider 作为 model 传入,裁判也可以离线运行;
这样整套评测就能在 CI 中免费跑。
运行评测¶
report = await evaluate(agent_or_factory, cases, concurrency=4, fail_fast=False,
price=lambda u: u.input_tokens * 3e-6 + u.output_tokens * 15e-6)
- Agent 或工厂(
AgentSourceunion)。零参工厂会按每个 sample调用;当 agent 有状态时 (scripted provider、有状态工具),请传工厂。 - 并发发生在 case 之间(默认 4);同一个 case 的 samples 串行运行;同一个 sample 的 checks 并发运行。
- 错误也会进入评测结果。 样本抛出异常或超时时,会记录对应的
error并标记为失败;其他样本仍会继续执行。fail_fast=True会串行运行 case,并在第一个失败 case 后停止。 price=把 usage 转成成本;报告会显示· $0.0421。
报告与基线¶
Report 里每个 case 对应一个 CaseResult,每个 case 下又有多个 SampleResult
(checks、output、usage、latency、可选 cost、error)。Report.passed、.pass_rate,以及每个 case 的
CaseResult.pass_rate / .pass_at_k(k)(无偏估计)覆盖数值指标;print(report) 会输出上面的摘要。
CI 中可以这样用:
report.save("eval-baseline.json") # 一次,在一个好结果上保存
current = await evaluate(agent, cases)
diff = current.compare(Report.load("eval-baseline.json"))
print(diff) # regressions / improvements / added / removed
assert diff.ok # 为真 ⇔ 没有 regression
compare 返回 Diff,按 case name 匹配(重复名称会报错;输入重复时请手动命名 case)。
improvement 和新增/删除 case 会出现在报告里,但不会让 diff.ok 失败。
注意事项¶
- 裁判成本按
samples × judge-checks × cases增长,每次 judge 评估都是一次模型调用。 只把 judge 用在真正需要语义判断的 case 上;匹配器是免费的。 - 现成
Agent实例会在 samples 间复用,除非设置了model=或传入工厂。无状态 agent 没问题; scripted agent 不适合,第二个 sample 会发现脚本已经耗尽。 samples是度量,不是修复。pass_threshold < 1.0表达的是你能接受的波动;如果某个 case 需要靠重试才通过,那是发现问题,不是噪声。no_error()只看工具错误。 运行本身抛异常时不会进入 checks;它已经是带error的失败 sample。
延伸阅读¶
- 测试:
ScriptedProvider,eval 和 judge 离线模式共用的引擎 - 护栏:输出 check 在运行时的对应物
- 示例:
28_eval.py:完整的离线 scripted 评测套件,带 scripted judge