mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 17:01:59 +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;和上一次比找到退步、归因做法变);通知事件、定时接口形状的老测试按新事件和新字段更新
127 lines
6.1 KiB
Python
127 lines
6.1 KiB
Python
"""评测的清理与工件回收(细案 01 做法 16)。
|
||
|
||
每次评测有几十次运行(作答、作答前后算期望)和它们的查询快照、报告文档,只增不减,库和工件目录会一直涨。按保留规则清掉更早
|
||
那几次的**明细**,总表(EvalRun.summary:准确率、区间、分布、和上一次比的计数)永久保留,界面上写「明细已清理」(cleaned_at)。
|
||
|
||
**保留哪些**(每个题集):最近 keep_runs 次(设置 eval.keep_runs,缺省 20);钉住的基线;以及这些里面 A/B 的另几臂(同一个
|
||
pair_id,配对表要两边都在)。还没结束的不动。按时间一刀切会把还在当基线的那次删掉,所以钉住的另算。
|
||
|
||
**删什么**:逐题结果行、它们的明细工件、底下的运行(作答、算期望的探索运行,来源都是 eval;事件随运行级联删掉)和这些运行
|
||
在工件引用表里的引用行。**工件文件**要等引用行删完之后一条引用都不剩才删(core/artifact_store.forget):同样的内容可能被别的
|
||
运行引用(同一份表结构快照、同一份查询结果),不能跟着删。评测运行全是探索运行,删正式运行的那道拦截(api/runs.delete_run)
|
||
不受影响;这里也只删来源是 eval、已经结束的运行。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
from typing import Any
|
||
|
||
from sqlalchemy import delete, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.db.base import SessionLocal, utcnow
|
||
from app.db.models_eval import EvalResult, EvalRun
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
#: 删运行时还在这几种状态的不删(运行在跑、停在审批上)
|
||
_LIVE = ("queued", "running", "interrupted")
|
||
_background: set[asyncio.Task[Any]] = set()
|
||
|
||
|
||
def keep_set(runs: list[EvalRun], keep: int) -> set[str]:
|
||
"""要留明细的那几次(见模块说明)。runs 是一个题集的全部评测,新的在前。"""
|
||
from app.core.job_runtime import ACTIVE
|
||
|
||
kept = {r.id for r in runs[:max(1, keep)]} | {r.id for r in runs if r.pinned or r.status in ACTIVE}
|
||
pairs = {r.pair_id for r in runs if r.id in kept and r.pair_id}
|
||
kept |= {r.id for r in runs if r.pair_id and r.pair_id in pairs}
|
||
return kept
|
||
|
||
|
||
async def _clean_one(session: AsyncSession, run: EvalRun) -> tuple[int, int, set[str]]:
|
||
"""清一次评测的明细:(删了几项结果, 删了几次运行, 要回收的工件 id)。不提交。"""
|
||
from app.db.models import Artifact, Run
|
||
|
||
results = list((await session.execute(select(EvalResult).where(EvalResult.eval_run_id == run.id))).scalars())
|
||
run_ids: set[str] = set()
|
||
artifacts: set[str] = set()
|
||
for r in results:
|
||
if r.run_id:
|
||
run_ids.add(r.run_id)
|
||
run_ids |= {x for x in r.gold_run_ids or [] if isinstance(x, str)}
|
||
if r.details_artifact:
|
||
artifacts.add(r.details_artifact)
|
||
await session.execute(delete(Artifact).where(Artifact.id == r.details_artifact, Artifact.run_id == "")
|
||
.execution_options(synchronize_session=False))
|
||
# 这次评测发起的、结果里没记下的运行(重来的前一次、停止时没写进结果的):按来源找
|
||
for rid, origin in (await session.execute(select(Run.id, Run.origin).where(Run.origin_kind == "eval"))).all():
|
||
if (origin or {}).get("eval_run_id") == run.id:
|
||
run_ids.add(rid)
|
||
removed = 0
|
||
for rid in sorted(run_ids):
|
||
row = await session.get(Run, rid)
|
||
if row is None or row.origin_kind != "eval" or row.status in _LIVE:
|
||
continue
|
||
artifacts |= set((await session.execute(select(Artifact.id).where(Artifact.run_id == rid))).scalars())
|
||
await session.execute(delete(Artifact).where(Artifact.run_id == rid).execution_options(synchronize_session=False))
|
||
await session.delete(row)
|
||
removed += 1
|
||
for r in results:
|
||
await session.delete(r)
|
||
run.cleaned_at = utcnow()
|
||
return len(results), removed, artifacts
|
||
|
||
|
||
async def clean_suite(suite_id: str, *, keep: int | None = None) -> dict[str, Any]:
|
||
"""按保留规则清一个题集:{runs: 清了几次评测, results, deleted_runs, files: 回收了几个工件文件}。"""
|
||
from app.api.settings import eval_settings
|
||
from app.core.artifact_store import forget
|
||
|
||
async with SessionLocal() as session:
|
||
if keep is None:
|
||
keep = int((await eval_settings(session))["keep_runs"])
|
||
runs = list((await session.execute(select(EvalRun).where(EvalRun.suite_id == suite_id)
|
||
.order_by(EvalRun.created_at.desc()))).scalars())
|
||
kept = keep_set(runs, keep)
|
||
targets = [r for r in runs if r.id not in kept and r.cleaned_at is None]
|
||
results = removed = 0
|
||
artifacts: set[str] = set()
|
||
for run in targets:
|
||
n, m, found = await _clean_one(session, run)
|
||
results += n
|
||
removed += m
|
||
artifacts |= found
|
||
await session.commit()
|
||
files = await forget(artifacts) if artifacts else []
|
||
if targets:
|
||
logger.info("评测清理:题集 %s 清了 %d 次评测的明细(%d 项结果、%d 次运行、%d 个工件文件)", suite_id, len(targets),
|
||
results, removed, len(files))
|
||
return {"runs": len(targets), "results": results, "deleted_runs": removed, "files": len(files)}
|
||
|
||
|
||
def spawn(suite_id: str) -> None:
|
||
"""在后台清理(评测收尾时调,不挡收尾)。出错只记日志。"""
|
||
if not suite_id:
|
||
return
|
||
|
||
async def go() -> None:
|
||
try:
|
||
await clean_suite(suite_id)
|
||
except Exception: # noqa: BLE001
|
||
logger.exception("评测清理出错(题集 %s)", suite_id)
|
||
|
||
task = asyncio.get_running_loop().create_task(go())
|
||
_background.add(task)
|
||
task.add_done_callback(_background.discard)
|
||
|
||
|
||
async def drain() -> None:
|
||
"""等后台的清理做完(测试、关停用)。"""
|
||
while _background:
|
||
await asyncio.gather(*list(_background), return_exceptions=True)
|
||
|
||
|
||
__all__ = ["clean_suite", "drain", "keep_set", "spawn"]
|