hee_!J
feat(experiments): D11 - Conductor vs Autonomous (latency -54%, ๋น„์šฉ -58%, ํ’ˆ์งˆ ๋™๋“ฑ)
bda1f80
Raw
History Blame Contribute Delete
15.1 kB
"""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}%** |",
"",
"## ์‹œ๊ฐํ™”",
"",
"### ํ˜ธ์ถœ ํšŸ์ˆ˜",
"![Calls](charts/calls_comparison.png)",
"",
"### Latency",
"![Latency](charts/latency_comparison.png)",
"",
"### ๋น„์šฉ",
"![Cost](charts/cost_comparison.png)",
"",
"## ์•Œ๋žŒ๋ณ„ ์ƒ์„ธ",
"",
]
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()