hee_!J
docs: D10 ์‹œ๋ฆฌ์ฆˆ ๊ฒ€์ฆ narrative + Journey 10๋‹จ๊ณ„ ์ถ”๊ฐ€
4347250
Raw
History Blame Contribute Delete
18.3 kB
"""RAG Paradigm Evolution - 5๋‹จ๊ณ„ ablation ๋น„๊ต ์‹คํ—˜
production RAG ์ง„ํ™” ๊ณผ์ •์„ 5๋‹จ๊ณ„๋กœ ๋ถ„ํ•ดํ•ด ๊ฐ ๋‹จ๊ณ„ ์ถ”๊ฐ€์˜ ํšจ๊ณผ๋ฅผ ์ •๋Ÿ‰ ์ธก์ •ํ•œ๋‹ค.
1. **No RAG**: LLM ๋‹จ๋… ์ถ”๋ก  (๋ฒ ์ด์Šค๋ผ์ธ, hallucination ์ธก์ •)
2. **Naive RAG (keyword)**: ํ‚ค์›Œ๋“œ ๋งค์นญ top-K
3. **Vector RAG (FAISS)**: dense embedding + cosine
4. **Hybrid RAG (BM25+FAISS+RRF)**: sparse + dense ๊ฒฐํ•ฉ
5. **Production RAG (Hybrid + Cross-encoder Rerank)**: ์ •๋ฐ€ ์žฌ์ •๋ ฌ
๊ฐ paradigm์„ 3๊ฐœ ์•Œ๋žŒ(A1ยทA2ยทA3)์— ์ ์šฉํ•ด RAGAS ์ง€ํ‘œ(faithfulness, answer_relevancy,
context_precision)์™€ latency๋ฅผ ์ˆ˜์ง‘ํ•˜๊ณ  matplotlib ์ฐจํŠธ๋กœ ์‹œ๊ฐํ™”ํ•œ๋‹ค.
์‹คํ–‰: python -m experiments.rag_paradigm.benchmark
๊ฒฐ๊ณผ: results.md + charts/*.png
"""
import json
import os
import statistics
import time
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datasets import Dataset
# ํ•œ๊ธ€ ํฐํŠธ (macOS ๊ธฐ๋ณธ Apple SD Gothic Neo, fallback DejaVu Sans)
plt.rcParams["font.family"] = ["Apple SD Gothic Neo", "AppleGothic", "NanumGothic", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
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,
LLMContextPrecisionWithoutReference,
ResponseRelevancy,
)
from agents.cause import TIER2_SCHEMA
# workflow ๋ชจ๋“œ (์‚ฌ์ „ retrieve ํ›„ ๋‹จ์ผ LLM ํ˜ธ์ถœ)์— ์ ํ•ฉํ•œ prompt
# agents.cause์˜ SYSTEM_PROMPT๋Š” agentic ๋ชจ๋“œ(tool ์ž์œจ ํ˜ธ์ถœ) ์ „์šฉ์ด๋ผ ๋ณ„๋„ ์ •์˜
WORKFLOW_SYSTEM_PROMPT = """๋‹น์‹ ์€ ๋ฐ˜๋„์ฒด ๊ณต์ • ์›์ธ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
์ฃผ์–ด์ง„ ์ด์ƒ ์•Œ๋žŒ๊ณผ ํƒ์ง€ ๊ฒฐ๊ณผ, ์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ๋ฅผ ๊ทผ๊ฑฐ๋กœ ๊ฐ€์žฅ ๊ฐ€๋Šฅ์„ฑ ๋†’์€ ์›์ธ์„
2~3๊ฐœ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์›์ธ์€ ๊ธฐ์—ฌ๋„(pct, %)๋ฅผ ๊ฐ€์ง€๋ฉฐ ํ•ฉ์ด 100์— ๊ฐ€๊น๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
๊ทผ๊ฑฐ(evidence)๋Š” ์ œ๊ณต๋œ ๋ฌธ์„œ ๋‚ด์šฉ์— ๊ธฐ๋ฐ˜ํ•ด ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑํ•˜๊ณ , citations์—๋Š”
๊ทผ๊ฑฐ๊ฐ€ ๋œ ๋ฌธ์„œ ID๋งŒ ์ •ํ™•ํžˆ ๊ธฐ์ž…ํ•ฉ๋‹ˆ๋‹ค. ์ œ๊ณต๋˜์ง€ ์•Š์€ ๋ฌธ์„œ๋Š” ์ธ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
๊ธฐ์—ฌ๋„๊ฐ€ ๋†’์€ ์›์ธ๋ถ€ํ„ฐ ์ˆœ์„œ๋Œ€๋กœ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.
๋ฐ˜๋“œ์‹œ JSON ์Šคํ‚ค๋งˆ์— ๋งž์ถฐ ์‘๋‹ตํ•˜์„ธ์š”."""
def _build_query(alarm: dict, tier1) -> str:
"""์› cause.py์— ์žˆ๋˜ query builder (agentic ์ „ํ™˜์œผ๋กœ ์ œ๊ฑฐ๋จ, ๋ณธ ์‹คํ—˜์šฉ์œผ๋กœ ์ธ๋ผ์ธ ๋ณด์กด)"""
feature = alarm.get("feature") or ""
sensors = " ".join(f["name"] for f in tier1["features"])
return f"{alarm['title']} {feature} {sensors} ์›์ธ ๋ถ„์„ ๊ณต์ • ์ด์ƒ"
from agents.detection import run_detection
from agents.llm import SUBAGENT_MODEL, client
from agents.rag.store import load_document, search
OUT_DIR = Path(__file__).parent
CHART_DIR = OUT_DIR / "charts"
CACHE_CSV = OUT_DIR / "samples.csv"
ALARMS = ["A1", "A2", "A3"]
PARADIGMS = [
("No RAG", None),
("Naive RAG (keyword)", "keyword"),
("Vector RAG (FAISS)", "faiss"),
("Hybrid (BM25+FAISS+RRF)", "hybrid"),
("Hybrid + Rerank", "hybrid_rerank"),
]
TOP_K = 3
def _alarm_by_id(alarm_id: str) -> dict:
from data.demo import DEFAULT_ALARMS
return next(a for a in DEFAULT_ALARMS if a["id"] == alarm_id)
def _run_cause_with_contexts(alarm: dict, tier1: dict, contexts: list[str]) -> dict:
"""run_cause๋ฅผ ์ง์ ‘ ์žฌํ˜„ํ•˜๋˜ contexts๋ฅผ ์™ธ๋ถ€์—์„œ ์ฃผ์ž…
paradigm๋งˆ๋‹ค retrieval ๋ฐฉ์‹๋งŒ ๋‹ค๋ฅด๋ฏ€๋กœ LLM ํ˜ธ์ถœ ์ฝ”๋“œ๋Š” ๊ณตํ†ตํ™”
contexts=[]๋ฉด No RAG (knowledge ์„น์…˜ ์ž์ฒด๋ฅผ ์ œ๊ฑฐ)
"""
knowledge = (
"\n\n".join(f"[doc_{i}]\n{c}" for i, c in enumerate(contexts))
if contexts
else "(์ œ๊ณต๋œ ์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ ์—†์Œ - ์ผ๋ฐ˜ ์ง€์‹์œผ๋กœ ์ถ”์ •)"
)
sensors = ", ".join(f["name"] for f in tier1["features"])
user_prompt = f"""## ์ด์ƒ ์•Œ๋žŒ
- ๊ณต์ •: {alarm['title']}
- lot: {alarm['lot_id']}
- ์ด์ƒ ํ”ผ์ฒ˜: {alarm.get('feature')} {alarm.get('feature_arrow') or ''}
## Tier 1 ์ด์ƒ ํƒ์ง€ ๊ฒฐ๊ณผ
- ์ด์ƒ ์ ์ˆ˜: {tier1['score']}
- ๊ธฐ์—ฌ ์„ผ์„œ(Top): {sensors}
## ์‚ฌ๋‚ด ์ง€์‹ ๋ฌธ์„œ
{knowledge}
์œ„ ์ •๋ณด๋ฅผ ๊ทผ๊ฑฐ๋กœ ์›์ธ์„ ๋ถ„์„ํ•ด ์ฃผ์„ธ์š”."""
resp = client().chat.completions.create(
model=SUBAGENT_MODEL,
messages=[
{"role": "system", "content": WORKFLOW_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
response_format={
"type": "json_schema",
"json_schema": {"name": "tier2", "schema": TIER2_SCHEMA, "strict": True},
},
)
content = resp.choices[0].message.content or "{}"
# ์ผ๋ถ€ ๋ชจ๋ธ์ด ```json ... ``` fence๋ฅผ ๋ถ™์ด๋Š” ๊ฒฝ์šฐ ๋ฐฉ์–ด
if content.strip().startswith("```"):
parts = content.split("```")
if len(parts) >= 2:
content = parts[1]
if content.startswith("json"):
content = content[4:]
return json.loads(content.strip())
def _format_answer(tier2: dict) -> str:
return "\n".join(
f"- {c['name']} ({c['pct']}%): {c['evidence']}" for c in tier2["causes"]
)
def _warmup():
"""ST ๋ชจ๋ธยทreranker ๋กœ๋“œ ์‹œ๊ฐ„์„ latency์—์„œ ์ œ์™ธํ•˜๊ธฐ ์œ„ํ•œ ์‚ฌ์ „ ํ˜ธ์ถœ"""
print("=== ์›Œ๋ฐ์—… (ST / Reranker ๋ชจ๋ธ ๋กœ๋“œ) ===")
for label, backend in PARADIGMS:
if backend is None:
continue
os.environ["RAG_BACKEND"] = backend
t0 = time.time()
search("warmup query", top_k=1)
print(f" {label}: {(time.time()-t0)*1000:.0f}ms")
def collect_samples():
"""5 paradigm ร— 3 ์•Œ๋žŒ = 15 sample ์ˆ˜์ง‘"""
_warmup()
print(f"\n=== Sample ์ˆ˜์ง‘ ({len(PARADIGMS)} paradigm ร— {len(ALARMS)} alarm) ===")
rows = []
for alarm_id in ALARMS:
alarm = _alarm_by_id(alarm_id)
tier1 = run_detection(alarm)
query = _build_query(alarm, tier1)
print(f"\n[{alarm_id}] {alarm['title']}")
for label, backend in PARADIGMS:
t0 = time.time()
if backend is None:
contexts = []
retrieval_ms = 0.0
else:
os.environ["RAG_BACKEND"] = backend
doc_ids = search(query, top_k=TOP_K)
retrieval_ms = (time.time() - t0) * 1000
contexts = [load_document(d) for d in doc_ids if load_document(d)]
t_gen = time.time()
tier2 = _run_cause_with_contexts(alarm, tier1, contexts)
gen_ms = (time.time() - t_gen) * 1000
# contexts๊ฐ€ ๋น„๋ฉด RAGAS context-metric์ด NaN ์ฒ˜๋ฆฌ๋˜๋„๋ก sentinel ํ•œ ์ค„ ์ฃผ์ž…
ragas_contexts = contexts if contexts else ["(๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ - LLM ๋‹จ๋… ์ถ”๋ก )"]
answer = _format_answer(tier2)
rows.append(
{
"paradigm": label,
"alarm": alarm_id,
"question": query,
"answer": answer,
"contexts": ragas_contexts,
"retrieval_ms": retrieval_ms,
"gen_ms": gen_ms,
"total_ms": retrieval_ms + gen_ms,
}
)
print(
f" {label:30s} retrieval={retrieval_ms:7.1f}ms "
f"gen={gen_ms:7.1f}ms causes={len(tier2['causes'])}"
)
return rows
def evaluate_ragas(rows: list[dict]):
print("\n=== RAGAS ํ‰๊ฐ€ ===")
eval_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini", temperature=0))
eval_emb = LangchainEmbeddingsWrapper(OpenAIEmbeddings(model="text-embedding-3-small"))
dataset = Dataset.from_dict(
{
"question": [r["question"] for r in rows],
"answer": [r["answer"] for r in rows],
"contexts": [r["contexts"] for r in rows],
}
)
metrics = [
Faithfulness(llm=eval_llm),
ResponseRelevancy(llm=eval_llm, embeddings=eval_emb),
LLMContextPrecisionWithoutReference(llm=eval_llm),
]
result = evaluate(dataset=dataset, metrics=metrics)
df = result.to_pandas()
df["paradigm"] = [r["paradigm"] for r in rows]
df["alarm"] = [r["alarm"] for r in rows]
df["retrieval_ms"] = [r["retrieval_ms"] for r in rows]
df["gen_ms"] = [r["gen_ms"] for r in rows]
df["total_ms"] = [r["total_ms"] for r in rows]
return df
def aggregate(df):
"""paradigm๋ณ„ ํ‰๊ท  ์‚ฐ์ถœ"""
metric_cols = [c for c in df.columns if c in (
"faithfulness", "answer_relevancy", "llm_context_precision_without_reference"
)]
agg = df.groupby("paradigm", sort=False)[metric_cols + ["retrieval_ms", "gen_ms", "total_ms"]].mean()
# paradigm ์ˆœ์„œ ๋ณด์กด
paradigm_order = [label for label, _ in PARADIGMS]
agg = agg.reindex(paradigm_order)
return agg, metric_cols
def make_charts(agg, metric_cols):
CHART_DIR.mkdir(exist_ok=True)
paradigms = list(agg.index)
colors = ["#94a3b8", "#cbd5e1", "#60a5fa", "#3b82f6", "#1e40af"]
# 1. RAGAS metric ๋น„๊ต (grouped bar)
fig, ax = plt.subplots(figsize=(11, 5.5))
x = np.arange(len(paradigms))
width = 0.26
metric_labels = {
"faithfulness": "Faithfulness",
"answer_relevancy": "Answer Relevancy",
"llm_context_precision_without_reference": "Context Precision",
}
for i, col in enumerate(metric_cols):
values = agg[col].fillna(0).values
bars = ax.bar(x + (i - 1) * width, values, width, label=metric_labels.get(col, col))
for bar, v, raw in zip(bars, values, agg[col].values):
label = "N/A" if np.isnan(raw) else f"{v:.2f}"
ax.text(bar.get_x() + bar.get_width() / 2, v + 0.015, label,
ha="center", fontsize=8)
ax.set_xticks(x)
ax.set_xticklabels(paradigms, rotation=15, ha="right", fontsize=9)
ax.set_ylim(0, 1.15)
ax.set_ylabel("Score (0~1, ๋†’์„์ˆ˜๋ก ์ข‹์Œ)")
ax.set_title("RAG Paradigm Evolution - RAGAS metric ํ‰๊ท  (3 alarm)")
ax.legend(loc="upper left", fontsize=9)
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
fig.savefig(CHART_DIR / "ragas_comparison.png", dpi=150)
plt.close(fig)
# 2. Latency ๋น„๊ต (stacked bar: retrieval + generation)
fig, ax = plt.subplots(figsize=(10, 5))
retrieval = agg["retrieval_ms"].values
generation = agg["gen_ms"].values
ax.bar(x, retrieval, color="#3b82f6", label="Retrieval")
ax.bar(x, generation, bottom=retrieval, color="#fbbf24", label="LLM Generation")
for i, (r, g) in enumerate(zip(retrieval, generation)):
ax.text(i, r + g + 200, f"{r+g:.0f}ms", ha="center", fontsize=9, fontweight="bold")
ax.set_xticks(x)
ax.set_xticklabels(paradigms, rotation=15, ha="right", fontsize=9)
ax.set_ylabel("ํ‰๊ท  Latency (ms)")
ax.set_title("Latency ๋ถ„ํ•ด (Retrieval + Generation)")
ax.legend(loc="upper left")
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
fig.savefig(CHART_DIR / "latency_comparison.png", dpi=150)
plt.close(fig)
# 3. Quality vs Latency trade-off (scatter)
fig, ax = plt.subplots(figsize=(8.5, 5.5))
quality = agg[metric_cols].mean(axis=1).values # 3 metric ํ‰๊ท 
latency = agg["total_ms"].values
for i, p in enumerate(paradigms):
ax.scatter(latency[i], quality[i], s=220, c=colors[i], edgecolors="black", linewidths=1.2, zorder=3)
ax.annotate(p, (latency[i], quality[i]),
xytext=(10, 5), textcoords="offset points", fontsize=9)
ax.set_xlabel("Total Latency (ms, โ†“)")
ax.set_ylabel("ํ‰๊ท  RAGAS Score (โ†‘)")
ax.set_title("Quality vs Latency Trade-off")
ax.grid(alpha=0.3)
fig.tight_layout()
fig.savefig(CHART_DIR / "tradeoff.png", dpi=150)
plt.close(fig)
print(f"--- ์ฐจํŠธ ์ €์žฅ: {CHART_DIR}/*.png ---")
def write_results(df, agg, metric_cols):
metric_labels = {
"faithfulness": "Faithfulness",
"answer_relevancy": "Answer Relevancy",
"llm_context_precision_without_reference": "Context Precision",
}
lines = [
"# RAG Paradigm Evolution - 5๋‹จ๊ณ„ ablation ๋น„๊ต",
"",
"production RAG ์ง„ํ™” ๊ณผ์ •์„ 5๋‹จ๊ณ„๋กœ ๋ถ„ํ•ดํ•ด ๊ฐ ๋‹จ๊ณ„ ์ถ”๊ฐ€์˜ ํšจ๊ณผ๋ฅผ ์ •๋Ÿ‰ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค.",
"๋™์ผํ•œ ์•Œ๋žŒยท๋™์ผํ•œ LLM(`gpt-5-mini`)ยท๋™์ผํ•œ prompt ํ•˜์—์„œ retrieval ๋ฐฉ์‹๋งŒ ๋ฐ”๊ฟ”",
"RAGAS ์ง€ํ‘œ(faithfulness / answer_relevancy / context_precision)์™€ latency๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค.",
"",
"## ์‹คํ—˜ ์„ค์ •",
"",
f"- ์•Œ๋žŒ: {', '.join(ALARMS)} (์ด {len(ALARMS)}๊ฑด, SECOM + PHM 2016 CMP)",
f"- Paradigm: {len(PARADIGMS)}์ข…",
"- ์ƒ์„ฑ ๋ชจ๋ธ: `gpt-5-mini`",
"- ํ‰๊ฐ€ ๋ชจ๋ธ: `gpt-4o-mini` (RAGAS ํ˜ธ์ŠคํŠธ LLM)",
"- ์ž„๋ฒ ๋”ฉ: `text-embedding-3-small`",
f"- Top-K: {TOP_K}",
"- Latency๋Š” ์›Œ๋ฐ์—… ํ›„ ์ธก์ • (STยทReranker ๋ชจ๋ธ ๋กœ๋“œ ์‹œ๊ฐ„ ์ œ์™ธ, ์‹ค์ œ production warm ์ƒํƒœ ๊ธฐ์ค€)",
"",
"## Paradigm ์ •์˜",
"",
"| # | Paradigm | ์„ค๋ช… |",
"|---|---|---|",
"| 1 | **No RAG** | LLM ๋‹จ๋… ์ถ”๋ก , ์‚ฌ๋‚ด ์ง€์‹ ๋ฏธ์ฃผ์ž… (๋ฒ ์ด์Šค๋ผ์ธ) |",
"| 2 | **Naive RAG (keyword)** | ๋‹จ์–ด ๋นˆ๋„ ๋งค์นญ top-K |",
"| 3 | **Vector RAG (FAISS)** | sentence-transformer dense embedding + cosine |",
"| 4 | **Hybrid (BM25+FAISS+RRF)** | sparse + dense, Reciprocal Rank Fusion |",
"| 5 | **Hybrid + Rerank** | Hybrid top-10์„ cross-encoder(BAAI/bge-reranker-base)๋กœ ์ •๋ฐ€ ์žฌ์ •๋ ฌ |",
"",
"## ๊ฒฐ๊ณผ ์š”์•ฝ (paradigm๋ณ„ ํ‰๊ท )",
"",
"| Paradigm | " + " | ".join(metric_labels[c] for c in metric_cols) + " | Retrieval (ms) | LLM (ms) | Total (ms) |",
"|---|" + "|".join(["---"] * (len(metric_cols) + 3)) + "|",
]
for paradigm in agg.index:
row = agg.loc[paradigm]
metric_cells = [f"{row[c]:.3f}" if not np.isnan(row[c]) else "N/A" for c in metric_cols]
lines.append(
f"| {paradigm} | " + " | ".join(metric_cells) +
f" | {row['retrieval_ms']:.1f} | {row['gen_ms']:.1f} | {row['total_ms']:.1f} |"
)
lines += [
"",
"## ์‹œ๊ฐํ™”",
"",
"### RAGAS metric ๋น„๊ต",
"",
"![RAGAS Comparison](charts/ragas_comparison.png)",
"",
"### Latency ๋ถ„ํ•ด",
"",
"![Latency Comparison](charts/latency_comparison.png)",
"",
"### ํ’ˆ์งˆ vs Latency Trade-off",
"",
"![Trade-off](charts/tradeoff.png)",
"",
"## ์•Œ๋žŒ๋ณ„ ์ƒ์„ธ ๊ฒฐ๊ณผ",
"",
]
for alarm_id in ALARMS:
sub = df[df["alarm"] == alarm_id]
lines.append(f"### {alarm_id}")
lines.append("")
lines.append("| Paradigm | " + " | ".join(metric_labels[c] for c in metric_cols) + " | Total (ms) |")
lines.append("|---|" + "|".join(["---"] * (len(metric_cols) + 1)) + "|")
for _, r in sub.iterrows():
metric_cells = [f"{r[c]:.3f}" if not np.isnan(r[c]) else "N/A" for c in metric_cols]
lines.append(f"| {r['paradigm']} | " + " | ".join(metric_cells) + f" | {r['total_ms']:.1f} |")
lines.append("")
lines += [
"## ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ",
"",
"1. **RAG ๋„์ž… ํšจ๊ณผ๊ฐ€ ๊ฒฐ์ •์ **: `No RAG` ๋Œ€๋น„ ์–ด๋–ค paradigm์„ ๋ถ™์—ฌ๋„ `faithfulness`๊ฐ€ 2๋ฐฐ ์ด์ƒ ์ƒ์Šนํ•ฉ๋‹ˆ๋‹ค.",
" ์‚ฌ๋‚ด ์‚ฌ๋ก€ยทSOPยทFMEA๋ฅผ ์ธ์šฉํ•˜์ง€ ๋ชปํ•˜๋Š” LLM์€ hallucination ์œ„ํ—˜์ด ํฌ๊ณ , ๋ฐ˜๋„์ฒด ๋„๋ฉ”์ธ์—์„œ๋Š” ์น˜๋ช…์ ์ž…๋‹ˆ๋‹ค.",
"",
"2. **Hybrid (BM25 + FAISS + RRF)๊ฐ€ ๋ณธ ์ฝ”ํผ์Šค์—์„œ ๋ชจ๋“  ์ง€ํ‘œ 1์œ„**์ž…๋‹ˆ๋‹ค.",
" sparse(BM25, ์ •ํ™• ์šฉ์–ด ๋งค์นญ) + dense(FAISS, ์˜๋ฏธ ๋งค์นญ)๋ฅผ Reciprocal Rank Fusion์œผ๋กœ ๊ฒฐํ•ฉํ•ด",
" ๊ฐ ๋‹จ์ผ backend์˜ ์•ฝ์ ์„ ์ƒ์‡„ํ•ฉ๋‹ˆ๋‹ค. Latency๋„ ๊ฐ€์žฅ ๋น ๋ฆ…๋‹ˆ๋‹ค.",
"",
"3. **Cross-encoder Rerank๋Š” ๋ณธ ์ฝ”ํผ์Šค์—์„œ ์ด๋“ ์—†์Œ**: `Hybrid + Rerank`๋Š” `Hybrid`์™€ ๋น„๊ตํ•ด",
" faithfulness ๋™๊ธ‰, `answer_relevancy`๋Š” ์˜คํžˆ๋ ค ํ•˜๋ฝํ–ˆ์Šต๋‹ˆ๋‹ค. ์›์ธ ์ถ”์ •:",
" - ์ฝ”ํผ์Šค๊ฐ€ ~10๋ฌธ์„œ๋กœ ์ž‘์•„ Hybrid top-3์ด ์ด๋ฏธ ์ •๋‹ต์— ๊ทผ์ ‘",
" - `BAAI/bge-reranker-base`๊ฐ€ ์˜์–ด ํ•™์Šต ๋ชจ๋ธ์ด๋ผ ํ•œ๊ตญ์–ด ๋„๋ฉ”์ธ ํ…์ŠคํŠธ์—์„œ ์ ์ˆ˜ ์‹ ํ˜ธ๊ฐ€ ์žก์Œ์— ๊ฐ€๊นŒ์›€",
" - ํ•œ๊ตญ์–ด reranker(์˜ˆ: `dongjin-kr/ko-reranker`) ๋˜๋Š” ์ฝ”ํผ์Šค ํ™•์žฅ(100๋ฌธ์„œ+) ์‹œ ํšจ๊ณผ ์žฌ๊ฒ€์ฆ ํ•„์š”",
"",
"## ์ฑ„ํƒ ๊ทผ๊ฑฐ",
"",
"**MVP ๊ธฐ๋ณธ backend = `Hybrid (BM25+FAISS+RRF)`**",
"",
"๋ณธ ์‹คํ—˜ ๋ฐ์ดํ„ฐ(3 ์•Œ๋žŒ ร— 5 paradigm)๋Š” `Hybrid`๊ฐ€ quality + latency ๋ชจ๋‘์—์„œ ์šฐ์œ„์ž„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.",
"production RAG ํ‘œ์ค€ ํŒจํ„ด์ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค (Microsoft Azure AI Search, LlamaIndex ๊ธฐ๋ณธ ๊ถŒ๊ณ ).",
"",
"**`Hybrid + Rerank`๋Š” ์˜ต์…˜์œผ๋กœ ์œ ์ง€** (ํ™˜๊ฒฝ๋ณ€์ˆ˜ `RAG_BACKEND=hybrid_rerank`):",
"- ์ฝ”ํผ์Šค๊ฐ€ 100+ ๋ฌธ์„œ๋กœ ํ™•์žฅ๋  ๋•Œ cross-encoder ์ •๋ฐ€ ์žฌ์ •๋ ฌ์ด ํ•„์š”ํ•ด์งˆ ๊ฐ€๋Šฅ์„ฑ ํผ",
"- ํ•œ๊ตญ์–ด reranker๋กœ ๊ต์ฒด ์‹œ ๋ณธ ์‹คํ—˜ ์žฌํ‰๊ฐ€ ๊ถŒ์žฅ",
"",
"**์ „์ฒด ์ฑ„ํƒ ์‹ ํ˜ธ**:",
"- ์–ด๋–ค RAG๋“  No RAG๋ณด๋‹ค ์••๋„์ ์œผ๋กœ ๋‚ซ๋‹ค โ†’ RAG๋Š” production ํ•„์ˆ˜",
"- ์ฝ”ํผ์Šค ๊ทœ๋ชจ์™€ ๋„๋ฉ”์ธ ์–ธ์–ด์— ๋งž์ถฐ paradigm์„ ์„ ํƒํ•ด์•ผ ํ•œ๋‹ค (๋ธ”๋ผ์ธ๋“œ ์ ์šฉ์€ ์—ญํšจ๊ณผ)",
"- ์ •๋Ÿ‰ ํ‰๊ฐ€(RAGAS)๊ฐ€ ์—†์œผ๋ฉด 'rerank๊ฐ€ ๋ฌด์กฐ๊ฑด ์ข‹๋‹ค'๋Š” ์˜คํ•ด๋ฅผ ๊ทธ๋Œ€๋กœ ๋Œ๊ณ  ๊ฐ”์„ ๊ฒƒ",
"",
]
(OUT_DIR / "results.md").write_text("\n".join(lines), encoding="utf-8")
print(f"--- ์ €์žฅ: {OUT_DIR / 'results.md'} ---")
def main():
import sys
charts_only = "--charts-only" in sys.argv
if charts_only and CACHE_CSV.exists():
print(f"=== ์บ์‹œ์—์„œ ๊ฒฐ๊ณผ ๋กœ๋“œ: {CACHE_CSV} ===")
df = pd.read_csv(CACHE_CSV)
else:
rows = collect_samples()
df = evaluate_ragas(rows)
df.to_csv(CACHE_CSV, index=False)
print(f"--- ์บ์‹œ ์ €์žฅ: {CACHE_CSV} ---")
agg, metric_cols = aggregate(df)
print("\n--- ์ง‘๊ณ„ ---")
print(agg.round(3))
make_charts(agg, metric_cols)
write_results(df, agg, metric_cols)
if __name__ == "__main__":
main()