mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 21:21:59 +00:00
4aa3e6f01f
价格目录里没有助手模型时(真实模型验收的接入就是这样),第一次评测估不出、照实写了;第二次起估算按上一次的每题平均 花费算,而每题平均是「估得出的那部分」除以题数 = 0,开跑对话框写「约 $0」、评测总览写「每题约 $0」,像不花钱。 - eval/stats.summarize:每题平均花费一分都没估出来(有估不出的调用、估得出的部分是 0)时为 None,估出了一部分的照给; - eval/runner.estimate:上一次的调用金额都估不出时照实写估不出(带次数和原因,金额上限只管得住估得出的调用);估出了一部分 的标 at_least、带上另有几次估不出,说明写「实际至少这么多」;老总表里每题平均记成 0、却有估不出的调用的也按估不出算; - 按角色用量里裁判那一份(runner.judge_usage)补上估不出的次数:以前只有金额和次数,裁判模型不在价格目录里时写成 $0; - 评测页:开跑对话框估出一部分时写「至少约 …(每项至少约 …)」,总览的每题平均有估不出的写「每题至少约 …」,一分都没估出来 的不写(金额那一格已是「金额无法估算」)。 providers/usage 的汇总形状不变:cost_usd 是估得出的那部分、unpriced_calls 是估不出的次数,和运行的 usage 是全系统一个口径 (细案 01 做法 3 定的),前端的金额写法(runCostText)一直按这两项一起写「≥」或「金额无法估算」;漏掉的是只读 cost_usd 去算 平均、做估算的这几处。模块说明里把这条写明了。 测试:test_eval_stats.py::test_the_per_case_cost_is_not_zero_when_calls_could_not_be_priced、 test_eval_runner.py::test_the_estimate_says_it_cannot_price_instead_of_zero(全估不出、估出一部分、目录价是 0 的才是真 不花钱)、test_eval_runner.py::test_the_judge_role_keeps_its_unpriced_calls。去掉修复回跑三条都失败。
95 lines
5.7 KiB
Python
95 lines
5.7 KiB
Python
"""评测的统计(app/eval/stats.py;细案 01 做法 11,评测-D10)。纯计算,和手算对照。
|
||
|
||
- Wilson 95% 区间、精确 McNemar 双侧 p 值、P50 / P90;
|
||
- 总表:准确率和可追溯准确率分开报(分母只算能判的)、作答路径、出具档位、复核、look、按角色的用量、引用率、逐题 E1、
|
||
重复 k 次的题给通过率。
|
||
和上一次比、A/B 配对表、选基准在 test_eval_compare.py。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
import app.db.models # noqa: F401 - conftest 按已导入的模型建表
|
||
from app.eval import compare, stats
|
||
|
||
|
||
def test_wilson_and_mcnemar_match_hand_computation():
|
||
assert stats.wilson(8, 10) == (0.4902, 0.9433)
|
||
assert stats.wilson(0, 0) is None
|
||
low, high = stats.wilson(0, 5)
|
||
assert low == 0.0 and 0.43 < high < 0.44
|
||
assert stats.mcnemar_exact(1, 0) == 1.0
|
||
assert stats.mcnemar_exact(6, 0) == 0.03125
|
||
assert stats.mcnemar_exact(7, 2) == 0.179688
|
||
assert stats.mcnemar_exact(0, 0) == 1.0
|
||
assert stats.percentile([5, 1, 3, 2, 4], 50) == 3 and stats.percentile([5, 1, 3, 2, 4], 90) == 5
|
||
assert stats.percentile([], 50) is None
|
||
|
||
|
||
def _row(case: str, verdict: str | None, *, path: str = "ran", status: str = "ok", repeat: int = 0, **extra) -> dict:
|
||
scores = [{"id": "e1", "kind": "scalar", "required": True,
|
||
"outcome": {"pass": "traced", "fail": "wrong"}.get(verdict or "", "ungradable")}]
|
||
return {"case_id": case, "case_seq": int(case[-1]) if case[-1].isdigit() else 1, "case_hash": f"h-{case}",
|
||
"verdict": verdict, "path": path, "status": status, "repeat": repeat, "scores": scores,
|
||
"traced": verdict == "pass", **extra}
|
||
|
||
|
||
def test_summary_counts_only_gradable_items_and_splits_roles():
|
||
usage = {"assistant": {"by_model": {"m": {"calls": 3, "cost_usd": 0.03}}, "calls": 3, "cost_usd": 0.03,
|
||
"input_tokens": 10, "output_tokens": 5, "unpriced_calls": 0},
|
||
"writer": {"by_model": {}, "calls": 1, "cost_usd": 0.01, "input_tokens": 1, "output_tokens": 1,
|
||
"unpriced_calls": 1},
|
||
"judge": {"cost_usd": 0.0, "calls": 0}}
|
||
rows = [
|
||
_row("c1", "pass", tier="formal", usage=usage, looks={"model": 3, "system": 0, "limit_hit": True},
|
||
timings={"build_ms": 1000, "total_ms": 4000, "review_ms": 30_000}, review={"verdict": "annotated",
|
||
"severity": "degraded"},
|
||
answer_stats={"numbers": 2, "numbers_cited": 1, "uncited_numbers": 1}, attempts=1,
|
||
sql_match={"status": "proven", "aspects": []}),
|
||
_row("c2", "fail", usage=usage, timings={"build_ms": 3000, "total_ms": 6000},
|
||
sql_match={"status": "related", "aspects": ["time"]}),
|
||
_row("c3", "ungradable"),
|
||
_row("c4", None, path=None, status="skipped"),
|
||
]
|
||
rows[0]["scores"][0]["outcome"] = "traced"
|
||
summary = stats.summarize(rows)
|
||
assert summary["accuracy"] == {"passed": 1, "n": 2, "rate": 0.5, "ci": stats.wilson(1, 2)}
|
||
assert summary["traced_accuracy"]["passed"] == 1
|
||
assert summary["verdicts"] == {"pass": 1, "fail": 1, "ungradable": 1, "skipped": 0} and summary["not_run"] == 1
|
||
assert summary["paths"] == {"ran": 3} and summary["tiers"]["formal"] == 1 and summary["tiers"]["explore"] == 2
|
||
assert summary["review"]["retries"] == 1 and summary["review"]["slow"] == 1
|
||
assert summary["review"]["severity"] == {"degraded": 1}
|
||
assert summary["looks"]["model"] == 3 and summary["looks"]["limit_hit"] == round(1 / 3, 4)
|
||
assert summary["timings"]["build_ms"] == {"p50": 1000, "p90": 3000, "n": 2}
|
||
assert summary["usage"]["assistant"]["calls"] == 6 and summary["usage"]["writer"]["unpriced_calls"] == 2
|
||
assert summary["cost_usd"] == 0.08 and summary["unpriced_calls"] == 2
|
||
assert summary["per_case_cost_usd"] == round(0.08 / 3, 6), "按评完了的题平均,没开评的不摊"
|
||
assert summary["usage"]["gold"] == {"cost_usd": 0.0, "calls": 0}, "期望探针不调模型"
|
||
assert summary["citations"]["rate"] == 0.5
|
||
assert summary["e1"]["passed"] == 1 and summary["e1"]["n"] == 2 and summary["e1"]["aspects"] == {"time": 1}
|
||
|
||
|
||
def test_the_per_case_cost_is_not_zero_when_calls_could_not_be_priced():
|
||
"""模型不在价格目录里(价格目录空):金额 0 只是「估得出的那部分」,每题平均花费不能写成 0(像不花钱)——一分都没估出来
|
||
的为 None(估不出),估出了一部分的照给、unpriced_calls 说明它只是下限。"""
|
||
unpriced = {"assistant": {"by_model": {}, "calls": 4, "cost_usd": 0.0, "input_tokens": 900, "output_tokens": 90,
|
||
"unpriced_calls": 4}}
|
||
summary = stats.summarize([_row("c1", "pass", usage=unpriced), _row("c2", "fail", usage=unpriced)])
|
||
assert summary["unpriced_calls"] == 8 and summary["per_case_cost_usd"] is None, summary["per_case_cost_usd"]
|
||
partly = {"assistant": {**unpriced["assistant"], "cost_usd": 0.02, "unpriced_calls": 1}}
|
||
summary = stats.summarize([_row("c1", "pass", usage=partly), _row("c2", "fail", usage=partly)])
|
||
assert summary["per_case_cost_usd"] == 0.02 and summary["unpriced_calls"] == 2
|
||
|
||
|
||
def test_repeats_give_a_pass_rate_per_case():
|
||
rows = [_row("c1", "pass", repeat=0), _row("c1", "fail", repeat=1), _row("c1", "pass", repeat=2)]
|
||
summary = stats.summarize(rows)
|
||
assert summary["pass_rates"]["c1"]["passed"] == 2 and summary["pass_rates"]["c1"]["n"] == 3
|
||
assert compare.case_outcome(rows)["verdict"] == "pass", "多数决"
|
||
|
||
|
||
@pytest.mark.parametrize("n, k", [(1, 0), (3, 3)])
|
||
def test_rates_carry_their_sample_size(n, k):
|
||
r = stats.rate(k, n)
|
||
assert r["n"] == n and r["passed"] == k and r["ci"] == stats.wilson(k, n)
|