Files
Agentswarm/benchmark/runners/backend.py
T
Songhaoz666andClaude Opus 4.8 baa67350e6 benchmark Group C:基线运行器 + 统一 BenchmarkRunRecord + 报告/回放(Closes #21)
在去中心化重构之上落地 benchmark 对比管线:5 个系统(single/strong/chain/sub_agent/swarm)
跑同一任务集、同一执行后端,产出统一 BenchmarkRunRecord → 评估器算 G_E/G_E,c → 报告 + 回放。

- benchmark/runners/:backend(Offline 确定性 / OpenAI 真实)+ base + 5 个 runner。各 runner
  用 held-out fixture 测试在 Group B 沙箱里评分得 TestPassRate(权威,非自评)。
- benchmark/tasksets/:统一任务集 + 加载器(coding-set-1,1 个 fixture)。
- benchmark/reports/、benchmark/replay/:G_E/G_E,c/coverage/confidence + 归档。
- benchmark/baselines/comparison.py:BenchmarkRunRecord 的 CodeReview/UserAcceptance 改为
  Optional(掩码归一,未采集即 None,规则 #9)。
- scripts/run-benchmark-suite.py harness + scripts/test-benchmark-runners.py。

与去中心化重构对齐:swarm runner 拓扑已**重指向去中心化流程**(种子→自选→自主分解→竞争→
同伴交叉评审→收敛,calls=6/review=1),非旧 Master「分解→派发→单评审」。仍用同一离线后端
建模以保证公平对比(驱动活体编排器会换后端→记录不可比;活体全流程由 test-workflow-e2e 验证)。

沙箱适配:runner 评分走 fail-closed 沙箱(#24),故 test + CI 步骤设 HEICODE_SANDBOX_ISOLATED=1
(仅 CI/隔离 Pod)。

影响范围:agent_swarm(benchmark 层 + 测试 + docs + CI)。不碰 orchestrator 编排逻辑、
不改 Manager↔Swarm 契约、不影响 Client/计费/密钥/审计/发布链路。

诚实边界:
- **离线后端只验证管线**:所有系统拿同一参考解 → quality 相同 → G_E=0、swarm_valid=False,
  刻意不显示蜂群优势(反造假)。真实 G_E>0 需 --backend openai + 足量冻结任务集 + 多次运行。
- 故 Closes #21(运行器 + 统一记录已落地并产出合规非 NaN 记录);Refs #20(仅 1/5 场景)、
  Refs #22(评估器/报告/回放已建,但 Quality 仅 TestPassRate,CodeReview/UserAcceptance 缺)、
  Refs #13(验收 EPIC,需真实 run 证明 Swarm>baselines,未满足)。

Closes #21
Refs #20
Refs #22
Refs #13

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

116 lines
4.7 KiB
Python

"""Execution backends for benchmark runners — the SHARED model gateway all systems run through.
Fairness (standard §9.1): swarm and every baseline must use the same backend on the same task
set; only the topology (call pattern) differs. Two backends:
- OfflineBackend: deterministic, no network, no key. Echoes the fixture's offline reference
solution and charges a notional per-call cost. Used for hermetic CI + pipeline validation.
It is IDENTICAL across systems, so offline runs can prove the pipeline (records → G_E →
report, no NaN) but CANNOT show a swarm quality advantage — by design (anti-fabrication).
- OpenAIBackend: real OpenAI-compatible generation (needs OPENAI_API_KEY / OPENAI_BASE_URL).
Produces real files + real token/cost. This is what yields REAL G_E numbers.
`generate()` returns the produced implementation files plus measured token/cost/time.
"""
from __future__ import annotations
import json
import os
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
# (path, content) pairs.
Files = List[Tuple[str, str]]
@dataclass
class GenerationResult:
files: Files = field(default_factory=list)
tokens: int = 0
cost_usd: float = 0.0
elapsed_s: float = 0.0
class ExecutionBackend(ABC):
name: str = "abstract"
@abstractmethod
def generate(self, *, objective: str, reference_files: Files, max_tokens: int) -> GenerationResult:
"""Produce implementation files for one objective."""
class OfflineBackend(ExecutionBackend):
"""Deterministic, key-free. Echoes the fixture reference solution; charges a notional cost.
cost_per_call / tokens_per_call / time_per_call let the harness model a 'stronger' (costlier)
configuration without a real model — see runners/strong.py.
"""
name = "offline"
def __init__(self, *, cost_per_call: float = 0.002, tokens_per_call: int = 800,
time_per_call: float = 0.5):
self.cost_per_call = cost_per_call
self.tokens_per_call = tokens_per_call
self.time_per_call = time_per_call
def generate(self, *, objective: str, reference_files: Files, max_tokens: int) -> GenerationResult:
# Deterministic: same reference output regardless of system/topology (no swarm bias).
return GenerationResult(
files=list(reference_files),
tokens=self.tokens_per_call,
cost_usd=self.cost_per_call,
elapsed_s=self.time_per_call,
)
class OpenAIBackend(ExecutionBackend):
"""Real OpenAI-compatible generation. Lazy client; raises clearly if no key is configured."""
name = "openai"
def __init__(self, *, model: Optional[str] = None, price_per_1k_tokens: float = 0.0):
self.model = model or os.getenv("OPENAI_MODEL", "gpt-4o-mini")
self.price_per_1k_tokens = price_per_1k_tokens
self._client = None
def _get_client(self):
if self._client is None:
from openai import OpenAI # lazy: keep offline/CI import-light
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError("OpenAIBackend requires OPENAI_API_KEY (use OfflineBackend for hermetic runs)")
self._client = OpenAI(base_url=os.getenv("OPENAI_BASE_URL") or None)
return self._client
@staticmethod
def _parse_files(content: str) -> Files:
# Accept a JSON {"files":[{"path","content"}]} (same shape the agent uses), else empty.
try:
text = re.sub(r"^```(json)?|```$", "", content.strip(), flags=re.MULTILINE).strip()
data = json.loads(text)
return [(f["path"], f["content"]) for f in data.get("files", []) if f.get("path")]
except Exception:
return []
def generate(self, *, objective: str, reference_files: Files, max_tokens: int) -> GenerationResult:
import time as _time
client = self._get_client()
prompt = (
f"{objective}\n\nReturn ONLY JSON: "
'{"files":[{"path":"relative/path.py","content":"complete file content"}]}'
)
start = _time.time()
resp = client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
)
elapsed = _time.time() - start
content = resp.choices[0].message.content or ""
usage = getattr(resp, "usage", None)
tokens = int(getattr(usage, "total_tokens", 0) or 0)
cost = tokens / 1000.0 * self.price_per_1k_tokens
return GenerationResult(files=self._parse_files(content), tokens=tokens,
cost_usd=cost, elapsed_s=elapsed)