"""E1 token 经济学实验脚本(论文主实验,本地确定性可跑)。 对比四种策略下 Architect(大模型)单请求输入 token 量: A1 全量上下文 :每轮把完整历史+工件全文发给 Architect(无压缩基线) A2 交流文本协议:只用 render_for_architect 压缩摘要(D7) A3 A2 + rollup :先把已完成步骤折叠为 archive 摘要行再渲染 A4 A3 + prefix :记录可被 --cache-reuse 命中的稳定前缀 token(降低 prefill 成本) 北极星指标(方案 1.0):A2/A3/A4 相对 A1 的 token 下降 ≥80%。 用法: python scripts/bench_tokens.py [--data eval/v2_sample.json] [--out research/v2_experiments] 本地模式:不调用真实 API,用 estimate_tokens 对策略做确定性测量,输出 CSV+MD。 --live 模式(可选,需 API key + 本地模型):走真实管线记录 usage。 """ from __future__ import annotations import argparse import csv import json import sys from pathlib import Path if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from router_system.workspace import Workspace, estimate_tokens # noqa: E402 # 每步模拟工件文本(本地模式用,代表真实产物体量) _ARTIFACT_TEMPLATE = ( "(工件){domain} 步骤实现说明:这是第 {i} 步的完整实现细节与说明文本," "包含关键逻辑、边界处理与可运行示例,长度适中以模拟真实产物。" ) def _brief_for(query: str, domain: str, n_steps: int = 3) -> dict: return { "goal": query, "constraints": ["遵守领域规范", "输出可交付"], "tags": [domain], "acceptance": [{"id": "a1", "check": "满足用户需求", "machine_checkable": True}], "plan": [ {"id": f"s{i+1}", "task": f"{domain} 步骤{i+1}:推进目标", "deps": [] if i == 0 else [f"s{i}"], "done_criteria": "达到步骤目标"} for i in range(n_steps) ], } def build_workspace(query: str, domain: str, n_steps: int = 3, n_rounds: int = 3) -> Workspace: """构造一个模拟进行到中后期的交流文本(含 progress/issues/decisions)。""" ws = Workspace.new("bench" + query.encode("utf-8").hex()[:8], query, api_token_cap=8000, rounds_cap=6) ws.apply_brief(_brief_for(query, domain, n_steps)) # 已完成前 n_rounds 步(至少 1),最后一步待办 done_steps = max(1, min(n_rounds, n_steps)) for i in range(done_steps): ws.add_progress(f"s{i+1}", "done", f"步骤{i+1}完成:{_ARTIFACT_TEMPLATE.format(domain=domain, i=i+1)[:60]}", artifact=f"a://s{i+1}.py" if domain == "code" else f"a://s{i+1}.md") # 加入 issue + decision(模拟一轮裁决) if done_steps < n_steps: iid = ws.add_issue(f"s{done_steps+1}", f"a://s{done_steps+1}.py#L1", "验证未通过", "达到目标", "已自修 2 次", "请裁决") ws.add_decision(iid, "按此方向继续推进", [{"id": f"s{done_steps+1}", "task": "按裁决修订"}]) ws.mark_round() return ws def _artifact_text(domain: str, i: int) -> str: return _ARTIFACT_TEMPLATE.format(domain=domain, i=i) def measure(ws: Workspace, n_steps: int = 3): """测量四种策略的单请求 Architect 输入 token。""" domain = (ws.get("brief") or {}).get("tags", ["general"])[0] # A1 全量上下文:把完整历史逐字发送(query + brief 全文 + 全部工件全文 + # 全部 issues/decisions/progress 全文),无任何压缩。 a1 = _full_context_tokens(ws, domain, n_steps) # A2 交流文本:render_for_architect a2 = estimate_tokens(ws.render_for_architect()) # A3 A2 + rollup ws3 = Workspace(ws.data) ws3.rollup() a3 = estimate_tokens(ws3.render_for_architect()) # A4 A3 + prefix:token 数同 A3;prefix_hit 为可复用稳定前缀 prefix_hit = estimate_tokens(_prefix_region(ws)) return {"a1": a1, "a2": a2, "a3": a3, "a4": a3, "prefix_hit": prefix_hit} def _full_context_tokens(ws: Workspace, domain: str, n_steps: int) -> int: """A1 基线:完整逐字上下文的 token 数。""" d = ws.data total = estimate_tokens(d.get("query", "")) # brief 全文(含 goal/constraints/plan 全部字段) total += estimate_tokens(json.dumps(d.get("brief"), ensure_ascii=False)) # 全部工件全文 total += sum(estimate_tokens(_artifact_text(domain, i + 1)) for i in range(n_steps)) # issues / decisions / progress 全文 for iss in d.get("issues", []) or []: total += estimate_tokens(json.dumps(iss, ensure_ascii=False)) for dec in d.get("decisions", []) or []: total += estimate_tokens(json.dumps(dec, ensure_ascii=False)) for p in d.get("progress", []) or []: total += estimate_tokens(json.dumps(p, ensure_ascii=False)) return total def _prefix_region(ws: Workspace) -> str: """稳定前缀(可被 prefix cache 命中)的文本。""" d = ws.data stable = {"version": d.get("version"), "request_id": d.get("request_id"), "query": d.get("query"), "brief": d.get("brief")} return json.dumps(stable, ensure_ascii=False) def run(data_path: str, out_dir: str, n_steps: int = 3) -> None: items = json.loads(Path(data_path).read_text(encoding="utf-8")) out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) rows = [] for it in items: ws = build_workspace(it["query"], it.get("domain", "general"), n_steps) m = measure(ws, n_steps) rows.append({ "id": it["id"], "domain": it.get("domain", "general"), "a1_full": m["a1"], "a2_ws": m["a2"], "a3_rollup": m["a3"], "a4_prefix": m["a4"], "prefix_hit": m["prefix_hit"], "reduction_a2": round(1 - m["a2"] / m["a1"], 4) if m["a1"] else 0, "reduction_a4": round(1 - m["a4"] / m["a1"], 4) if m["a1"] else 0, }) # CSV csv_path = out / "E1_token_economics.csv" with open(csv_path, "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) w.writeheader() w.writerows(rows) # 聚合 n = len(rows) avg = {k: round(sum(r[k] for r in rows) / n, 2) for k in ("a1_full", "a2_ws", "a3_rollup", "a4_prefix", "prefix_hit")} red_a2 = round(1 - avg["a2_ws"] / avg["a1_full"], 4) red_a4 = round(1 - avg["a4_prefix"] / avg["a1_full"], 4) md = _render_md(rows, avg, red_a2, red_a4) (out / "E1_token_economics.md").write_text(md, encoding="utf-8") print(f"写入: {csv_path}") print(f"写入: {out / 'E1_token_economics.md'}") print(f"汇总: A1={avg['a1_full']} A2={avg['a2_ws']} A3={avg['a3_rollup']} " f"A4={avg['a4_prefix']} prefix_hit={avg['prefix_hit']}") print(f"token 下降: A2 相对 A1 = {red_a2*100:.1f}% | A4 相对 A1 = {red_a4*100:.1f}%") def _render_md(rows, avg, red_a2, red_a4) -> str: lines = [ "# E1 token 经济学(本地确定性测量)", "", "> 模式:本地 estimate_tokens 测量(不调用真实 API)。真实数据需 --live + API key + 本地模型。", "", f"- 样例数:{len(rows)}", f"- A1 全量上下文均值:**{avg['a1_full']} token**", f"- A2 交流文本均值:**{avg['a2_ws']} token**", f"- A3 A2+rollup 均值:**{avg['a3_rollup']} token**", f"- A4 A3+prefix 均值:**{avg['a4_prefix']} token**(prefix 可命中 {avg['prefix_hit']} token)", "", f"## 北极星指标(token 下降 ≥80%)", "", f"- A2 相对 A1:**{red_a2*100:.1f}%**", f"- A4 相对 A1:**{red_a4*100:.1f}%**", "", "### 说明(诚实解读)", "", "1. 本报告为本地确定性测量(estimate_tokens),未调用真实 API。", "2. A3(rollup)收益为规模相关:小样例下 archive 增量可能抵消收益,长会话才显现。", "3. 前缀稳定性(T10)已验证,配合 llama-server --cache-reuse 可复用稳定前缀。", "4. 北极星 ≥80% 需在 --live 模式(API key + 本地模型)下由 E1 实验确认。", "", "## 明细", "", "| id | domain | A1 | A2 | A3 | A4 | prefix_hit |", "|----|--------|----|----|----|----|----|", ] for r in rows: lines.append(f"| {r['id']} | {r['domain']} | {r['a1_full']} | {r['a2_ws']} | " f"{r['a3_rollup']} | {r['a4_prefix']} | {r['prefix_hit']} |") return "\n".join(lines) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--data", default="eval/v2_sample.json") ap.add_argument("--out", default="research/v2_experiments") ap.add_argument("--steps", type=int, default=3) ap.add_argument("--live", action="store_true", help="真实 API(需 key + 本地模型)") args = ap.parse_args() if args.live: print("[warn] --live 需 API key + 本地 llama-server;当前未实现自动跑数,请接入后使用。") run(args.data, args.out, args.steps) return 0 if __name__ == "__main__": sys.exit(main())