Part I · Foundations Chapter 04

RAG 및 코드 생성 평가

"Information retrieval is the foundation of knowledge work."— Gerard Salton, 정보 검색(Information Retrieval) 분야의 창시자

RAGAS, ARES, TruLens, SWE-bench, HumanEval — "구성 요소별 분해"로 파이프라인을 해부하는 도메인 특화 평가의 두 축

Ch.04 개요 — 왜 RAG와 코드 생성은 별도의 평가를 요구하는가

Ch.01~03에서 우리는 BLEU·ROUGE 같은 표면 일치 메트릭(surface-overlap metric)에서 BERTScore 같은 의미 기반 메트릭(semantic metric), 그리고 인간·LLM 심사(human/LLM-as-judge)까지 LLM 평가의 일반론을 살펴봤다. 그러나 RAG(Retrieval-Augmented Generation)와 코드 생성(code generation)은 "최종 출력만 보면 안 되는" 두 가지 대표적 응용 영역이다. RAG는 검색이 틀렸으면 답이 맞아도 문제이고, 코드 생성은 돌아가도 비효율·취약하면 문제다.

이 장의 일관된 주제는 하나다 — 파이프라인을 블랙박스로 평가하지 말고 구성 요소별로 분해(component-wise decomposition)하라. RAG에서는 Retrieval과 Generation을 분리해 진단하고, 코드 생성에서는 실행 성공(pass/fail)을 넘어 복잡도·안전성·가독성으로 차원을 분해한다. 이 분해의 도구로 RAGAS, ARES, TruLens, DeepEval, SWE-bench, HumanEval을 심층 해부한다.

4.0 장 구성 로드맵

이 장은 크게 두 부분으로 나뉜다. 전반부(4.1~4.9)는 RAG 평가를, 후반부(4.10~4.19)는 코드 생성 평가를 다룬다. 두 부분 모두 "왜 다른가 → 메트릭의 수학적 정의 → 프레임워크 비교 → 실무 구현 → 사례 연구"의 동일한 흐름을 따른다.

섹션주제핵심 내용
4.1RAG 평가는 왜 다른가5단계 파이프라인, 세 가지 실패 양식
4.2RAG 평가의 3레벨Retrieval / Generation / End-to-End
4.3RAGAS 심층 — Faithfulnessclaim 추출 + NLI 판정, 3개 시나리오
4.4RAGAS 심층 — Answer Relevance역질문 생성 + 코사인 유사도
4.5RAGAS 심층 — Context Precision/Recall순위 가중 정밀도, 재현율
4.6RAGAS의 한계합성 데이터, NLI 오류, claim 추출 난이도
4.7프레임워크 비교RAGAS·ARES·TruLens·DeepEval 메트릭 매트릭스
4.8프레임워크 선택 가이드결정 트리, 비용·신뢰도 트레이드오프
4.9RAG 평가 실무동작하는 파이프라인 + 고객 지원 사례 연구
4.10코드 생성 평가는 왜 다른가실행 가능성, 정답의 다양성
4.11Pass@k 재고찰불편 추정량 유도, n·k 민감도
4.12실행 기반 vs 정적 분석샌드박스, AST, 타입 체크, SAST
4.13pass/fail을 넘어선 코드 품질복잡도, 에러 핸들링, 가독성, 보안
4.14SWE-bench 심층이슈→패치→테스트, Lite/Verified
4.15코드 평가 프레임워크 비교HumanEval·MBPP·SWE-bench·LiveCodeBench
4.16오염과 데이터 누출훈련 데이터 오염, LiveCodeBench의 해법
4.17평가 비용·지연 엔지니어링계층적 필터링, 샘플링, 비용 모델
4.18통합 관점 — RAG와 코드의 공통 교훈분해·실행·다차원의 보편 원리
4.19요약과 다음 장 예고Ch.05 근본적 도전과제로의 연결

4.1 RAG 평가는 왜 다른가

RAG는 외부 지식 베이스(knowledge base)에서 관련 문서를 검색하여 LLM의 생성에 주입하는 아키텍처다. Lewis et al. (2020, "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks", arXiv:2005.11401)이 정식화한 이래, 환각(hallucination)을 줄이고 지식을 최신 상태로 유지하는 사실상의 표준 패턴이 되었다. 문제는 평가다. 순수 LLM은 입력→출력의 단일 함수지만, RAG는 여러 단계가 직렬로 연결된 파이프라인이다. 어느 단계에서 실패했는지를 구분하지 못하면 개선의 방향을 잡을 수 없다.

4.1.1 RAG 파이프라인의 다섯 구성 요소

전형적인 RAG 파이프라인은 다음 다섯 단계로 분해된다. 각 단계는 독립적인 실패 지점(failure point)이며, 따라서 독립적인 평가 대상이다.

1

Query Understanding

질문을 검색 가능한 형태로 변환·확장

2

Retrieval

벡터·키워드 검색으로 후보 문서 회수

3

Context Selection

재순위화·필터링으로 컨텍스트 구성

4

Generation

컨텍스트 기반 LLM 답변 생성

5

Response

최종 응답 + 인용·출처

예컨대 "Query Understanding"에서 질문의 핵심 의도를 잘못 파악하면, 그 뒤의 모든 단계가 잘못된 방향으로 흘러간다. 반대로 검색과 컨텍스트 선택이 완벽해도 Generation 단계에서 LLM이 컨텍스트를 무시하고 자신의 파라미터 지식(parametric knowledge)으로 답하면 환각이 발생한다. 따라서 RAG 평가는 본질적으로 단계별 책임 귀속(attribution) 문제다.

4.1.2 핵심 통찰: "답변이 맞더라도, 검색이 잘못되었으면 문제다"

RAG 평가에서 가장 반직관적인 명제가 이것이다. 최종 답변이 사실적으로 정확하더라도, 그 정확성이 검색된 컨텍스트가 아니라 LLM의 내부 지식에서 비롯되었다면 그 RAG 시스템은 신뢰할 수 없다. 왜냐하면 그 시스템은 지식 베이스가 업데이트되어도 반영하지 못하고, 출처를 제시할 수 없으며, 도메인 특화 정보(예: 사내 문서)에서는 환각으로 직행할 것이기 때문이다. 즉 "운 좋게 맞은" 답은 평가에서 감점되어야 한다.

RAG 평가의 본질: "검색이 맞는가"와 "생성이 맞는가"는 별개 문제다

RAG에서 최종 정답률(end-to-end accuracy)만 측정하면 두 가지 독립적 실패가 한 숫자로 뭉개진다. 검색은 완벽한데 생성이 망쳤을 수도, 생성은 충실한데 검색이 엉뚱한 문서를 가져왔을 수도 있다. 이 둘은 처방이 정반대다 — 전자는 프롬프트·디코딩을 고치고, 후자는 임베딩·청킹(chunking)·재순위화를 고친다. 따라서 RAG 평가의 첫 번째 계명은 "검색과 생성을 절대 한 숫자로 합치지 말라"이다.

4.1.3 RAG의 세 가지 실패 양식

RAG 시스템이 실패하는 방식은 표면적으로는 "틀린 답"이지만, 그 원인은 세 가지로 명확히 구분된다. 이 분류는 RAGAS의 네 메트릭이 무엇을 잡으려 하는지를 이해하는 출발점이다.

실패 양식정의책임 단계대응 RAGAS 메트릭
Hallucination (환각)검색된 컨텍스트에 없는 내용을 답변이 지어냄GenerationFaithfulness ↓
Missed Information (정보 누락)답에 필요한 정보를 애초에 검색하지 못함RetrievalContext Recall ↓
Irrelevant Context (무관 컨텍스트)관련 없는 문서가 컨텍스트를 오염시켜 답을 흐림Context SelectionContext Precision ↓

세 양식은 서로 다른 메트릭으로 잡힌다. Faithfulness가 낮으면 생성 단계의 환각, Context Recall이 낮으면 검색 단계의 누락, Context Precision이 낮으면 컨텍스트 선택 단계의 잡음이다. 이 대응 관계가 RAGAS 설계 철학의 핵심이며, 이를 통해 "어디를 고쳐야 하는가"가 즉시 도출된다.

주의: "무관 컨텍스트"가 LLM을 헷갈리게 한다

직관과 달리, 컨텍스트를 많이 넣을수록 좋은 것이 아니다. Shi et al. (2023, "Large Language Models Can Be Easily Distracted by Irrelevant Context", arXiv:2302.00093)은 무관한 문장 하나가 추론 정확도를 크게 떨어뜨림을 보였다. 또한 Liu et al. (2023, "Lost in the Middle: How Language Models Use Long Contexts", arXiv:2307.03172)은 긴 컨텍스트의 중간에 위치한 정보가 무시되는 U자형 위치 편향(positional bias)을 보고했다. 따라서 Context Precision은 단순한 검색 품질이 아니라 생성 품질의 선행 지표이기도 하다.

4.2 RAG 평가의 세 레벨

RAG 평가는 위계적으로 세 레벨로 구성된다. 아래에서 위로 올라갈수록 통합적이지만 진단력은 떨어진다. 실무에서는 세 레벨을 모두 측정하되, 문제 발생 시 위에서 아래로 드릴다운(drill-down)한다.

① Retrieval Quality (검색 품질)

관련 문서를 얼마나 잘, 얼마나 높은 순위로 회수했는가. Context Precision/Recall, MRR, nDCG로 측정. 정답 라벨(relevance label)이 필요.

② Generation Quality (생성 품질)

주어진 컨텍스트 안에서 얼마나 충실하고 관련성 있게 답했는가. Faithfulness, Answer Relevance로 측정. 검색을 고정한 채 생성만 평가.

③ End-to-End Quality (전체 품질)

사용자 관점의 최종 만족도. Answer Correctness, 인간 평가, 다운스트림 태스크 성공률. 가장 중요하지만 진단력은 가장 낮음.

4.2.1 레벨 1: Retrieval Quality와 고전 IR 메트릭

검색 품질은 정보 검색(Information Retrieval, IR) 분야가 수십 년간 정제해 온 메트릭으로 측정한다. 가장 기본은 Precision@k와 Recall@k다. 상위 k개 결과 중 관련 문서의 비율(정밀도)과, 전체 관련 문서 중 상위 k에 포함된 비율(재현율)이다.

Precision@k와 Recall@k
$$\text{P@}k = \frac{|\{\text{relevant docs}\} \cap \{\text{top-}k \text{ retrieved}\}|}{k}, \qquad \text{R@}k = \frac{|\{\text{relevant docs}\} \cap \{\text{top-}k \text{ retrieved}\}|}{|\{\text{relevant docs}\}|}$$

P@k는 "회수한 것 중 맞은 비율", R@k는 "맞아야 할 것 중 회수한 비율". RAG에서는 둘 다 중요하지만 트레이드오프 관계

그러나 P@k와 R@k는 순위(rank)를 무시한다. 관련 문서가 1위에 있든 10위에 있든 같은 점수다. 검색에서는 순위가 결정적이므로, 순위를 반영하는 메트릭이 필요하다. 대표적으로 MRR(Mean Reciprocal Rank)과 nDCG(normalized Discounted Cumulative Gain)가 있다.

MRR과 nDCG
$$\text{MRR} = \frac{1}{|Q|}\sum_{q=1}^{|Q|}\frac{1}{\text{rank}_q}, \qquad \text{nDCG@}k = \frac{1}{\text{IDCG@}k}\sum_{i=1}^{k}\frac{2^{rel_i}-1}{\log_2(i+1)}$$

$\text{rank}_q$: 질의 q에서 첫 관련 문서의 순위. $rel_i$: i번째 문서의 관련도 등급. IDCG는 이상적 순위에서의 DCG(정규화 상수)

이 IR 메트릭들은 정답 관련도 라벨만 있으면 LLM 호출 없이 즉시 계산할 수 있어, RAG 검색 단계의 빠르고 저렴한 진단 도구다. 아래는 nDCG와 MRR을 함께 산출하는 구현이다.

import numpy as np def dcg(rels: list[float]) -> float: return sum((2 ** r - 1) / np.log2(i + 2) for i, r in enumerate(rels)) def ndcg_at_k(rels: list[float], k: int) -> float: """rels: 검색 순위순 관련도 등급, k: 절단 위치""" ideal = sorted(rels, reverse=True)[:k] idcg = dcg(ideal) return dcg(rels[:k]) / idcg if idcg > 0 else 0.0 def mrr(rels_per_query: list[list[int]]) -> float: """각 질의의 순위순 0/1 리스트 → 첫 관련 문서 역순위 평균""" total = 0.0 for rels in rels_per_query: for i, r in enumerate(rels, start=1): if r: total += 1 / i break return total / len(rels_per_query) print(round(ndcg_at_k([3, 0, 2, 0, 1], 5), 3)) # 상위에 고관련 문서 → 높음 print(round(mrr([[0, 1, 0], [1, 0, 0]]), 3)) # (1/2 + 1/1)/2 = 0.75

4.2.2 레벨 2: Generation Quality와 reference-free 평가

생성 품질의 핵심 난점은 "정답(reference)이 없거나 다양하다"는 것이다. RAG의 답변은 검색된 컨텍스트에 의존하므로, 고정된 정답 문장과의 BLEU·ROUGE 비교는 의미가 없다. 그래서 RAGAS는 reference-free 평가를 지향한다 — 정답 문장 대신 "답변이 컨텍스트에 충실한가", "답변이 질문에 직접 답하는가"를 LLM과 NLI(Natural Language Inference) 모델로 판정한다. 이것이 다음 절(4.3~4.5)의 주제다.

4.2.3 레벨 3: End-to-End Quality와 그 함정

전체 품질은 사용자가 실제로 체감하는 것이지만, 그 자체로는 진단 정보가 거의 없다. "정확도 72%"라는 숫자는 무엇을 고쳐야 할지 알려주지 않는다. 더구나 앞서 말한 "운 좋게 맞은 답" 문제 때문에, 높은 E2E 정확도가 좋은 RAG 시스템을 보장하지도 않는다. 따라서 E2E는 최종 KPI로 추적하되, 개선 작업은 항상 레벨 1·2로 내려가서 수행한다.

레벨 간 인과 사슬을 추적하라

이상적인 RAG 디버깅은 다음 사슬을 따른다: E2E 정확도가 낮다 → Faithfulness를 본다 → 낮으면 생성 문제(프롬프트·디코딩), 높으면 → Context Recall을 본다 → 낮으면 검색 누락(임베딩·청킹), 높으면 → Context Precision을 본다 → 낮으면 잡음(재순위화·필터). 이렇게 위에서 아래로 내려가며 책임 단계를 격리하면, "전체가 안 좋다"는 막연함을 "검색 재현율이 0.6이라 정보를 못 가져온다"는 행동 가능한 진단으로 바꿀 수 있다.

4.3 RAGAS 심층 (1) — Faithfulness

RAGAS(Es et al., 2024, "RAGAS: Automated Evaluation of Retrieval Augmented Generation", arXiv:2309.15217)는 RAG 평가의 사실상 표준 프레임워크다. 네 가지 핵심 메트릭 — Faithfulness, Answer Relevance, Context Precision, Context Recall — 을 reference-free로 산출하며, LlamaIndex·LangChain에 공식 통합되어 있다. 가장 먼저 Faithfulness(충실도)부터 해부한다.

4.3.1 정의: 모든 claim이 컨텍스트에서 지지되는가

Faithfulness는 "생성된 답변의 모든 주장(claim)이 검색된 컨텍스트에 의해 지지되는가"를 측정한다. 직관적으로 이것은 환각(hallucination)의 정량적 반대 개념이다. 답변을 개별 claim들로 분해한 뒤, 각 claim이 컨텍스트로부터 추론 가능한지를 NLI(자연어 추론) 방식으로 판정한다.

Faithfulness
$$F = \frac{|\{\text{supported claims}\}|}{|\{\text{total claims}\}|}$$

분자: 컨텍스트에서 지지(entailment)되는 claim 수, 분모: 답변에서 추출된 전체 claim 수. $F \in [0,1]$, 1에 가까울수록 충실

4.3.2 계산 절차: claim 추출 → NLI 판정

Faithfulness 계산은 두 단계로 이뤄진다. 첫째, LLM을 사용해 답변을 원자적(atomic) claim들로 분해한다. 둘째, 각 claim에 대해 "이 claim이 주어진 컨텍스트로부터 추론될 수 있는가?"를 NLI 모델 또는 LLM 판사로 판정한다(entailment / contradiction / neutral). entailment로 판정된 claim의 비율이 Faithfulness 점수다.

# Faithfulness 계산의 개념적 구현 (RAGAS 스타일) from dataclasses import dataclass @dataclass class FaithfulnessResult: score: float claims: list verdicts: list # True=지지, False=미지지 class FaithfulnessEvaluator: """답변의 각 claim이 컨텍스트에서 지지되는 비율을 산출""" def __init__(self, llm, nli): self.llm = llm # claim 추출용 LLM self.nli = nli # entailment 판정용 NLI 모델 def extract_claims(self, answer: str) -> list[str]: """답변을 독립적으로 검증 가능한 원자적 claim으로 분해""" prompt = ( "다음 답변을 독립적으로 검증 가능한 단일 사실 문장들로 분해하라.\n" f"답변: {answer}\n각 줄에 하나의 claim:" ) raw = self.llm.generate(prompt) return [c.strip("- ") for c in raw.splitlines() if c.strip()] def verify(self, claim: str, context: str) -> bool: """claim이 context로부터 entailment 되는지 판정""" label = self.nli.predict(premise=context, hypothesis=claim) return label == "entailment" def evaluate(self, answer: str, context: str) -> FaithfulnessResult: claims = self.extract_claims(answer) if not claims: return FaithfulnessResult(0.0, [], []) verdicts = [self.verify(c, context) for c in claims] score = sum(verdicts) / len(verdicts) return FaithfulnessResult(score, claims, verdicts)

4.3.3 시나리오 1 — 완전한 환각

구체적 예시로 메커니즘을 보자. 컨텍스트와 답변, 그리고 claim별 판정이다.

컨텍스트: "에펠탑은 1889년 파리 만국박람회를 위해 건설되었으며 높이는 약 330미터다."

답변: "에펠탑은 1889년에 건설되었고, 높이는 약 330미터이며, 귀스타브 에펠이 노벨상을 받았다."

claim 1: "에펠탑은 1889년에 건설되었다" — 컨텍스트에서 지지됨 (entailment)
claim 2: "높이는 약 330미터다" — 컨텍스트에서 지지됨 (entailment)
claim 3: "귀스타브 에펠이 노벨상을 받았다" — 컨텍스트에 없음 (환각, neutral)

3개 claim 중 2개가 지지되므로 $F = 2/3 \approx 0.67$이다. 답변의 일부는 정확하지만 환각이 섞여 있어 충실도가 감점되었다. 주목할 점은 claim 3이 사실 여부와 무관하게 감점된다는 것이다. 설령 에펠이 실제로 노벨상을 받았더라도(받지 않았지만), 그것이 컨텍스트에 없으면 RAG 관점에서는 "근거 없는 진술"이다.

4.3.4 시나리오 2 — 부분 충실(추론 비약)

컨텍스트: "이 약물은 임상 1상에서 안전성이 확인되었다."

답변: "이 약물은 안전하며 시판이 승인되었다."

claim 1: "이 약물은 안전하다" — 컨텍스트는 "1상 안전성 확인"이지 일반적 안전성 보증이 아님 (과잉 일반화, neutral)
claim 2: "시판이 승인되었다" — 컨텍스트에 없음 (환각, neutral)

$F = 0/2 = 0.0$. 이 사례는 Faithfulness의 진가를 보여준다. 답변이 명백한 거짓말은 아니지만 컨텍스트가 보장하지 않는 강한 결론으로 비약했다. 의료·법률처럼 정밀함이 생명인 도메인에서 이런 "근거 없는 단정"은 치명적이며, Faithfulness가 정확히 이를 잡아낸다.

4.3.5 시나리오 3 — 완전 충실

컨텍스트: "회사의 2023년 매출은 50억 원으로 전년 대비 20% 증가했다."

답변: "2023년 매출은 50억 원이며 전년보다 20% 늘었다."

claim 1: "2023년 매출은 50억 원" — 지지됨
claim 2: "전년보다 20% 늘었다" — 지지됨

$F = 2/2 = 1.0$. 모든 주장이 컨텍스트에 근거하므로 완전 충실하다. 이것이 RAG가 지향하는 이상적 답변이다 — 컨텍스트를 충실히 종합하되 그 너머로 나아가지 않는다.

Faithfulness = Hallucination의 반대? 반드시 그렇지는 않다

흔한 오해는 "Faithfulness 1.0 = 환각 0 = 정답"이라는 등식이다. 셋은 같지 않다. (1) Faithfulness는 컨텍스트 대비 충실도이지 세계 진실(world truth) 대비가 아니다 — 컨텍스트 자체가 틀렸으면 충실한 답도 거짓이 된다(garbage-in, garbage-out). (2) 답변이 컨텍스트의 일부만 충실히 옮기고 핵심을 누락해도 Faithfulness는 1.0일 수 있다(이는 Answer Relevance·Recall이 잡는다). (3) NLI 판정 자체가 틀릴 수 있다. 따라서 Faithfulness는 환각의 강한 신호이지 완전한 보증이 아니다. 네 메트릭을 함께 봐야 하는 이유다.

4.3.6 인터랙티브 데모 — Faithfulness 추정기

아래 데모는 답변의 claim 수와 그중 컨텍스트에서 지지되는 수를 입력하면 Faithfulness를 즉시 계산한다(실제 RAGAS는 LLM/NLI가 이 판정을 자동화한다).

🧮 Faithfulness 계산기
전체 claim 수와 지지된 claim 수를 입력하고 계산을 누르세요.

4.3.7 NLI 모델이라는 토대 — Faithfulness의 신뢰도 상한

Faithfulness와 Context Recall은 모두 NLI(자연어 추론) 판정에 의존하므로, 그 신뢰도가 메트릭의 신뢰도 상한을 결정한다. NLI는 전제(premise)와 가설(hypothesis) 쌍을 entailment·contradiction·neutral로 분류하는 과제로, MNLI(Williams et al., 2018, "A Broad-Coverage Challenge Corpus for Sentence Understanding through Inference", NAACL 2018, arXiv:1704.05426) 같은 대규모 코퍼스로 학습된 모델이 표준이다. RAGAS는 컨텍스트를 전제로, 각 claim을 가설로 두어 entailment 여부를 판정한다.

문제는 NLI 모델이 수량("3개 이상" vs "정확히 3개"), 부정("안전하지 않다"의 이중 부정), 시제, 함축("1상 안전"으로부터 "안전")에서 자주 틀린다는 점이다. 이 때문에 도메인이 정밀할수록(의료·법률) 범용 NLI 모델의 오판이 늘어, 4.6.2에서 지적한 한계가 증폭된다. 실무에서는 (1) 도메인 NLI 파인튜닝, (2) 강한 LLM을 NLI 판사로 대체, (3) 4.6의 권고대로 인간 골드셋으로 NLI 판정의 정확도 자체를 검증하는 보정 절차를 둔다.

4.4 RAGAS 심층 (2) — Answer Relevance

Faithfulness가 "답변이 컨텍스트에 충실한가"라면, Answer Relevance(답변 관련성)는 "답변이 질문에 직접 답하는가"를 측정한다. 충실하지만 동문서답인 답변이 가능하므로, 이 둘은 독립적이다.

4.4.1 정의와 직관: 역질문(reverse question)

RAGAS의 Answer Relevance는 기발한 아이디어에 기반한다 — "이 답변에서 어떤 질문이 유도되는가?"를 LLM에게 묻고, 그 역질문(reverse question)이 원래 질문과 얼마나 유사한지를 측정한다. 직관은 명료하다. 답변이 질문에 잘 답했다면, 그 답변만 보고도 원래 질문을 복원할 수 있어야 한다. 동문서답이거나 장황한 답변은 원래 질문과 동떨어진 역질문을 유도한다.

Answer Relevance (역질문 코사인 유사도 평균)
$$\text{AR} = \frac{1}{n}\sum_{i=1}^{n}\cos\!\big(E(q),\, E(q_i')\big)$$

$q$: 원래 질문, $q_i'$: 답변으로부터 생성된 i번째 역질문($n$개 생성), $E(\cdot)$: 임베딩 함수, $\cos$: 코사인 유사도. 여러 역질문을 생성해 평균함으로써 LLM 생성의 분산을 완화

4.4.2 코사인 유사도의 정의

역질문과 원질문의 유사도는 임베딩 공간에서의 코사인 유사도로 측정한다. 코사인 유사도는 두 벡터가 이루는 각도의 코사인으로, 벡터의 크기가 아닌 방향만 비교하므로 길이가 다른 문장 간 의미 유사도에 적합하다.

코사인 유사도
$$\cos(\mathbf{a},\mathbf{b}) = \frac{\mathbf{a}\cdot\mathbf{b}}{\|\mathbf{a}\|\,\|\mathbf{b}\|} = \frac{\sum_{k=1}^{d} a_k b_k}{\sqrt{\sum_{k=1}^{d} a_k^2}\,\sqrt{\sum_{k=1}^{d} b_k^2}}$$

$\mathbf{a},\mathbf{b}$: $d$차원 임베딩 벡터. 결과 범위 $[-1,1]$, 임베딩이 보통 양의 영역에 분포하므로 실무적으로는 $[0,1]$에 가까움

4.4.3 구현

import numpy as np def cosine(a, b): return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9)) class AnswerRelevanceEvaluator: """답변에서 역질문을 n개 생성 → 원질문과의 평균 코사인 유사도""" def __init__(self, llm, embedder, n_questions=3): self.llm = llm self.embed = embedder self.n = n_questions def generate_reverse_questions(self, answer: str) -> list[str]: prompt = ( f"다음 답변을 보고, 이 답변이 답하고 있는 질문을 추론해 작성하라.\n" f"답변: {answer}\n질문:" ) return [self.llm.generate(prompt) for _ in range(self.n)] def evaluate(self, question: str, answer: str) -> float: q_vec = self.embed(question) reverse_qs = self.generate_reverse_questions(answer) sims = [cosine(q_vec, self.embed(rq)) for rq in reverse_qs] return float(np.mean(sims)) # 동문서답 사례: 질문 "에펠탑 높이는?" → 답변 "파리는 프랑스의 수도다" # 역질문은 "프랑스의 수도는?"이 되어 원질문과 유사도가 낮음 → AR ↓
역질문 기법이 "장황함"을 잡는 이유

답변이 질문에 답하면서 불필요한 정보를 잔뜩 붙이면, 그 답변으로부터 유도되는 역질문은 원질문보다 넓거나 다른 방향이 된다. 예컨대 "에펠탑 높이는?"에 "에펠탑은 1889년 건설된 파리의 상징으로 높이는 330m이고 매년 수백만 명이 방문한다"라고 답하면, 역질문은 "에펠탑에 대해 설명해줘"에 가까워져 유사도가 떨어진다. 따라서 Answer Relevance는 단순한 정답 여부가 아니라 "질문에 초점을 맞췄는가(focus)"를 측정하는 메트릭이다. 이는 RAG뿐 아니라 일반 QA 시스템 평가에도 유용하다.

4.4.4 Answer Correctness — 의미 유사도와 사실 일치의 결합

Faithfulness·Answer Relevance가 reference-free였다면, ground truth 답변이 있을 때는 더 직접적인 Answer Correctness(답변 정확성)를 측정할 수 있다. RAGAS의 Answer Correctness는 두 성분의 가중합이다 — (1) 답변과 정답의 의미 유사도(semantic similarity, 임베딩 코사인), (2) 사실 단위의 일치도(factual overlap, claim 수준 F1). 표면 일치가 아닌 의미·사실 수준에서 비교한다는 점이 핵심이다.

Answer Correctness (RAGAS)
$$\text{AC} = w_s \cdot \underbrace{\cos\!\big(E(a), E(g)\big)}_{\text{의미 유사도}} + w_f \cdot \underbrace{\frac{2\,|\text{TP}|}{2\,|\text{TP}| + |\text{FP}| + |\text{FN}|}}_{\text{사실 F1}}$$

$a$: 생성 답변, $g$: ground truth. TP: 양쪽에 있는 사실, FP: 답변에만 있는 사실(환각), FN: 정답에만 있는 사실(누락). 보통 $w_s{=}w_f{=}0.5$

사실 F1이 환각(FP)과 누락(FN)을 동시에 벌점한다는 점에 주목하라. 이는 Faithfulness(환각만)와 Context Recall(누락만)을 ground truth 기준으로 합친 형태로 볼 수 있다. 의미 유사도 성분은 표현이 다르되 뜻이 같은 답변(의역)에 관대함을 부여한다.

def answer_correctness(answer, ground_truth, embed, claim_match, ws=0.5, wf=0.5): """의미 유사도 + 사실 F1의 가중합 (RAGAS Answer Correctness)""" sem = cosine(embed(answer), embed(ground_truth)) tp, fp, fn = claim_match(answer, ground_truth) # claim 집합 비교 f1 = (2 * tp) / (2 * tp + fp + fn + 1e-9) return ws * sem + wf * f1 # 의역이지만 사실은 같은 답 → 의미 유사도 높고 F1 높음 → AC 높음 # 표현은 같지만 핵심 수치가 틀린 답 → F1이 떨어져 AC 감점

4.5 RAGAS 심층 (3) — Context Precision과 Context Recall

앞의 두 메트릭이 생성 품질(레벨 2)이었다면, Context Precision과 Context Recall은 검색 품질(레벨 1)을 본다. 전자는 "관련 문서를 상위에 잘 배치했는가", 후자는 "필요한 정보를 다 가져왔는가"이다.

4.5.1 Context Precision — 순위 가중 정밀도

Context Precision은 검색된 문서들 중 관련 문서가 상위 순위에 배치되었는지를 측정한다. 단순 Precision@k와 달리 순위를 가중하므로, 관련 문서가 1위에 있으면 10위에 있을 때보다 높은 점수를 받는다. 이는 정보 검색의 Average Precision(AP)과 동일한 철학이다.

Context Precision (순위 가중)
$$\text{CP} = \frac{1}{\sum_{j}\text{rel}(d_j)}\sum_{i=1}^{|D|}\text{precision@}i \cdot \text{rel}(d_i), \quad \text{precision@}i = \frac{\sum_{j=1}^{i}\text{rel}(d_j)}{i}$$

$D$: 검색된 문서 리스트(순서 있음), $\text{rel}(d_i)\in\{0,1\}$: i번째 문서의 관련 여부. 관련 문서가 위에 모일수록 CP↑. 형태는 Average Precision(AP)과 동형

예를 들어 5개 문서가 검색되었고 관련도 패턴이 $[1,0,1,0,0]$(1·3위가 관련)이라면, precision@1=1.0, precision@3=2/3이고 관련 문서 위치에서만 합산하므로 $\text{CP}=\frac{1.0+0.667}{2}\approx 0.83$이다. 반대로 패턴이 $[0,0,1,0,1]$(3·5위)이라면 precision@3=1/3, precision@5=2/5로 $\text{CP}=\frac{0.333+0.4}{2}\approx 0.37$로 크게 낮아진다. 같은 수의 관련 문서라도 순위가 낮으면 감점된다.

4.5.2 Context Recall — 답변 문장의 컨텍스트 출처 비율

Context Recall은 "답을 구성하는 데 필요한 정보가 컨텍스트에 포함되었는가"를 측정한다. RAGAS는 ground truth 답변의 핵심 문장을 추출한 뒤, 각 문장이 검색된 컨텍스트에서 근거를 찾을 수 있는지의 비율로 계산한다.

Context Recall
$$R = \frac{|\{\text{sentences in ground truth attributable to context}\}|}{|\{\text{total sentences in ground truth}\}|}$$

분자: ground truth의 문장 중 검색된 컨텍스트로 귀속 가능한 문장 수, 분모: ground truth의 전체 핵심 문장 수. 낮으면 검색이 필요한 정보를 누락한 것

# Context Precision / Context Recall 계산 def context_precision(relevances: list[int]) -> float: """relevances: 검색 순위순 0/1 리스트 (1=관련)""" total_rel = sum(relevances) if total_rel == 0: return 0.0 cumulative, weighted = 0, 0.0 for i, rel in enumerate(relevances, start=1): cumulative += rel if rel: # 관련 문서 위치에서만 precision@i 합산 weighted += (cumulative / i) return weighted / total_rel def context_recall(gt_sentences: list[str], context: str, nli) -> float: """ground truth 문장 중 컨텍스트에서 지지되는 비율""" if not gt_sentences: return 0.0 attributable = sum( 1 for s in gt_sentences if nli.predict(premise=context, hypothesis=s) == "entailment" ) return attributable / len(gt_sentences) print(context_precision([1, 0, 1, 0, 0])) # ≈ 0.83 print(context_precision([0, 0, 1, 0, 1])) # ≈ 0.37

4.5.3 네 메트릭의 종합 — RAG 진단 사분면

네 메트릭을 두 축으로 묶으면 RAG 시스템의 상태를 사분면으로 진단할 수 있다. 가로축은 검색 품질(Context Precision·Recall), 세로축은 생성 품질(Faithfulness·Answer Relevance)이다.

검색 품질생성 품질진단처방
높음높음건강한 RAG유지·미세 튜닝
높음낮음좋은 컨텍스트를 못 살림프롬프트·디코딩·모델 교체
낮음높음나쁜 컨텍스트에 충실(쓰레기에 충실)임베딩·청킹·재순위화 개선
낮음낮음전면 재설계 필요검색·생성 동시 점검

RAGAS 4메트릭 — 시스템 A vs B

Context Precision: 순위에 따른 점수 변화

4.5.4 검색 내부 (1) — 임베딩과 유사도 검색

Context Precision·Recall이 낮을 때 무엇을 고쳐야 하는지를 알려면 검색 단계의 내부 메커니즘을 이해해야 한다. 가장 흔한 RAG 검색은 밀집 검색(dense retrieval)이다 — 질문과 문서를 같은 임베딩 공간으로 사상한 뒤, 질문 벡터와 가장 가까운 문서 벡터를 회수한다. 거리(또는 유사도)는 보통 코사인 유사도(4.4.2)나 내적(inner product)을 쓴다.

밀집 검색의 점수 함수
$$\text{score}(q, d) = \cos\!\big(E(q),\, E(d)\big), \qquad \text{Retrieved}_k = \operatorname*{arg\,top\text{-}}_{d \in \mathcal{D}} {}^{k}\ \text{score}(q,d)$$

$E$: 임베딩 인코더, $\mathcal{D}$: 문서 컬렉션. 상위 $k$개를 회수. 임베딩이 질문·문서의 의미를 잘 정렬할수록 Context Recall↑

밀집 검색은 의미적으로 유사하지만 단어가 다른 문서를 잘 찾는 반면, 정확한 키워드(고유명사·코드 식별자)에는 약하다. 그래서 실무에서는 밀집 검색과 희소 검색(sparse retrieval, BM25)을 결합한 하이브리드 검색을 자주 쓴다. BM25는 단어 빈도(term frequency)와 역문서빈도(inverse document frequency)에 문서 길이 정규화를 더한 고전 IR 점수 함수다.

BM25 (희소 검색 점수)
$$\text{BM25}(q,d) = \sum_{t \in q} \text{IDF}(t)\cdot\frac{f(t,d)\,(k_1+1)}{f(t,d) + k_1\big(1 - b + b\,\frac{|d|}{\text{avgdl}}\big)}$$

$f(t,d)$: 문서 d에서 용어 t의 빈도, $|d|$: 문서 길이, avgdl: 평균 문서 길이, $k_1,b$: 튜닝 상수(보통 $k_1{=}1.2, b{=}0.75$). 정확한 용어 매칭에 강함

import numpy as np class HybridRetriever: """밀집(코사인) + 희소(BM25) 점수를 정규화 후 가중 결합""" def __init__(self, dense_index, bm25, alpha=0.6): self.dense = dense_index # 임베딩 검색기 self.bm25 = bm25 self.alpha = alpha # 밀집 가중치 (1-alpha = 희소) def _minmax(self, scores): lo, hi = min(scores), max(scores) return [(s - lo) / (hi - lo + 1e-9) for s in scores] def retrieve(self, query: str, docs: list[str], k=8) -> list[int]: d_scores = self._minmax(self.dense.score(query, docs)) s_scores = self._minmax(self.bm25.score(query, docs)) fused = [self.alpha * d + (1 - self.alpha) * s for d, s in zip(d_scores, s_scores)] return list(np.argsort(fused)[::-1][:k]) # 상위 k 인덱스

4.5.5 검색 내부 (2) — 청킹과 재순위화

검색 품질을 좌우하는 또 다른 결정적 요소는 청킹(chunking)이다. 문서를 너무 크게 자르면(사례 연구 1) 한 청크에 여러 주제가 섞여 임베딩이 흐려지고, 너무 작게 자르면 문맥이 끊겨 답에 필요한 정보가 분산된다. 그래서 의미 단위 청킹(semantic chunking)이나 오버랩(overlap)을 둔 슬라이딩 윈도우가 쓰인다. 청킹 후에는 1차 검색 결과를 정밀 모델로 다시 정렬하는 재순위화(reranking)가 Context Precision을 크게 끌어올린다 — 보통 cross-encoder가 질문·문서 쌍을 함께 인코딩해 더 정확한 관련도를 매긴다.

2단계 검색: 1차 회수 후 재순위화
$$\text{Final}_m = \operatorname*{arg\,top\text{-}}_{d \in \text{Retrieved}_k} {}^{m}\ \text{CE}(q, d), \qquad m \le k$$

$\text{CE}$: cross-encoder 관련도 점수(질문·문서를 함께 입력). 1차에서 $k$개를 빠르게 회수하고, 비싼 CE로 상위 $m$개만 정밀 재정렬. recall은 1차가, precision은 재순위화가 책임

4.6 RAGAS의 한계

RAGAS는 강력하지만 만능이 아니다. 그 한계를 정확히 이해해야 잘못된 신뢰를 피할 수 있다. 한계는 크게 세 갈래다.

4.6.1 합성 데이터와 실제 데이터의 격차

RAGAS는 평가용 테스트셋을 LLM으로 합성(synthetic generation)하는 기능을 제공한다. 이는 빠르고 저렴하지만, 합성 질문은 실제 사용자 질문의 분포(distribution)와 다르다. 실제 사용자는 모호하고, 오타가 있고, 다중 의도를 담고, 도메인 은어를 쓴다. 합성 데이터로 0.9를 받은 시스템이 실 사용자에게 0.6을 받는 일이 흔하다. 따라서 합성 데이터는 개발 초기 스모크 테스트(smoke test)로 쓰되, 최종 판단은 실제 로그 기반 데이터로 해야 한다.

4.6.2 NLI 모델·LLM 판사 자체의 오류

Faithfulness와 Context Recall은 NLI 판정(또는 LLM 판사)에 의존한다. 그런데 NLI 모델 자체가 부정·수량·시제·함축 같은 어려운 추론에서 자주 틀린다. "이 약은 1상에서 안전"으로부터 "이 약은 안전"이 entailment인지 neutral인지는 NLI 모델마다 다르게 판정한다. 즉 메트릭의 신뢰도가 그 메트릭을 계산하는 모델의 신뢰도에 상한이 걸린다(Ch.03의 LLM-as-judge 편향 논의와 연결된다).

4.6.3 claim 추출의 비결정성

Faithfulness의 첫 단계인 claim 추출은 LLM 생성이므로 비결정적(non-deterministic)이다. 같은 답변이라도 실행할 때마다 claim의 개수와 경계가 달라진다. claim을 세분화할수록 분모가 커져 점수 분포가 바뀐다. RAGAS는 여러 번 샘플링해 평균하여 이를 완화하지만, 근본적으로 점수에 노이즈가 끼는 원천이다.

메트릭의 메트릭: 평가기를 평가하라(meta-evaluation)

RAGAS 점수를 신뢰하려면 먼저 RAGAS 자체가 인간 판단과 얼마나 일치하는지를 검증해야 한다. 소규모(예: 100건) 인간 라벨 골드셋을 만들고, RAGAS 점수와 인간 점수 간 상관(Spearman ρ, Kendall τ)을 측정하라. 상관이 낮으면 NLI 모델 교체, 프롬프트 수정, 또는 도메인 보정(calibration)이 필요하다. ARES가 이 보정을 명시적으로 도입한 이유가 바로 이것이다(4.7.1).

4.7 RAG 평가 프레임워크 비교

RAGAS 외에도 여러 RAG 평가 프레임워크가 경쟁한다. 각각 철학과 강점이 다르므로, 상황에 맞게 선택해야 한다. 대표 네 가지 — RAGAS, ARES, TruLens, DeepEval — 을 비교한다.

4.7.1 ARES — 합성 데이터 + 통계적 보정

ARES(Saad-Falcon et al., 2024, "ARES: An Automated Evaluation Framework for Retrieval-Augmented Generation Systems", NAACL 2024, arXiv:2311.09476)는 RAGAS와 유사하게 LLM 판사를 쓰지만, 예측-기반 추론(prediction-powered inference, PPI)으로 통계적 보정을 추가한다. LLM 판사의 점수를 소량의 인간 라벨로 보정하여, 편향을 제거한 신뢰구간(confidence interval)이 있는 추정치를 제공한다. "판사를 평가하라"는 4.6.3의 권고를 프레임워크 차원에서 자동화한 셈이다.

4.7.2 TruLens — RAG Triad

TruLens는 RAG 평가를 세 꼭짓점의 삼각형(RAG Triad)으로 정식화한다 — Context Relevance(컨텍스트가 질문과 관련 있는가), Groundedness(답변이 컨텍스트에 근거하는가), Answer Relevance(답변이 질문과 관련 있는가). 이 세 가지가 모두 높아야 RAG가 신뢰할 수 있다는 직관적 모델로, 개발자가 빠르게 이해하고 적용하기에 좋다.

질문 (Query)
↙ Context RelevanceAnswer Relevance ↘
컨텍스트
답변
↑ Groundedness (컨텍스트 ↔ 답변)

4.7.3 DeepEval — 단위 테스트 패러다임 + 통계적 검정

DeepEval은 RAG 평가를 소프트웨어 단위 테스트(unit test)처럼 다룬다. pytest 스타일로 assertion을 작성하고, Hallucination·Answer Relevance·Faithfulness 등 메트릭에 임계값을 걸어 CI에서 자동 검증한다. G-Eval(Liu et al., 2023, "G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment", arXiv:2303.16634) 기반 LLM 메트릭과 통계적 유의성 검정을 함께 제공해, "이번 버전이 이전보다 통계적으로 유의하게 나빠졌는가"를 판정할 수 있다.

4.7.4 메트릭 커버리지 매트릭스

프레임워크FaithfulnessAnswer Rel.Context Prec.Context RecallLLM 의존통계 보정오픈소스
RAGAS높음✓ (Apache 2.0)
ARES✓ (PPV)부분높음✓ (PPI)✓ (MIT)
TruLens✓ (Groundedness)✓ (Context Rel.)높음✓ (MIT)
DeepEval부분높음✓ (검정)✓ (Apache 2.0)

프레임워크 기능 커버리지 (레이더)

메트릭당 LLM 호출 비용 (상대)

4.7.5 LLM 의존의 공통 약점과 임베딩 기반 보완

네 프레임워크 모두 LLM 판사에 크게 의존하므로, 비용·지연·편향·재현성의 공통 약점을 공유한다. 이를 보완하는 한 방법이 임베딩 기반 보조 메트릭이다. 예컨대 BERTScore(Zhang et al., 2020, "BERTScore: Evaluating Text Generation with BERT", ICLR 2020, arXiv:1904.09675)로 답변과 컨텍스트의 의미 중첩을 빠르게 근사하여, 비싼 LLM 호출 전에 1차 필터링할 수 있다.

BERTScore (RAG 보조 메트릭으로 응용)
$$\text{BERTScore}_P = \frac{1}{|C|}\sum_{i\in C}\max_{j\in R}\cos(\mathbf{c}_i,\mathbf{r}_j), \quad \text{BERTScore}_F = \frac{2 P \cdot R}{P+R}$$

$C$: 후보(답변) 토큰, $R$: 참조(컨텍스트) 토큰. 각 후보 토큰을 가장 유사한 참조 토큰에 greedy 매칭. n-gram 표면 일치 대비 인간 판단과의 상관이 30~50% 높다(Zhang et al., 2020)

물론 BERTScore는 표면 의미 중첩만 보므로 환각·추론 비약을 잡지 못한다. 따라서 BERTScore는 RAGAS를 대체하는 것이 아니라 저비용 1차 스크리닝으로 결합하는 것이 실용적이다 — BERTScore가 명백히 낮으면 LLM 호출 없이 탈락, 애매한 경우만 RAGAS로 정밀 판정한다.

4.8 프레임워크 선택 가이드

"어떤 프레임워크를 써야 하는가"는 비용, 신뢰도 요구, CI 통합, 통계적 엄밀성에 따라 달라진다. 아래 결정 트리가 출발점이다.

Q1. 통계적 신뢰구간·편향 보정이 필요한가? (예: 규제 보고, 논문)
: ARES (PPI 보정 + 신뢰구간)
아니오: Q2로
Q2. CI/CD 파이프라인에 단위 테스트로 통합하려는가?
: DeepEval (pytest 스타일 + 통계 검정)
아니오: Q3로
Q3. 빠른 직관적 디버깅·관측가능성(observability)이 우선인가?
: TruLens (RAG Triad + 추적 UI)
아니오 / 표준 4메트릭이면 충분: RAGAS (사실상 표준, 생태계 최대)
상황권장이유
프로토타입 빠른 평가RAGAS생태계 통합, 4메트릭 즉시 사용
규제·감사 대상 시스템ARES신뢰구간 + 인간 라벨 보정으로 방어 가능
CI 게이트(회귀 차단)DeepEval단위 테스트 + 통계적 유의성 검정
프로덕션 모니터링TruLens실시간 추적 + RAG Triad 대시보드
대규모·저비용 1차 스크리닝BERTScore + RAGAS임베딩 필터 후 선택적 LLM 판정
프레임워크보다 골드셋이 먼저다

어떤 프레임워크를 고르든, 그 출력을 신뢰하려면 도메인 골드셋(인간 라벨 평가셋)이 선행되어야 한다. 프레임워크는 "측정 도구"일 뿐 "정답"이 아니다. 100~300건의 인간 라벨로 (1) 프레임워크 점수와 인간 점수의 상관을 확인하고, (2) 임계값을 보정하며, (3) 도메인 특이 실패 양식을 발견하라. 골드셋 없이 프레임워크 점수만 보는 것은 보정 안 된 저울로 몸무게를 재는 것과 같다.

4.8.1 RAG 평가의 흔한 안티패턴

도구 선택만큼 중요한 것이 "하지 말아야 할 것"을 아는 것이다. 실무에서 반복되는 RAG 평가의 안티패턴(anti-pattern)을 정리한다. 이들은 대부분 4.1의 첫 번째 계명("검색과 생성을 한 숫자로 합치지 말라")이나 4.6의 한계(평가기 신뢰성)를 무시한 데서 비롯된다.

안티패턴증상왜 위험한가교정
E2E 정확도 단일 추적"정확도 72%"만 보고검색·생성 실패가 뭉개져 진단 불가4메트릭 분해(4.5.3 사분면)
합성 데이터로만 평가개발셋 0.9, 실사용 0.6실 사용자 분포와 괴리(4.6.1)실제 로그 기반 골드셋 병행
판사 검증 생략RAGAS 점수를 곧 진실로 간주NLI/LLM 판사 오류가 그대로 전파(4.6.2)인간 골드셋과 상관 측정
평균만 보고꼬리의 체계적 실패 누락특정 질문군에서만 환각분포·하위 10% 직접 검토
컨텍스트 과다 주입top-k를 무작정 늘림무관 컨텍스트가 생성 오염(Lost-in-the-Middle)재순위화로 precision 확보
한 번 평가 후 방치지식 베이스 갱신 후 재평가 안 함드리프트로 성능 저하 미감지CI 회귀 게이트(4.9.4)

이 안티패턴들의 공통 교훈은 명확하다 — RAG 평가는 일회성 점수 산출이 아니라, 분해된 메트릭을 골드셋으로 보정하며 지속적으로 추적하는 운영 프로세스(operational process)다. 단발성 0.85라는 숫자보다, "검색 재현율이 지난 버전 대비 0.05 떨어졌고 그 원인이 새 문서 청킹 변경"이라는 추적 가능한 서사가 훨씬 가치 있다.

4.9 RAG 평가 실무 — 동작하는 파이프라인

이론을 종합해 실제로 돌아가는 RAG 평가 파이프라인을 구성한다. 핵심은 네 메트릭을 한 번의 평가 호출로 묶고, 데이터셋 단위로 집계·시각화하는 것이다.

4.9.1 RAGEvaluationPipeline 클래스

from dataclasses import dataclass, field from statistics import mean @dataclass class RAGSample: question: str answer: str contexts: list[str] # 검색된 문서들(순위순) ground_truth: str = "" relevances: list[int] = field(default_factory=list) # 문서별 0/1 @dataclass class RAGScore: faithfulness: float answer_relevance: float context_precision: float context_recall: float def harmonic(self) -> float: """네 메트릭의 조화 평균 — 하나라도 낮으면 크게 깎임""" vals = [self.faithfulness, self.answer_relevance, self.context_precision, self.context_recall] vals = [v for v in vals if v > 0] if not vals: return 0.0 return len(vals) / sum(1 / v for v in vals) class RAGEvaluationPipeline: """RAGAS 스타일 4메트릭 평가 파이프라인""" def __init__(self, faith_eval, ar_eval, llm, nli): self.faith = faith_eval # FaithfulnessEvaluator self.ar = ar_eval # AnswerRelevanceEvaluator self.llm = llm self.nli = nli def _split_sentences(self, text: str) -> list[str]: import re return [s.strip() for s in re.split(r"(?<=[.!?。])\s+", text) if s.strip()] def evaluate_sample(self, s: RAGSample) -> RAGScore: context = "\n".join(s.contexts) f = self.faith.evaluate(s.answer, context).score ar = self.ar.evaluate(s.question, s.answer) cp = context_precision(s.relevances) if s.relevances else 0.0 gt_sents = self._split_sentences(s.ground_truth) cr = context_recall(gt_sents, context, self.nli) if gt_sents else 0.0 return RAGScore(f, ar, cp, cr) def evaluate_dataset(self, samples: list[RAGSample]) -> dict: scores = [self.evaluate_sample(s) for s in samples] return { "faithfulness": mean(s.faithfulness for s in scores), "answer_relevance": mean(s.answer_relevance for s in scores), "context_precision": mean(s.context_precision for s in scores), "context_recall": mean(s.context_recall for s in scores), "n": len(scores), }

4.9.2 ContextRelevanceEvaluator — TruLens 스타일 보조기

RAG Triad의 Context Relevance를 별도로 측정하면, 검색이 질문과 무관한 문서를 가져오는 문제를 조기에 잡을 수 있다. 아래는 청크별 관련도를 LLM으로 채점하는 보조 평가기다.

from statistics import mean class ContextRelevanceEvaluator: """검색된 각 청크가 질문과 얼마나 관련 있는지 0~1로 채점""" def __init__(self, llm): self.llm = llm def score_chunk(self, question: str, chunk: str) -> float: prompt = ( f"질문에 답하는 데 아래 문서가 얼마나 관련 있는가? 0~10 정수로만 답하라.\n" f"질문: {question}\n문서: {chunk}\n점수:" ) try: return min(int(self.llm.generate(prompt).strip()), 10) / 10 except ValueError: return 0.0 def evaluate(self, question: str, contexts: list[str]) -> dict: per_chunk = [self.score_chunk(question, c) for c in contexts] return { "mean_relevance": mean(per_chunk) if per_chunk else 0.0, "max_relevance": max(per_chunk) if per_chunk else 0.0, "noise_ratio": sum(1 for p in per_chunk if p < 0.3) / max(len(per_chunk), 1), }
🎧
사례 연구 1 — 고객 지원 RAG 시스템 평가
50개 테스트 케이스, 커스텀 ground truth 기반

배경. 한 SaaS 기업이 제품 문서 2,400페이지를 지식 베이스로 삼아 고객 지원 RAG 챗봇을 구축했다. 출시 전, 50개의 실제 고객 문의 로그를 골드셋으로 만들고(각각 인간이 작성한 ground truth 답변 포함) RAGAS 4메트릭으로 평가했다.

초기 결과. Faithfulness 0.91, Answer Relevance 0.84, Context Precision 0.58, Context Recall 0.52. 생성 품질(레벨 2)은 좋은데 검색 품질(레벨 1)이 낮았다. 4.5.3의 사분면으로 보면 "나쁜 컨텍스트에 충실"한 좌상단 상태에 가까웠다 — 챗봇은 가져온 문서에는 충실히 답했지만, 애초에 필요한 문서를 절반밖에 못 가져왔다.

근본 원인 분석. Context Recall이 낮은 케이스들을 드릴다운하니, 공통점은 "한 문의가 여러 제품 영역에 걸친 복합 질문"이었다. 청킹 전략이 페이지 단위(평균 1,800자)여서 한 청크에 너무 많은 주제가 섞였고, 임베딩이 특정 주제에 치우쳐 다른 주제 문서를 놓쳤다.

개입. (1) 청킹을 의미 단위(semantic chunking, 평균 400자)로 변경, (2) 검색 top-k를 5→8로 증가, (3) 재순위화(cross-encoder reranker) 추가. 재평가 결과 Context Recall 0.52→0.81, Context Precision 0.58→0.79로 상승. Faithfulness는 0.91→0.89로 미세 하락(컨텍스트가 길어져 LLM이 약간 더 비약)했으나, End-to-End 인간 만족도는 71%→88%로 개선되었다.

교훈. 만약 E2E 정확도만 봤다면 "챗봇이 71%니까 프롬프트를 고치자"는 잘못된 처방으로 갔을 것이다. 메트릭 분해 덕분에 진짜 병목이 검색임을 정확히 짚었다.

4.9.3 데이터셋 단위 결과 시각화

50개 케이스의 메트릭 분포를 함께 보면 "평균은 괜찮은데 꼬리가 나쁜" 케이스를 발견할 수 있다. 아래 차트는 개입 전후의 4메트릭 평균 변화를 보여준다.

고객 지원 RAG — 개입 전후 메트릭 변화

평균이 아니라 분포를 보라

RAG 평가에서 흔한 실수는 데이터셋 평균만 보고하는 것이다. Faithfulness 평균 0.9가 "10%는 0.9, 90%는 0.91"인 경우와 "80%는 1.0, 20%는 0.5"인 경우는 완전히 다르다. 후자는 특정 질문 유형에서 체계적으로 환각한다는 뜻이다. 항상 (1) 히스토그램으로 분포를, (2) 하위 10% 케이스의 실제 답변을 직접 읽어 실패 패턴을 군집화하라. 평균은 의사결정 KPI일 뿐, 개선 작업은 꼬리에서 이뤄진다.

4.9.4 회귀 게이트 — 버전 간 통계적 비교

RAG 평가를 CI에 통합하려면 "이번 버전이 이전보다 통계적으로 유의하게 나빠졌는가"를 판정해야 한다. 데이터셋 평균이 0.82→0.80으로 떨어졌을 때, 이것이 진짜 회귀인지 측정 노이즈인지를 구분하려면 표본 분산을 고려한 검정이 필요하다. 케이스별 점수 쌍을 가진 경우 대응표본 t-검정(paired t-test) 또는 부트스트랩 신뢰구간을 사용한다.

대응표본 t-검정 (버전 간 회귀 판정)
$$t = \frac{\bar{d}}{s_d / \sqrt{n}}, \qquad d_i = \text{score}^{\text{new}}_i - \text{score}^{\text{old}}_i$$

$\bar{d}$: 케이스별 점수 차이의 평균, $s_d$: 그 표준편차, $n$: 케이스 수. $t$가 유의하게 음수(예: $p<0.05$이고 $\bar{d}<0$)이면 회귀로 판정 → 배포 차단

from scipy import stats def regression_gate(old_scores, new_scores, alpha=0.05, min_drop=0.01): """케이스별 점수 쌍으로 회귀 여부 판정 (대응표본 t-검정)""" diffs = [n - o for o, n in zip(old_scores, new_scores)] mean_diff = sum(diffs) / len(diffs) t_stat, p_two = stats.ttest_rel(new_scores, old_scores) # 단측 검정: 새 버전이 더 낮은지 regressed = (mean_diff < -min_drop) and (p_two / 2 < alpha) and (t_stat < 0) return { "mean_diff": round(mean_diff, 4), "p_value": round(p_two / 2, 4), "decision": "BLOCK (회귀)" if regressed else "PASS", } # 사소한 평균 하락이라도 분산이 크면 통계적으로 유의하지 않아 PASS # 작은 하락이라도 일관되게(저분산) 나타나면 BLOCK

4.10 코드 생성 평가는 왜 다른가

이제 두 번째 도메인, 코드 생성으로 넘어간다. RAG가 "검색과 생성의 분리"를 요구했다면, 코드 생성은 또 다른 특수성을 가진다 — 출력을 실제로 실행(execute)해서 검증할 수 있다는 점, 그리고 정답이 무한히 다양하다는 점이다.

4.10.1 코드의 특수성: 실행 가능성과 정답의 다양성

자연어 생성에서는 "정답"을 정의하기 어렵다. 같은 의미를 무한히 많은 문장으로 표현할 수 있어서 BLEU·ROUGE 같은 표면 비교가 한계를 가진다. 코드도 정답이 다양하다는 점은 같지만(같은 함수를 무수히 많은 방식으로 구현 가능), 결정적 차이가 있다 — 코드는 실행해서 정답인지 객관적으로 검증할 수 있다. 구현이 어떻게 생겼든, 테스트 케이스를 통과하면 기능적으로 정답이다. 이것이 코드 생성 평가가 자연어 평가보다 더 객관적일 수 있는 이유이자, 실행 기반 평가(execution-based evaluation)가 표준이 된 이유다.

측면자연어 생성코드 생성
정답의 다양성무한 (의역)무한 (구현 방식)
객관적 검증어려움 (의미 판단 필요)가능 (테스트 실행)
주 평가 방식표면/의미 유사도, LLM 판사실행 기반 (functional correctness)
실패의 명확성모호 (얼마나 틀렸나?)이진(통과/실패) + 에러 메시지
부분 점수연속적테스트 통과율로 부분 점수 가능

4.10.2 표면 비교(BLEU)가 코드에서 실패하는 이유

초기 코드 생성 연구는 CodeBLEU 같은 표면 비교를 시도했지만, 코드에서는 특히 부적합하다. 변수명을 바꾸거나 동등한 구문(예: 리스트 컴프리헨션 vs for 루프)을 쓰면 BLEU는 떨어지지만 기능은 동일하다. 반대로 한 글자(예: < vs <=)만 틀려 완전히 다르게 동작하는 코드가 높은 BLEU를 받을 수 있다. Chen et al. (2021, "Evaluating Large Language Models Trained on Code", arXiv:2107.03374)이 HumanEval과 함께 실행 기반 pass@k를 도입한 핵심 동기가 이것이다.

코드 평가의 제1원칙: 표면이 아니라 동작을 평가하라

"코드가 정답 코드와 얼마나 비슷한가"는 거의 무의미한 질문이다. 올바른 질문은 "코드가 명세(specification)를 만족하는가"이며, 명세는 보통 테스트 케이스로 표현된다. 따라서 코드 생성 평가의 출발점은 항상 실행이다. 표면 유사도(BLEU/편집거리)는 디버깅 보조 신호로는 쓸 수 있어도, 정답 판정의 기준이 될 수 없다.

4.11 Pass@k 재고찰

Ch.02에서 pass@k를 소개했지만, 여기서는 그 수학적 깊이를 파고든다. pass@k는 "k개의 샘플을 생성했을 때 적어도 하나가 모든 테스트를 통과할 확률"이다. 단순해 보이지만, 이를 편향 없이 추정하는 것은 미묘하다.

4.11.1 순진한 추정의 편향과 불편 추정량

k개를 생성해 하나라도 통과하면 1, 아니면 0으로 세는 순진한 방법은 분산이 크고 비싸다. Chen et al. (2021)은 더 많은 $n$개($n \geq k$)를 한 번 생성해 그중 통과한 개수 $c$를 세고, 거기서 pass@k를 불편(unbiased) 추정하는 공식을 제시했다. 핵심은 "$n$개 중 $k$개를 뽑았을 때 하나도 통과 못 할 확률"을 조합으로 계산하는 것이다.

Pass@k 불편 추정량 (Chen et al., 2021)
$$\text{pass@}k = \mathbb{E}_{\text{problems}}\left[1 - \frac{\binom{n-c}{k}}{\binom{n}{k}}\right]$$

$n$: 생성한 총 샘플 수, $c$: 그중 통과한 샘플 수, $k$: 평가하려는 시도 수. $\binom{n-c}{k}/\binom{n}{k}$는 "k개 모두 실패에서 뽑힐 확률". $n-c < k$이면 항이 0이 되어 pass@k=1

이 공식이 왜 불편인가? $\binom{n-c}{k}$는 통과 샘플($c$개)을 한 번도 뽑지 않고 실패 샘플($n-c$개)에서만 $k$개를 고르는 경우의 수이고, $\binom{n}{k}$는 전체 경우의 수다. 그 비율이 "k번 시도가 모두 실패할 확률"의 불편 추정이며, 1에서 빼면 "적어도 하나 성공"이 된다. 순진한 추정과 달리 모든 $\binom{n}{k}$ 조합을 암묵적으로 평균하므로 분산이 작다. 수치적으로는 큰 $n$에서 조합 계산이 오버플로하므로, 아래처럼 곱셈 누적 형태로 안정화한다.

수치 안정 형태
$$1 - \frac{\binom{n-c}{k}}{\binom{n}{k}} = 1 - \prod_{i=n-c+1}^{n}\frac{i-k}{i}$$

조합비를 곱셈으로 전개하면 큰 $n$에서도 오버플로 없이 계산 가능. $i \le k$이면 곱이 0이 되는 경우를 별도 처리

import numpy as np from math import comb def pass_at_k(n: int, c: int, k: int) -> float: """단일 문제의 불편 pass@k 추정 (Chen et al., 2021)""" if n - c < k: # 실패 샘플이 k개 미만 → 반드시 하나는 통과 return 1.0 return 1.0 - comb(n - c, k) / comb(n, k) def pass_at_k_stable(n, c, k): """수치 안정 버전: 큰 n에서 comb 오버플로 방지""" if n - c < k: return 1.0 # prod_{i=n-c+1}^{n} (1 - k/i) 형태로 곱셈 누적 return 1.0 - float(np.prod(1.0 - k / np.arange(n - c + 1, n + 1))) # 데이터셋 전체 pass@k = 문제별 pass@k의 평균 def dataset_pass_at_k(results: list[tuple[int, int]], k: int) -> float: """results: 문제별 (n, c) 튜플 리스트""" return float(np.mean([pass_at_k_stable(n, c, k) for n, c in results])) print(round(pass_at_k(200, 20, 1), 3)) # ≈ 0.100 (pass@1) print(round(pass_at_k(200, 20, 100), 3)) # ≈ 1.000 (pass@100)

4.11.2 n과 k의 민감도: 엄격함의 스펙트럼

같은 모델이라도 k에 따라 점수가 극적으로 달라진다. pass@1은 "첫 시도에 맞히는가"로 매우 엄격하고, pass@100은 "100번 중 한 번이라도 맞히는가"로 관대하다. 예를 들어 어떤 모델이 문제당 평균 통과율 $p=0.1$(즉 10%)이라면, pass@1=0.1이지만 pass@100 $\approx 1-(0.9)^{100}\approx 0.9999$로 거의 100%가 된다.

단일 시도 통과율 ppass@1pass@10pass@100해석
0.01 (1%)0.0100.0960.634매우 어려운 문제
0.05 (5%)0.0500.4010.994어려운 문제
0.10 (10%)0.1000.651~1.000중간 난이도
0.30 (30%)0.3000.972~1.000쉬운 문제

(위 값은 $\text{pass@}k \approx 1-(1-p)^k$ 근사로 계산. p=10%, k=100이면 거의 확실히 하나는 통과)

Pass@k 곡선 — 단일 통과율(p)별 k에 따른 변화

Pass@100이 Pass@1보다 높으면 '때려 맞추기'일 수 있다

pass@100과 pass@1의 간극이 크다는 것은, 모델이 가끔은 맞히지만 일관되게 맞히지는 못한다는 뜻이다. 즉 정답을 "알고" 있다기보다 "운 좋게 샘플링"하는 것에 가깝다. 실사용 관점에서 사용자는 보통 1~3개 결과만 본다. 따라서 pass@100이 높다고 좋아할 것이 아니라, pass@1과 pass@100의 비율을 봐야 한다. 이 비율이 1에 가까울수록 모델이 안정적으로 정답에 도달한다는 의미다. 반대로 pass@1이 낮고 pass@100만 높으면, 그것은 "검증기(verifier)와 결합한 best-of-n" 시스템에서만 가치가 있다.

4.11.3 추정 분산과 신뢰구간

pass@k는 추정치이므로 분산을 동반한다. 데이터셋 전체 pass@k는 문제별 pass@k의 평균이고, 문제별 값은 독립이라 가정하면 평균의 표준오차로 신뢰구간을 구성할 수 있다. 작은 데이터셋(예: HumanEval 164개)에서는 이 구간이 의외로 넓어, 두 모델의 pass@1이 1~2%p 차이라면 통계적으로 구분 불가능할 수 있다.

데이터셋 pass@k의 표준오차
$$\widehat{\text{pass@}k} = \frac{1}{|T|}\sum_{t\in T} \hat{p}_t^{(k)}, \qquad \text{SE} = \frac{s}{\sqrt{|T|}}, \quad s^2 = \frac{1}{|T|-1}\sum_{t\in T}\big(\hat{p}_t^{(k)} - \widehat{\text{pass@}k}\big)^2$$

$\hat{p}_t^{(k)}$: 문제 t의 pass@k 추정, $|T|$: 문제 수. 95% 신뢰구간 $\approx \widehat{\text{pass@}k} \pm 1.96\,\text{SE}$. $|T|$가 작으면 구간이 넓어 모델 간 비교 신뢰도 하락

이는 4.16에서 다룰 "단일 벤치마크의 위험"과 직결된다. 작은 벤치마크의 점수 차이는 우연일 수 있으므로, 모델 비교는 반드시 신뢰구간과 함께 보고하고, 여러 벤치마크로 삼각측량해야 한다. 점 추정치 하나로 순위를 단정하는 것은 통계적으로 부주의하다. 실무에서는 부트스트랩(bootstrap)으로 신뢰구간을 구하고, 두 모델의 구간이 겹치면 "유의한 차이 없음"으로 보고하는 것이 정직한 관행이다.

4.11.4 pass@1 vs pass@k: 어느 것을 보고할 것인가

벤치마크 리더보드는 종종 큰 k의 pass@k를 자랑하지만, 제품 의사결정에는 pass@1(또는 사용자가 실제로 보는 후보 수에 해당하는 k)이 더 정직하다. 자율 코딩 에이전트라면 자가 검증·재시도 루프가 있으므로 작은 k의 pass@k가 적절할 수 있다. 핵심은 k를 실제 사용 시나리오에 맞추는 것이며, k를 명시하지 않은 pass@k 수치는 비교 불가능하다.

4.12 실행 기반 평가 vs 정적 분석

코드 평가는 크게 두 축이다 — 실제로 돌려보는 실행 기반(execution-based)과, 돌리지 않고 코드 구조를 분석하는 정적 분석(static analysis). 둘은 경쟁이 아니라 상호 보완적이다.

4.12.1 실행 기반 평가와 샌드박스

실행 기반 평가는 생성된 코드를 테스트 케이스와 함께 실제로 실행하여 통과 여부를 본다. 가장 신뢰할 수 있지만, 임의 코드를 실행하는 것은 보안 위험(파일 삭제, 네트워크 접근, 무한 루프)을 동반한다. 따라서 반드시 격리된 샌드박스(sandbox) — 컨테이너, 시간/메모리 제한, 시스템 콜 차단 — 안에서 실행해야 한다.

import subprocess, tempfile, os class SandboxExecutor: """생성 코드를 시간 제한 하에 격리 실행하고 테스트 통과 여부 반환""" def __init__(self, timeout_sec=5, mem_limit_mb=256): self.timeout = timeout_sec self.mem = mem_limit_mb def run(self, code: str, test_code: str) -> dict: program = code + "\n\n" + test_code with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as f: f.write(program) path = f.name try: proc = subprocess.run( ["python", path], capture_output=True, text=True, timeout=self.timeout, # 실제로는 컨테이너/seccomp/ulimit으로 추가 격리 ) passed = (proc.returncode == 0) return {"passed": passed, "stderr": proc.stderr[:500]} except subprocess.TimeoutExpired: return {"passed": False, "stderr": "TIMEOUT"} finally: os.unlink(path) # HumanEval 스타일: 함수 구현 + assert 기반 테스트 executor = SandboxExecutor(timeout_sec=5) code = "def add(a, b):\n return a + b" tests = "assert add(2, 3) == 5\nassert add(-1, 1) == 0" print(executor.run(code, tests)) # {'passed': True, ...}
절대 호스트에서 직접 실행하지 말 것

위 코드는 개념 설명을 위해 subprocess로 단순화했지만, 실제로는 절대 평가 호스트에서 직접 실행해서는 안 된다. 생성 코드는 신뢰할 수 없으며, os.system("rm -rf /") 같은 파괴적 코드나 암호화폐 채굴, 데이터 유출을 시도할 수 있다. HumanEval 공식 구현도 실행 비활성화를 기본값으로 두고 명시적 opt-in을 요구한다. 프로덕션 평가는 Docker/gVisor/Firecracker 같은 강한 격리, 네트워크 차단, 읽기 전용 파일시스템, CPU·메모리·시간 제한을 모두 적용해야 한다.

4.12.2 정적 분석: AST, 타입 체크, 린트

정적 분석은 코드를 실행하지 않고 구조를 검사한다. 추상 구문 트리(Abstract Syntax Tree, AST)를 파싱해 위험 패턴을 탐지하거나, 타입 체커(mypy)로 타입 오류를, 린터(ruff, pylint)로 스타일·잠재 버그를 잡는다. 실행 전 1차 필터로 유용하다 — 컴파일도 안 되는 코드를 비싼 샌드박스에 보내기 전에 걸러낸다.

import ast class StaticAnalyzer: """실행 전 정적 검사: 구문 유효성 + 위험 호출 탐지 + 복잡도""" DANGEROUS = {"eval", "exec", "__import__", "compile"} def analyze(self, code: str) -> dict: try: tree = ast.parse(code) except SyntaxError as e: return {"valid": False, "error": str(e)} dangerous, branches = [], 0 for node in ast.walk(tree): if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): if node.func.id in self.DANGEROUS: dangerous.append(node.func.id) if isinstance(node, (ast.If, ast.For, ast.While, ast.And, ast.Or)): branches += 1 return { "valid": True, "dangerous_calls": dangerous, "cyclomatic_complexity": branches + 1, # 근사 } print(StaticAnalyzer().analyze("def f(x):\n return eval(x)")) # {'valid': True, 'dangerous_calls': ['eval'], ...}
측면실행 기반 (Execution)정적 분석 (Static)
측정 대상기능적 정확성 (동작)구조·스타일·안전 패턴
신뢰도높음 (실제 동작)중간 (휴리스틱)
비용·위험높음 (샌드박스 필요)낮음 (안전)
거짓 음성테스트 불완전 시 발생런타임 버그 못 잡음
적합 역할최종 정답 판정실행 전 1차 필터
계층적 게이트: 싸고 안전한 검사 먼저

이상적 코드 평가 파이프라인은 RAG의 "BERTScore 1차 필터"와 같은 계층 구조다 — (1) 정적 분석으로 구문 오류·위험 호출을 걸러내고(싸고 안전), (2) 통과한 코드만 샌드박스 실행으로 기능 검증(비싸고 위험), (3) 통과한 코드에 대해서만 복잡도·가독성·보안 같은 품질 메트릭을 매긴다. 이렇게 하면 비용을 최소화하면서 위험을 통제할 수 있다.

4.13 pass/fail을 넘어선 코드 품질

테스트를 통과했다고 좋은 코드는 아니다. "돌아가는가?"는 최소 조건이고, 실무에서는 "올바르게, 효율적으로, 안전하게, 유지보수 가능하게 돌아가는가?"가 중요하다. 단순 pass/fail을 넘어서는 다차원 품질 메트릭을 살펴본다.

4.13.1 시간·공간 복잡도

두 코드가 모두 테스트를 통과해도, 하나는 $O(n^2)$이고 다른 하나는 $O(n\log n)$이면 큰 입력에서 전자는 실용 불가다. 복잡도는 정적 분석(루프 중첩 분석)으로 근사하거나, 입력 크기를 키워가며 실행 시간을 측정해 경험적으로 추정한다.

경험적 복잡도 추정 (로그-로그 회귀)
$$\log t(n) \approx \alpha \log n + \beta \;\Rightarrow\; t(n) \approx e^{\beta} n^{\alpha}$$

여러 입력 크기 $n$에 대해 실행 시간 $t(n)$을 측정 후 로그-로그 평면에서 선형 회귀. 기울기 $\alpha$가 다항 차수의 추정치($\alpha\approx 1$이면 선형, $\approx 2$이면 이차)

4.13.2 에러 핸들링과 견고성

HumanEval식 "행복 경로(happy path)" 테스트만 통과하는 코드는 엣지 케이스(빈 입력, None, 음수, 오버플로)에서 무너진다. 견고한 코드 평가는 정상 입력 테스트 외에 적대적 입력(adversarial input) 테스트를 추가하고, 예외 처리 구조의 존재 여부를 정적으로 확인한다. EvalPlus(Liu et al., 2023, "Is Your Code Generated by ChatGPT Really Correct? Rigorous Evaluation of Large Language Models for Code Generation", NeurIPS 2023, arXiv:2305.01210)는 HumanEval의 테스트가 너무 빈약해 통과율이 과대평가됨을 보이고, 테스트를 대폭 증강한 HumanEval+를 제안했다.

4.13.3 가독성과 린터 점수

유지보수 가능성은 가독성에 크게 의존한다. 변수명, 함수 길이, 중첩 깊이, 주석 등은 린터로 정량화할 수 있다. 이는 주관적이지만, 팀 컨벤션과의 정합성을 측정하는 대리 지표(proxy)로 유용하다. 다만 가독성은 기능 정확성과 독립이므로, 별도 차원으로 보고해야지 통과율에 섞으면 안 된다.

4.13.4 보안 취약점 (SAST)

생성된 코드가 SQL 인젝션, 명령 주입, 하드코딩된 비밀키, 안전하지 않은 역직렬화 같은 취약점을 포함할 수 있다. 정적 애플리케이션 보안 테스트(Static Application Security Testing, SAST) 도구(예: Bandit, Semgrep)로 이를 탐지한다. Pearce et al. (2022, "Asleep at the Keyboard? Assessing the Security of GitHub Copilot's Code Contributions", IEEE S&P 2022, arXiv:2108.09293)는 Copilot이 생성한 코드의 약 40%가 보안 약점을 포함했다고 보고했다 — 기능적으로 맞아도 안전하지 않을 수 있음을 보여주는 경고다.

from dataclasses import dataclass @dataclass class CodeQualityReport: functional: float # 테스트 통과율 (0~1) complexity: float # 복잡도 점수 (낮을수록 좋음 → 반전) robustness: float # 엣지케이스 통과율 readability: float # 린터 점수 (0~1) security: float # 1 - (취약점 가중합) def composite(self, weights=None) -> float: """다차원 코드 품질의 가중 종합 (Ch.13 점수 산출과 동형)""" w = weights or {"functional": 0.45, "complexity": 0.10, "robustness": 0.20, "readability": 0.10, "security": 0.15} # 보안은 거부권: 심각 취약점 있으면 종합 점수 강등 (Ch.13 Safety Gate와 동일 철학) if self.security < 0.3: return min(0.39, self.functional * w["functional"]) return (w["functional"] * self.functional + w["complexity"] * self.complexity + w["robustness"] * self.robustness + w["readability"] * self.readability + w["security"] * self.security) report = CodeQualityReport(functional=1.0, complexity=0.7, robustness=0.6, readability=0.8, security=0.9) print(round(report.composite(), 3)) # ≈ 0.83

4.13.5 순환 복잡도(Cyclomatic Complexity)의 정의

복잡도를 정량화하는 고전 지표가 McCabe(1976)의 순환 복잡도다. 제어 흐름 그래프(control flow graph)에서 독립적인 실행 경로의 수를 세는데, 직관적으로는 분기(if/for/while/and/or 등)의 수에 1을 더한 값이다. 값이 클수록 테스트해야 할 경로가 많고 버그 위험이 높다.

순환 복잡도 (McCabe, 1976)
$$M = E - N + 2P$$

$E$: 제어 흐름 그래프의 간선 수, $N$: 노드 수, $P$: 연결 성분 수(단일 함수면 1). 실무 근사로는 "결정 분기 수 + 1". $M \le 10$이면 양호, $> 20$이면 리팩터링 권장

4.12.2의 StaticAnalyzer가 분기 노드를 세어 복잡도를 근사한 것이 바로 이 지표의 단순화 버전이다. 생성 코드의 복잡도를 정상 정답(ground truth) 구현의 복잡도와 비교하면, "불필요하게 복잡한 구현"을 감지할 수 있다.

4.13.6 다차원 코드 평가 파이프라인

지금까지의 구성 요소 — 정적 분석, 샌드박스 실행, 복잡도, 보안 — 를 하나의 계층적 파이프라인으로 묶는다. 4.12.2 인사이트의 "싸고 안전한 검사 먼저" 원칙을 코드로 구현한 것이다.

class CodeEvaluationPipeline: """정적 → 실행 → 품질의 계층적 코드 평가 (조기 종료 포함)""" def __init__(self, static, sandbox, sast): self.static = static # StaticAnalyzer self.sandbox = sandbox # SandboxExecutor self.sast = sast # 보안 스캐너 def evaluate(self, code: str, tests: list[str], edge_tests: list[str]) -> dict: # 단계 1: 정적 분석 (싸고 안전) — 실패 시 조기 종료 s = self.static.analyze(code) if not s["valid"]: return {"grade": "F", "reason": "syntax_error", "functional": 0.0} # 단계 2: 기능 실행 (비싸고 위험) — 정상 + 엣지 케이스 normal = [self.sandbox.run(code, t)["passed"] for t in tests] edge = [self.sandbox.run(code, t)["passed"] for t in edge_tests] functional = sum(normal) / max(len(normal), 1) robustness = sum(edge) / max(len(edge), 1) # 단계 3: 보안 스캔 (거부권) — 심각 취약점이면 강등 vulns = self.sast.scan(code) security = max(0.0, 1.0 - 0.5 * len([v for v in vulns if v["severity"] == "high"])) complexity_norm = min(1.0, 10 / max(s["cyclomatic_complexity"], 1)) report = CodeQualityReport(functional, complexity_norm, robustness, readability=0.8, security=security) return {"composite": round(report.composite(), 3), "functional": functional, "robustness": robustness, "security": security, "vulns": vulns}
코드 평가의 궁극적 목표: "돌아가는가?" → "올바르게 돌아가는가?" → "유지보수 가능한가?"

코드 평가는 세 단계의 성숙도를 거친다. (1) 돌아가는가 — 구문 유효성·실행 성공(가장 낮은 기준). (2) 올바르게 돌아가는가 — 정상·엣지 케이스 테스트 통과, 복잡도 적정(실용 기준). (3) 유지보수 가능한가 — 가독성·안전성·확장성(프로덕션 기준). pass@k는 (1)과 (2)의 일부만 측정한다. 진짜 프로덕션 평가는 세 단계를 모두 다차원으로 측정하고, 보안 같은 항목은 Ch.13의 Safety Gate처럼 거부권으로 다뤄야 한다 — 기능이 완벽해도 심각한 취약점이 있으면 배포 불가다.

모델별 코드 품질 5차원 비교

HumanEval vs HumanEval+ 통과율 하락

4.14 SWE-bench 심층

HumanEval·MBPP가 단일 함수의 짧은 문제라면, SWE-bench(Jimenez et al., 2024, "SWE-bench: Can Language Models Resolve Real-World GitHub Issues?", ICLR 2024, arXiv:2310.06770)는 차원이 다르다 — 실제 GitHub 이슈를 받아 리포지토리 전체를 이해하고 패치(patch)를 만들어, 기존 테스트 스위트를 통과시켜야 한다. 이것은 "코딩 테스트"가 아니라 "실제 소프트웨어 엔지니어링 워크플로우"다.

4.14.1 구조: 이슈 → 리포지토리 컨텍스트 → 패치 → 테스트

SWE-bench의 각 태스크는 다음으로 구성된다. (1) GitHub 이슈 설명(자연어 버그 리포트 또는 기능 요청), (2) 해당 시점의 리포지토리 스냅샷(수만~수십만 줄), (3) 모델이 생성해야 할 코드 패치, (4) 그 패치가 통과해야 할 테스트(이슈를 실제로 해결한 PR이 추가/수정한 테스트). 모델은 어디를 고쳐야 할지조차 스스로 찾아야 한다 — 파일 위치도 주어지지 않는다.

1

GitHub Issue

자연어 버그·기능 설명

2

Repo Snapshot

수만 줄 코드베이스 탐색

3

Patch 생성

관련 파일 수정 diff

4

Test 실행

FAIL_TO_PASS + PASS_TO_PASS

4.14.2 평가: resolved vs unresolved

평가 기준은 단순하고 객관적이다. 패치 적용 후 (1) 이슈와 관련된 테스트(FAIL_TO_PASS: 패치 전엔 실패, 후엔 통과해야 함)가 모두 통과하고, (2) 기존에 통과하던 테스트(PASS_TO_PASS: 회귀 방지)가 여전히 통과하면 resolved, 아니면 unresolved다. 이 이중 조건이 중요하다 — 버그는 고쳤지만 다른 기능을 망가뜨리면(회귀) 실패로 친다. 실제 엔지니어링과 동일한 기준이다.

SWE-bench Resolve Rate
$$\text{Resolve Rate} = \frac{1}{|T|}\sum_{t\in T}\mathbb{1}\big[\text{F2P}(t)=\text{all pass} \,\wedge\, \text{P2P}(t)=\text{all pass}\big]$$

$T$: 태스크 집합, $\mathbb{1}[\cdot]$: 지시함수, F2P=FAIL_TO_PASS, P2P=PASS_TO_PASS. 두 테스트 집합이 모두 통과해야 1점. 회귀(기존 테스트 깨짐)는 즉시 0점

4.14.3 SWE-bench Lite와 Verified

원본 SWE-bench는 12개 인기 Python 리포지토리에서 2,294개 태스크를 담고 있어 평가에 막대한 비용(태스크당 수만~수십만 토큰)이 든다. 이를 완화하고 신뢰성을 높이기 위해 두 부분집합이 만들어졌다.

버전태스크 수선정 방식특징용도
SWE-bench (full)2,29412개 리포 전체가장 포괄적, 가장 비쌈종합 평가
SWE-bench Lite300자기완결적·명확한 문제 선별빠른 반복, 저비용개발 중 빠른 평가
SWE-bench Verified500인간 엔지니어가 검증모호·불완전 테스트 제거, 최고 신뢰도공식 리더보드·논문

SWE-bench Verified는 OpenAI가 96명의 전문 개발자와 협업하여, 원본의 문제 중 "테스트가 불완전하거나 이슈 설명이 모호한" 케이스를 걸러내 만든 500개 검증셋이다. 원본에서 모델이 실패한 일부는 사실 문제 자체의 결함(불가능하거나 애매한 테스트)이었기에, Verified에서는 같은 모델이 더 높은 점수를 받는다. 이는 4.6.1에서 본 "평가셋 자체의 품질" 문제가 코드 평가에도 그대로 적용됨을 보여준다.

4.14.4 성능 현황과 해석

시스템 (대표)resolve rate (근사)비고
초기 SWE-agent (2024 초)~12%에이전트 스캐폴딩 초기
Devin (자율 SWE, 2024 초)~13% (full)자율 에이전트 루프
GPT-4 기반 최적 도구 (2024)~33% (Verified)도구 사용 + 재시도
2024 하반기 최상위 에이전트~50% 이상 (Verified)강한 모델 + 정교한 스캐폴딩
해당 리포에 익숙한 인간거의 100%상한선 (단, 시간 소요)

(수치는 시점·구성에 따라 변동하는 근사치다. 정확한 최신 값은 공식 리더보드 swebench.com을 참조하라.)

🐛
사례 연구 2 — SWE-bench 태스크 워크스루: django 이슈 해결
이슈→탐색→패치→테스트 전 과정 해부

태스크. django 리포지토리의 한 이슈 — "QuerySet의 특정 메서드가 빈 입력에서 예외 대신 잘못된 결과를 반환한다." 모델에게는 이슈 텍스트와 전체 리포지토리(약 35만 줄)만 주어진다. 어느 파일의 어느 함수가 문제인지는 알려주지 않는다.

단계 1 — 위치 탐색(localization). 에이전트는 이슈의 키워드로 리포지토리를 검색(grep/임베딩 검색)하여 후보 파일을 좁힌다. 이는 4.1의 RAG와 동형이다 — 거대한 코드베이스에서 관련 컨텍스트를 검색하는 문제다. 실제로 SWE-bench에서 실패의 큰 비중이 "엉뚱한 파일을 고치는" localization 실패에서 온다.

단계 2 — 패치 생성. 후보 함수를 읽고, 빈 입력 처리를 추가하는 diff를 생성한다. 여기서 흔한 실패는 (a) 문법은 맞지만 의도와 다른 수정, (b) 한 케이스는 고쳤지만 다른 입력을 깨뜨림(회귀)이다.

단계 3 — 테스트 검증. FAIL_TO_PASS 테스트(이 이슈를 검증하는 새 테스트)와 PASS_TO_PASS 테스트(기존 django 테스트 수천 개)를 모두 실행한다. 모델의 패치가 빈 입력은 고쳤지만 기존의 정상 입력 동작을 미묘하게 바꿔 PASS_TO_PASS 중 3개가 깨지면 → unresolved다.

교훈. SWE-bench가 측정하는 것은 단순 코딩 능력이 아니라 탐색(retrieval) + 이해(comprehension) + 정밀 수정(precise editing) + 회귀 회피(regression avoidance)의 결합이다. 이것이 "코딩 테스트가 아니라 엔지니어링 워크플로우"인 이유이며, RAG의 검색 문제와 코드의 실행 검증 문제가 한 태스크에서 만나는 지점이다.

4.14.5 에이전트 스캐폴딩과 성능 분해

SWE-bench resolve rate는 모델 단독 능력이 아니라 모델과 스캐폴딩(scaffolding) — 즉 에이전트가 코드베이스를 탐색·편집·실행하는 도구 루프 — 의 결합 성능이다. 같은 기반 모델이라도 스캐폴딩에 따라 점수가 두 배 이상 차이 난다. 대표 패러다임이 ReAct(Yao et al., 2023, "ReAct: Synergizing Reasoning and Acting in Language Models", ICLR 2023, arXiv:2210.03629)의 추론-행동 교차 루프와, 이를 코드 환경에 특화한 SWE-agent(Yang et al., 2024, "SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering", NeurIPS 2024, arXiv:2405.15793)의 에이전트-컴퓨터 인터페이스다.

SWE-bench 성능을 분해하려면 단계별 성공률을 계측해야 한다. 아래는 localization·patch·test 단계의 성공 여부를 추적해, 어느 단계가 병목인지 진단하는 평가 래퍼다.

from dataclasses import dataclass @dataclass class SWEStageResult: located: bool # 정답 파일을 후보에 포함했는가 (검색=RAG와 동형) patched: bool # 적용 가능한 diff를 생성했는가 f2p_pass: bool # FAIL_TO_PASS 테스트 통과 p2p_pass: bool # PASS_TO_PASS 테스트 통과 (회귀 없음) def resolved(self) -> bool: return self.f2p_pass and self.p2p_pass def decompose_swe_bench(results: list[SWEStageResult]) -> dict: """단계별 성공률로 병목 진단 (검색 vs 수정 vs 회귀)""" n = len(results) return { "localization_rate": sum(r.located for r in results) / n, "patch_rate": sum(r.patched for r in results) / n, "f2p_rate": sum(r.f2p_pass for r in results) / n, "regression_rate": sum(r.f2p_pass and not r.p2p_pass for r in results) / n, "resolve_rate": sum(r.resolved() for r in results) / n, } # localization_rate가 낮으면 검색(RAG) 문제 → 코드 인덱싱 개선 # regression_rate가 높으면 수정이 과도 → 더 보수적 편집 전략
SWE-bench는 RAG와 코드 평가의 교차점이다

흥미롭게도 SWE-bench는 이 장의 두 주제를 하나로 묶는다. localization 단계는 본질적으로 RAG의 검색 문제(거대 코드베이스에서 관련 파일 찾기)이고, 패치 검증 단계는 코드의 실행 기반 평가다. 따라서 SWE-bench 성능을 분해하면 "검색(localization) 정확도"와 "수정(editing) 정확도"로 나눌 수 있으며, 이는 RAG의 레벨 분해(4.2)와 정확히 같은 사고방식이다. 파이프라인을 구성 요소로 분해해 진단하라는 이 장의 일관된 교훈이 여기서 다시 확인된다.

4.15 코드 평가 프레임워크 비교

코드 생성 벤치마크는 단일 함수에서 리포지토리 규모까지, 정적 데이터셋에서 오염 방지 라이브 평가까지 스펙트럼을 이룬다. 대표 벤치마크를 비교한다.

벤치마크단위문제 수평가 방식대표 메트릭핵심 한계
HumanEval (Chen 2021)단일 함수164실행 (assert)pass@k테스트 빈약, 오염 의심
MBPP (Austin 2021)단일 함수~974실행 (assert)pass@k난이도 낮음
HumanEval+ (Liu 2023)단일 함수164실행 (증강 테스트)pass@k여전히 함수 단위
SWE-bench (Jimenez 2024)리포지토리2,294실행 (테스트 스위트)resolve ratePython 한정, 고비용
LiveCodeBench (Jain 2024)경쟁 프로그래밍지속 갱신실행 (히든 테스트)pass@k (시간 구간별)경쟁 문제에 편향

4.15.1 MBPP와 Austin et al.

MBPP(Austin et al., 2021, "Program Synthesis with Large Language Models", arXiv:2108.07732)는 입문~중급 난이도의 약 974개 Python 문제로, 자연어 설명과 3개 정도의 테스트로 구성된다. HumanEval과 함께 가장 널리 쓰이지만, 문제 난이도가 낮아 강한 모델 간 변별력이 부족하다(천장 효과, ceiling effect).

4.15.2 LiveCodeBench와 오염 방지

LiveCodeBench(Jain et al., 2024, "LiveCodeBench: Holistic and Contamination Free Evaluation of Large Language Models for Code", arXiv:2403.07974)는 경쟁 프로그래밍 사이트(LeetCode, AtCoder, CodeForces)의 문제를 출제 날짜와 함께 지속적으로 수집한다. 모델의 훈련 컷오프(training cutoff) 이후에 출제된 문제만으로 평가하면, 그 문제는 훈련 데이터에 있을 수 없으므로 오염(contamination)이 원천 차단된다. 이것이 다음 절의 핵심 주제다.

4.16 오염과 데이터 누출

코드 벤치마크의 가장 심각한 신뢰성 위협은 데이터 오염(data contamination)이다. HumanEval·MBPP는 공개된 지 오래되어 거의 모든 LLM의 훈련 데이터에 포함되었을 가능성이 높다. 모델이 문제를 "푸는" 것이 아니라 "외운" 것이라면, 그 점수는 일반화 능력을 반영하지 못한다.

4.16.1 오염의 증거와 진단

오염을 진단하는 한 방법은 시간적 분할(temporal split)이다. 훈련 컷오프 이전 문제와 이후 문제에서 성능 격차가 크면 오염을 의심한다. LiveCodeBench가 출제 날짜를 기록하는 이유가 이것이다 — 같은 모델을 컷오프 전후 문제로 나눠 평가하면, 컷오프 이전 문제에서만 비정상적으로 높은 점수가 나오는 "기억(memorization) 신호"를 볼 수 있다.

오염 신호: 시간적 성능 격차
$$\Delta_{\text{contam}} = \text{pass@}k_{\text{(before cutoff)}} - \text{pass@}k_{\text{(after cutoff)}}$$

$\Delta_{\text{contam}}$이 유의하게 양수면 컷오프 이전 문제를 외웠을 가능성. 난이도가 통제된 동질 문제군에서 측정해야 유효

4.16.2 오염 방지 전략

오염을 막는 전략은 세 갈래다. (1) 라이브 평가 — LiveCodeBench처럼 컷오프 이후 문제만 사용. (2) 비공개 테스트 — SWE-bench가 일부 테스트를 숨기고, 히든 테스트로 채점하여 답을 외우기 어렵게 함. (3) 변형(perturbation) — 문제의 변수명·구조를 바꿔 표면 기억을 무력화. 다만 변형은 의미가 보존되는지 검증이 어렵다는 한계가 있다.

방지 전략메커니즘장점한계
라이브 평가컷오프 이후 신규 문제만오염 원천 차단문제 수집 지속 운영 필요
비공개 테스트히든 테스트로 채점정답 암기 무력화재현성·투명성 저하
변형(perturbation)변수명·구조 변경기존 데이터셋 재활용의미 보존 검증 어려움
난이도 통제 분할동질 문제군 시간 분할$\Delta_{\text{contam}}$ 직접 측정충분한 표본 필요

4.16.3 오염 진단 자동화

오염을 운영 단계에서 자동 감시하려면 시간적 격차를 통계적으로 검정해야 한다. 컷오프 전후 동질 문제군의 통과율 차이를 부트스트랩으로 신뢰구간과 함께 추정하면, 격차가 우연인지 체계적 기억인지 판정할 수 있다.

import numpy as np def contamination_signal(before_pass: list[int], after_pass: list[int], n_boot=5000): """컷오프 전/후 통과율 격차의 부트스트랩 신뢰구간""" before, after = np.array(before_pass), np.array(after_pass) deltas = [] for _ in range(n_boot): b = np.random.choice(before, len(before), replace=True).mean() a = np.random.choice(after, len(after), replace=True).mean() deltas.append(b - a) lo, hi = np.percentile(deltas, [2.5, 97.5]) return { "delta": round(before.mean() - after.mean(), 3), "ci_95": (round(lo, 3), round(hi, 3)), # 신뢰구간 하한이 0보다 크면 유의한 오염 신호 "contaminated": lo > 0.05, } # 컷오프 이전 67% vs 이후 41% → delta=0.26, CI가 0을 포함하지 않으면 오염
높은 HumanEval 점수를 액면 그대로 믿지 말 것

2024년 이후 많은 모델이 HumanEval에서 90% 이상을 기록하지만, 이것이 "거의 모든 프로그래밍 문제를 푼다"는 의미는 아니다. (1) HumanEval은 164개의 짧은 함수일 뿐이고, (2) 테스트가 빈약해 HumanEval+에서는 10~20%p 하락하며, (3) 오염 가능성이 높다. 진짜 코딩 능력을 보려면 오염 방지 벤치마크(LiveCodeBench)와 리포지토리 규모 벤치마크(SWE-bench Verified)를 함께 봐야 한다. 단일 벤치마크 점수는 능력의 한 단면일 뿐이다.

🔬
사례 연구 3 — 오염 탐지: HumanEval 고득점 모델의 라이브 검증
시간적 분할로 "외운 점수" 들춰내기

상황. 한 팀이 사내 코드 어시스턴트로 두 모델 A, B를 검토했다. HumanEval에서 A=92%, B=89%로 A가 우세해 보였다. 그러나 HumanEval이 오래된 공개 벤치마크임을 우려해 추가 검증을 설계했다.

검증 1 — HumanEval+. 증강 테스트(HumanEval+)로 재평가하니 A=78%, B=80%로 순위가 역전되었다. A는 빈약한 원본 테스트에 최적화(혹은 기억)되어 있었고, 엣지 케이스에서 B보다 약했다.

검증 2 — LiveCodeBench 시간 분할. 두 모델의 훈련 컷오프 이후 출제된 경쟁 문제만으로 평가하니 A=41%, B=47%로 격차가 더 벌어졌다. A의 컷오프 이전 문제 성능은 67%로, $\Delta_{\text{contam}}=67\%-41\%=26\%$p의 큰 시간적 격차를 보였다 — 강한 오염 신호다. B는 이전 60%, 이후 47%로 격차가 13%p로 더 작았다.

검증 3 — SWE-bench Lite. 실제 리포지토리 태스크에서 A=18%, B=24%. 최종적으로 팀은 B를 선택했다.

교훈. 단일 벤치마크(HumanEval)만 봤다면 잘못된 모델 A를 골랐을 것이다. (1) 테스트 증강(HumanEval+), (2) 시간적 분할(LiveCodeBench), (3) 실전 태스크(SWE-bench)의 세 검증이 일관되게 B의 우위를 가리켰다. 평가의 견고성은 다중 벤치마크 삼각측량(triangulation)에서 온다.

4.17 평가의 비용·지연 엔지니어링

이 장의 거의 모든 고급 메트릭(RAGAS 4메트릭, G-Eval, Answer Correctness, 샌드박스 실행)은 LLM 호출이나 코드 실행을 동반하므로 느리고 비싸다. 수천 개 케이스를 매 커밋마다 평가하면 비용과 지연이 개발 속도를 잠식한다. 따라서 평가 자체도 엔지니어링 대상이다 — 정확도를 거의 잃지 않으면서 비용을 줄이는 전략이 필요하다.

4.17.1 비용 모델과 병목 식별

RAG 평가의 총 비용은 케이스 수, 메트릭 수, 메트릭당 LLM 호출 수, 호출당 토큰 비용의 곱으로 근사된다. 4.7.4의 비용 차트에서 보았듯 Faithfulness·Answer Relevance가 claim/역질문 생성으로 호출이 많아 병목이다.

평가 총 비용 근사
$$\text{Cost} = N \cdot \sum_{m \in \mathcal{M}} c_m \cdot (\tau^{\text{in}}_m \, p^{\text{in}} + \tau^{\text{out}}_m \, p^{\text{out}})$$

$N$: 케이스 수, $\mathcal{M}$: 메트릭 집합, $c_m$: 메트릭 m의 LLM 호출 수, $\tau^{\text{in/out}}_m$: 입출력 토큰, $p^{\text{in/out}}$: 토큰 단가. 호출 수 $c_m$이 지배적 → 호출 수를 줄이는 것이 가장 효과적

4.17.2 계층적 필터링과 샘플링

비용을 줄이는 두 축은 (1) 저비용 1차 필터(4.7.5의 BERTScore, 4.12의 정적 분석)로 명백한 케이스를 LLM 호출 없이 처리하고, (2) 전수 평가 대신 층화 샘플링(stratified sampling)으로 대표 부분집합만 평가하는 것이다. 단 회귀 게이트(4.9.4)에는 충분한 표본이 필요하므로 샘플 크기를 검정력(statistical power) 기준으로 정해야 한다.

def tiered_evaluate(samples, cheap_metric, expensive_eval, threshold=0.4): """1차 저비용 필터 → 애매한 케이스만 비싼 LLM 평가로 escalate""" results, escalated = [], 0 for s in samples: cheap = cheap_metric(s) # 예: BERTScore (임베딩, 저렴) if cheap < threshold: results.append({"score": cheap, "tier": "cheap_reject"}) else: results.append({"score": expensive_eval(s), "tier": "llm"}) escalated += 1 return results, escalated / len(samples) # escalation 비율 = 비용 척도
전략비용 절감위험적용 위치
저비용 1차 필터높음 (50~80%)경계 케이스 오분류대규모 스크리닝
층화 샘플링높음 (N에 비례)드문 실패 양식 누락일상 회귀 체크
결과 캐싱중간비결정성 무시변경 없는 케이스
저렴한 판사 모델중간판정 품질 하락비핵심 메트릭
배치 API중간 (단가 할인)지연 증가비실시간 평가
평가 예산도 메트릭의 일부다

"가장 정확한 평가"가 항상 옳은 것은 아니다. 매 커밋마다 전수 LLM 평가를 돌리면 비용·지연 때문에 결국 아무도 평가를 돌리지 않게 된다 — 안 쓰이는 완벽한 평가보다, 자주 쓰이는 충분한 평가가 낫다. 따라서 (1) 일상 CI에는 저비용 1차 필터 + 샘플링으로 빠른 신호를, (2) 릴리스 게이트에는 전수 정밀 평가를 배치하는 2계층 전략이 실용적이다. 평가의 정확도와 빈도·비용 사이의 트레이드오프를 명시적으로 설계하라.

4.18 통합 관점 — RAG와 코드 평가의 공통 교훈

표면적으로 RAG 평가와 코드 평가는 다른 영역이지만, 이 장을 관통하는 세 가지 보편 원리를 공유한다. 이 원리들은 Part III에서 다룰 에이전트 6차원 평가 프레임워크로도 이어진다.

4.18.1 원리 1: 분해(Decomposition)

두 영역 모두 파이프라인을 블랙박스로 평가하지 않고 구성 요소로 분해한다. RAG는 검색·생성으로, 코드는 기능·복잡도·견고성·가독성·보안으로 분해한다. SWE-bench는 한 걸음 더 나아가 localization(검색)과 editing(수정)으로 분해된다. 분해 없이는 진단도 개선도 없다.

4.18.2 원리 2: 실행과 근거(Grounding)

두 영역 모두 "표면 비교"를 거부하고 "객관적 근거"를 추구한다. RAG는 컨텍스트라는 근거에 답변을 묶고(grounding), 코드는 테스트라는 근거에 정확성을 묶는다(execution). 둘 다 BLEU 같은 표면 유사도를 신뢰하지 않는다는 점이 공통이다.

4.18.3 원리 3: 다차원과 거부권(Multi-dimensionality & Veto)

두 영역 모두 단일 점수의 한계를 인정하고 다차원으로 본다. 그리고 일부 차원은 다른 차원으로 보상될 수 없는 거부권(veto)을 가진다 — 코드에서 심각한 보안 취약점, RAG에서 PII 노출이 그렇다. 이는 Ch.13의 Safety Gate 철학과 정확히 일치한다.

공통 원리RAG에서의 구현코드에서의 구현6차원 프레임워크 연결
분해검색·생성 레벨 분리기능·품질·보안 차원 분리6개 독립 차원
실행·근거컨텍스트 grounding (Faithfulness)테스트 실행 (pass@k)Task Completion 검증
다차원·거부권PII·환각 거부권보안 취약점 거부권Safety Gate
평가기 신뢰성NLI/LLM 판사 보정 (ARES)테스트셋 품질 (Verified)메타 평가
오염·일반화합성 vs 실제 데이터 격차훈련 오염 (LiveCodeBench)견고성·일반화
도메인 특화 평가가 가르쳐주는 보편 원리

RAG와 코드 생성은 "특수한" 도메인처럼 보이지만, 사실 이들이 가르쳐주는 교훈은 보편적이다 — 어떤 복잡한 AI 시스템이든 (1) 구성 요소로 분해하고, (2) 표면이 아닌 객관적 근거로 검증하며, (3) 다차원으로 보되 절대 양보할 수 없는 차원은 거부권으로 보호하라. 이 세 원리는 이 책의 후반부 전체(Part II·III)에서 멀티 에이전트 시스템의 6차원 평가로 일반화된다. 즉 이 장은 단순한 "응용 사례"가 아니라 프레임워크 설계 철학의 구체적 예고편이다.

4.19 요약과 다음 장 예고

이 장에서 우리는 RAG와 코드 생성이라는 두 도메인 특화 평가를 깊이 다뤘다. 핵심을 정리하면 다음과 같다.

4.19.1 핵심 요약

주제한 문장 요약
RAG 분해검색과 생성을 분리해 평가하라 — 답이 맞아도 검색이 틀렸으면 문제다
RAGAS 4메트릭Faithfulness(충실)·Answer Relevance(관련)·Context Precision/Recall(검색)로 실패 단계를 격리
프레임워크 선택RAGAS(표준)·ARES(보정)·TruLens(관측)·DeepEval(CI)을 상황에 맞게, 그러나 골드셋이 먼저
Pass@kk를 사용 시나리오에 맞춰라 — pass@100만 높으면 '때려 맞추기'
실행 vs 정적싸고 안전한 정적 검사로 거른 뒤 비싼 샌드박스 실행으로 판정
코드 다차원'돌아가는가'를 넘어 복잡도·견고성·가독성·보안까지, 보안은 거부권
SWE-bench코딩 테스트가 아니라 검색+수정+회귀회피의 엔지니어링 워크플로우
오염단일 벤치마크를 믿지 말고 라이브·증강·실전 벤치마크로 삼각측량

4.19.2 다음 장 예고 — Ch.05 근본적 도전과제

이 장에서 우리는 도메인 특화 평가가 일반 LLM 평가보다 더 정밀할 수 있음을 보았다 — 검색을 근거로, 테스트를 근거로 묶을 수 있기 때문이다. 그러나 이 정밀함에도 불구하고 모든 평가가 공유하는 근본적 한계들이 있다. NLI 모델·LLM 판사의 신뢰성, 평가셋 자체의 오염과 편향, 합성 데이터와 실제 분포의 격차, 그리고 "측정하는 순간 최적화 대상이 되어 메트릭이 무의미해지는" 굿하트의 법칙(Goodhart's Law)이 그것이다.

Ch.05 "근본적 도전과제(Fundamental Challenges)"에서는 이러한 평가의 본질적 한계들을 정면으로 다룬다. 왜 완벽한 평가는 불가능한가, 그럼에도 어떻게 신뢰할 수 있는 평가에 점근(漸近)할 것인가를 논한다. 그 논의는 Part II에서 단일 모델 평가를 넘어 에이전트·멀티 에이전트 평가로 패러다임이 전환되는 토대가 된다.

이 장의 한 문장 요약

RAG와 코드 생성 평가의 정수는 "파이프라인을 구성 요소로 분해하고, 표면이 아닌 객관적 근거(컨텍스트·테스트)에 묶어, 다차원으로 측정하되 양보할 수 없는 차원은 거부권으로 보호하는 것"이다 — 이 세 원리가 다음 장들의 에이전트 6차원 평가 프레임워크로 일반화된다.

4.19.3 독자 가이드 — 역할별 진입 경로

독자의 역할에 따라 이 장에서 우선 읽어야 할 절이 다르다. 아래 경로는 각자의 목표에 가장 빠르게 도달하도록 구성했다.

독자 유형추천 경로건너뛰어도 되는 절
RAG 실무 엔지니어4.1 → 4.2 → 4.3~4.5 → 4.9 → 4.174.11, 4.14~4.16
코드 에이전트 개발자4.10 → 4.11 → 4.12 → 4.14 → 4.164.3~4.5, 4.7
평가 플랫폼/MLOps4.8 → 4.9.4 → 4.12 → 4.174.3.3~4.3.5(시나리오)
ML 연구자(이론 깊이)4.5.4~4.5.5 → 4.11.1 → 4.13.5 → 4.16.34.0, 4.8.1
리더·의사결정자4.1.2 → 4.6 → 4.16 → 4.18모든 코드 블록

4.19.4 핵심 용어 사전

이 장에서 도입한 핵심 용어를 한곳에 정리했다. 후속 장에서 다시 등장하므로 참조용으로 활용하라.

용어정의
RAG외부 지식 베이스에서 검색한 컨텍스트를 LLM 생성에 주입하는 아키텍처(Retrieval-Augmented Generation)
Faithfulness답변의 각 claim이 검색된 컨텍스트에서 지지되는 비율 — 환각의 정량적 반대
Answer Relevance답변에서 유도한 역질문이 원질문과 얼마나 유사한지 — "질문에 초점을 맞췄는가"
Context Precision검색 문서 중 관련 문서가 상위 순위에 배치된 정도(순위 가중, AP와 동형)
Context Recall정답에 필요한 정보가 검색된 컨텍스트에 포함된 비율
GroundednessTruLens RAG Triad에서 답변이 컨텍스트에 근거하는 정도(Faithfulness와 유사)
reference-free고정 정답 문장 없이 컨텍스트·질문만으로 평가하는 방식
Pass@kk개 샘플 중 적어도 하나가 모든 테스트를 통과할 확률(불편 추정)
실행 기반 평가코드를 샌드박스에서 실제 실행해 테스트 통과로 정확성을 판정
resolve rateSWE-bench에서 FAIL_TO_PASS·PASS_TO_PASS 테스트를 모두 통과한 태스크 비율
데이터 오염벤치마크 문제가 훈련 데이터에 포함되어 점수가 기억을 반영하는 현상
거부권(Veto)한 차원(보안·PII)의 치명적 결함이 다른 차원으로 보상될 수 없게 하는 규칙