AgentEvalTool/backend/agenteval/evaluation/rules/response_time.py
sinohqb c1a3cdbaa9
All checks were successful
CI / test (pull_request) Successful in 4m5s
fix(rules): 修复代码审查发现的三处规则层缺陷
- response_time: 仅配置 max_ms 时恢复 v0.3 评分语义(最后一轮评分 + 超限线性惩罚),扩展指标共存时才用均值评分
- safety: 移除 moderation API 的黑名单命中跳过守卫,两层安全检查独立执行、发现均上报
- llm_score: 多维度评分添加 Semaphore 并发上限(5),防止维度数多时无限扇出模型请求
2026-08-25 18:13:11 +08:00

98 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Response time evaluation rule."""
from agenteval.evaluation.rules.base import EvalRule, RuleResult, register_rule
from agenteval.models import Case, Turn
@register_rule
class ResponseTimeRule(EvalRule):
"""Check whether the reply latency is within the configured threshold.
Supports multiple metrics:
- max_ms: threshold for single turn latency (backward compatible)
- avg_latency_max_ms: threshold for average latency across all turns
- throughput_min: minimum throughput in turns per minute
"""
name = "response_time"
async def evaluate(self, case: Case, dialog: list[Turn]) -> RuleResult:
if not dialog:
return RuleResult(passed=False, reason="无回复记录")
# Collect latencies from all turns
latencies = [t.latency_ms for t in dialog if t.latency_ms is not None]
if not latencies:
return RuleResult(passed=False, reason="无法获取响应时间")
# Backward compatible: check last turn against max_ms threshold
threshold_ms = self.params.get("max_ms")
if threshold_ms is None:
threshold_ms = case.expectations.response_time_max_ms
results = []
all_passed = True
# 1. Single turn latency check (backward compatible)
if threshold_ms is not None:
last_latency = latencies[-1]
passed = last_latency <= threshold_ms
if not passed:
all_passed = False
results.append(f"最后一轮 {last_latency}ms {'' if passed else '>'} {threshold_ms}ms")
# 2. Average latency check
avg_latency_max_ms = self.params.get("avg_latency_max_ms")
if avg_latency_max_ms is not None:
avg_latency = sum(latencies) / len(latencies)
passed = avg_latency <= avg_latency_max_ms
if not passed:
all_passed = False
results.append(f"平均延迟 {avg_latency:.0f}ms {'' if passed else '>'} {avg_latency_max_ms}ms")
# 3. Throughput check (turns per minute)
throughput_min = self.params.get("throughput_min")
if throughput_min is not None and len(dialog) >= 2:
first_sent = dialog[0].sent_at
last_received = dialog[-1].received_at
if first_sent and last_received:
duration_minutes = (last_received - first_sent).total_seconds() / 60
if duration_minutes > 0:
throughput = len(dialog) / duration_minutes
passed = throughput >= throughput_min
if not passed:
all_passed = False
results.append(f"吞吐量 {throughput:.1f} turns/min {'' if passed else '<'} {throughput_min}")
# If no metrics configured, just report the last turn latency
if not results:
last_latency = latencies[-1]
return RuleResult(passed=True, score=1.0, reason=f"响应时间 {last_latency}ms")
avg_latency = sum(latencies) / len(latencies)
has_extended_metrics = avg_latency_max_ms is not None or throughput_min is not None
if threshold_ms is not None and not has_extended_metrics:
# 仅 max_ms保持 v0.3 原语义——基于最后一轮评分,超限有惩罚
last_latency = latencies[-1]
if last_latency > threshold_ms:
score = max(0.0, 1.0 - (last_latency - threshold_ms) / threshold_ms)
else:
score = 1.0 if last_latency <= 0 else min(1.0, threshold_ms / last_latency)
elif threshold_ms is not None:
score = 1.0 if avg_latency <= 0 else min(1.0, threshold_ms / avg_latency)
else:
score = 1.0
verdict = "通过" if all_passed else "未通过"
return RuleResult(
passed=all_passed,
score=score,
reason=f"响应时间指标 {verdict}{'; '.join(results)}",
details={
"latencies": latencies,
"avg_latency_ms": avg_latency,
"min_latency_ms": min(latencies),
"max_latency_ms": max(latencies),
},
)