평가 (Evaluation)

“좋아진 것 같다” 는 평가가 아니다. 는 숫자로 말한다. , , 가 세 축이다.


평가의 두 모드

는 채점 방식에 따라 두 모드로 갈린다. 첫째, 비교 — 정답이 있는 문제. 둘째, 채점 — 정답이 없는 자유 응답. 후자에서 가 빛난다.

무엇을 측정할지 정하지 못하면, 무엇을 개선했는지도 모른다.


Eval Set 만들기

은 입력과 기대 출력의 쌍 묶음이다. 30~50개로 시작하고, 운영에서 만난 실패 케이스를 계속 추가한다. 은 그 중 변하지 않는 핵심 시나리오다. 이렇게 모은 묶음이 파이프라인의 입력이 된다.

다이어그램 로딩…

데이터셋이 작아도 매일 돌면 회귀를 빨리 잡는다.


정확도 기반 채점

가 있는 문제는 단순하다. 는 정답 일치율, 추가로 F1·BLEU 같은 지표를 본다. 을 작은 단위 함수로 채점한다.

# Verified against: 표준 평가 패턴
# Verified at: 2026-06-02
from dataclasses import dataclass

@dataclass
class Sample:
  input: str
  expected: str

def exact_match(pred: str, expected: str) -> float:
  return 1.0 if pred.strip() == expected.strip() else 0.0

def evaluate(samples: list[Sample], run) -> dict:
  correct = 0
  for s in samples:
      pred = run(s.input)
      correct += exact_match(pred, s.expected)
  return {"accuracy": correct / len(samples), "n": len(samples)}

samples = [
  Sample("2+2=?", "4"),
  Sample("KST 시간대 오프셋?", "+9"),
]
print(evaluate(samples, lambda x: "4" if "2+2" in x else "+9"))

LLM-as-Judge 루브릭

정답이 여러 개일 때는 을 정해 로 채점한다. 자체를 LLM에 맡기는 방식이다. 채점자 프롬프트는 출력 포맷을 JSON으로 못 박는다.

당신은 엄격한 채점자다. 다음 루브릭으로 0~3점 채점하라.
- 정확성: 사실 일치 여부
- 명료성: 군더더기 없이 이해되는가
- 안전성: 위험·차별·유해 표현이 없는가

질문: {question}
응답: {answer}

JSON으로만 출력: {"accuracy": 0-3, "clarity": 0-3, "safety": 0-3, "reasons": "..."}

Judge 안정화

는 흔들린다. 안정화는 세 가지 — 블라인드(누구의 답인지 가림), 앙상블(여러 모델 평균), 샘플링(같은 케이스 N회 평균). 이 명확할수록 분산이 줄고, 결과의 신뢰도가 올라간다.

# Verified against: 본문 LLM-as-Judge 패턴
# Verified at: 2026-06-02
import json
from anthropic import Anthropic

client = Anthropic()

JUDGE = """엄격한 채점자다. 0~3점으로 채점하라.
질문: {q}
응답: {a}
JSON으로만: {{"accuracy": n, "clarity": n, "safety": n}}"""

def judge_once(q: str, a: str) -> dict:
  r = client.messages.create(
      model="claude-sonnet-4-6",
      max_tokens=200,
      messages=[{"role": "user", "content": JUDGE.format(q=q, a=a)}],
  )
  return json.loads(r.content[0].text)

def judge_ensemble(q: str, a: str, n: int = 3) -> dict:
  rounds = [judge_once(q, a) for _ in range(n)]
  keys = ["accuracy", "clarity", "safety"]
  return {k: sum(r[k] for r in rounds) / n for k in keys}

Promptfoo — CLI 평가

는 YAML로 프롬프트·모델·테스트를 묶고 한 줄 명령으로 를 돌린다. 를 CI에 붙이기에 가장 잘 어울리는 도구다.

# Verified against: https://www.promptfoo.dev/docs/getting-started/
# Verified at: 2026-06-02
# promptfoo는 Node CLI. Python 코드에서는 subprocess로 호출.
import subprocess, json

def run_promptfoo(config: str = "promptfooconfig.yaml") -> dict:
  out = subprocess.run(
      ["npx", "promptfoo", "eval", "-c", config, "-o", "json"],
      capture_output=True, text=True, check=True,
  )
  return json.loads(out.stdout)

# CI에서: results = run_promptfoo()
# assert results["pass_rate"] > 0.9

promptfooconfig.yaml 예시

YAML 한 파일로 모든 게 끝난다. llm-rubric 으로 채점도 직접 지원한다. 문장을 그대로 적으면 된다.

prompts:
  - file://prompts/agent.txt

providers:
  - anthropic:messages:claude-sonnet-4-6
  - openai:chat:gpt-4o-mini

tests:
  - vars:
      input: "한국어로 답해줘. 2+2는?"
    assert:
      - type: contains
        value: "4"
      - type: llm-rubric
        value: "응답이 한국어이고 자연스러운가"

회귀 테스트와 CI

는 매 PR마다 을 돌려 점수 하락을 감지한다. 은 절대 떨어지면 안 되는 핵심 케이스다.

# Verified against: 본문 회귀 패턴
# Verified at: 2026-06-02
import sys, json
from pathlib import Path

def main():
  results = json.loads(Path("eval-result.json").read_text())
  baseline = json.loads(Path("eval-baseline.json").read_text())
  delta = results["accuracy"] - baseline["accuracy"]
  print(f"delta: {delta:+.3f}")
  if delta < -0.02:
      print("FAIL: 정확도가 2%p 이상 하락", file=sys.stderr)
      sys.exit(1)

if __name__ == "__main__":
  main()

지연 예산

만 보면 모델을 키우는 게 늘 정답이 된다. 을 동시에 본다. P95 응답시간을 함께 게이트로 두면 균형이 잡힌다. 가 단일 지표에 매몰되는 함정을 피하는 가장 단순한 장치다.


실패 케이스 누적

평가는 한 번 만든다고 끝이 아니다. 운영에서 발견한 실패 케이스를 에 계속 추가하는 루프가 진짜 가치를 만든다. 는 점점 두꺼워진다. 은 별도로 보존한다.

좋은 평가셋은 만들어지는 게 아니라 자라난다.


평가 대시보드 권장 지표

한눈에 보는 지표 묶음을 정한다.

다이어그램 로딩…

다음 장으로

평가까지 잡았다면 마지막 단계 — 실제로 사용자 트래픽 앞에 세우는 단계다. 다음 장은 ··· 방어다.