bench_fc.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/usr/bin/env python3
  2. """FC vs 文本路径 benchmark — 可复现地对比"原生 function-calling"对真实 agent 质量的影响。
  3. 在内核里直接跑(绕过 HTTP 鉴权),对同一 golden 任务、同一 agent、同一模型(qwen-plus),
  4. 一次开 nativeToolCalls 一次关,收产物 → default_judge 打分 → 出对比表。改了 FC 一键重测。
  5. 用法:
  6. python3 evals/bench_fc.py [golden/literature-mapper.yaml ...] # 默认跑 literature-mapper
  7. MODEL=qwen-plus MAXSTEPS=14 python3 evals/bench_fc.py
  8. 要 ~/.agentpaas/.env 里的 DASHSCOPE_API_KEY + 本地 ollama(judge)。会真调 LLM、花钱。
  9. """
  10. from __future__ import annotations
  11. import json
  12. import os
  13. import shutil
  14. import sys
  15. import tempfile
  16. import time
  17. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "agentpaas", "src"))
  18. sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lambdagent", "src"))
  19. # 加载 keys
  20. _envf = os.path.expanduser("~/.agentpaas/.env")
  21. if os.path.exists(_envf):
  22. for line in open(_envf):
  23. if "=" in line and not line.strip().startswith("#"):
  24. k, v = line.strip().split("=", 1)
  25. os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
  26. import sqlite3 # noqa: E402
  27. import yaml # noqa: E402
  28. from lambdagent.fromconfig import from_config # noqa: E402
  29. from lambdagent.core import Context # noqa: E402
  30. from lambdagent.builtin_tools.shell_tools import _set_cwd # noqa: E402
  31. from lambdagent.builtin_tools._sandbox import set_sandbox_root # noqa: E402
  32. from agentpaas.engine.agent_eval import EvalTask, run_eval_task # noqa: E402
  33. from agentpaas.engine.pipeline import default_judge # noqa: E402
  34. MODEL = os.environ.get("MODEL", "qwen-plus")
  35. MAXSTEPS = int(os.environ.get("MAXSTEPS", "14"))
  36. DB = os.path.expanduser("~/.agentpaas/data/agentpaas.db")
  37. def _agent_cfg_for(template: str) -> dict:
  38. con = sqlite3.connect(DB)
  39. con.row_factory = sqlite3.Row
  40. a = con.execute("SELECT id,current_version FROM agents WHERE agent_template=? "
  41. "AND status='active' LIMIT 1", (template,)).fetchone()
  42. if not a:
  43. raise RuntimeError(f"没有 {template} 的已安装实例")
  44. cfg = json.loads(con.execute("SELECT config FROM agent_versions WHERE agent_id=? AND version=?",
  45. (a["id"], a["current_version"])).fetchone()["config"])
  46. # 统一成 qwen-plus + 限步数,公平对比
  47. cfg["model"] = {"provider": "dashscope", "name": MODEL, "temperature": 0.0, "maxTokens": 4096}
  48. cfg.setdefault("react", {})
  49. cfg["react"]["maxSteps"] = MAXSTEPS
  50. cfg["react"]["toolTimeout"] = 60
  51. return cfg
  52. def _run_path(cfg: dict, fc: bool, task: EvalTask) -> dict:
  53. d = tempfile.mkdtemp(prefix="bench_")
  54. _set_cwd(d); set_sandbox_root(d)
  55. try:
  56. c = json.loads(json.dumps(cfg))
  57. c["react"]["nativeToolCalls"] = fc
  58. with tempfile.NamedTemporaryFile("w", suffix=".yml", delete=False, encoding="utf-8") as f:
  59. yaml.dump(c, f, allow_unicode=True); p = f.name
  60. term = from_config(p); os.unlink(p)
  61. ctx = Context(workspace_path=d, run_id="bench")
  62. t0 = time.time(); out = term.apply(task.input, ctx); dt = time.time() - t0
  63. files = {fn: open(os.path.join(d, fn), encoding="utf-8", errors="ignore").read()
  64. for fn in os.listdir(d) if os.path.isfile(os.path.join(d, fn))}
  65. return {"output": str(out), "steps": len(getattr(ctx, "trace", []) or []),
  66. "cost_usd": 0.0, "workspace_path": d, "_files": files, "_dt": dt,
  67. "_term": term._name}
  68. finally:
  69. set_sandbox_root(None); shutil.rmtree(d, ignore_errors=True)
  70. def bench_task(cfg: dict, task: EvalTask) -> dict:
  71. row = {}
  72. for label, fc in [("text", False), ("fc", True)]:
  73. holder = {}
  74. def wrapped(t, _fc=fc, _h=holder):
  75. r = _run_path(cfg, _fc, t); _h.update(r); return r
  76. r = run_eval_task(task, run_fn=wrapped,
  77. collect_artifacts_fn=lambda ws, _h=holder: _h.get("_files", {}),
  78. judge_fn=default_judge)
  79. row[label] = {"files_ok": r.files_ok, "judge": round(r.judge_score, 2),
  80. "passed": r.passed, "dt": round(holder.get("_dt", 0)), "term": holder.get("_term", "")}
  81. return row
  82. def main():
  83. paths = sys.argv[1:] or [os.path.join(os.path.dirname(__file__), "golden", "literature-mapper.yaml")]
  84. print(f"FC vs 文本 benchmark | model={MODEL} maxSteps={MAXSTEPS}\n")
  85. allrows = {}
  86. for p in paths:
  87. spec = yaml.safe_load(open(p, encoding="utf-8"))
  88. cfg = _agent_cfg_for(spec["agent_template"])
  89. for t in spec.get("tasks", []):
  90. task = EvalTask(id=t["id"], input=t.get("input", ""),
  91. must_produce=t.get("must_produce", []),
  92. rubric=t.get("rubric", ""), threshold=float(t.get("threshold", 0.6)))
  93. print(f"--- {spec['agent_template']} / {task.id} ---")
  94. row = bench_task(cfg, task)
  95. allrows[task.id] = row
  96. for lbl in ("text", "fc"):
  97. r = row[lbl]
  98. print(f" {lbl:5} files_ok={r['files_ok']} judge={r['judge']:.2f} "
  99. f"passed={r['passed']} {r['dt']}s")
  100. print()
  101. print("=== 汇总(judge: 文本 → FC) ===")
  102. for tid, row in allrows.items():
  103. print(f" {tid:20} {row['text']['judge']:.2f} → {row['fc']['judge']:.2f} "
  104. f"{'FC↑' if row['fc']['judge'] > row['text']['judge'] else ''}")
  105. out = os.path.join(os.path.dirname(__file__), "bench_fc_report.json")
  106. json.dump(allrows, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
  107. print(f"\n报告:{out}")
  108. if __name__ == "__main__":
  109. main()