bench_providers.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #!/usr/bin/env python3
  2. """多 provider / 车道扫描 benchmark — 用数据决定每个 agent 该用哪条执行路径。
  3. 同一 golden 任务,自动跑一组 (provider, model, nativeToolCalls) 组合,收产物 →
  4. default_judge 打分 → 出对比表。回答"这个 agent 用 claude-code-文本 / qwen-文本 /
  5. qwen-FC 哪个质量高"——而非拍脑袋。
  6. 纠正一个常见误判:非 workspace 的 claude-code agent **不走 native 车道**(那只给
  7. workspace.assistant),而是 react-over-text + claude-code 当纯文本 provider —— 最脆的路。
  8. 所以"换 qwen-FC 是降级"未必成立,要扫描才知道。
  9. 用法:
  10. python3 evals/bench_providers.py [golden/literature-mapper.yaml]
  11. COMBOS="claude-code:sonnet:0,dashscope:qwen-plus:0,dashscope:qwen-plus:1" python3 evals/bench_providers.py
  12. 要 ~/.agentpaas/.env(各 provider key)+ 本地 ollama(judge)。真调 LLM、花钱。
  13. """
  14. from __future__ import annotations
  15. import json
  16. import os
  17. import shutil
  18. import sys
  19. import tempfile
  20. import time
  21. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "agentpaas", "src"))
  22. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lambdagent", "src"))
  23. _envf = os.path.expanduser("~/.agentpaas/.env")
  24. if os.path.exists(_envf):
  25. for line in open(_envf):
  26. if "=" in line and not line.strip().startswith("#"):
  27. k, v = line.strip().split("=", 1)
  28. os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
  29. import sqlite3 # noqa: E402
  30. import yaml # noqa: E402
  31. from lambdagent.fromconfig import from_config # noqa: E402
  32. from lambdagent.core import Context # noqa: E402
  33. from lambdagent.builtin_tools.shell_tools import _set_cwd # noqa: E402
  34. from lambdagent.builtin_tools._sandbox import set_sandbox_root # noqa: E402
  35. from agentpaas.engine.agent_eval import EvalTask, run_eval_task # noqa: E402
  36. from agentpaas.engine.pipeline import default_judge # noqa: E402
  37. DB = os.path.expanduser("~/.agentpaas/data/agentpaas.db")
  38. MAXSTEPS = int(os.environ.get("MAXSTEPS", "14"))
  39. RUNS = int(os.environ.get("RUNS", "1")) # 每个组合重复跑几次取均值(消 LLM 方差)
  40. def _agg(results: list) -> dict:
  41. """对同一组合的 N 次 EvalResult 聚合:均值 + 极值 + 达标率。"""
  42. n = len(results) or 1
  43. js = [r.judge_score for r in results]
  44. return {
  45. "runs": len(results),
  46. "judge_avg": round(sum(js) / n, 2),
  47. "judge_min": round(min(js), 2) if js else 0.0,
  48. "judge_max": round(max(js), 2) if js else 0.0,
  49. "files_ok_rate": round(sum(1 for r in results if r.files_ok) / n, 2),
  50. "pass_rate": round(sum(1 for r in results if r.passed) / n, 2),
  51. }
  52. # 组合:provider:model:fc(1/0)。默认 claude-code文本 vs qwen文本 vs qwen-FC。
  53. _DEFAULT = "claude-code:sonnet:0,dashscope:qwen-plus:0,dashscope:qwen-plus:1"
  54. COMBOS = [tuple(c.split(":")) for c in os.environ.get("COMBOS", _DEFAULT).split(",")]
  55. def _base_cfg(template: str) -> dict:
  56. con = sqlite3.connect(DB); con.row_factory = sqlite3.Row
  57. a = con.execute("SELECT id,current_version FROM agents WHERE agent_template=? "
  58. "AND status='active' LIMIT 1", (template,)).fetchone()
  59. if not a:
  60. raise RuntimeError(f"没有 {template} 的已安装实例")
  61. cfg = json.loads(con.execute("SELECT config FROM agent_versions WHERE agent_id=? AND version=?",
  62. (a["id"], a["current_version"])).fetchone()["config"])
  63. cfg.setdefault("react", {})["maxSteps"] = MAXSTEPS
  64. cfg["react"]["toolTimeout"] = 90
  65. return cfg
  66. def _run_combo(base: dict, provider: str, model: str, fc: bool, task: EvalTask) -> dict:
  67. d = tempfile.mkdtemp(prefix="bench_"); _set_cwd(d); set_sandbox_root(d)
  68. try:
  69. c = json.loads(json.dumps(base))
  70. c["model"] = {"provider": provider, "name": model, "temperature": 0.0, "maxTokens": 4096}
  71. c["react"]["nativeToolCalls"] = fc
  72. with tempfile.NamedTemporaryFile("w", suffix=".yml", delete=False, encoding="utf-8") as f:
  73. yaml.dump(c, f, allow_unicode=True); p = f.name
  74. term = from_config(p); os.unlink(p)
  75. ctx = Context(workspace_path=d, run_id="bench")
  76. t0 = time.time(); out = term.apply(task.input, ctx); dt = time.time() - t0
  77. files = {fn: open(os.path.join(d, fn), encoding="utf-8", errors="ignore").read()
  78. for fn in os.listdir(d) if os.path.isfile(os.path.join(d, fn))}
  79. return {"output": str(out), "steps": len(getattr(ctx, "trace", []) or []),
  80. "cost_usd": 0.0, "workspace_path": d, "_files": files, "_dt": dt}
  81. finally:
  82. set_sandbox_root(None); shutil.rmtree(d, ignore_errors=True)
  83. def main():
  84. paths = sys.argv[1:] or [os.path.join(os.path.dirname(__file__), "golden", "literature-mapper.yaml")]
  85. print(f"provider/车道扫描 | maxSteps={MAXSTEPS}")
  86. print(f"组合: {['/'.join(c) for c in COMBOS]}\n")
  87. report = {}
  88. for p in paths:
  89. spec = yaml.safe_load(open(p, encoding="utf-8"))
  90. base = _base_cfg(spec["agent_template"])
  91. for t in spec.get("tasks", []):
  92. task = EvalTask(id=t["id"], input=t.get("input", ""),
  93. must_produce=t.get("must_produce", []),
  94. rubric=t.get("rubric", ""), threshold=float(t.get("threshold", 0.6)))
  95. print(f"=== {spec['agent_template']} / {task.id} ===")
  96. rows = {}
  97. for provider, model, fcs in COMBOS:
  98. fc = fcs in ("1", "true", "True")
  99. label = f"{provider}/{model}{'·FC' if fc else '·文本'}"
  100. run_results = []
  101. for run_i in range(RUNS):
  102. holder = {}
  103. def wrapped(tk, _p=provider, _m=model, _fc=fc, _h=holder):
  104. r = _run_combo(base, _p, _m, _fc, tk); _h.update(r); return r
  105. try:
  106. rr = run_eval_task(task, run_fn=wrapped,
  107. collect_artifacts_fn=lambda ws, _h=holder: _h.get("_files", {}),
  108. judge_fn=default_judge)
  109. run_results.append(rr)
  110. print(f" {label:26} run{run_i+1}/{RUNS} judge={rr.judge_score:.2f} "
  111. f"files_ok={rr.files_ok} {round(holder.get('_dt',0))}s"
  112. + (f" err:{rr.error[:30]}" if rr.error else ""))
  113. except Exception as e:
  114. print(f" {label:26} run{run_i+1}/{RUNS} ✗ {str(e)[:50]}")
  115. if run_results:
  116. rows[label] = _agg(run_results)
  117. report[task.id] = rows
  118. print(f" --- {task.id} 聚合({RUNS} 轮均值)---")
  119. for lbl, a in sorted(rows.items(), key=lambda kv: -kv[1]["judge_avg"]):
  120. print(f" {lbl:26} judge均={a['judge_avg']:.2f} "
  121. f"[{a['judge_min']:.2f}~{a['judge_max']:.2f}] "
  122. f"落盘率={a['files_ok_rate']:.0%} 通过率={a['pass_rate']:.0%}")
  123. if rows:
  124. best = max(rows.items(), key=lambda kv: kv[1]["judge_avg"])
  125. print(f" → 最优: {best[0]} (judge均 {best[1]['judge_avg']:.2f})\n")
  126. out = os.path.join(os.path.dirname(__file__), "bench_providers_report.json")
  127. json.dump(report, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
  128. print(f"报告: {out}")
  129. if __name__ == "__main__":
  130. main()