Files
Agentswarm/benchmark/metrics.py
T
Songhaoz666andClaude Opus 4.8 d487923646 benchmark: 落地决策层(τ/η/P)、质量(Q_quality)、通信遥测;关闭 #10 #23
四块互相交织的 benchmark 覆盖增量,统一提交:

1) 通信遥测(#23):orchestrator 路由 peer 消息时按 correlation_id 计请求/应答到
   SwarmRun.collaboration(内部状态,不进 Manager 事件流);collector 算 s_communication。
   治理计数由 run.approvals 派生(合规/总数)→ s_governance。

2) Q_quality 掩码归一(v2.1 裁定):metrics.quality_score 改为对 present 输入加权归一,
   非编码任务自动忽略 TestPassRate,全缺 → NaN(不伪造)。

3) 质量插桩 / Group B:新增 Pod 内代码测试沙箱(orchestrator/sandbox.py,环境清洗 +
   超时强杀 + 资源限额 + 路径越界校验,门控 ENABLE_QUALITY_EVAL)与 held-out fixture
   (benchmark/fixtures/);run 完成时用留出测试评分得 TestPassRate → Q_quality →
   collector 合成 reward。安全边界见 docs/integration/security-boundary.md §8.1。

4) 决策引擎 / Group A(#10,Option A score-at-pull):新增 orchestrator/decision_engine.py
   —— 信息素 τ(Redis 持久、(role,agent) 键控、冷启动 0.5、ρ 蒸发、夹紧、学习常开)+
   η 启发式评分 + ε-greedy 概率采样;每次 dispatch 产一条 DecisionTrace →
   SwarmRun.decisions;collector 算 tau/eta/p_decision。概率选择门控 ENABLE_ACO_DISPATCH
   (默认关,CI 用 ACO_SEED 固定)。

覆盖:单次 run 真实可算字段由 4 提升至最多 10/15(新增 communication/reward/tau/eta/
p_decision,外加 governance 有条件)。

测试:新增 test-sandbox / test-quality / test-decision-engine;扩充 collector/metrics 用例;
CI 纳入全部 benchmark 套件 + flag-on 的 ACO e2e。本地 11 项 gate 全绿。

诚实边界(未越界声称):
- Group A 为单边匹配(Option B 待 Group C);概率派发优于贪心未证;默认关闭。
- reward 的 CodeReview/UserAcceptance 未采集(掩码忽略);P_risk 为审批派生低估。
- s_gain/s_swarm/g_e/g_e_cost/benchmark 仍 NaN —— 需基线(#21/#13),本 PR 不动验收。

影响范围:Swarm(orchestrator + benchmark + docs + CI)。不改 Manager↔Swarm 事件契约
(遥测均为运行时内部状态);不影响 Client/计费/密钥/发布链路。新增 ENABLE_QUALITY_EVAL /
ENABLE_ACO_DISPATCH 两个开关,默认关闭。

Closes #10
Closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 12:51:32 +08:00

210 lines
9.5 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.
"""Swarm benchmark metric formulas + SwarmMetrics schema.
Aligned to **Agent 蜂群指标量化与标准 v2.0**. Pure formulas only (given inputs → value, unit-testable);
data collection (turning runtime signals into inputs) is partial — see collectors/run_collector.py
and docs/benchmark/metric-coverage-gaps.md. Recommended weights/hyperparameters from v2.0 are
provided as defaults below.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Optional
# --- v2.0 recommended weights / hyperparameters ---
TAU_WEIGHTS = {"success": 0.25, "quality": 0.20, "acceptance": 0.20,
"cost": 0.10, "time": 0.10, "risk": 0.08, "rollback": 0.07}
ETA_WEIGHTS = {"match": 0.25, "urgency": 0.15, "dependency": 0.15, "resource": 0.15,
"confidence": 0.10, "risk": 0.10, "budget_pressure": 0.10}
REWARD_WEIGHTS = {"s_task": 0.20, "q_quality": 0.20, "v_speed": 0.12, "e_cost": 0.13,
"r_robust": 0.13, "g_gov": 0.10, "p_risk": 0.07, "p_rework": 0.05}
# Sub-weights inside Q_quality (v2.0 §4.1). Masked & renormalized per task — see quality_score().
QUALITY_WEIGHTS = {"test_pass_rate": 0.4, "code_review_score": 0.3, "user_acceptance": 0.3}
SWARM_WEIGHTS = {"completion": 0.25, "gain": 0.20, "collaboration": 0.15, "communication": 0.10,
"cost": 0.10, "robustness": 0.10, "governance": 0.10}
LAMBDA_WEIGHTS = {"lambda1": 0.30, "lambda2": 0.20, "lambda3": 0.25, "lambda4": 0.15, "lambda5": 0.10} # Σ = 1.0
THETA_DEFAULTS = {"alpha": 1.0, "beta": 2.0, "rho": 0.10, "n_agent": 5, "epsilon": 0.10}
# Q_base reference coefficients (standard §6.1), by baseline type.
# ⚠️ PROVISIONAL / METADATA ONLY: these are assumed constants — the standard gives NO derivation.
# They are reported as `base_coefficient` but used in NO formula (G_E/G_E,c use raw Q_base).
# TODO(benchmark): quantify empirically later, e.g. coefficient = Q_baseline / Q_reference on the
# shared task set (reference = strongest baseline or an oracle/acceptance ceiling). Until then,
# do not wire into scoring. See docs/benchmark/baseline-comparison.md §1.1.
BASE_COEFFICIENTS = {"single": 0.75, "chain": 0.85, "sub": 0.90, "strong": 0.95}
@dataclass
class SwarmMetrics:
"""v2.0 §8.1 collection interface."""
tau: float # 信息素得分
eta: float # 启发式得分
p_decision: float # 决策概率
reward: float # 执行奖励
s_completion: float
s_gain: float
s_collaboration: float
s_communication: float
s_cost: float
s_robustness: float
s_governance: float
s_swarm: float # 蜂群总分
g_e: float # 涌现增益
g_e_cost: float # 成本归一化增益
benchmark: float # 综合评分
def _ratio_pct(numerator: float, denominator: float, *, empty: float = 0.0) -> float:
if not denominator:
return empty
return 100.0 * numerator / denominator
# --- 决策层(v2.0 §3) ---
def pheromone(*, success: float, quality: float, acceptance: float, cost: float, time: float,
risk: float, rollback: float, weights: dict = TAU_WEIGHTS) -> float:
w = weights
return (w["success"] * success + w["quality"] * quality + w["acceptance"] * acceptance
- w["cost"] * cost - w["time"] * time - w["risk"] * risk - w["rollback"] * rollback)
def heuristic(*, match: float, urgency: float, dependency: float, resource: float,
confidence: float, risk: float, budget_pressure: float, weights: dict = ETA_WEIGHTS) -> float:
g = weights
return (g["match"] * match + g["urgency"] * urgency + g["dependency"] * dependency
+ g["resource"] * resource + g["confidence"] * confidence
- g["risk"] * risk - g["budget_pressure"] * budget_pressure)
def action_probability(tau: float, eta: float, alpha: float, beta: float,
candidates: list[tuple[float, float]]) -> float:
# P(s,a,r) = τ^α·η^β / Σ τ_i^α·η_i^β
denom = sum((t ** alpha) * (e ** beta) for t, e in candidates)
if not denom:
return 0.0
return ((tau ** alpha) * (eta ** beta)) / denom
def p_decision(tau: float, eta: float, alpha: float = THETA_DEFAULTS["alpha"],
beta: float = THETA_DEFAULTS["beta"]) -> float:
# v2.0 §3.3: P_decision = τ^α · η^β · 100
return (tau ** alpha) * (eta ** beta) * 100.0
# --- 执行层(v2.0 §4) ---
def quality_score(
test_pass_rate: Optional[float] = None,
code_review_score: Optional[float] = None,
user_acceptance: Optional[float] = None,
*,
weights: dict = QUALITY_WEIGHTS,
) -> float:
"""Q_quality as a MASKED, RENORMALIZED weighted mean over the inputs that apply (v2.1 ruling).
An input that does not apply to the task is passed as ``None`` and drops out of BOTH the
weighted sum and the weight normalizer, so the score stays on [0,100] and remains comparable
across task types. Canonical case: a non-code prompt has no tests → ``test_pass_rate=None`` →
Q_quality is renormalized over {code_review_score, user_acceptance}. The same masking applies
to any absent input, not just test_pass_rate.
Q_quality = Σ_{i∈present} wᵢ·xᵢ / Σ_{i∈present} wᵢ
All inputs absent → NaN (org rule #9: never fabricate a 0/100 for an uncollected quantity).
With all three present and the default weights this reduces to the v2.0 0.4/0.3/0.3 blend.
"""
inputs = {
"test_pass_rate": test_pass_rate,
"code_review_score": code_review_score,
"user_acceptance": user_acceptance,
}
num = sum(weights[k] * x for k, x in inputs.items() if x is not None)
denom = sum(weights[k] for k, x in inputs.items() if x is not None)
if not denom:
return math.nan
return num / denom
def speed_score(target_time: float, actual_time: float) -> float:
return _ratio_pct(target_time, actual_time)
def cost_efficiency_score(expected_cost: float, actual_cost: float) -> float:
# Owner ruling (v2.1): E_cost (§4.1) == S_cost (§5.1) == CostEfficiency (§6.2) — one quantity,
# 100×Budget/ActualCost (Budget≡ExpectedCost, ActualUsage≡ActualCost). Same math as cost_score().
return _ratio_pct(expected_cost, actual_cost)
def rework_penalty(rework_count: int, total_tasks: int) -> float:
return _ratio_pct(rework_count, total_tasks)
def reward(*, s_task: float, q_quality: float, v_speed: float, e_cost: float, r_robust: float,
g_gov: float, p_risk: float, p_rework: float, weights: dict = REWARD_WEIGHTS) -> float:
w = weights
return (w["s_task"] * s_task + w["q_quality"] * q_quality + w["v_speed"] * v_speed
+ w["e_cost"] * e_cost + w["r_robust"] * r_robust + w["g_gov"] * g_gov
- w["p_risk"] * p_risk - w["p_rework"] * p_rework)
# --- 蜂群层(v2.0 §5) ---
def completion_score(completed_tasks: int, total_tasks: int) -> float:
return _ratio_pct(completed_tasks, total_tasks)
def collaboration_score(handoff_success_rate: float, dependency_resolution_rate: float,
workload_balance_score: float) -> float:
return (0.5 * handoff_success_rate + 0.3 * dependency_resolution_rate
+ 0.2 * workload_balance_score)
def communication_score(successful_messages: int, total_messages: int) -> float:
return _ratio_pct(successful_messages, total_messages)
def cost_score(budget: float, actual_usage: float) -> float:
return _ratio_pct(budget, actual_usage)
def robustness_score(recovered_failures: int, total_failures: int) -> float:
return _ratio_pct(recovered_failures, total_failures, empty=100.0)
def governance_score(compliant_operations: int, total_operations: int) -> float:
return _ratio_pct(compliant_operations, total_operations, empty=100.0)
def swarm_score(*, completion: float, gain: float, collaboration: float, communication: float,
cost: float, robustness: float, governance: float, weights: dict = SWARM_WEIGHTS) -> float:
w = weights
return (w["completion"] * completion + w["gain"] * gain + w["collaboration"] * collaboration
+ w["communication"] * communication + w["cost"] * cost
+ w["robustness"] * robustness + w["governance"] * governance)
# --- 涌现增益(v2.0 §6) ---
def emergence_gain(q_swarm: float, q_base: float) -> float:
# G_E = Q_swarm − Q_base
return q_swarm - q_base
def swarm_cost(n_agent: int, cost_efficiency: float) -> float:
# v2.0 §6.2: C_swarm = N_agent × (CostEfficiency/100 + 0.5)
return n_agent * (cost_efficiency / 100.0 + 0.5)
def cost_normalized_gain(q_swarm: float, c_swarm: float, q_base: float, c_base: float = 1.0) -> float:
# v2.0 §6.2 (CHANGED from v1 ratio-of-ratios to a DIFFERENCE):
# G_E,c = (Q_swarm / C_swarm) − (Q_base / C_base)
if not c_swarm or not c_base:
raise ValueError("cost_normalized_gain needs non-zero C_swarm / C_base")
return (q_swarm / c_swarm) - (q_base / c_base)
# --- 综合 Benchmark(v2.0 §7) ---
def benchmark_agent(*, s_swarm: float, g_e: float, reward: float, observability: float,
governance: float, weights: dict = LAMBDA_WEIGHTS, validate: bool = True) -> float:
"""Benchmark_Agent = λ1·S_swarm + λ2·G_E + λ3·R + λ4·O + λ5·Gov, with Σλ = 1.0 (v2.0 §7.1)."""
if validate and abs(sum(weights.values()) - 1.0) > 1e-6:
raise ValueError(f"λ weights must sum to 1.0 (got {sum(weights.values())})")
return (weights["lambda1"] * s_swarm + weights["lambda2"] * g_e + weights["lambda3"] * reward
+ weights["lambda4"] * observability + weights["lambda5"] * governance)