mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:01:57 +00:00
2371e75f17
为什么:第 2 波只有题集和期望答案的来源,评测本身跑不起来。这一段把细案 01 做法 9–12、16、17 的后端接上, 量的是用户在问数据页拿到的同一轮,而不是另抄一份编排。 - eval/answer.py:无头作答,串起 answer_ops → chat/turn 的决定 → launch_run(origin=eval,带目录视图、只读、遮罩) → 等终态(停在审批的取消、记「评测不代批」;驳回记路径「被驳回」)→ review_of_run → 按同一规则重来一次; 工作流题按钉住版本的图跑探索运行(不发正式运行,免得被 pick_base 和应用视图取到)。用量按角色记 - eval/runner.py:用 core/job_runtime,一次评测一个任务,题 × 重复 × 臂一项;A/B 几臂共用期望、同一题同时开跑; 开评拍目录快照、记上传表格版本和表结构哈希的前后;并发不超过 max_parallel 和全局并发的四分之一;每题时限; 评测单独一本账(开跑前估算、每题预留后找补、触顶不再开新题);止损;能停、只重试没做完的、重启标中断不续跑 - eval/score.py:六种期望、五种结论(量纲不同单列)、维度配对与弱匹配、路径不对直接不通过、口径 dual 两版都报; 逐题 E1 用口径账本的 compare_queries(总方案 X2,不另写 signals);SCORER_VERSION 可重评(eval/rescore.py,只读封存) - eval/judging.py:结论裁判走按需裁判同一条路(evidence._judge_now),文字要点由裁判判、单列不计准确率;花费经 judge.charged_to 记在评测那本账上(engine/judge.py 只加了一个上下文变量和取账本的一处) - eval/stats.py、eval/compare.py:准确率与可追溯准确率 + Wilson 区间、各项分布、P50/P90、按角色成本、引用率、逐题 E1; 选基准、逐题转移与归因、指纹差异、A/B 配对表与精确 McNemar;「目录贡献」预设对照(当前 vs 关,可加只用已核实) - eval/cleanup.py + core/artifact_store.forget:保留最近 N 次 + 钉住的基线 + A/B 另几臂,删结果、明细工件、底下的运行和 引用行,工件文件一条引用都不剩才删;总表永久保留 - engine/tolerance.py:双盲复算的 _agree / _delta_pct 原样搬来共用(dual_check 改 import,行为不变) - 设置加 eval 一组(builder 读、admin 写)和 GET /api/settings/eval/spend;题集预算在全站缺省上覆盖,每日上限只能往下收 - api/evals.py:发起(单臂 / A/B / 目录贡献预设)、估算、进度、逐题、停止、重试、重评、钉基线、和上一次比、配对表、清理 - main.py 的 lifespan:影子起草和评测的启动整理、关停(评测先于运行停,停时取消自己发起的运行) - 测试:test_eval_e2e(dual_check_shop + 剧本模型:按支付日 330 通过且 SQL 等价,按下单日 310 不通过、差在 time; 目录对照 1:0;和上一次比找到退步、归因做法变);通知事件、定时接口形状的老测试按新事件和新字段更新
77 lines
4.3 KiB
Python
77 lines
4.3 KiB
Python
"""评测题集的定时(细案 01 做法 15;总方案 X4:定时只有一套)。
|
||
|
||
scheduler/service.py 按目标种类导入本模块(_TARGET_MODULES):认领、补跑(错过的按任务的宽限补一次)、重叠跳过、触发记录都是
|
||
定时那一套,这里只管两件事:
|
||
|
||
- fire:到点(或立即运行)发起一次评测。变体用题集的缺省变体,定时跑 1 次(评测-D10:对照才跑 3 次),截至时间就是这次
|
||
计划触发的时间(题目里的 {{ schedule.* }} 按它、按这条定时的 cron 渲染,和工作流的定时同一份变量);基准是同一变体的
|
||
上一次(runner 收尾时按 compare.pick_baseline 挑)。这个题集上一次评测还没结束就记 skipped_overlap、写清是哪一次;题集
|
||
不在、归档了、没有启用的题、变体里的模型用不了,记 failed_to_start 和原因。发起了记 started,ScheduleFire.eval_run_id 指向
|
||
这一次评测(用 service.record_fire 写,和工作流同一套去重:同一次触发撞上唯一约束就交回已有的那一条)。
|
||
- check_target:建任务、改目标时核对题集在、没归档、有启用的题。
|
||
|
||
有退步、没评完的通知在评测收尾时推(runner.py → notify/service.notify_eval),只推定时发起的。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
|
||
async def fire(*, schedule_id: str, fire_at: datetime, kind: str, actor: str | None, target_id: str, name: str,
|
||
tz: str, cron_expr: str) -> Any:
|
||
from app.db.base import SessionLocal
|
||
from app.db.models_eval import EvalSuite
|
||
from app.eval import runner, variants
|
||
from app.eval.cases import CaseInvalid
|
||
from app.scheduler import service
|
||
|
||
async with SessionLocal() as session:
|
||
suite = await session.get(EvalSuite, target_id)
|
||
if suite is None:
|
||
return await service.record_fire(schedule_id, fire_at, kind, "failed_to_start", "评测题集不存在,可能已被删除")
|
||
busy = await runner.active_run(session, suite.id)
|
||
if busy is not None:
|
||
reason = f"题集「{suite.name}」上一次评测 #{busy.id[:6]} 还没结束,本次触发已跳过"
|
||
return await service.record_fire(schedule_id, fire_at, kind, "skipped_overlap", reason)
|
||
existing = await service._existing(schedule_id, fire_at, kind)
|
||
if existing is not None:
|
||
return existing
|
||
try:
|
||
from app.api.settings import eval_settings
|
||
|
||
judge = bool((await eval_settings(session))["judge"])
|
||
variant = variants.normalize_variant(dict(suite.default_variant or {}) | {"repeats": variants.SCHEDULE_REPEATS},
|
||
judge_default=judge)
|
||
[run] = await runner.start(session, suite, [variant], actor=actor or service.actor_label(name),
|
||
trigger="schedule", schedule_id=schedule_id, as_of=fire_at, cron=cron_expr)
|
||
except CaseInvalid as e:
|
||
return await service.record_fire(schedule_id, fire_at, kind, "failed_to_start", f"没有发起评测:{e.message}")
|
||
return await service.record_fire(schedule_id, fire_at, kind, "started", None, eval_run_id=run.id)
|
||
|
||
|
||
async def check_target(session: Any, target_id: str) -> str | None:
|
||
"""题集在、没归档、有启用的题。不合规返回给人看的一句。"""
|
||
from sqlalchemy import func, select
|
||
|
||
from app.db.models_eval import EvalCase, EvalSuite
|
||
|
||
suite = await session.get(EvalSuite, target_id)
|
||
if suite is None:
|
||
return "评测题集不存在,可能已被删除"
|
||
if suite.archived:
|
||
return f"题集「{suite.name}」已归档,不能定时评测"
|
||
enabled = await session.scalar(select(func.count()).select_from(EvalCase).where(
|
||
EvalCase.suite_id == target_id, EvalCase.enabled.is_(True), EvalCase.archived.is_(False)))
|
||
if not enabled:
|
||
return f"题集「{suite.name}」里没有启用的题目"
|
||
return None
|
||
|
||
|
||
async def target_name(session: Any, target_id: str) -> str | None:
|
||
"""接口里给人看的目标名(题集名);不在了为 None。"""
|
||
from app.db.models_eval import EvalSuite
|
||
|
||
suite = await session.get(EvalSuite, target_id)
|
||
return suite.name if suite is not None else None
|