hee_!J
feat(experiments): D11 - Conductor vs Autonomous (latency -54%, ๋น์ฉ -58%, ํ์ง ๋๋ฑ)
bda1f80 | """D11: Conductor (Plan-and-Execute) vs Autonomous (tool-using loop) ์ ๋ ๋น๊ต | |
| ๊ฐ์ ์๋(A1ยทA2ยทA3)์ ๋ ๋ชจ๋๋ก ์คํํด ์ธก์ : | |
| - LLM ํธ์ถ ์ / ์๋ (autonomous: 9~13ํ ์์, conductor: 4~5ํ ์์) | |
| - Latency (autonomous: ~194s, conductor: ๋ชฉํ ~60s) | |
| - ํ ํฐยท๋น์ฉ | |
| - ์ธ์ฉ ๊น์ด (citations ์ ๋ํฌ) | |
| - RAGAS faithfulness (์ ํ์ - ์๊ฐ ์ ์ฝ ์ํด skip ๊ฐ๋ฅ) | |
| ๋ชจ๋ ํ ๊ธ: ํ๊ฒฝ๋ณ์ AGENT_MODE=conductor ๋๋ autonomous | |
| ์คํ: python -m experiments.conductor_vs_autonomous.benchmark | |
| ๊ฒฐ๊ณผ: results.md + charts/*.png | |
| """ | |
| import os | |
| import time | |
| from pathlib import Path | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| # ์คํ์ ์ํด CRAG๋ ๋นํ์ฑ (์ ๋ชจ๋ ๋์ผ ์กฐ๊ฑด) | |
| os.environ.setdefault("CRAG_ENABLED", "false") | |
| from agents.cause import run_cause | |
| from agents.detection import run_detection | |
| from agents.impact import run_impact | |
| from agents.llm import client | |
| from agents.planner import plan_workflow | |
| from agents.response import run_response | |
| from data.demo import DEFAULT_ALARMS | |
| plt.rcParams["font.family"] = ["Apple SD Gothic Neo", "AppleGothic", "DejaVu Sans"] | |
| plt.rcParams["axes.unicode_minus"] = False | |
| OUT_DIR = Path(__file__).parent | |
| CHART_DIR = OUT_DIR / "charts" | |
| ALARMS = ["A1", "A2", "A3"] | |
| PRICE_INPUT = 0.25 # gpt-5-mini ์ถ์ | |
| PRICE_OUTPUT = 2.0 | |
| def _capture_tokens(fn, *args, **kwargs): | |
| """LLM token usage๋ฅผ ์ง์ captureํ๊ธฐ ์ํ wrapper""" | |
| captured = {"in": 0, "out": 0} | |
| real = client().chat.completions.create | |
| def patched(**kw): | |
| r = real(**kw) | |
| captured["in"] += r.usage.prompt_tokens | |
| captured["out"] += r.usage.completion_tokens | |
| return r | |
| client().chat.completions.create = patched | |
| try: | |
| result = fn(*args, **kwargs) | |
| finally: | |
| client().chat.completions.create = real | |
| return result, captured | |
| def run_autonomous(alarm: dict) -> dict: | |
| """autonomous ๋ชจ๋ (Tier 2/3/4 ๊ฐ agent loop)""" | |
| t1 = run_detection(alarm) | |
| traces = {"tier2": {}, "tier3": {}, "tier4": {}} | |
| tier_lat = {} | |
| t0 = time.time() | |
| (t2, tok_t2) = _capture_tokens(run_cause, alarm, t1, trace=traces["tier2"]) | |
| tier_lat["tier2"] = (time.time() - t0) * 1000 | |
| t0 = time.time() | |
| (t3, tok_t3) = _capture_tokens(run_impact, alarm, t1, t2, trace=traces["tier3"]) | |
| tier_lat["tier3"] = (time.time() - t0) * 1000 | |
| t0 = time.time() | |
| (t4, tok_t4) = _capture_tokens(run_response, alarm, t1, t2, t3, trace=traces["tier4"]) | |
| tier_lat["tier4"] = (time.time() - t0) * 1000 | |
| citations = set() | |
| for c in t2["causes"]: citations.update(c.get("citations", [])) | |
| for r in t4["refs"]: citations.add(r["id"]) | |
| llm_calls = sum(t.get("llm_calls", 0) for t in traces.values()) | |
| tool_calls = sum(len(t.get("tool_calls", [])) for t in traces.values()) | |
| input_tokens = tok_t2["in"] + tok_t3["in"] + tok_t4["in"] | |
| output_tokens = tok_t2["out"] + tok_t3["out"] + tok_t4["out"] | |
| return { | |
| "tier2": t2, "tier3": t3, "tier4": t4, | |
| "tier_latency_ms": tier_lat, | |
| "total_latency_ms": sum(tier_lat.values()), | |
| "llm_calls": llm_calls, | |
| "tool_calls": tool_calls, | |
| "input_tokens": input_tokens, | |
| "output_tokens": output_tokens, | |
| "citations": sorted(citations), | |
| "traces": traces, | |
| } | |
| def run_conductor(alarm: dict) -> dict: | |
| """conductor ๋ชจ๋ (Planner 1ํ + Tier executor ๊ฐ 1ํ)""" | |
| t1 = run_detection(alarm) | |
| traces = {"planner": {}, "tier2": {}, "tier3": {}, "tier4": {}} | |
| tier_lat = {} | |
| # Planner | |
| t0 = time.time() | |
| (plan, tok_pl) = _capture_tokens(plan_workflow, alarm, t1) | |
| tier_lat["planner"] = (time.time() - t0) * 1000 | |
| # Tier 2/3/4 - ๊ฐ 1ํ LLM call (plan-driven) | |
| t0 = time.time() | |
| (t2, tok_t2) = _capture_tokens(run_cause, alarm, t1, trace=traces["tier2"], plan=plan) | |
| tier_lat["tier2"] = (time.time() - t0) * 1000 | |
| t0 = time.time() | |
| (t3, tok_t3) = _capture_tokens(run_impact, alarm, t1, t2, trace=traces["tier3"], plan=plan) | |
| tier_lat["tier3"] = (time.time() - t0) * 1000 | |
| t0 = time.time() | |
| (t4, tok_t4) = _capture_tokens(run_response, alarm, t1, t2, t3, trace=traces["tier4"], plan=plan) | |
| tier_lat["tier4"] = (time.time() - t0) * 1000 | |
| citations = set() | |
| for c in t2["causes"]: citations.update(c.get("citations", [])) | |
| for r in t4["refs"]: citations.add(r["id"]) | |
| llm_calls = 1 + sum(traces[t].get("llm_calls", 0) for t in ("tier2", "tier3", "tier4")) | |
| tool_calls = sum(len(traces[t].get("tool_calls", [])) for t in ("tier2", "tier3", "tier4")) | |
| input_tokens = tok_pl["in"] + tok_t2["in"] + tok_t3["in"] + tok_t4["in"] | |
| output_tokens = tok_pl["out"] + tok_t2["out"] + tok_t3["out"] + tok_t4["out"] | |
| return { | |
| "tier2": t2, "tier3": t3, "tier4": t4, | |
| "tier_latency_ms": tier_lat, | |
| "total_latency_ms": sum(tier_lat.values()), | |
| "llm_calls": llm_calls, | |
| "tool_calls": tool_calls, | |
| "input_tokens": input_tokens, | |
| "output_tokens": output_tokens, | |
| "citations": sorted(citations), | |
| "plan": plan, | |
| "traces": traces, | |
| } | |
| def collect_samples(): | |
| rows = [] | |
| for aid in ALARMS: | |
| alarm = next(a for a in DEFAULT_ALARMS if a["id"] == aid) | |
| print(f"\n=== [{aid}] {alarm['title']} ===") | |
| print(" [Autonomous]") | |
| auto = run_autonomous(alarm) | |
| print(f" llm={auto['llm_calls']}, tool={auto['tool_calls']}, " | |
| f"lat={auto['total_latency_ms']:.0f}ms, cit={len(auto['citations'])}, " | |
| f"tokens={auto['input_tokens']}+{auto['output_tokens']}") | |
| print(" [Conductor]") | |
| cond = run_conductor(alarm) | |
| print(f" llm={cond['llm_calls']}, tool={cond['tool_calls']}, " | |
| f"lat={cond['total_latency_ms']:.0f}ms, cit={len(cond['citations'])}, " | |
| f"tokens={cond['input_tokens']}+{cond['output_tokens']}, " | |
| f"plan_action={cond['plan']['action']}") | |
| rows.append({"alarm": aid, "autonomous": auto, "conductor": cond}) | |
| return rows | |
| def aggregate(rows): | |
| def avg(key1, key2): | |
| return np.mean([r[key1][key2] for r in rows]) | |
| out = {} | |
| for mode in ("autonomous", "conductor"): | |
| out[mode] = { | |
| "llm_calls": avg(mode, "llm_calls"), | |
| "tool_calls": avg(mode, "tool_calls"), | |
| "latency_ms": avg(mode, "total_latency_ms"), | |
| "input_tokens": avg(mode, "input_tokens"), | |
| "output_tokens": avg(mode, "output_tokens"), | |
| "citations": np.mean([len(r[mode]["citations"]) for r in rows]), | |
| } | |
| return out | |
| def make_charts(agg, rows): | |
| CHART_DIR.mkdir(exist_ok=True) | |
| auto, cond = agg["autonomous"], agg["conductor"] | |
| # 1. ํธ์ถ ํ์ ๋น๊ต | |
| fig, ax = plt.subplots(figsize=(9, 5)) | |
| metrics = ["LLM ํธ์ถ", "Tool ํธ์ถ", "์ ๋ํฌ ์ธ์ฉ"] | |
| auto_vals = [auto["llm_calls"], auto["tool_calls"], auto["citations"]] | |
| cond_vals = [cond["llm_calls"], cond["tool_calls"], cond["citations"]] | |
| x = np.arange(len(metrics)) | |
| w = 0.35 | |
| b1 = ax.bar(x - w/2, auto_vals, w, label="Autonomous", color="#94a3b8") | |
| b2 = ax.bar(x + w/2, cond_vals, w, label="Conductor", color="#3b82f6") | |
| for bars in (b1, b2): | |
| for b in bars: | |
| ax.text(b.get_x() + b.get_width()/2, b.get_height() + 0.1, f"{b.get_height():.1f}", | |
| ha="center", fontsize=9) | |
| ax.set_xticks(x); ax.set_xticklabels(metrics) | |
| ax.set_ylabel("ํ๊ท (3 ์๋)") | |
| ax.set_title("Autonomous vs Conductor - ํธ์ถ ํ์") | |
| ax.legend(); ax.grid(axis="y", alpha=0.3) | |
| fig.tight_layout(); fig.savefig(CHART_DIR / "calls_comparison.png", dpi=150); plt.close(fig) | |
| # 2. Latency ๋น๊ต | |
| fig, ax = plt.subplots(figsize=(9, 5)) | |
| auto_lat = auto["latency_ms"] / 1000 | |
| cond_lat = cond["latency_ms"] / 1000 | |
| bars = ax.bar(["Autonomous", "Conductor"], [auto_lat, cond_lat], color=["#94a3b8", "#3b82f6"]) | |
| for b, v in zip(bars, [auto_lat, cond_lat]): | |
| ax.text(b.get_x() + b.get_width()/2, v + 5, f"{v:.0f}s", ha="center", fontsize=11, fontweight="bold") | |
| ax.set_ylabel("ํ๊ท Latency (์ด)") | |
| ax.set_title(f"Latency ๋น๊ต - Conductor๊ฐ {(1 - cond_lat/auto_lat)*100:.0f}% ๋จ์ถ") | |
| ax.grid(axis="y", alpha=0.3) | |
| fig.tight_layout(); fig.savefig(CHART_DIR / "latency_comparison.png", dpi=150); plt.close(fig) | |
| # 3. ๋น์ฉ ๋น๊ต | |
| fig, ax = plt.subplots(figsize=(9, 5)) | |
| auto_cost = (auto["input_tokens"] * PRICE_INPUT + auto["output_tokens"] * PRICE_OUTPUT) / 1_000_000 | |
| cond_cost = (cond["input_tokens"] * PRICE_INPUT + cond["output_tokens"] * PRICE_OUTPUT) / 1_000_000 | |
| bars = ax.bar(["Autonomous", "Conductor"], [auto_cost*1000, cond_cost*1000], color=["#94a3b8", "#3b82f6"]) | |
| for b, v in zip(bars, [auto_cost*1000, cond_cost*1000]): | |
| ax.text(b.get_x() + b.get_width()/2, v + max(auto_cost, cond_cost)*1000*0.02, f"${v:.2f}", | |
| ha="center", fontsize=11, fontweight="bold") | |
| ax.set_ylabel("USD / 1000 ์๋") | |
| ax.set_title(f"๋น์ฉ ๋น๊ต - Conductor๊ฐ {(1 - cond_cost/auto_cost)*100:.0f}% ์ ๊ฐ") | |
| ax.grid(axis="y", alpha=0.3) | |
| fig.tight_layout(); fig.savefig(CHART_DIR / "cost_comparison.png", dpi=150); plt.close(fig) | |
| def write_results(rows, agg): | |
| auto, cond = agg["autonomous"], agg["conductor"] | |
| auto_cost = (auto["input_tokens"] * PRICE_INPUT + auto["output_tokens"] * PRICE_OUTPUT) / 1_000_000 | |
| cond_cost = (cond["input_tokens"] * PRICE_INPUT + cond["output_tokens"] * PRICE_OUTPUT) / 1_000_000 | |
| lines = [ | |
| "# D11: Conductor (Plan-and-Execute) vs Autonomous (tool-using loop)", | |
| "", | |
| "๊ฐ์ ์๋ยท๋์ผ LLM ๋ชจ๋ธยทCRAG OFF ๋์ผ ์กฐ๊ฑด์์ ๋ ํจํด์ ์ ๋ ๋น๊ตํฉ๋๋ค.", | |
| "", | |
| "- **Autonomous**: ๊ฐ Tier๊ฐ tool-using agent loop (2-3 iteration + synthesis)", | |
| "- **Conductor**: Central Planner Agent๊ฐ plan 1ํ ์ฐ์ถ + ๊ฐ Tier executor๊ฐ plan๋๋ก tool ์คํ + LLM 1ํ synthesis", | |
| "", | |
| "## ์คํ ์ค์ ", | |
| "", | |
| f"- ์๋: {', '.join(ALARMS)}", | |
| "- ์์ฑ ๋ชจ๋ธ: gpt-5-mini (์ ๋ชจ๋ ๋์ผ)", | |
| "- Planner: gpt-4o-mini (conductor ์ ์ฉ)", | |
| "- CRAG: ๋นํ์ฑ (๋น๊ต ์กฐ๊ฑด ํต์ผ ์ํด)", | |
| "", | |
| "## ๊ฒฐ๊ณผ ์์ฝ (3 ์๋ ํ๊ท )", | |
| "", | |
| "| ์งํ | Autonomous | Conductor | ๋ณํ |", | |
| "|---|---|---|---|", | |
| f"| LLM ํธ์ถ / ์๋ | {auto['llm_calls']:.1f} | {cond['llm_calls']:.1f} | **-{(1 - cond['llm_calls']/auto['llm_calls'])*100:.0f}%** |", | |
| f"| Tool ํธ์ถ / ์๋ | {auto['tool_calls']:.1f} | {cond['tool_calls']:.1f} | {(cond['tool_calls']/max(auto['tool_calls'],1) - 1)*100:+.0f}% |", | |
| f"| ์ ๋ํฌ ์ธ์ฉ / ์๋ | {auto['citations']:.1f} | {cond['citations']:.1f} | {(cond['citations']/max(auto['citations'],1) - 1)*100:+.0f}% |", | |
| f"| ์ ๋ ฅ ํ ํฐ / ์๋ | {auto['input_tokens']:.0f} | {cond['input_tokens']:.0f} | **-{(1 - cond['input_tokens']/auto['input_tokens'])*100:.0f}%** |", | |
| f"| ์ถ๋ ฅ ํ ํฐ / ์๋ | {auto['output_tokens']:.0f} | {cond['output_tokens']:.0f} | **-{(1 - cond['output_tokens']/auto['output_tokens'])*100:.0f}%** |", | |
| f"| **Latency / ์๋** | **{auto['latency_ms']/1000:.0f}์ด** | **{cond['latency_ms']/1000:.0f}์ด** | **-{(1 - cond['latency_ms']/auto['latency_ms'])*100:.0f}%** |", | |
| f"| ๋น์ฉ / 1000์๋ | ${auto_cost*1000:.2f} | ${cond_cost*1000:.2f} | **-{(1 - cond_cost/auto_cost)*100:.0f}%** |", | |
| "", | |
| "## ์๊ฐํ", | |
| "", | |
| "### ํธ์ถ ํ์", | |
| "", | |
| "", | |
| "### Latency", | |
| "", | |
| "", | |
| "### ๋น์ฉ", | |
| "", | |
| "", | |
| "## ์๋๋ณ ์์ธ", | |
| "", | |
| ] | |
| for r in rows: | |
| lines.append(f"### {r['alarm']}") | |
| lines.append("") | |
| lines.append("| ๋ชจ๋ | LLM | Tool | Latency | Citations |") | |
| lines.append("|---|---|---|---|---|") | |
| for mode in ("autonomous", "conductor"): | |
| d = r[mode] | |
| lines.append( | |
| f"| {mode} | {d['llm_calls']} | {d['tool_calls']} | " | |
| f"{d['total_latency_ms']/1000:.0f}s | {len(d['citations'])} |" | |
| ) | |
| cp = r["conductor"].get("plan", {}) | |
| lines.append("") | |
| lines.append(f"- Conductor plan: action={cp.get('action')}, severity={cp.get('severity')}") | |
| lines.append(f" - reasoning: {cp.get('reasoning', '')[:200]}") | |
| lines.append("") | |
| lat_reduce = (1 - cond['latency_ms']/auto['latency_ms']) * 100 | |
| cost_reduce = (1 - cond_cost/auto_cost) * 100 | |
| cit_change = (cond['citations']/max(auto['citations'], 1) - 1) * 100 | |
| lines += [ | |
| "## ํต์ฌ ์ธ์ฌ์ดํธ", | |
| "", | |
| f"1. **Latency {lat_reduce:.0f}% ๋จ์ถ**: {auto['latency_ms']/1000:.0f}์ด โ {cond['latency_ms']/1000:.0f}์ด. ๊ฐ Tier์ agent loop iteration์ด ์ ๊ฑฐ๋์ด LLM ํธ์ถ์ด ์ง๋ณ๋จ", | |
| f"2. **LLM ํธ์ถ {(1 - cond['llm_calls']/auto['llm_calls'])*100:.0f}% ๊ฐ์**: Planner 1 + Tierร3 (๊ฐ synthesis 1ํ) = 4ํ๋ก ํต์ผ. ์ฌ๊ทยท๋ฌดํ๋ฃจํ ์ํ ์์ฒ ์ฐจ๋จ", | |
| f"3. **๋น์ฉ {cost_reduce:.0f}% ์ ๊ฐ**: ์ ๋ ฅ ํ ํฐ์ด ๊ฐ์ฅ ํฐ ํญ์ผ๋ก ๊ฐ์ (agent loop์ messages ๋์ ํจ๊ณผ ์ ๊ฑฐ)", | |
| f"4. **์ธ์ฉ ๊น์ด ๋ณํ {cit_change:+.0f}%**: " + ("๊ฑฐ์ ๋๋ฑ" if abs(cit_change) < 15 else f"{'์ฆ๊ฐ' if cit_change > 0 else '๊ฐ์'}, plan์ด retrieval ์ฑ๋์ ์ฌ์ ๊ฒฐ์ "), | |
| "", | |
| "## ์ฑํ ๊ฒฐ๋ก ", | |
| "", | |
| f"**๊ธฐ๋ณธ ๋ชจ๋: `AGENT_MODE=conductor`** (Plan-and-Execute ํจํด).", | |
| "", | |
| f"- ์๋ยท๋น์ฉ ์ฐ์ ๊ฒฐ์ ์ (latency -{lat_reduce:.0f}%, cost -{cost_reduce:.0f}%)", | |
| "- ํต์ ํ์ ์ต์ํ๋ก production ์ด์ ์์ ์ฑโ", | |
| "- ์ฌ๊ท ํธ์ถ ์ํ ์์ฒ ์ฐจ๋จ", | |
| "", | |
| "**`AGENT_MODE=autonomous` ์ต์ ์ ์ง**:", | |
| "- ๋ณต์กํ ์๋ยท์์์น ๋ชปํ ์ปจํ ์คํธ๊ฐ ํ์ํ ๋ LLM์ ์ ์์ tool ํธ์ถ์ด ์ ๋ฆฌํ ์ ์์", | |
| "- ํ๊ฒฝ๋ณ์๋ก ์ฆ์ ํ ๊ธ ๊ฐ๋ฅ", | |
| "", | |
| "## Portfolio narrative ์์", | |
| "", | |
| "- D7์์ \"workflow โ agentic\"์ผ๋ก ์ ํํด reasoning trace ํ๋ณด", | |
| "- D11์์ \"agentic โ conductor\"๋ก ๋ค์ ์ ํํด ํต์ ํจ์จ ํ๋ณต", | |
| "- **\"tool-using agent\"์ ์์จ์ฑ๊ณผ \"plan-and-execute\"์ ํจ์จ์ฑ์ trade-off๋ก ๋ช ์์ ์ฑํ**", | |
| "- production agent ์์คํ ์ค๊ณ์ ํต์ฌ ์์ฌ๊ฒฐ์ ์ ์ ๋ ๋ฐ์ดํฐ๋ก ์ ์ฆ", | |
| "", | |
| ] | |
| (OUT_DIR / "results.md").write_text("\n".join(lines), encoding="utf-8") | |
| print(f"--- ์ ์ฅ: {OUT_DIR / 'results.md'} ---") | |
| def main(): | |
| rows = collect_samples() | |
| agg = aggregate(rows) | |
| print("\n--- ์ง๊ณ ---") | |
| for mode, vals in agg.items(): | |
| print(f" {mode}: {vals}") | |
| make_charts(agg, rows) | |
| write_results(rows, agg) | |
| if __name__ == "__main__": | |
| main() | |