최신 트렌드

LLM-as-Judge 실전 구축 - AI 에이전트 품질을 자동 채점하는 판사 모델 파이프라인

백엔드 개발자 김승원 2026. 4. 21. 23:09

들어가며

지난 AI 에이전트 방어선 구축 실전에서 비용·폭주·드리프트를 잡는 3-레이어를 깔았습니다. 골든 세트 회귀 테스트까지는 왔는데, 채점 방식이 단순 문자열 매칭이었다는 게 솔직한 한계였습니다.

"ProblemDetail"이라는 단어가 응답에 들어있다고 좋은 답변인지, "def"가 있다고 함수 설계가 맞는 건지. 진짜 품질은 의미 단위로 평가해야 합니다. 그걸 자동화하는 방법이 LLM-as-Judge입니다.

오늘 글은 판사 모델로 골든 세트를 자동 채점하고, 인간 라벨과의 상관을 검증하고, 비용을 합리 수준으로 누르는 실전 판사 파이프라인을 코드 레벨로 쌓습니다.

  • 1부 - 판사 모델 선택과 프롬프트 설계 (편향·자기선호 회피)
  • 2부 - 채점 파이프라인 구현 (점수 + 이유 JSON 출력)
  • 3부 - 인간 라벨 대비 상관계수(Pearson/Kappa) 측정
  • 4부 - 비용 최적화 (Haiku로 1차, Opus로 재검)
  • 5부 - 배포와 운영 (샘플링, 대시보드, 알림 통합)

1. 왜 판사 모델인가

골든 세트 채점 방식은 크게 네 가지입니다. 각각의 장단점이 선명하기 때문에 선택 기준부터 잡아야 합니다.

방식 비용 정확도 반복성 적합 상황
문자열 매칭 $0 낮음 100% 구조화된 코드 출력
정규식·AST $0 중간 100% 특정 포맷 검증
LLM-as-Judge 높음 90%+ 자연어 품질 평가
인간 라벨링 매우 높음 최고 80% 최종 검증

LLM-as-Judge의 핵심 가치는 "논리 전개·근거·톤·안전성 같은 서술형 품질"을 수치로 바꿀 수 있다는 점입니다. 정규식으로는 절대 못 잡습니다.

자주 쓰는 판사 시나리오

  • 페어와이즈 비교: A와 B 중 어느 응답이 더 낫나? (RLHF 계열)
  • 루브릭 채점: 기준 5개 × 5점 척도로 절대 점수
  • 사실 검증: 응답이 주어진 근거 문서와 일치하나?
  • 위반 탐지: PII 노출·잘못된 도구 호출·정책 위반이 있나?

오늘은 루브릭 채점을 중심으로 짚되, 2부 말미에 페어와이즈를 덧붙입니다.

2. 판사 모델 선택

판사는 "똑똑하면서 편향이 적은 모델"을 써야 합니다. 2026년 4월 기준 권장 조합은 이렇습니다.

역할 모델 이유
1차 스크리너 claude-haiku-4-5 빠르고 저렴, 루브릭 판단 안정
메인 판사 claude-opus-4-7
또는 gpt-5-4
복잡한 추론·장문 평가
상호 검증 다른 벤더 모델 자기선호 편향 상쇄

중요한 규칙은 피평가자와 판사를 다른 모델로 두는 것입니다. Claude 생성물을 Claude로 채점하면 자기 출력 특유의 구조·표현에 후한 점수를 주는 경향이 관측됩니다. Stanford AI Index 2026에서도 자기선호 편향은 여전히 해결되지 않은 이슈로 언급되었습니다.

3. 판사 프롬프트 설계

프롬프트가 곧 판사의 재판관입니다. 잘못 쓰면 점수가 들쭉날쭉해집니다. 다섯 가지 원칙을 지킵니다.

  1. 루브릭을 명시적으로 제시: 각 기준의 0~5점 의미까지 서술
  2. JSON 강제 출력: 파싱 실패를 0에 수렴시킴
  3. 이유(rationale) 먼저, 점수 나중: CoT로 점수 흔들림 감소
  4. 앵커링 방지: 예시 응답의 점수는 보여주지 않음
  5. 최소한의 컨텍스트만: 필요 없는 문서는 판사에게 주지 않음

3-1. 기본 루브릭 프롬프트 템플릿

# judge_prompts.py
JUDGE_SYSTEM = """당신은 AI 에이전트 출력물을 평가하는 엄격한 판사입니다.
루브릭에 따라 각 기준을 0~5점으로 채점합니다.
점수 산정 이유를 먼저 서술하고, 마지막에 JSON으로 점수를 출력합니다.
JSON 외 다른 텍스트를 JSON 뒤에 추가하지 마십시오."""

JUDGE_USER_TMPL = """
# 과제
{task}

# 에이전트 응답
<<<
{response}
>>>

# 루브릭 (각 0~5점)
{rubric}

# 출력 형식
다음 순서로 작성하세요:
1. 각 기준별 근거 (2~3문장)
2. 마지막 줄에만 아래 JSON:

{{"scores": {{"기준1": N, "기준2": N, ...}}, "overall": N}}

- overall은 각 기준의 평균을 0~5로
- 출력은 반드시 한국어
"""

RUBRIC_BACKEND = """
- 정확성(correctness): 요구사항을 누락 없이 구현했는가
- 설계(design): 책임 분리·네이밍·확장성이 적절한가
- 보안(security): 입력 검증·인증·권한 처리가 적절한가
- 성능(performance): 불필요한 I/O·N+1·병목이 없는가
- 설명력(clarity): 선택 이유와 트레이드오프를 짚었는가
"""

3-2. 5점 척도 앵커

"각 점수가 무엇인지"를 명확히 주면 판사의 점수 분포가 덜 튑니다.

SCALE_ANCHOR = """
- 5: 경험 많은 시니어 수준. 수정 없이 그대로 운영 가능
- 4: 소수 보완만 하면 운영 가능
- 3: 큰 뼈대는 맞으나 수정이 필요
- 2: 중대한 결함 존재
- 1: 방향은 있으나 사실상 재작성 필요
- 0: 요구사항을 이해하지 못함
"""

4. 판사 파이프라인 구현

지난 글의 wrapper.py를 그대로 쓰면서 채점 레이어만 얹습니다.

4-1. Judge 호출 함수

# judge.py
import json, re
from wrapper import call_claude
from judge_prompts import JUDGE_SYSTEM, JUDGE_USER_TMPL, RUBRIC_BACKEND, SCALE_ANCHOR

JSON_RE = re.compile(r"\{[^{}]*\"scores\"[^{}]*\}", re.DOTALL)

def judge(task: str, response: str,
          model: str = "claude-opus-4-7",
          rubric: str = RUBRIC_BACKEND) -> dict:
    user = JUDGE_USER_TMPL.format(
        task=task,
        response=response,
        rubric=rubric + "\n" + SCALE_ANCHOR,
    )
    resp, meta = call_claude(
        agent="judge", model=model, max_tokens=1200,
        messages=[
            {"role": "user", "content": user}
        ],
        system=JUDGE_SYSTEM,
    )
    text = resp.content[0].text
    parsed = _parse_json(text)
    parsed["judge_trace_id"] = meta["trace_id"]
    parsed["judge_cost"] = meta["cost"]
    parsed["raw"] = text
    return parsed

def _parse_json(text: str) -> dict:
    m = JSON_RE.search(text)
    if not m:
        return {"scores": {}, "overall": 0, "parse_error": True}
    try:
        obj = json.loads(m.group(0))
        obj["parse_error"] = False
        return obj
    except json.JSONDecodeError:
        return {"scores": {}, "overall": 0, "parse_error": True}

4-2. 골든 세트 채점 러너

# judge_runner.py
import yaml, sqlite3
from wrapper import call_claude
from judge import judge

def load_golden():
    with open("golden_set.yaml") as f:
        return yaml.safe_load(f)["cases"]

def ensure_tables():
    with sqlite3.connect("ai_usage.db") as db:
        db.execute("""
            CREATE TABLE IF NOT EXISTS judge_scores (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                case_id TEXT, agent TEXT, judge_model TEXT,
                overall REAL, scores_json TEXT, parse_error INT,
                cost_usd REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)

def run(judge_model: str = "claude-opus-4-7"):
    ensure_tables()
    cases = load_golden()
    results = []

    with sqlite3.connect("ai_usage.db") as db:
        for c in cases:
            resp, _ = call_claude(
                agent=c["agent"],
                model="claude-opus-4-7",
                messages=[{"role": "user", "content": c["input"]}],
                max_tokens=2000,
            )
            output = resp.content[0].text
            j = judge(c["input"], output, model=judge_model)

            db.execute("""INSERT INTO judge_scores
                (case_id, agent, judge_model, overall, scores_json,
                 parse_error, cost_usd) VALUES (?, ?, ?, ?, ?, ?, ?)""",
                (c["id"], c["agent"], judge_model,
                 j.get("overall", 0), json.dumps(j.get("scores", {})),
                 1 if j.get("parse_error") else 0,
                 j.get("judge_cost", 0)))
            results.append((c["id"], j))

    return results

if __name__ == "__main__":
    import json
    for cid, j in run():
        print(cid, j.get("overall"), j.get("scores"))

4-3. 출력 예시

py-func-001 4.2 {'correctness': 5, 'design': 4, 'security': 3, 'performance': 5, 'clarity': 4}
api-design-001 3.6 {'correctness': 4, 'design': 4, 'security': 3, 'performance': 3, 'clarity': 4}
py-func-002 1.8 {'correctness': 2, 'design': 2, 'security': 1, 'performance': 2, 'clarity': 2}

문자열 매칭이 놓쳤던 "보안 허술함" · "성능 병목"까지 점수로 드러나는 게 핵심 차이입니다.

4-4. 페어와이즈 변형

모델 A와 B의 우위를 물을 때는 순서 편향을 없애기 위해 두 번 물어보고 평균을 냅니다.

# pairwise.py
PAIRWISE_TMPL = """
다음 두 응답 중 어느 것이 더 나은지 판단하세요.

# 과제
{task}

# 응답 A
<<<{a}>>>

# 응답 B
<<<{b}>>>

# 루브릭
{rubric}

마지막 줄에만:
{{"winner": "A" | "B" | "tie", "confidence": 0.0~1.0}}
"""

def pairwise(task, a, b, rubric, model="claude-opus-4-7"):
    # 정방향
    r1 = _ask(task, a, b, rubric, model)
    # 역방향(A·B 스왑)
    r2 = _ask(task, b, a, rubric, model)
    return _reconcile(r1, r2)

def _reconcile(r1, r2):
    # r2는 A/B가 스왑되어 있으므로 결과도 반전
    flip = {"A": "B", "B": "A", "tie": "tie"}
    r2_fixed = flip[r2["winner"]]
    if r1["winner"] == r2_fixed:
        return {"winner": r1["winner"],
                "confidence": (r1["confidence"] + r2["confidence"]) / 2}
    return {"winner": "tie", "confidence": 0.5}

5. 인간 라벨과의 상관 검증

판사 모델을 신뢰하려면 "판사 점수와 사람 점수가 얼마나 일치하는가"를 수치화해야 합니다. 최소 50개 샘플에 두 명 이상이 라벨링한 결과를 기준선으로 삼습니다.

5-1. 라벨 수집 포맷

# human_labels.yaml
- case_id: py-func-001
  labelers:
    alice: 4
    bob: 4
- case_id: api-design-001
  labelers:
    alice: 3
    bob: 4
- case_id: py-func-002
  labelers:
    alice: 2
    bob: 2
# ... 50개 이상

5-2. 상관계수 · Cohen's Kappa

# calibrate.py
import yaml, sqlite3
from scipy import stats
from sklearn.metrics import cohen_kappa_score

def load_labels():
    with open("human_labels.yaml") as f:
        return yaml.safe_load(f)

def judge_scores(judge_model):
    with sqlite3.connect("ai_usage.db") as db:
        rows = db.execute("""
            SELECT case_id, AVG(overall) FROM judge_scores
            WHERE judge_model = ?
            GROUP BY case_id
        """, (judge_model,)).fetchall()
    return dict(rows)

def analyze(judge_model="claude-opus-4-7"):
    labels = load_labels()
    jscores = judge_scores(judge_model)

    pairs = []
    for item in labels:
        cid = item["case_id"]
        if cid not in jscores:
            continue
        human = sum(item["labelers"].values()) / len(item["labelers"])
        pairs.append((human, jscores[cid]))

    h, j = zip(*pairs)
    pearson = stats.pearsonr(h, j)
    spearman = stats.spearmanr(h, j)

    # 정수화한 뒤 kappa
    h_int = [round(x) for x in h]
    j_int = [round(x) for x in j]
    kappa = cohen_kappa_score(h_int, j_int, weights="quadratic")

    print(f"Pearson  r={pearson.statistic:.3f} (p={pearson.pvalue:.4f})")
    print(f"Spearman r={spearman.statistic:.3f}")
    print(f"Weighted Kappa = {kappa:.3f}")
    print(f"N = {len(pairs)}")

if __name__ == "__main__":
    analyze()

5-3. 합격선

지표 최소 양호 매우 좋음
Pearson r 0.60 0.75 0.85+
Spearman 0.55 0.70 0.82+
Quadratic Kappa 0.50 0.65 0.80+

Pearson이 0.6 미만이면 판사를 교체하거나 루브릭을 다시 쓰는 게 답입니다. 허술한 판사의 점수로 운영 판단을 하면 오히려 독이 됩니다.

5-4. 인간 라벨러 간 일치도

판사를 욕하기 전에 사람끼리 일치하는지부터 봐야 합니다. Alice와 Bob 둘 다 같은 케이스를 라벨링했다면 인간 IAA(Inter-Annotator Agreement)도 Kappa로 측정합니다. 사람끼리도 Kappa 0.6이 안 나오면 루브릭 자체가 모호한 것이므로, 판사를 고치기 전에 루브릭 정의를 다듬어야 합니다.

6. 비용 최적화 - 계단식 판사

Opus로 전 건을 돌리면 빠르게 비용이 터집니다. 실용적인 운영은 "싼 판사로 1차, 비싼 판사로 재검" 2단계 구조입니다.

6-1. 2단 계단식 설계

# cascade.py
from judge import judge

FAST_MODEL = "claude-haiku-4-5"
SLOW_MODEL = "claude-opus-4-7"
RECHECK_THRESHOLD = 3.5  # 이 값 미만이면 Opus로 재검
BORDERLINE_BAND = 0.5    # 이 범위 내면 재검

def cascade_judge(task, response):
    fast = judge(task, response, model=FAST_MODEL)
    s = fast.get("overall", 0)

    # 확실히 낮거나 경계선 근처면 재검
    needs_recheck = (
        s < RECHECK_THRESHOLD or
        abs(s - RECHECK_THRESHOLD) <= BORDERLINE_BAND
    )
    if not needs_recheck:
        return {**fast, "final_model": FAST_MODEL}

    slow = judge(task, response, model=SLOW_MODEL)
    return {
        **slow,
        "fast_overall": fast.get("overall"),
        "slow_overall": slow.get("overall"),
        "final_model": SLOW_MODEL,
    }

6-2. 예상 비용 절감

시나리오 비용 절감률
Opus만 (20케이스/주) $1.20 기준
Haiku만 $0.08 93% ↓
Cascade (30%만 Opus) $0.44 63% ↓

골든 세트 20개는 싸지만, 샘플링 채점을 매일 1%씩 돌리기 시작하면 금방 커집니다(하루 트래픽 10만 건 × 1% = 1000건). 이때 cascade가 결정적으로 차이가 납니다.

6-3. 캐시 활용

동일 프롬프트에 대한 반복 채점은 Anthropic prompt caching으로 누릅니다. 루브릭·시스템 프롬프트는 고정이므로 캐시 타겟으로 딱입니다.

# Anthropic prompt cache 사용
resp = client.messages.create(
    model="claude-opus-4-7",
    system=[{
        "type": "text",
        "text": JUDGE_SYSTEM + "\n\n" + RUBRIC_BACKEND + SCALE_ANCHOR,
        "cache_control": {"type": "ephemeral"}
    }],
    messages=[...],
    max_tokens=1200,
)

500 토큰 이상의 시스템 프롬프트가 캐시되면 같은 골든 세트 배치를 돌릴 때 입력 비용이 90% 이상 떨어집니다.

7. 편향과 오류 완화

판사 모델은 인간과 다른 종류의 편향을 갖습니다. 운영에서 반드시 챙겨야 할 5가지입니다.

7-1. 자기선호 편향(Self-preference)

같은 모델군은 서로에게 후합니다. 운영 모델이 Claude라면 판사는 GPT-5.4로 두거나, 반대로 하세요. 상호 검증 배치를 주 1회 돌려 두 판사의 결과가 크게 어긋나는 케이스만 인간이 보는 구조가 가장 경제적입니다.

7-2. 길이 편향(Length bias)

긴 응답이 무조건 높은 점수를 받는 현상. 루브릭에 "간결성"을 명시하고, 평가 후 문자수 대비 점수를 주기적으로 그려보면 눈에 보입니다.

-- 길이-점수 상관 체크
SELECT LENGTH(r.output) AS len, js.overall
FROM judge_scores js
JOIN agent_outputs r ON r.trace_id = js.case_id
WHERE js.created_at >= datetime('now', '-7 days')
ORDER BY len;

Pearson이 0.4를 넘으면 길이 편향이 유의미하게 살아있다는 뜻입니다.

7-3. 위치 편향(Position bias)

페어와이즈에서 먼저 보여준 응답에 후한 경향. 앞서 구현한 "정방향+역방향 평균" 구조로 상쇄합니다.

7-4. 앵커 편향

프롬프트에 "예시: 4점짜리 응답"을 넣으면 점수가 4 근처로 쏠립니다. 예시를 주지 않거나, 준다면 0점·5점 양 극단만 주세요.

7-5. 형식 오류

JSON 파싱 실패는 심각한 이슈입니다. 파싱 실패한 건은 자동으로 0점 처리하지 말고 재시도하세요. 0점으로 처리하면 판사 불안정이 품질 하락으로 가장됩니다.

def judge_with_retry(task, response, model, max_tries=3):
    for i in range(max_tries):
        r = judge(task, response, model=model)
        if not r.get("parse_error"):
            return r
    return {**r, "failed_after_retries": True}

8. 방어 스택과의 통합

지난 글 방어 스택 위에 판사 결과를 얹으면 비로소 "품질까지 자동 감시"가 완성됩니다.

8-1. Prometheus 메트릭 확장

# judge_metrics.py
from prometheus_client import Gauge, Counter

judge_overall_gauge = Gauge(
    "ai_judge_overall", "Latest overall judge score",
    ["agent", "case_id"]
)
judge_parse_errors = Counter(
    "ai_judge_parse_errors_total", "Judge JSON parse errors",
    ["model"]
)

def record(c_id, agent, result):
    judge_overall_gauge.labels(agent, c_id).set(result.get("overall", 0))
    if result.get("parse_error"):
        judge_parse_errors.labels(result.get("judge_model", "?")).inc()

8-2. Grafana 대시보드 추가 패널

패널 쿼리
에이전트별 평균 품질 avg(ai_judge_overall) by (agent)
7일 품질 추세 avg_over_time(ai_judge_overall[7d])
파싱 오류율 rate(ai_judge_parse_errors_total[1h])
드리프트 케이스 ai_judge_overall < 3.0

8-3. Slack 알림 규칙 추가

# judge_alert.py
import sqlite3, requests, os

def check_quality_drop():
    with sqlite3.connect("ai_usage.db") as db:
        rows = db.execute("""
            SELECT agent,
                AVG(CASE WHEN created_at >= datetime('now', '-1 day')
                    THEN overall END) AS today,
                AVG(CASE WHEN created_at BETWEEN datetime('now', '-8 day')
                    AND datetime('now', '-1 day') THEN overall END) AS last_week
            FROM judge_scores
            GROUP BY agent
        """).fetchall()

    alerts = []
    for agent, today, last_week in rows:
        if today and last_week and today < last_week * 0.85:
            alerts.append(f"- {agent}: {last_week:.2f} → {today:.2f} "
                          f"({(today-last_week)/last_week*100:+.0f}%)")
    if alerts:
        requests.post(os.environ["SLACK_WEBHOOK"], json={
            "text": "🔻 *AI 품질 하락 감지*\n" + "\n".join(alerts)
        })
# cron - 매 6시간
0 */6 * * * cd /opt/ai-defense && /usr/bin/python3 judge_alert.py

9. 샘플링 전략

전수 채점은 비용 문제입니다. 샘플링을 잘 설계해야 합니다.

  • 계층 샘플링(stratified): 에이전트별·모델별 각각 최소 N건 확보
  • 고비용 우선: 비용이 비싼 호출을 우선 채점(그게 사고 나면 크니까)
  • 저신뢰 우선: 에이전트가 stop_reason=length나 tool_error로 끝난 건
  • 랜덤 1%: 베이스라인 드리프트 탐지용
# sampling.py
import sqlite3, random

SAMPLE_SIZE_PER_AGENT = 10  # 일 기준

def pick_samples():
    with sqlite3.connect("ai_usage.db") as db:
        # 각 에이전트별 어제 호출 중 상위 비용 5건 + 랜덤 5건
        agents = [r[0] for r in db.execute(
            "SELECT DISTINCT agent_name FROM llm_calls"
        ).fetchall()]

        samples = []
        for a in agents:
            high = db.execute("""
                SELECT trace_id FROM llm_calls
                WHERE agent_name = ? AND DATE(created_at) = DATE('now', '-1 day')
                ORDER BY cost_usd DESC LIMIT 5
            """, (a,)).fetchall()
            rand = db.execute("""
                SELECT trace_id FROM llm_calls
                WHERE agent_name = ? AND DATE(created_at) = DATE('now', '-1 day')
                ORDER BY RANDOM() LIMIT 5
            """, (a,)).fetchall()
            samples.extend([r[0] for r in high + rand])
        return list(set(samples))

10. 운영 체크리스트

항목 주기 체크 방법
판사-인간 상관계수 재측정 월 1회 신규 50케이스로 Pearson 갱신
파싱 오류율 매일 2% 초과 시 프롬프트 재점검
길이-점수 상관 주 1회 0.4 초과 시 루브릭 수정
자기선호 편향 월 1회 판사 모델 교차 비교
Cascade Opus 전환율 매일 30% 초과 시 임계값 재조정
Slack 알림 피로 주 1회 잘못된 경보 누적 시 임계 수정

11. 알려진 한계

  • 사실 확인에는 약함: 외부 문서 검증은 판사 모델이 꿈(hallucinate)을 꿀 수 있음. RAG 기반 fact-checker를 별도로 붙여야 함.
  • 도메인 특화 지식 부족: 법률·의료처럼 전문 판사가 필요한 도메인은 LLM 판사로 커버되지 않음.
  • 동일 오류 반복: 판사 모델이 체계적으로 틀리는 패턴이 있으면 샘플을 전수 돌려도 잡히지 않음. 인간 스폿체크가 여전히 필요.
  • 점수 인플레이션: 시간이 지나며 평균이 서서히 올라가는 현상. 월 1회 리캘리브레이션 필수.

12. 확장 아이디어

  • 판사 앙상블: Claude + GPT-5.4 + Gemini 3개 판사 투표(voting)
  • 메타-판사: 판사의 rationale을 다시 평가해 점수 신뢰도 산출
  • 자동 루브릭 진화: 사람 수정 이력을 학습해 루브릭 개선 제안
  • 코드 전용 판사: 테스트 실행 + AST 분석 + LLM 판사 혼합 파이프라인
  • LangSmith·Phoenix 연동: 판사 결과를 기존 LLM 옵저버빌리티 툴로 통합

마치며

LLM-as-Judge 실전 구축의 포인트를 정리합니다.

  • 문자열 매칭은 오늘까지. 품질을 숫자로 보고 싶다면 루브릭 기반 판사 프롬프트로 넘어가야 합니다. 단어 매칭으로는 방어 스택의 "드리프트" 레이어가 반쪽짜리입니다.
  • 판사와 피평가자는 반드시 다른 모델. 자기선호 편향은 진짜로 존재하고 수치로 관측됩니다. Claude 응답은 GPT로, GPT 응답은 Claude로 - 최소한 주 1회 상호 검증 배치를 돌리세요.
  • 판사도 검증받아야 합니다. 최소 50건의 인간 라벨과 Pearson r, 가중 Kappa 측정. 0.6 미만이면 즉시 프롬프트를 갈아엎거나 판사를 교체해야 합니다.
  • 계단식 판사로 비용 절감. Haiku가 확실히 낮거나 확실히 높으면 그대로 쓰고, 경계선 구간만 Opus로 재검. 실무에서 63% 비용 절감은 충분히 현실적입니다.
  • 편향을 측정하지 않으면 조용히 왜곡됩니다. 길이 편향은 Pearson 0.4로, 위치 편향은 페어와이즈 일관성으로, 자기선호 편향은 교차 모델 비교로 주기적으로 체크하세요.
  • Prometheus + Slack 통합이 실질적 마지막 단계. 판사 결과가 대시보드에 들어가지 않으면 아무도 안 봅니다. 품질 드리프트가 비용 드리프트만큼 알림으로 뜨는 구조가 진짜 방어선입니다.

이제 비용·폭주·품질 3축이 모두 자동으로 감시되는 스택이 완성됐습니다. 다음 포스트에서는 이 위에 PII 스캐너와 LLM 보안 가드레일(프롬프트 인젝션·탈옥 탐지·민감정보 마스킹)을 얹는 방법을 다룰 예정입니다. 방어선의 마지막 한 축, "안전"이 남았습니다.