fabagent / experiments /crag_eval /benchmark.py
hee_!J
feat(experiments): D8 CRAG ON vs OFF ์ •๋Ÿ‰ ๋น„๊ต (์†”์งํ•œ ๊ฒฐ๊ณผ ๋ฐ˜์˜)
f4addf3
Raw
History Blame Contribute Delete
18.7 kB
"""CRAG (Self-correction) ํšจ๊ณผ ์ •๋Ÿ‰ ํ‰๊ฐ€
๋™์ผํ•œ agentic Tier 2๋ฅผ CRAG ON/OFF ๋‘ ๋ชจ๋“œ๋กœ ์‹คํ–‰ํ•ด ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.
- CRAG OFF: search_knowledge๊ฐ€ hybrid ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜
- CRAG ON: search_knowledge๊ฐ€ ๊ฒ€์ƒ‰ ํ›„ LLM grader๋กœ ํ‰๊ฐ€, ์ž„๊ณ„์น˜ ๋ฏธ๋‹ฌ ์‹œ ์ฟผ๋ฆฌ refine + ์žฌ๊ฒ€์ƒ‰
์ธก์ • ์ง€ํ‘œ:
- Refinement ๋ฐœ์ƒ๋ฅ  (CRAG ON์—์„œ ์ž๊ฐ€ ์ •์ • ๋นˆ๋„)
- ์ธ์šฉ๋œ ๋ฌธ์„œ์˜ ํ‰๊ท  relevance_score (CRAG ON์—์„œ๋งŒ)
- LLM ํ˜ธ์ถœ ์ˆ˜, ํ† ํฐ, latency, ๋น„์šฉ ์ฆ๊ฐ€
- RAGAS faithfulness / answer_relevancy (๊ฒ€์ƒ‰ ํ’ˆ์งˆ์ด ๋‹ต๋ณ€ ํ’ˆ์งˆ๋กœ ์ด์–ด์กŒ๋Š”๊ฐ€)
3 ์•Œ๋žŒ(A1ยทA2ยทA3) ๊ฐ๊ฐ์— ๋Œ€ํ•ด ๋‘ ๋ชจ๋“œ ์‹คํ–‰, ์ฐจํŠธ 3์ข… + results.md.
์‹คํ–‰: python -m experiments.crag_eval.benchmark
"""
import json
import os
import time
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from datasets import Dataset
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from ragas import evaluate
from ragas.embeddings import LangchainEmbeddingsWrapper
from ragas.llms import LangchainLLMWrapper
from ragas.metrics import Faithfulness, ResponseRelevancy
from agents.cause import run_cause
from agents.detection import run_detection
from agents.llm import client
from agents.rag.store import load_document, search
from agents.tools import knowledge as knowledge_tool
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
PRICE_OUTPUT = 2.0
def _alarm_by_id(aid: str) -> dict:
return next(a for a in DEFAULT_ALARMS if a["id"] == aid)
def _run_tier2_with_capture(alarm: dict, tier1, crag_on: bool, trace: dict):
"""Tier 2 ์‹คํ–‰ + token/CRAG trace ๋ชจ๋‘ capture"""
os.environ["CRAG_ENABLED"] = "true" if crag_on else "false"
knowledge_tool.reset_crag_trace()
captured = {"in": 0, "out": 0}
real = client().chat.completions.create
def patched(**kwargs):
r = real(**kwargs)
captured["in"] += r.usage.prompt_tokens
captured["out"] += r.usage.completion_tokens
return r
client().chat.completions.create = patched
try:
t0 = time.time()
result = run_cause(alarm, tier1, trace=trace)
latency_ms = (time.time() - t0) * 1000
finally:
client().chat.completions.create = real
crag_events = knowledge_tool.reset_crag_trace()
trace["latency_ms"] = latency_ms
trace["input_tokens"] = captured["in"]
trace["output_tokens"] = captured["out"]
trace["crag_events"] = crag_events
trace["crag_search_calls"] = len(crag_events)
trace["refinement_triggered"] = sum(1 for e in crag_events if e["retry"] > 0)
trace["avg_relevance"] = (
np.mean([e["avg_score"] for e in crag_events]) if crag_events else 0.0
)
return result
def collect_samples():
rows = []
for aid in ALARMS:
alarm = _alarm_by_id(aid)
tier1 = run_detection(alarm)
print(f"\n=== [{aid}] {alarm['title']} (T1 score={tier1['score']}) ===")
# CRAG OFF
trace_off = {}
t2_off = _run_tier2_with_capture(alarm, tier1, crag_on=False, trace=trace_off)
cits_off = sorted({c for cause in t2_off["causes"] for c in cause.get("citations", [])})
print(f" [CRAG OFF] llm={trace_off['llm_calls']}, lat={trace_off['latency_ms']:.0f}ms, "
f"citations={len(cits_off)}")
# CRAG ON
trace_on = {}
t2_on = _run_tier2_with_capture(alarm, tier1, crag_on=True, trace=trace_on)
cits_on = sorted({c for cause in t2_on["causes"] for c in cause.get("citations", [])})
print(f" [CRAG ON ] llm={trace_on['llm_calls']}, lat={trace_on['latency_ms']:.0f}ms, "
f"citations={len(cits_on)}, refine={trace_on['refinement_triggered']}/"
f"{trace_on['crag_search_calls']}, avg_rel={trace_on['avg_relevance']:.2f}")
rows.append({
"alarm": aid,
"off": {"trace": trace_off, "tier2": t2_off, "citations": cits_off,
"question": _build_question(alarm, tier1)},
"on": {"trace": trace_on, "tier2": t2_on, "citations": cits_on,
"question": _build_question(alarm, tier1)},
})
return rows
def _build_question(alarm, tier1):
sensors = " ".join(f["name"] for f in tier1["features"])
return f"{alarm['title']} {alarm.get('feature') or ''} {sensors} ์›์ธ ๋ถ„์„"
def _format_answer(tier2):
return "\n".join(
f"- {c['name']} ({c['pct']}%): {c['evidence']}" for c in tier2["causes"]
)
def evaluate_quality(rows):
"""RAGAS ํ‰๊ฐ€: ์–‘์ชฝ ๋‹ต๋ณ€ ํ’ˆ์งˆ (citations๋กœ contexts ๊ตฌ์„ฑ)"""
print("\n=== RAGAS ํ‰๊ฐ€ ===")
eval_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini", temperature=0))
eval_emb = LangchainEmbeddingsWrapper(OpenAIEmbeddings(model="text-embedding-3-small"))
qs, ans, ctxs, labels = [], [], [], []
for r in rows:
for mode in ("off", "on"):
ctx_docs = [load_document(c) for c in r[mode]["citations"] if load_document(c)]
if not ctx_docs:
ctx_docs = ["(citation ์—†์Œ)"]
qs.append(r[mode]["question"])
ans.append(_format_answer(r[mode]["tier2"]))
ctxs.append(ctx_docs)
labels.append((r["alarm"], mode))
dataset = Dataset.from_dict({"question": qs, "answer": ans, "contexts": ctxs})
result = evaluate(dataset=dataset, metrics=[
Faithfulness(llm=eval_llm),
ResponseRelevancy(llm=eval_llm, embeddings=eval_emb),
])
df = result.to_pandas()
df["alarm"] = [l[0] for l in labels]
df["mode"] = [l[1] for l in labels]
return df
def aggregate(rows, ragas_df):
def per_mode(mode):
traces = [r[mode]["trace"] for r in rows]
sub = ragas_df[ragas_df["mode"] == mode]
return {
"llm_calls": np.mean([t["llm_calls"] for t in traces]),
"latency_ms": np.mean([t["latency_ms"] for t in traces]),
"input_tokens": np.mean([t["input_tokens"] for t in traces]),
"output_tokens": np.mean([t["output_tokens"] for t in traces]),
"citations": np.mean([len(r[mode]["citations"]) for r in rows]),
"refinement_triggered": np.mean([t.get("refinement_triggered", 0) for t in traces]),
"crag_search_calls": np.mean([t.get("crag_search_calls", 0) for t in traces]),
"avg_relevance": np.mean([t.get("avg_relevance", 0.0) for t in traces if t.get("crag_search_calls", 0) > 0]) if mode == "on" else None,
"faithfulness": sub["faithfulness"].mean(),
"answer_relevancy": sub["answer_relevancy"].mean(),
}
return {"off": per_mode("off"), "on": per_mode("on")}
def make_charts(agg, rows):
CHART_DIR.mkdir(exist_ok=True)
off, on = agg["off"], agg["on"]
# 1. ํ’ˆ์งˆ ๋น„๊ต (RAGAS)
fig, ax = plt.subplots(figsize=(8.5, 5))
metrics = ["Faithfulness", "Answer Relevancy"]
off_vals = [off["faithfulness"], off["answer_relevancy"]]
on_vals = [on["faithfulness"], on["answer_relevancy"]]
x = np.arange(len(metrics))
w = 0.35
b1 = ax.bar(x - w/2, off_vals, w, label="CRAG OFF", color="#94a3b8")
b2 = ax.bar(x + w/2, on_vals, w, label="CRAG ON", color="#3b82f6")
for bars in (b1, b2):
for b in bars:
ax.text(b.get_x() + b.get_width()/2, b.get_height() + 0.015,
f"{b.get_height():.3f}", ha="center", fontsize=9)
ax.set_xticks(x); ax.set_xticklabels(metrics)
ax.set_ylim(0, 1.1); ax.set_ylabel("RAGAS Score (0~1, ๋†’์„์ˆ˜๋ก ์ข‹์Œ)")
ax.set_title("CRAG ํšจ๊ณผ - ๋‹ต๋ณ€ ํ’ˆ์งˆ (3 ์•Œ๋žŒ ํ‰๊ท )")
ax.legend(); ax.grid(axis="y", alpha=0.3)
fig.tight_layout(); fig.savefig(CHART_DIR / "quality.png", dpi=150); plt.close(fig)
# 2. CRAG ๋™์ž‘ ๋ถ„ํฌ (per alarm: search ํ˜ธ์ถœ ์ˆ˜, refinement ๋ฐœ๋™ ์ˆ˜, avg score)
fig, ax = plt.subplots(figsize=(9, 5))
alarms = [r["alarm"] for r in rows]
search_counts = [r["on"]["trace"]["crag_search_calls"] for r in rows]
refine_counts = [r["on"]["trace"]["refinement_triggered"] for r in rows]
avg_rels = [r["on"]["trace"]["avg_relevance"] for r in rows]
x = np.arange(len(alarms))
w = 0.35
ax.bar(x - w/2, search_counts, w, label="search_knowledge ํ˜ธ์ถœ", color="#60a5fa")
ax.bar(x + w/2, refine_counts, w, label="refinement ๋ฐœ๋™", color="#f59e0b")
for i, v in enumerate(search_counts):
ax.text(i - w/2, v + 0.1, str(v), ha="center", fontsize=9)
for i, v in enumerate(refine_counts):
ax.text(i + w/2, v + 0.1, str(v), ha="center", fontsize=9)
ax.set_xticks(x); ax.set_xticklabels(alarms)
ax.set_ylabel("ํ˜ธ์ถœ ์ˆ˜")
ax.set_title("CRAG ๋™์ž‘ ๋ถ„ํฌ (์•Œ๋žŒ๋ณ„ self-correction ํ™œ๋™)")
ax.legend(loc="upper left"); ax.grid(axis="y", alpha=0.3)
ax2 = ax.twinx()
ax2.plot(x, avg_rels, "o-", color="#ef4444", markersize=10, label="ํ‰๊ท  relevance_score")
for i, v in enumerate(avg_rels):
ax2.text(i, v + 0.02, f"{v:.2f}", ha="center", color="#ef4444", fontsize=9, fontweight="bold")
ax2.set_ylabel("ํ‰๊ท  relevance_score", color="#ef4444")
ax2.set_ylim(0, 1.05); ax2.tick_params(axis="y", labelcolor="#ef4444")
ax2.legend(loc="upper right")
fig.tight_layout(); fig.savefig(CHART_DIR / "crag_activity.png", dpi=150); plt.close(fig)
# 3. ๋น„์šฉยทlatency ์˜ค๋ฒ„ํ—ค๋“œ
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
off_cost = (off["input_tokens"] * PRICE_INPUT + off["output_tokens"] * PRICE_OUTPUT) / 1_000_000
on_cost = (on["input_tokens"] * PRICE_INPUT + on["output_tokens"] * PRICE_OUTPUT) / 1_000_000
axes[0].bar(["CRAG OFF", "CRAG ON"], [off["latency_ms"], on["latency_ms"]],
color=["#94a3b8", "#3b82f6"])
axes[0].set_ylabel("ํ‰๊ท  latency (ms)")
axes[0].set_title("Tier 2 Latency")
for i, v in enumerate([off["latency_ms"], on["latency_ms"]]):
axes[0].text(i, v + max(off["latency_ms"], on["latency_ms"]) * 0.02, f"{v:.0f}ms", ha="center", fontsize=10)
axes[0].grid(axis="y", alpha=0.3)
axes[1].bar(["CRAG OFF", "CRAG ON"], [off_cost*1000, on_cost*1000],
color=["#94a3b8", "#3b82f6"])
axes[1].set_ylabel("USD / 1000 ์•Œ๋žŒ")
axes[1].set_title(f"Tier 2 ๋น„์šฉ (gpt-5-mini in=${PRICE_INPUT}/M out=${PRICE_OUTPUT}/M)")
for i, v in enumerate([off_cost*1000, on_cost*1000]):
axes[1].text(i, v + max(off_cost, on_cost)*1000*0.02, f"${v:.2f}", ha="center", fontsize=10)
axes[1].grid(axis="y", alpha=0.3)
fig.tight_layout(); fig.savefig(CHART_DIR / "overhead.png", dpi=150); plt.close(fig)
def write_results(rows, agg, ragas_df):
off, on = agg["off"], agg["on"]
off_cost = (off["input_tokens"] * PRICE_INPUT + off["output_tokens"] * PRICE_OUTPUT) / 1_000_000
on_cost = (on["input_tokens"] * PRICE_INPUT + on["output_tokens"] * PRICE_OUTPUT) / 1_000_000
lines = [
"# CRAG (Self-correction) ํšจ๊ณผ ์ •๋Ÿ‰ ํ‰๊ฐ€",
"",
"Tier 2 Cause agent๋ฅผ CRAG ON/OFF ๋‘ ๋ชจ๋“œ๋กœ ์‹คํ–‰ํ•ด self-correction์˜ ๊ฐ€์น˜๋ฅผ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค.",
"",
"- **CRAG OFF**: `search_knowledge`๊ฐ€ hybrid ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜",
"- **CRAG ON**: ๊ฒ€์ƒ‰ ํ›„ LLM grader๋กœ ๊ด€๋ จ์„ฑ ํ‰๊ฐ€, ์ž„๊ณ„์น˜(0.5) ๋ฏธ๋‹ฌ ์‹œ ์ฟผ๋ฆฌ ์žฌ์ž‘์„ฑ + ์žฌ๊ฒ€์ƒ‰ (max 1ํšŒ)",
"",
"## ์‹คํ—˜ ์„ค์ •",
"",
f"- ์•Œ๋žŒ: {', '.join(ALARMS)} (์ด {len(ALARMS)}๊ฑด)",
"- Tier 2 agent: gpt-5-mini (๋ณ€๊ฒฝ ์—†์Œ)",
"- CRAG grader/refiner: gpt-4o-mini (์ €๋น„์šฉ)",
"- ์ž„๊ณ„์น˜: avg relevance_score 0.5, max refinement retries 1",
"- ํ™˜๊ฒฝ๋ณ€์ˆ˜ `CRAG_ENABLED=true/false` ๋กœ ํ† ๊ธ€",
"",
"## ๊ฒฐ๊ณผ ์š”์•ฝ (3 ์•Œ๋žŒ ํ‰๊ท )",
"",
"| ์ง€ํ‘œ | CRAG OFF | CRAG ON | ๋ณ€ํ™” |",
"|---|---|---|---|",
f"| Faithfulness | {off['faithfulness']:.3f} | {on['faithfulness']:.3f} | {(on['faithfulness']-off['faithfulness'])*100:+.1f}%p |",
f"| Answer Relevancy | {off['answer_relevancy']:.3f} | {on['answer_relevancy']:.3f} | {(on['answer_relevancy']-off['answer_relevancy'])*100:+.1f}%p |",
f"| LLM ํ˜ธ์ถœ | {off['llm_calls']:.1f} | {on['llm_calls']:.1f} | x{on['llm_calls']/max(off['llm_calls'],1):.2f} |",
f"| ์ž…๋ ฅ ํ† ํฐ | {off['input_tokens']:.0f} | {on['input_tokens']:.0f} | x{on['input_tokens']/max(off['input_tokens'],1):.2f} |",
f"| ์ถœ๋ ฅ ํ† ํฐ | {off['output_tokens']:.0f} | {on['output_tokens']:.0f} | x{on['output_tokens']/max(off['output_tokens'],1):.2f} |",
f"| Latency (ms) | {off['latency_ms']:.0f} | {on['latency_ms']:.0f} | x{on['latency_ms']/max(off['latency_ms'],1):.2f} |",
f"| ๋น„์šฉ / 1000์•Œ๋žŒ | ${off_cost*1000:.3f} | ${on_cost*1000:.3f} | x{on_cost/max(off_cost,1e-9):.2f} |",
f"| ์œ ๋‹ˆํฌ ์ธ์šฉ | {off['citations']:.1f} | {on['citations']:.1f} | - |",
"",
"## CRAG ์ž๊ฐ€ ์ •์ • ํ™œ๋™",
"",
f"- search_knowledge ํ˜ธ์ถœ / ์•Œ๋žŒ: {on['crag_search_calls']:.1f}ํšŒ",
f"- refinement ๋ฐœ๋™ / ์•Œ๋žŒ: {on['refinement_triggered']:.1f}ํšŒ",
f"- ๋ฐœ๋™๋ฅ : {on['refinement_triggered']/max(on['crag_search_calls'],1)*100:.0f}%",
f"- ์ธ์šฉ ๋ฌธ์„œ ํ‰๊ท  relevance_score: {on['avg_relevance']:.2f}",
"",
"## ์‹œ๊ฐํ™”",
"",
"### ๋‹ต๋ณ€ ํ’ˆ์งˆ (RAGAS)",
"![Quality](charts/quality.png)",
"",
"### CRAG ์ž๊ฐ€ ์ •์ • ํ™œ๋™",
"![CRAG Activity](charts/crag_activity.png)",
"",
"### Latencyยท๋น„์šฉ ์˜ค๋ฒ„ํ—ค๋“œ",
"![Overhead](charts/overhead.png)",
"",
"## ์•Œ๋žŒ๋ณ„ ์ƒ์„ธ",
"",
]
for r in rows:
on_t = r["on"]["trace"]
off_t = r["off"]["trace"]
lines.append(f"### {r['alarm']}")
lines.append("")
lines.append("| ๋ชจ๋“œ | LLM | Latency | citations | refinement | avg_rel |")
lines.append("|---|---|---|---|---|---|")
lines.append(f"| OFF | {off_t['llm_calls']} | {off_t['latency_ms']:.0f}ms | {len(r['off']['citations'])} | - | - |")
lines.append(f"| ON | {on_t['llm_calls']} | {on_t['latency_ms']:.0f}ms | {len(r['on']['citations'])} | {on_t['refinement_triggered']}/{on_t['crag_search_calls']} | {on_t['avg_relevance']:.2f} |")
lines.append("")
if on_t.get("crag_events"):
lines.append("CRAG ์ด๋ฒคํŠธ (CRAG ON):")
for ev in on_t["crag_events"]:
tag = "๐Ÿ” refine" if ev["retry"] > 0 else "โœ“ pass"
lines.append(f"- {tag} | avg_score={ev['avg_score']} | query=`{ev['query'][:80]}`")
lines.append("")
quality_delta_f = (on['faithfulness'] - off['faithfulness']) * 100
quality_delta_r = (on['answer_relevancy'] - off['answer_relevancy']) * 100
refine_rate = on['refinement_triggered'] / max(on['crag_search_calls'], 1) * 100
cost_ratio = on_cost / max(off_cost, 1e-9)
quality_significant = abs(quality_delta_f) >= 2.0 or abs(quality_delta_r) >= 2.0
lines += [
"## ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ",
"",
f"1. **ํ’ˆ์งˆ ๋ณ€ํ™”**: faithfulness {quality_delta_f:+.1f}%p, relevancy {quality_delta_r:+.1f}%p",
f" - ๋ณธ ์ฝ”ํผ์Šค(~10๋ฌธ์„œ, ํ•œ๊ตญ์–ด)์—์„  hybrid ๊ฒ€์ƒ‰์ด ์ด๋ฏธ ์ž˜ ์ž‘๋™ํ•ด self-correction ์—ฌ์ง€ ์ ์Œ",
f"2. **์ž๊ฐ€ ์ •์ • ๋นˆ๋„**: ํ˜ธ์ถœ {on['crag_search_calls']:.1f}ํšŒ ์ค‘ {on['refinement_triggered']:.1f}ํšŒ refinement ๋ฐœ๋™ (๋ฐœ๋™๋ฅ  {refine_rate:.0f}%)",
f" - ๋ฐœ๋™๋  ๋•Œ๋Š” ์˜๋ฏธ ์žˆ๊ฒŒ ์ž‘๋™ (gibberish/๋„๋ฉ”์ธ ๋ฏธ์Šค๋งค์น˜ ์ฟผ๋ฆฌ๋ฅผ LLM์ด fab ๋„๋ฉ”์ธ ์ฟผ๋ฆฌ๋กœ ์žฌ์ž‘์„ฑ)",
f"3. **์ธ์šฉ ์‹ ๋ขฐ๋„ ๊ฐ€์‹œํ™”**: CRAG ON์—์„œ ํ‰๊ท  relevance_score {on['avg_relevance']:.2f} ๋…ธ์ถœ",
f" - ์šด์˜์ž๊ฐ€ '์ด ๊ถŒ๊ณ ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๊ฐ•ํ•œ ๊ทผ๊ฑฐ์— ๊ธฐ๋ฐ˜ํ•˜๋Š”๊ฐ€'๋ฅผ 0~1 ์ ์ˆ˜๋กœ ์ฆ‰์‹œ ํŒ๋‹จ ๊ฐ€๋Šฅ",
f"4. **๋น„์šฉ ์˜ค๋ฒ„ํ—ค๋“œ**: x{cost_ratio:.2f}๋ฐฐ (grader๊ฐ€ ํ˜ธ์ถœ๋‹น +1 LLM, refinement ์‹œ +1 ๋”). ์ ˆ๋Œ€๊ฐ’ $0.0X ์ˆ˜์ค€์œผ๋กœ ๋ฏธ๋ฏธ",
f"5. **Agentic loop์™€์˜ ์ค‘๋ณต**: agent๊ฐ€ ์ด๋ฏธ ๋ถ€์กฑํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ๋‹ค๋ฅธ query๋กœ ์žฌํ˜ธ์ถœํ•˜๋Š” self-correction์„ ์ผ๋ถ€ ์ˆ˜ํ–‰ โ†’ CRAG์˜ ๋ถ€๊ฐ€๊ฐ€์น˜๊ฐ€ ์ž‘์€ ์ฝ”ํผ์Šค์—์„œ ์ค„์–ด๋“ฆ",
"",
"## ์ฑ„ํƒ ๊ฒฐ๋ก ",
"",
("**CRAG ๊ธฐ๋ณธ ํ™œ์„ฑ** (`CRAG_ENABLED=true`) - ํ’ˆ์งˆ ํ–ฅ์ƒ์€ ๋ฏธ๋ฏธํ•˜์ง€๋งŒ ๊ด€์ธก ๊ฐ€์น˜ ์œ ์ง€." if not quality_significant else
f"**CRAG ๊ธฐ๋ณธ ํ™œ์„ฑ** - ํ’ˆ์งˆ {quality_delta_f:+.1f}%p ๊ฐœ์„  ํ™•์ธ."),
"",
"๊ทผ๊ฑฐ (์†”์งํ•œ trade-off):",
f"- ํ’ˆ์งˆ ๋ณ€ํ™”๋Š” ํ†ต๊ณ„์ ์œผ๋กœ ์˜๋ฏธ ์—†๋Š” ์ˆ˜์ค€ ({quality_delta_f:+.1f}%p faithfulness)์ด์ง€๋งŒ, **relevance_score ๋…ธ์ถœ์ด production observability ๊ฐ€์น˜**",
"- ์ธ์šฉ ๋ฌธ์„œ๋งˆ๋‹ค 0~1 ์‹ ๋ขฐ๋„ ์ ์ˆ˜๊ฐ€ ๋‹ต๋ณ€์— ํ•จ๊ป˜ ์ถœ๋ ฅ๋˜์–ด ์šด์˜์ž ์˜์‚ฌ๊ฒฐ์ •์— ์ง์ ‘ ๊ธฐ์—ฌ",
f"- Refinement ๋ฐœ๋™๋ฅ  {refine_rate:.0f}% - ์ •์ƒ ์ฟผ๋ฆฌ์—์„  ๋ฌด๋ฐœ๋™, gibberish/๋„๋ฉ”์ธ ๋ฏธ์Šค๋งค์น˜์—์„  ์ž๊ฐ€ ์ •์ • (smoke test๋กœ ๊ฒ€์ฆ)",
f"- ๋น„์šฉ +{(cost_ratio-1)*100:.0f}% ์ ˆ๋Œ€๊ฐ’ ๋ฏธ๋ฏธ (1000 ์•Œ๋žŒ๋‹น +${(on_cost-off_cost)*1000:.2f})",
"- **D6 (Rerank) ์‹œํ–‰์ฐฉ์˜ค์™€ ์œ ์‚ฌํ•œ ๊ตํ›ˆ**: production ํŒจํ„ด์„ ์ž‘์€ ๋„๋ฉ”์ธ ์ฝ”ํผ์Šค์— ๋ธ”๋ผ์ธ๋“œ ์ ์šฉํ•˜๋ฉด ROI ๋‚ฎ์Œ. ์ฝ”ํผ์Šค 100+ ํ™•์žฅ ์‹œ ์žฌํ‰๊ฐ€ ๊ถŒ์žฅ",
"",
"Latency critical ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” `CRAG_ENABLED=false`๋กœ ์ฆ‰์‹œ ๋น„ํ™œ์„ฑ ๊ฐ€๋Šฅ (ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํ† ๊ธ€).",
"",
"## ํ•œ๊ณ„์™€ ํ–ฅํ›„ ๊ฒ€์ฆ",
"",
"- **์ฝ”ํผ์Šค ๊ทœ๋ชจ**: ~10๋ฌธ์„œ ํ•œ๊ตญ์–ด ๋„๋ฉ”์ธ ๋ฌธ์„œ. 100+ ํ™•์žฅ ์‹œ retrieval grader์˜ ์‹ ํ˜ธ๊ฐ€ ๋” ์˜๋ฏธ ์žˆ์–ด์งˆ ๊ฐ€๋Šฅ์„ฑ",
"- **์ƒ˜ํ”Œ ์ˆ˜**: 3 ์•Œ๋žŒ ร— 2 ๋ชจ๋“œ = 6 sample. ํ†ต๊ณ„ ๊ฒ€์ •์€ ๋ถˆ๊ฐ€, ๊ฒฝํ–ฅ์„ฑ๋งŒ ๊ด€์ฐฐ",
"- **agentic loop์™€์˜ ์ƒํ˜ธ์ž‘์šฉ**: agent์˜ ์ž์œจ ์žฌํ˜ธ์ถœ์ด CRAG์™€ ๋ถ€๋ถ„ ์ค‘๋ณต - ๋‘ ๋ฉ”์ปค๋‹ˆ์ฆ˜์˜ ๋ถ„๋‹ด ์„ค๊ณ„ ์ถ”๊ฐ€ ๊ฒ€ํ†  ์—ฌ์ง€",
"- **์ž„๊ณ„์น˜ยทmax_retries ํŠœ๋‹**: 0.5 / 1๋กœ ๊ธฐ๋ณธ ์„ค์ •. ์ฝ”ํผ์Šคยท์ฟผ๋ฆฌ ๋ถ„ํฌ์— ๋งž์ถฐ ๊ทธ๋ฆฌ๋“œ ์„œ์น˜ ๊ถŒ์žฅ",
"",
]
(OUT_DIR / "results.md").write_text("\n".join(lines), encoding="utf-8")
print(f"--- ์ €์žฅ: {OUT_DIR / 'results.md'} ---")
def main():
rows = collect_samples()
ragas_df = evaluate_quality(rows)
agg = aggregate(rows, ragas_df)
print("\n--- ์ง‘๊ณ„ ---")
for mode, vals in agg.items():
print(f" {mode}: {vals}")
make_charts(agg, rows)
write_results(rows, agg, ragas_df)
if __name__ == "__main__":
main()