AgentEvalTool/backend/agenteval/evaluation/go_no_go.py
sinohqb 2314ebe3bc
All checks were successful
CI / test (pull_request) Successful in 4m7s
feat(report): 上线验收报告 go/no-go 结论
新增纯函数模块 evaluation/go_no_go.py,基于 RunSummary 指标自动生成
go/no-go/conditional 三级结论。

- AcceptanceCriteria 模型:judged_pass_rate_min、pass_rate_min、avg_latency_max_ms
- GoNoGoVerdict 模型:decision、summary、criteria_results
- evaluate_go_no_go() 纯函数:接收 summary dict + criteria -> 返回 verdict
- 集成到 generate_report(),报告 dict 自动附带 go_no_go 字段
- 三级结论:go(全部达标)、no_go(核心未达标)、conditional(有风险)
- 新增 9 项单元测试(642 tests passed)

Closes #20
2026-08-25 14:28:55 +08:00

138 lines
5.5 KiB
Python

"""Go/No-Go acceptance verdict for evaluation runs."""
from datetime import datetime, timezone
from typing import Any, Optional
from pydantic import BaseModel, Field
class AcceptanceCriteria(BaseModel):
"""Acceptance criteria for go/no-go verdict."""
judged_pass_rate_min: float = Field(default=0.95, description="Minimum judged pass rate (0-1)")
pass_rate_min: float = Field(default=0.90, description="Minimum overall pass rate (0-1)")
avg_latency_max_ms: Optional[float] = Field(default=None, description="Maximum average latency in ms")
availability_min: Optional[float] = Field(default=None, description="Minimum availability (0-1), for campaign level")
class CriterionResult(BaseModel):
"""Result of checking a single criterion."""
criterion: str = Field(description="Criterion name")
threshold: float = Field(description="Threshold value")
actual: float = Field(description="Actual value")
passed: bool = Field(description="Whether the criterion passed")
detail: str = Field(default="", description="Human-readable detail")
class GoNoGoVerdict(BaseModel):
"""Go/No-Go verdict for an evaluation run."""
decision: str = Field(description="go | no_go | conditional")
summary: str = Field(description="Human-readable summary")
criteria_results: list[CriterionResult] = Field(default_factory=list)
generated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
def evaluate_go_no_go(
summary: dict[str, Any],
criteria: AcceptanceCriteria | None = None,
) -> GoNoGoVerdict:
"""Evaluate go/no-go verdict based on run summary and acceptance criteria.
Args:
summary: Run summary dict with keys like judged_pass_rate, pass_rate, avg_latency_ms
criteria: Acceptance criteria. If None, uses defaults.
Returns:
GoNoGoVerdict with decision, summary, and criteria results.
"""
if criteria is None:
criteria = AcceptanceCriteria()
results: list[CriterionResult] = []
# Check judged pass rate (only if present in summary)
judged_pass_rate = summary.get("judged_pass_rate")
if judged_pass_rate is None:
judged_pass_rate = summary.get("pass_rate")
if judged_pass_rate is not None:
passed = judged_pass_rate >= criteria.judged_pass_rate_min
results.append(CriterionResult(
criterion="judged_pass_rate",
threshold=criteria.judged_pass_rate_min,
actual=judged_pass_rate,
passed=passed,
detail=f"判定型通过率 {judged_pass_rate*100:.1f}% {'>=' if passed else '<'} {criteria.judged_pass_rate_min*100:.0f}%",
))
# Check overall pass rate (only if different from judged and present)
pass_rate = summary.get("pass_rate")
if pass_rate is not None and pass_rate != judged_pass_rate:
passed = pass_rate >= criteria.pass_rate_min
results.append(CriterionResult(
criterion="pass_rate",
threshold=criteria.pass_rate_min,
actual=pass_rate,
passed=passed,
detail=f"全量通过率 {pass_rate*100:.1f}% {'>=' if passed else '<'} {criteria.pass_rate_min*100:.0f}%",
))
# Check average latency
avg_latency_ms = summary.get("avg_latency_ms")
if avg_latency_ms is not None and criteria.avg_latency_max_ms is not None:
passed = avg_latency_ms <= criteria.avg_latency_max_ms
results.append(CriterionResult(
criterion="avg_latency_ms",
threshold=criteria.avg_latency_max_ms,
actual=avg_latency_ms,
passed=passed,
detail=f"平均延迟 {avg_latency_ms:.0f}ms {'<=' if passed else '>'} {criteria.avg_latency_max_ms:.0f}ms",
))
# Check availability (for campaign level)
availability = summary.get("overall_availability") or summary.get("availability")
if availability is not None and criteria.availability_min is not None:
passed = availability >= criteria.availability_min
results.append(CriterionResult(
criterion="availability",
threshold=criteria.availability_min,
actual=availability,
passed=passed,
detail=f"可用性 {availability*100:.1f}% {'>=' if passed else '<'} {criteria.availability_min*100:.0f}%",
))
# Determine overall decision
if not results:
return GoNoGoVerdict(
decision="conditional",
summary="无可用指标进行评估",
criteria_results=[],
)
all_passed = all(r.passed for r in results)
if all_passed:
decision = "go"
main_metric = results[0]
summary_text = f"通过率 {main_metric.actual*100:.0f}%,达标,建议上线"
else:
# Check if core metrics (pass rate) failed
core_failed = any(
not r.passed and r.criterion in ("judged_pass_rate", "pass_rate", "availability")
for r in results
)
if core_failed:
decision = "no_go"
failed_metrics = [r for r in results if not r.passed]
summary_text = f"核心指标未达标({', '.join(r.criterion for r in failed_metrics)}),不建议上线"
else:
decision = "conditional"
risky_metrics = [r for r in results if not r.passed]
summary_text = f"部分指标达标,存在风险项({', '.join(r.criterion for r in risky_metrics)}),建议修复后复测"
return GoNoGoVerdict(
decision=decision,
summary=summary_text,
criteria_results=results,
)