四块互相交织的 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>
210 lines
9.5 KiB
Python
210 lines
9.5 KiB
Python
"""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)
|