제6부 응용: 프롬프트·RAG·에이전트
6.4 RAG II — 고급·평가
기본 RAG(6.3)는 데모에서 인상적이고 제품에서 자주 붕괴한다. 문서를 잘라 임베딩하고, 질문을 임베딩해 벡터 검색으로 상위 몇 개를 꺼내(6.2), 프롬프트에 붙여 생성한다 — 이 파이프라인은 질문이 명료하고 문서가 잘 정리돼 있고 답이 한 청크에 전부 들어 있을 때는 훌륭하다. 그런데 실제 사용자를 붙이면 세 종류의 불평이 며칠 안에 올라온다. "이 뻔한 문서를 왜 못 찾지." "문서는 제대로 찾았는데 왜 엉뚱한 답을 하지." "고친 뒤로 정말 좋아진 게 맞긴 한가." 이 세 불평이 이 장의 골격이다. 각각 검색의 실패, 생성의 실패, 그리고 측정의 부재다.
이 장 전체를 관통하는 한 문장이 있다. RAG 품질의 상한은 검색이 정한다. 아무리 좋은 LLM도 관련 문서가 컨텍스트에 없으면 답할 수 없다 — 있는 척 지어낼 뿐이고, 그 지어냄이 바로 환각이다. 반대로 관련 없는 청크가 잔뜩 섞여 들어오면 컨텍스트에 답이 있어도 놓치거나 흐린다. 쓰레기가 들어가면 쓰레기가 나온다(garbage in, garbage out). 그래서 이 장의 고급 기법 대부분은 생성 단계가 아니라 검색·컨텍스트 단계에 몰려 있다 — 그곳이 손실 지점이기 때문이다.
두 갈래로 다룬다. 하나는 검색·컨텍스트 품질을 높이는 기법군이다 — 리랭킹(2단계 검색), 하이브리드 검색(밀집+어휘), 쿼리 변환(재작성·HyDE·분해·step-back), 메타데이터 필터·컨텍스트 정제·문맥적 검색, 그리고 "한 번 검색해 청크 몇 개를 꺼낸다"는 틀 자체를 깨는 Graph RAG·에이전틱 RAG. 다른 하나는 그중 무엇을 넣을지 정하게 해 주는 평가다 — 검색이 좋은 문서를 꺼냈는지 재는 검색 지표(recall·precision·MRR·nDCG)와 최종 답이 좋은지 재는 종단 지표(충실성·답변 적합성·문맥 적합성). 평가를 먼저 세우지 않으면 앞의 기법을 감으로 고르게 되고, 감은 지연·비용·복잡도만 늘린 채 어느 것이 실제로 도왔는지 못 가르게 만든다. 그래서 이 장의 기법은 하나같이 조건부 이득이다 — 도메인·데이터·질의 분포에 강하게 의존하며, 자기 데이터에서 재기 전에는 어떤 개선도 가정할 수 없다.
6.4.1 어디서 손실되고 무엇으로 막나
기법을 나열하기 전에 왜 그것들이 필요한지부터 정리하자. RAG는 검색(retrieve)·증강(augment)·생성(generate) 세 단계이고, 각 단계에 특유의 실패가 있다. 고급 기법은 결국 특정 실패를 겨냥한 처방이다. 처방을 실패와 짝지어 두면 무턱대고 다 넣는 대신 내 시스템이 실제로 손실되는 곳에만 투자할 수 있다.
세 단계에서 손실되는 양이 같지 않다. 실무에서 압도적으로 흔한 실패는 첫 단계 — 관련 문서가 애초에 컨텍스트에 들어오지 못하는 검색 실패다. 생성 모델은 갈수록 좋아져서 컨텍스트만 맞으면 대체로 답을 뽑아낸다. 그래서 이 장의 무게중심이 검색에 있다. 아래 표가 이 장의 목차이자 진단 도구다 — 왼쪽의 증상을 자기 시스템에서 관찰하면 오른쪽 절로 바로 가면 된다.
| 손실 지점 | 증상 | 처방 | 절 |
|---|---|---|---|
| 검색 순위가 거칠다 | 관련 문서는 후보엔 있으나 상위 $k$ 밖에 묻힘 | 리랭킹(2단계) | 6.4.2 |
| 정확 용어를 놓친다 | 고유명사·ID·코드·희귀어에서 밀집 검색 헛발질 | 하이브리드(dense+BM25) | 6.4.3 |
| 질문 표현이 문서와 어긋난다 | 짧거나 모호하거나 대화 맥락에 의존하거나 복합적 | 쿼리 변환 | 6.4.5 |
| 범위·권한·최신성이 틀리다 | 오래된·무관·다른 부서 문서가 섞임 | 메타데이터 필터 | 6.4.6 |
| 컨텍스트가 길고 노이즈·중복 | 정답이 노이즈에 묻히고 비용·지연↑ | 정제·중복 제거·재정렬 | 6.4.7 |
| 청크가 고립돼 맥락 상실 | "그 회사는…" 청크에 회사명이 없음 | 문맥적 검색 | 6.4.8 |
| 관계·다중홉 질문 | "A의 상사의 고향은?" 한 청크에 답이 없음 | Graph RAG·에이전틱 RAG | 6.4.9 |
| 무엇이 유출되는지 모른다 | 고쳐도 좋아졌는지 확신 못 함 | 검색·종단 평가 | 6.4.10~6.4.12 |
6.4.2 리랭킹 — 2단계 검색과 cross-encoder
6.4.2.1 bi-encoder가 거칠 수밖에 없는 이유
기본 벡터 검색(6.2)은 bi-encoder(이중 인코더) 구조다. 질문과 문서를 각각 따로 하나의 벡터로 인코딩하고, 두 벡터의 코사인 유사도로 순위를 매긴다. 이 구조의 결정적 이점은 문서 벡터를 색인 시점에 미리 계산해 둘 수 있다는 것이다. 질의가 들어오면 질문 하나만 인코딩하고, 미리 만들어 둔 수백만 개의 문서 벡터를 ANN(근사 최근접 이웃)으로 밀리초에 훑는다. 그래서 빠르다.
빠름의 대가가 거칢이다. 질문과 문서가 서로를 한 번도 보지 못한 채 각자 벡터로 압축된다. 문서 전체의 의미를 고정 크기 벡터 하나에 압축해 담으면 세부가 구분되지 않는다 — 질문의 어느 구절이 문서의 어느 문장과 맞는지, 그 토큰 수준의 상호작용을 표현할 자리가 없다. 결과적으로 bi-encoder 순위는 "대충 관련 있는 것들"은 잘 그러모으지만 그 안의 정밀한 순서는 자주 틀린다. 정답 문서가 후보 20위 안팎에 처박히고 그저 그런 문서가 3위에 올라오는 일이 흔하다. 컨텍스트 창이 좁아 상위 5개만 넣어야 하는 상황이면, 이 순위 오차가 곧바로 답 품질로 드러난다.
6.4.2.2 cross-encoder — 질문과 문서를 함께 읽기
cross-encoder(교차 인코더)는 반대로 작동한다. 질문과 문서를 이어 붙여 하나의 입력([질문] [SEP] [문서])으로 트랜스포머에 넣고, 셀프어텐션이 질문 토큰과 문서 토큰을 서로 교차해 보게 한 뒤, 하나의 관련도 점수(scalar)를 뽑는다. 어텐션(1.4)이 "질문의 이 단어가 문서의 저 문장을 직접 가리킨다"를 층마다 계산하므로, 압축으로 잃었던 토큰 수준 상호작용이 되살아난다. 그만큼 순위가 정밀하다.
정밀함의 대가는 계산이다. cross-encoder는 질문·문서 쌍마다 트랜스포머를 한 번씩 실행해야 한다 — 문서 벡터를 미리 계산해 둘 수 없다. 질문이 붙어야 비로소 계산이 시작되니까. 수백만 문서 전체에 cross-encoder를 매 질의마다 실행하는 것은 지연·비용상 불가능하다. 그래서 둘의 장점만 취하는 배치가 등장한다.
6.4.2.3 2단계 검색 — recall과 precision의 분업
해법은 역할 분담이다. 값싼 bi-encoder(또는 BM25)로 먼저 넓게 후보를 좁히고, 비용이 큰 cross-encoder로 그 소수만 정밀하게 다시 매긴다. 이것이 2단계 검색(two-stage retrieval) = 검색(retrieval) + 리랭킹(reranking)이다.
두 단계의 목표가 깨끗하게 갈린다. 1단계는 재현율(recall)을 책임진다 — 관련 문서가 후보 $N$개 안에 들어오기만 하면 되고, 그 안에서의 순서는 대충이어도 좋다. 그래서 $N$을 충분히 크게(예: 100~200) 설정해 빠뜨리지 않는 데 집중한다. 2단계는 정밀도(precision)를 책임진다 — 후보 $N$개의 순서를 바로잡아 실제 상위 $k$개(예: 5)를 위로 높인다. cross-encoder를 $N$개에만 실행하므로 감당할 수 있다.
여기에 놓치기 쉬운 함정이 있다. 1단계 재현율이 2단계의 상한이다. 관련 문서가 1단계 후보 100개 안에 없으면 리랭커는 그것을 되살릴 수 없다 — 없는 것을 상위에 배치할 방법은 없다. 그래서 리랭킹을 넣을 때는 1단계 $N$을 충분히 키우는 것이 짝을 이룬다. 현장에서 "리랭커를 붙였는데 왜 안 좋아지지?"의 가장 흔한 원인이 바로 이것이다 — $N$이 10처럼 작아 애초에 관련 문서가 후보에 없는데, 순서만 바꾼들 소용이 없다.
6.4.2.4 이득과 비용을 숫자로
이득의 원리는 곱셈이다. 1단계는 넓지만 거칠고, 2단계는 좁지만 정밀하다. 이어 붙이면 "넓고 정밀"을 근사한다 — 전수 cross-encoder의 정밀함을 그 계산량의 극히 일부로 얻는다. 얼마나 일부인지 실제로 세어 보자. 코퍼스 100만 청크, ANN 색인은 HNSW라고 하자.
전수 cross-encoder(현실엔 없지만 상한을 보기 위해): 매 질의마다 100만 쌍을 채점해야 한다. cross-encoder가 배치 처리로 초당 대략 수천 쌍을 채점한다고 하면(모델 크기·시퀀스 길이에 크게 의존, 개략값, 확인 필요), 가령 2,000쌍/초로 가정해도
$$\frac{10^6\ \text{쌍}}{2{,}000\ \text{쌍/s}} = 500\ \text{초} \approx 8\text{분 이상 / 질의}.$$
온라인 서빙에서 논외다. 게다가 이 비용을 질의마다 새로 치른다 — 문서 벡터처럼 미리 계산해 재사용할 수가 없으니까.
2단계 검색: 1단계 ANN이 100만 벡터에서 상위 $N=100$을 뽑는 데 대략 한 자릿수 밀리초, 2단계가 그 100쌍만 채점하니
$$\frac{100\ \text{쌍}}{2{,}000\ \text{쌍/s}} = 0.05\ \text{초} = 50\ \text{ms}.$$
8분 대 50ms. 리랭킹 비용은 후보 수 $N$에 정확히 선형이고, $N$은 100만이 아니라 100이다. 정밀함은 거의 그대로 얻으면서 계산은 1만 분의 1로 줄인 셈이다. 이것이 2단계가 이기는 산술적 이유다.
물론 무비용은 아니다. 대가는 정직하게 세 가지다.
| 축 | 영향 |
|---|---|
| 지연 | 2단계가 추가 홉 — cross-encoder $N$개 순전파가 TTFT에 직접 더해짐(5.6). 위 예에서 +50ms |
| 비용 | 리랭커 추론 비용 — 자체 GPU 서빙이든 리랭킹 API 과금이든 질의당 $N$회 채점 |
| 복잡도 | 서빙 파이프라인에 컴포넌트·장애점·모델 버전 관리가 하나 늘어남 |
내 견해로는 리랭킹이 고급 RAG에서 가장 먼저 시도할 만한 축이다 — 투자 대비 효과가 큰 편이고, 특히 1단계가 이미 관련 문서를 후보엔 넣지만 순서가 나쁠 때 그렇다. 다만 1단계 재현율 자체가 낮다면(관련 문서를 후보에도 못 넣는다면) 리랭킹은 헛수고다. 그때는 하이브리드나 쿼리 변환으로 재현율을 먼저 높여야 한다. 순서는 "리랭커부터"가 아니라 "무엇이 새는지 재고 새는 곳부터"다(6.4.10). 그리고 벤더 벤치마크의 화려한 개선치는 그 벤치마크의 질의 분포에서 얻은 값이라, 내 도메인이 다르면 재현되지 않는다 — 자기 골든셋에서 리랭킹 전후로 nDCG·정밀도@k를 재서 확인하고, 이득이 미미하면 지연·비용만 늘린 것이니 빼는 것이 맞다.
6.4.3 하이브리드 검색 — 밀집이 지고 어휘가 이기는 곳
6.4.3.1 정확 일치의 세계와 BM25
밀집(dense) 임베딩 검색은 의미로 찾는다 — "자동차 고장"과 "차량 결함"을 가깝게 놓는다. 강력하지만, 바로 그 "의미로 뭉개는" 성질 때문에 정확한 토큰 일치가 중요한 질의에서 진다. 제품 코드 RTX-4090, 오류 코드 ERR_0x8007, 함수명 parse_header(), 계약 번호, 그대로 인용해야 하는 문구 — 이런 희귀 토큰은 임베딩 모델이 학습 때 거의 못 봤거나 아예 못 봐서, 밀집 공간에서 좌표가 부정확하다. RTX-4090을 물으면 "비슷하게 생긴 다른 코드"를 상위에 배치하기 쉽다. 의미 유사가 아니라 문자 그대로가 정답인 질의에서 밀집은 구조적으로 약하다.
이 세계의 오랜 강자가 어휘(lexical) 검색 — 대표적으로 BM25다. BM25는 의미를 모른다. 오직 정확한 용어 겹침(term overlap)을 점수화한다.
$$\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$가 나온 빈도로, 많이 나올수록 점수가 오르되 $k_1$이 포화시켜 무한정 오르지는 않게 한다. $|d|/\text{avgdl}$은 문서 길이 정규화로, 긴 문서가 단지 길어서 유리해지는 것을 막는다($b$로 강도 조절, 대표값 $k_1\approx1.2\text{–}2.0$, $b\approx0.75$). 그리고 핵심이 $\text{IDF}(t)$ — 역문서빈도다. 희귀한 용어일수록 가중치가 크다. RTX-4090처럼 코퍼스 전체에서 몇 문서에만 나오는 토큰은 IDF가 크게 증가해, 그 토큰을 포함한 문서가 급격히 부각된다. 이것이 고유명사·희귀 코드·ID에서 BM25가 밀집을 이기는 정확한 메커니즘이다.
BM25의 약점은 정반대 방향에 있다. 정확 토큰이 겹쳐야 점수가 나므로 동의어·바꿔 말하기에 약하다 — "자동차"로 물으면 "차량"만 쓴 문서를 못 찾는다. 즉 밀집과 어휘는 서로의 사각지대를 정확히 메운다.
| 밀집(dense) | 어휘(BM25) | |
|---|---|---|
| 매칭 기준 | 의미(임베딩 근접) | 정확 토큰 겹침 |
| 강한 곳 | 동의어·바꿔 말하기·개념 | 고유명사·코드·ID·정확 인용·희귀어 |
| 약한 곳 | 희귀 토큰·정확 일치 | 어휘 불일치(동의어·의역) |
| 도메인 밖 신조어 | 좌표 엉킴 | 토큰만 맞으면 잡음 |
| 색인 | 벡터 색인(ANN) | 역색인(inverted index) |
| 튜닝 조정 변수 | 임베딩 모델 선택·파인튜닝 | 토큰화·불용어·$k_1,b$ |
6.4.3.2 RRF — 점수 대신 순위로 융합
둘이 상보적이니 함께 쓰는 것이 자연스럽다. 이것이 하이브리드 검색(hybrid search)이다. 문제는 "두 순위를 어떻게 하나로 합치나"인데, 여기서 걸림돌이 하나 있다. 두 시스템의 점수 스케일이 전혀 다르다. 코사인 유사도는 대략 0~1, BM25는 0에서 수십까지 열려 있다. 단순히 더하면 BM25가 스케일만으로 지배한다. 정규화해서 더하는 방법(convex combination)도 있지만 각 시스템의 점수 분포에 민감해 가중치 튜닝이 까다롭고 코퍼스가 바뀌면 다시 맞춰야 한다.
그래서 널리 쓰이는 것이 점수를 아예 버리고 순위(rank)만 쓰는 융합 — RRF(Reciprocal Rank Fusion, 역순위 융합)다. 각 문서에 대해, 그 문서가 각 순위 목록에서 몇 등인지의 역수를 더한다.
$$\text{RRF}(d) = \sum_{r \in \mathcal{R}} \frac{1}{k + \text{rank}_r(d)}$$
$\mathcal{R}$은 융합할 순위 목록들(밀집 순위, BM25 순위), $\text{rank}_r(d)$는 목록 $r$에서 문서 $d$의 등수, 목록에 없으면 그 항은 0이다. $k$는 평활 상수로 대표값 60을 쓴다 — 크면 상위·하위 등수의 차이가 완만해지고, 작으면 최상위를 강하게 우대한다.
RRF의 매력은 스케일 무관이다. 점수의 절대값을 보지 않고 등수만 보므로, 코사인과 BM25의 이질적 스케일을 정규화 없이 섞는다. 게다가 두 목록 모두에서 상위인 문서가 자연히 위로 올라온다 — 한쪽에서만 반짝 뜬 문서보다 밀집·어휘 양쪽이 동의한 문서를 신뢰하는, 일종의 합의(consensus) 효과다. 튜닝 파라미터가 $k$ 하나뿐이라 실무 기본값으로 널리 쓰인다. 다만 RRF가 늘 최선은 아니다. 두 시스템의 품질 차가 크면 나쁜 쪽이 좋은 쪽을 끌어내릴 수 있다 — 둘을 같은 무게로 취급하기 때문이다. 그래서 목록별로 가중치를 주는 변형도 있다. 무엇을 쓰든 밀집 단독·BM25 단독·하이브리드를 자기 데이터에서 모두 재서 비교해야 한다. 하이브리드가 항상 이긴다는 보장은 어디에도 없다.
6.4.4 직접 계산 — RRF로 두 순위 융합하기
식은 봐도 미끄러지니 작은 예제를 직접 끝까지 계산하자. 모두 설명용 개략값이고 $k=60$을 쓴다. 질문 하나에 두 검색기가 각각 상위 4개를 아래 순위로 냈다(문서 라벨 A~E).
| 등수 | 밀집(dense) | BM25 |
|---|---|---|
| 1 | A | C |
| 2 | B | A |
| 3 | C | E |
| 4 | D | B |
각 문서의 RRF 점수는 등장한 등수마다 $\frac{1}{60+\text{rank}}$를 더한 값이다.
- A: 밀집 1등, BM25 2등 → $\frac{1}{61} + \frac{1}{62} \approx 0.01639 + 0.01613 = \mathbf{0.03252}$
- C: 밀집 3등, BM25 1등 → $\frac{1}{63} + \frac{1}{61} \approx 0.01587 + 0.01639 = \mathbf{0.03227}$
- B: 밀집 2등, BM25 4등 → $\frac{1}{62} + \frac{1}{64} \approx 0.01613 + 0.01563 = \mathbf{0.03175}$
- E: BM25 3등만 → $\frac{1}{63} \approx \mathbf{0.01587}$
- D: 밀집 4등만 → $\frac{1}{64} \approx \mathbf{0.01563}$
RRF로 재정렬한 최종 순위는 이렇다.
| 융합 등수 | 문서 | RRF 점수 | 왜 |
|---|---|---|---|
| 1 | A | 0.03252 | 양쪽 모두 상위(1·2등) — 합의 |
| 2 | C | 0.03227 | 한쪽 1등 + 다른쪽 3등 |
| 3 | B | 0.03175 | 양쪽 중위 |
| 4 | E | 0.01587 | BM25에만 등장 |
| 5 | D | 0.01563 | 밀집에만 등장 |
세 가지가 눈에 들어온다. 첫째, 합의가 이긴다 — A는 어느 목록에서도 밀집에서만 1등이지만 양쪽에서 고르게 상위라 종합 1등이 됐다. C는 BM25 1등이라는 강한 신호가 있는데도, 밀집에서 3등으로 처진 탓에 A에 밀렸다. RRF는 두 관점이 함께 지지하는 문서를 신뢰한다. 둘째, 한쪽에만 나온 D·E는 아래로 밀렸다 — 다른 관점의 지지가 없으니 신중히 취급된다. 셋째, $k=60$이라 점수 차가 작다(0.032 대 0.031). $k$를 10처럼 작게 하면 최상위 등수 우대가 커져 순위 차가 벌어지고, 크게 하면 더 평평해진다 — $k$는 "상위를 얼마나 강하게 믿을지"의 조정 변수다.
def rrf_fuse(rank_lists, k=60):
# rank_lists: [{doc: rank, ...}, ...] (rank는 1부터)
scores = {}
for rl in rank_lists:
for doc, rank in rl.items():
scores[doc] = scores.get(doc, 0.0) + 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
dense = {"A": 1, "B": 2, "C": 3, "D": 4}
bm25 = {"C": 1, "A": 2, "E": 3, "B": 4}
# rrf_fuse([dense, bm25]) → [('A', .0325), ('C', .0323), ('B', .0318), ('E', .0159), ('D', .0156)]
이 융합 결과를 다시 리랭커(6.4.2)에 넣으면 하이브리드+리랭킹이 된다 — 넓게(하이브리드로 재현율) 모으고 정밀하게(cross-encoder로 순서) 매기는, 실무에서 가장 흔한 상단 파이프라인이다. RRF는 여기서 두 검색기의 후보를 하나의 후보 풀로 합쳐 리랭커에 넘기는 다리 역할을 한다.
6.4.5 쿼리 변환 — 질문을 검색에 맞게 다시 쓰기
지금까지는 검색기를 손봤다. 다른 축은 질문을 손보는 것이다. 사용자의 원 질문은 검색에 이상적인 경우가 드물다 — 너무 짧거나, 모호하거나, 앞 대화에 의존하거나, 여러 질문이 얽혀 있다. 쿼리 변환(query transformation)은 질문을 검색에 유리한 형태로 다시 쓰는 기법군이고, 각 변형은 서로 다른 실패를 겨냥한다. 공통점은 모두 LLM 호출을 하나 더 붙인다는 것이라, 지연·비용·환각 위험이 함께 늘어난다.
6.4.5.1 재작성·확장 — 대화 맥락 해소와 용어 보강
재작성(rewriting)은 원 질문을 검색 친화적 문장으로 고친다. 대화형 RAG에서 가장 중요한 것이 맥락 해소(coreference resolution)다. 멀티턴 대화에서 사용자는 "그럼 그거는 얼마야?"라고 묻는다 — "그거"가 무엇인지는 앞 대화에 있다. 이 질문을 그대로 임베딩하면 검색이 무의미하다. "그거"의 벡터로는 아무것도 못 찾는다. LLM에 대화 맥락을 넣어 "그럼 RTX-4090의 가격은 얼마야?"로 먼저 재작성한 뒤 검색해야 한다. 이 단계를 빠뜨린 대화형 RAG는 두 번째 턴부터 조용히 망가진다 — 첫 턴은 완전한 질문이라 잘 되지만, 이후 턴이 대명사·생략으로 짧아질수록 검색이 실패한다.
확장(expansion)은 질문에 동의어·관련어를 덧붙여 어휘 불일치를 줄인다("자동차 고장" → "자동차 차량 고장 결함 수리 오류"). 정확 토큰 겹침에 의존하는 BM25에서 특히 효과가 크다 — 원 질문에 없던 "차량"·"결함" 토큰이 들어가면서 그 표현을 쓴 문서까지 걸린다.
6.4.5.2 HyDE — 가짜 답변으로 검색하기
HyDE(Hypothetical Document Embeddings, 가설 문서 임베딩)는 영리한 뒤집기다. 문제의식은 질문과 문서의 문체가 다르다는 것이다. 질문은 짧은 의문문("파이썬에서 파일 어떻게 읽어?"), 정답 문서는 서술형 설명문("파일을 읽으려면 open()으로…"). 임베딩 공간에서 질문 벡터와 정답 문서 벡터가 이 표현 격차 때문에 생각보다 멀 수 있다 — 둘은 내용이 같아도 형태가 다르니까.
HyDE의 착상은 질문으로 검색하지 말고 LLM이 지어낸 가짜 답변으로 검색하자는 것이다. LLM에 질문의 답을 상상해 쓰게 하고(사실 여부는 중요치 않다), 그 가설 문서를 임베딩해, 그 벡터로 검색한다. 가설 문서는 실제 답변 문서와 같은 문체·같은 어휘 분포를 갖는지라, 표현 격차를 건너뛰어 정답 문서에 더 가깝게 착지한다. 다만 이것은 양날의 검이다. 착상은 우아하고 도메인 지식이 옅은 일반 질의에서 도움이 된다는 보고가 있으나, LLM이 지어낸 가설 문서가 틀린 방향으로 구체적이면 — 환각한 고유명사, 잘못 붙인 개념 — 검색을 잘못된 곳으로 끌고 간다. 지연·비용도 는다(질의마다 생성 1회 추가). 도메인 특화·희귀 사실 질의에서는 오히려 해로울 수 있어, 자기 데이터에서 검증하지 않고 기본으로 켜서는 안 된다.
6.4.5.3 다중 쿼리 — 한 질문을 여러 각도로
다중 쿼리(multi-query)는 원 질문을 여러 변형으로 바꿔 각각 검색하고 결과를 RRF로 합친다. 한 표현에는 편향이 있다 — 특정 단어 선택이 특정 문서만 부른다. LLM에 "이 질문을 서로 다른 관점의 세 질문으로 바꿔라"고 시켜 여러 그물을 던지면, 한 그물이 놓친 문서를 다른 그물이 포착해 재현율이 오른다. 대가는 검색 횟수가 배로 늘고 융합이 추가되는 것이다. 재현율이 병목일 때 쓰는 무기지, 정밀도가 병목이면 노이즈만 더 긁어 온다.
6.4.5.4 분해 — 복합·다중홉 질문 분해
분해(decomposition)는 하나의 검색으로 못 푸는 복합 질문을 하위 질문으로 나눈다. "A사와 B사 중 작년 매출 성장률이 높은 곳은?"은 어느 단일 청크에도 답이 없다 — A사 매출과 B사 매출을 따로 검색해 각각 답하고 비교해야 한다. 이것은 사실상 에이전틱 RAG로 넘어가는 다리다(6.4.9) — 하위 질문을 순차·병렬로 검색하고 종합하는 다단계 추론이니까. 직선 파이프라인 안에 억지로 밀어 넣으려 하면 한계가 곧 드러나고, 그 한계가 다음 절의 동기가 된다.
6.4.5.5 step-back — 원리로 물러서기
step-back 프롬프팅은 반대 방향이다. 너무 구체적인 질문은 정확히 맞는 문서가 아예 없을 수 있다("2019년형 모델 X의 3번 실린더 점화 순서는?"). 이때 한 걸음 물러난 상위 질문("모델 X 엔진의 점화 순서 규칙은?")을 먼저 검색해 원리·배경 문서를 확보하면, LLM이 그 원리로부터 구체 답을 끌어낼 수 있다. 원리·개념 문서가 풍부한 도메인 — 과학·공학·법 — 에서 유용하고, 사실 하나하나가 흩어져 있는 도메인에서는 별 이득이 없다.
쿼리 변환은 통틀어 재현율을 겨냥하는 무기고다 — 검색이 관련 문서를 애초에 못 꺼낼 때 쓴다. 다만 모두 LLM 호출을 더하므로 "다 켜기"는 나쁜 전략이다. 실패 유형에 맞춰 고른다. 대화형이면 재작성(맥락 해소)을 가장 먼저, 복합 질문이 많으면 분해를, 어휘 격차가 크면 확장·HyDE를. 무엇이 실제로 이득인지는 결국 평가로만 안다(6.4.10).
6.4.6 메타데이터 필터 — 벡터 유사도만으로 부족할 때
벡터 유사도는 "의미가 비슷한가"만 본다. 그러나 실무 질의에는 하드 제약이 따른다 — "작년 보고서에서", "내 팀 문서만", "공개 문서 중에서". 의미가 아무리 비슷해도 범위·권한·최신성이 틀리면 답에 넣으면 안 된다. 폐기된 정책, 다른 부서의 기밀, 3년 전 가격표는 "관련 있어 보이지만" 정답이 아니라 오답의 원천이다. 그래서 각 청크에 메타데이터(날짜·출처·부서·권한·문서유형·언어)를 붙여 두고, 검색을 그 조건으로 필터링한다.
results = index.search(
query_vector=embed(query),
filter={"dept": "eng", "date": {"$gte": "2025-01-01"}, "access": "public"},
top_k=100,
)
두 가지가 물린다. 첫째는 필터를 ANN 앞에 거나 뒤에 거냐다. 앞에 걸면(pre-filter) 후보군 자체가 줄어 정확하지만, HNSW 같은 벡터 색인 구조와 맞물려 구현이 까다롭고 필터가 강하면 후보가 부족해질 수 있다 — 조건에 맞는 벡터가 색인 그래프에서 드문드문 흩어져 있으면 ANN 탐색이 이웃을 못 찾고도 결과 없이 끝난다. 뒤에 걸면(post-filter) 상위 $N$을 먼저 뽑고 거른 뒤 남는 것이 $k$보다 적어질 위험이 있다 — 상위 100개가 전부 다른 부서 문서면 필터 후에 0개가 남는다. 벡터 DB마다 지원 방식과 성능 특성이 달라, 강한 필터를 자주 쓰는 워크로드면 이 동작을 직접 확인해야 한다.
둘째는 훨씬 부담이 크다. 권한 필터는 검색 품질 문제가 아니라 보안 경계다. 접근 권한 필터가 실패하면 다른 사용자·부서의 문서가 답변에 노출된다 — 이것은 "덜 관련된 결과"가 아니라 데이터 유출이다. 그래서 권한 필터는 반드시 서버의 신뢰 경계 안에서 강제해야 하고, 사용자 입력이 필터 조건을 우회하거나 덮어쓰지 못하게 막아야 한다. RAG 시스템의 검색 결과가 그대로 답에 실린다는 특성상, 검색 필터의 구멍은 곧바로 유출로 이어진다(7.4).
6.4.7 컨텍스트 정제 — 압축·중복 제거·재정렬
검색이 상위 $k$를 꺼냈다고 그대로 다 넣는 것이 최선은 아니다. "많이 넣을수록 좋다"는 흔한 통념은 틀렸다 — 긴 노이즈 컨텍스트는 정답을 묻고(주의 분산, 6.4.13), 토큰 비용과 지연을 키운다(5.6). 꺼낸 것을 다듬는 세 가지 손질이 있다.
압축(compression)은 각 청크에서 질문과 무관한 부분을 잘라낸다. 추출적 압축(질문 관련 문장만 남김)이나 LLM 요약으로 관련 조각의 밀도를 높여 노이즈를 줄인다 — 다만 LLM 압축은 비용을 더하고, 요약 과정에서 정작 필요한 세부를 버릴 위험이 있다. 중복 제거(de-duplication)는 벡터 검색이 거의 같은 청크를 여럿 꺼내기 쉽다는 문제를 겨냥한다. 인접 청크나 중복 문서가 같은 내용으로 컨텍스트를 채우면, 다양성이 사라지고 귀한 컨텍스트 자리를 낭비한다. MMR(Maximal Marginal Relevance)처럼 관련성과 다양성을 함께 최적화하는 선택 규칙을 쓰면, 이미 뽑은 것과 겹치는 후보를 감점해 서로 다른 근거를 고르게 담는다.
재정렬(reordering)은 관련도 높은 청크를 컨텍스트의 양 끝 — 맨 앞과 맨 뒤 — 에 배치하는 전략이다. 모델이 긴 컨텍스트의 중간을 덜 본다는 "lost in the middle" 경향(1.6)을 겨냥한 배치인데, 이 경향 자체가 모델·프롬프트에 의존하고 세대가 바뀌며 완화되고 있어, 재정렬의 이득은 자기 셋에서 확인해야 한다. 세 손질을 관통하는 원칙은 하나다 — 컨텍스트는 넓히는 것이 아니라 정밀하게 좁히는 대상이다.
6.4.8 문맥적 검색 — 고립된 청크에 맥락을 주입
기본 RAG에는 근본 결함이 하나 있다. 청킹이 맥락을 끊는다. 긴 문서를 잘라 청크로 만들면 개별 청크는 자기가 무엇에 대한 것인지를 잃는다. "그 회사의 매출은 3분기에 12% 성장했다"라는 청크에는 회사 이름이 없다 — 이름은 문서 앞부분에 있었고 잘려 나갔다. 이 청크는 "삼성전자 매출 성장"을 물어도 검색되지 않고, 어쩌다 검색돼도 어느 회사 이야기인지 모른 채 컨텍스트에 실린다.
문맥적 검색(contextual retrieval)은 각 청크에 그 청크가 속한 문서의 맥락을 짧게 주입해 이 결함을 고친다. 청크를 색인하기 전에 LLM에 "이 청크가 전체 문서에서 무엇에 관한 것인지 한두 문장으로 설명하는 맥락을 붙여라"고 시켜, 맥락 문장을 청크 앞에 덧댄다.
[생성된 맥락] 이 청크는 삼성전자 2025년 3분기 실적 보고서의 반도체 부문 매출 절이다.
[원본 청크] 그 회사의 매출은 3분기에 12% 성장했다. …
이렇게 맥락이 덧대진 청크를 임베딩·BM25 양쪽으로 색인하면, "삼성전자 3분기 반도체 매출"로 물어도 이 청크가 검색되고, 검색된 뒤에도 무엇에 관한 것인지 명확하다. 밀집과 어휘 색인 모두에 맥락 토큰이 들어가므로 하이브리드(6.4.3)와 잘 맞물린다.
문제는 비용이다. 색인 시점에 청크마다 LLM 호출이 필요해 색인 비용·시간이 크게 는다. 숫자로 보자. 문서 10,000개, 문서당 평균 20청크(총 200,000청크), 문서 하나가 평균 10,000토큰이라 하자. 순진하게 구현하면 각 청크의 맥락을 생성할 때마다 문서 전체를 프롬프트에 다시 넣게 된다 — 청크 20개 × 문서 10,000토큰 = 문서당 200,000 입력토큰, 전체로
$$10{,}000\ \text{문서} \times 20\ \text{청크} \times 10{,}000\ \text{토큰} = 2 \times 10^{9}\ \text{입력토큰}.$$
같은 문서를 20번씩 다시 읽는 셈이라 20억 토큰이 든다. 이것을 구하는 핵심이 프롬프트 캐시(5.6)다 — 문서를 한 번만 캐시에 적재하고, 그 문서의 20개 청크 맥락 생성이 캐시된 문서를 재사용한다. 캐시 쓰기(신규 입력)는 문서당 한 번, 캐시 읽기(재사용)가 나머지를 담당한다. 캐시 읽기 단가가 신규 입력의 대략 1/10이라 가정하면(정확한 요율은 claude-api 확인), 신규 토큰은 $10{,}000 \times 10{,}000 = 10^{8}$, 캐시 읽기 $2\times10^{9}$을 1/10 값으로 환산하면 $2\times10^{8}$, 합쳐 약 $3\times10^{8}$의 "신규 입력 환산" — 캐시 없이 20억을 온전히 치르는 것보다 예닐곱 배 저렴하다. 문맥적 검색과 프롬프트 캐시가 늘 짝으로 소개되는 이유가 이 계산에 있다. 그래도 색인 비용은 남으므로, 정적이고 고가치인 코퍼스에 특히 맞고, 문서가 자주 바뀌면 재색인 비용이 누적된다. 더 큰 청크·부모 문서 확장·메타데이터 같은 값싼 대안과 비교해 선택할 일이다.
6.4.9 Graph RAG · 에이전틱 RAG 개관
지금까지의 기법은 모두 "한 번 검색해서 청크 몇 개를 꺼낸다"는 틀 안에 있다. 이 틀 자체가 안 맞는 질의가 있다.
6.4.9.1 Graph RAG — 관계·다중홉·전역 요약
다중홉(multi-hop) 질의를 보자 — "김대리의 상사의 고향은?"은 김대리의 상사가 누구인지, 그 사람의 고향이 어디인지, 두 사실을 연결해야 답한다. 어느 단일 청크에도 이 연결이 없다. 전역 질의("이 문서 전체의 핵심 주제 다섯은?")는 또 다르다 — 몇 개 청크가 아니라 코퍼스 전반의 구조를 봐야 한다. 벡터 검색은 상위 $k$ 청크를 꺼낼 뿐이라 이런 관계·전역 질의에 구조적으로 약하다.
Graph RAG는 문서에서 엔티티(개체)와 관계를 추출해 지식 그래프를 만들고, 검색을 그래프 위에서 한다. 다중홉은 그래프의 경로 탐색으로 풀고(김대리 → [상사] → 이부장 → [고향] → 대구), 전역 요약은 그래프를 커뮤니티로 묶어 계층적으로 요약해 답한다. 대가가 크다. 그래프 구축에 대량의 LLM 추출 호출(엔티티·관계)이 들고, 문서가 갱신될 때 그래프를 유지·재구성하는 일이 복잡하며, 추출 품질이 곧 답 품질의 상한이 된다 — 관계를 잘못 뽑으면 경로 탐색이 틀린 답을 자신 있게 낸다. 관계 중심·다중홉·전역 이해가 핵심인 도메인(수사·의료 기록·조직 분석)에 값어치가 있고, 단순 사실 조회에는 과하다.
6.4.9.2 에이전틱 RAG — 검색을 도구로 쓰는 루프
지금까지의 RAG는 한 번 검색하고 한 번 생성하는 직선 파이프라인이다. 에이전틱 RAG(agentic RAG)는 이것을 루프로 바꾼다 — LLM이 스스로 판단해 검색을 반복적으로, 조건적으로 부른다.
이 루프가 여는 것들이 앞 절들의 한계를 정확히 메운다. 필요할 때만 검색하고(잡담엔 검색 안 함), 복합 질문을 하위 검색으로 분해하며(6.4.5의 분해가 여기서 자연스럽게 실현된다), 첫 검색이 불충분하면 질문을 바꿔 재검색하고, 벡터 검색·웹·DB·API 중 알맞은 소스를 골라 쓴다. 검색이 하나의 도구가 되고, 에이전트가 그 도구를 언제·어떻게 쓸지 스스로 정한다. 이것이 함수 호출·도구 사용(6.5)과 에이전트 루프(ReAct 등, 6.6)의 응용이다.
대가는 예측 그대로다 — 여러 번의 LLM·검색 왕복으로 지연·비용·비결정성이 크게 늘고, 루프가 헤매거나 종료 못 하고 실행되는 위험이 있으며, 디버깅·평가가 어렵다. 단순 조회는 직선 RAG로 충분하고, 에이전틱은 다단계·탐색적 질의에서 값한다. 이 장은 "직선 RAG의 확장선에 에이전틱 RAG가 있다"는 지도만 세우고, 계획·메모리·반성 루프의 본격 논의는 6.6로 넘긴다.
6.4.10 검색 평가 — recall·precision·MRR·nDCG
여기서 축이 바뀐다. 지금까지의 기법 중 무엇을 넣을지는 추측이 아니라 측정으로 정한다. RAG 평가는 두 층이다 — 검색이 좋은 문서를 꺼냈는지(검색 평가, 이 절)와 최종 답이 좋은지(종단 평가, 6.4.12). 검색이 이미 실패했는데 종단만 보면 어디를 고칠지 못 가른다. 그래서 검색부터 잰다. 검색 평가는 정보 검색(IR)의 고전 지표를 쓰고, 전제로 질의마다 어떤 문서가 관련(relevant)인지 라벨한 정답 집합 — 검색용 골든셋 — 이 있어야 한다(4.2의 골든셋과 같은 정신이다).
6.4.10.1 Precision@k · Recall@k
상위 $k$개를 꺼냈을 때 둘을 이렇게 정의한다.
$$\text{Precision@}k = \frac{|\{\text{관련 문서}\} \cap \{\text{상위 }k\}|}{k}, \qquad \text{Recall@}k = \frac{|\{\text{관련 문서}\} \cap \{\text{상위 }k\}|}{|\{\text{관련 문서 전체}\}|}$$
Precision@k는 꺼낸 $k$개 중 몇 개가 관련인가 — 순도다(컨텍스트에 노이즈가 적은가). Recall@k는 관련 문서 전체 중 몇 개를 $k$ 안에 포함했나 — 빠짐 없음이다. RAG에서 둘의 역할이 나뉜다는 점이 중요하다. 1단계 검색은 recall이 생명이고(후보에 없으면 리랭커가 못 살린다, 6.4.2), 최종 상위 $k$는 precision이 중요하다(노이즈가 답을 흐린다). 그래서 흔히 1단계는 큰 $N$으로 recall@N을, 리랭킹 후는 작은 $k$로 precision@k를 나눠 잰다 — 같은 시스템의 두 지점을 다른 지표로 감시하는 것이다.
6.4.10.2 MRR — 첫 정답이 얼마나 위에 있나
정답이 하나이거나 사용자가 위에서부터 읽는 질의에는 MRR(Mean Reciprocal Rank, 평균 역순위)을 쓴다. 질의마다 첫 번째 관련 문서의 등수의 역수를 평균한다.
$$\text{MRR} = \frac{1}{|Q|}\sum_{q=1}^{|Q|} \frac{1}{\text{rank}_q}, \qquad \text{rank}_q = q\text{의 첫 관련 문서 등수}$$
첫 정답이 1등이면 $1/1=1$, 3등이면 $1/3$이다. 세 질의의 첫 정답 등수가 각각 1, 3, 2였다면
$$\text{MRR} = \frac{1}{3}\left(\frac{1}{1} + \frac{1}{3} + \frac{1}{2}\right) = \frac{1}{3}\cdot\frac{11}{6} \approx 0.611.$$
MRR은 첫 정답의 위치만 본다 — 나머지 정답이 어디 있는지, 등급이 얼마나 관련인지는 무시한다. FAQ·사실 조회처럼 "정답 하나만 찾으면 되는" 질의에 맞고, 여러 관련 문서를 종합해야 하는 질의에는 부족하다.
6.4.10.3 nDCG — 등급과 위치를 함께 보는 표준
가장 널리 쓰이는 순위 지표는 nDCG(normalized Discounted Cumulative Gain)다. 두 가지를 함께 반영한다 — 문서마다 관련 등급(graded relevance: 0=무관, 1=약간, 2=관련, 3=매우 관련)과, 위치가 낮을수록 할인(아래에 있는 정답은 덜 유용하다). 먼저 DCG@k(할인 누적 이득)를 이렇게 쌓는다.
$$\text{DCG@}k = \sum_{i=1}^{k} \frac{2^{rel_i}-1}{\log_2(i+1)}$$
분자 $2^{rel_i}-1$은 이득(gain)으로, 등급이 높을수록 지수적으로 커진다 — 등급 3인 정답 하나가 등급 1인 여럿보다 훨씬 값지다는 판단이 담겨 있다. 분모 $\log_2(i+1)$은 위치 할인(discount)으로, 등수 $i$가 낮을수록 나눠서 깎는다(1등은 $\log_2 2=1$이라 안 깎이고, 아래로 갈수록 커진다). DCG는 절대값이라 질의끼리 비교가 어렵다 — 관련 문서가 많은 질의는 무조건 큰 DCG가 나온다. 그래서 이상적 순위(정답을 완벽히 정렬했을 때)의 DCG인 IDCG로 나눠 정규화한다.
$$\text{nDCG@}k = \frac{\text{DCG@}k}{\text{IDCG@}k} \in [0, 1]$$
nDCG=1이면 완벽한 순위, 낮을수록 관련 문서를 아래에 묻었다는 뜻이다. 등급 있는 관련성과 순위 품질을 하나의 수로 보므로 검색·리랭킹 평가의 사실상 표준이다. 한 가지 주의는 분자를 $2^{rel}-1$ 대신 선형 $rel_i$로 쓰는 정의도 흔하다는 것 — 지수형이 상위 등급을 더 강하게 보상한다. 라이브러리마다 기본값이 다를 수 있으니, 팀 안에서 정의를 하나로 고정하지 않으면 nDCG 값끼리 비교가 무의미해진다.
6.4.11 직접 계산 — nDCG@5
작은 예제로 nDCG@5를 직접 끝까지 계산하자. 지수형 이득 $2^{rel}-1$, 등급 0~3을 쓴다. 필요한 로그값은 $\log_2 2=1$, $\log_2 3\approx1.585$, $\log_2 4=2$, $\log_2 5\approx2.322$, $\log_2 6\approx2.585$. 어떤 검색기가 상위 5개를 꺼냈고 각 위치의 관련 등급이 아래와 같다 하자 — 정답을 상위에 배치하지 못한, 그저 그런 순위다.
| 위치 $i$ | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 관련 등급 $rel_i$ | 1 | 0 | 3 | 0 | 2 |
먼저 DCG@5 — 각 위치의 $\frac{2^{rel_i}-1}{\log_2(i+1)}$을 더한다.
| 위치 | $rel$ | 이득 $2^{rel}-1$ | 할인 $\log_2(i+1)$ | 항 |
|---|---|---|---|---|
| 1 | 1 | 1 | 1.000 | 1.000 |
| 2 | 0 | 0 | 1.585 | 0 |
| 3 | 3 | 7 | 2.000 | 3.500 |
| 4 | 0 | 0 | 2.322 | 0 |
| 5 | 2 | 3 | 2.585 | 1.160 |
$$\text{DCG@}5 = 1.000 + 0 + 3.500 + 0 + 1.160 = 5.660$$
다음 IDCG@5 — 같은 등급들을 가장 좋은 순서, 내림차순 $[3, 2, 1, 0, 0]$으로 재배치한 DCG다.
| 위치 | $rel$ | 이득 | 할인 | 항 |
|---|---|---|---|---|
| 1 | 3 | 7 | 1.000 | 7.000 |
| 2 | 2 | 3 | 1.585 | 1.893 |
| 3 | 1 | 1 | 2.000 | 0.500 |
| 4 | 0 | 0 | 2.322 | 0 |
| 5 | 0 | 0 | 2.585 | 0 |
$$\text{IDCG@}5 = 7.000 + 1.893 + 0.500 = 9.393$$
정규화하면
$$\text{nDCG@}5 = \frac{\text{DCG@}5}{\text{IDCG@}5} = \frac{5.660}{9.393} \approx \mathbf{0.603}.$$
이 검색기의 nDCG@5는 약 0.60이다. 무슨 뜻인가 — 등급 3인 정답을 3등에 묻어 위치 할인을 크게 먹었고, 1등에는 등급 1인 약한 문서를 상위에 배치했다. 이상적 순위 대비 60% 수준이다. 이제 리랭킹의 효과를 같은 지표로 보자. cross-encoder 리랭커(6.4.2)가 이 5개를 완벽히 재정렬해 $[3, 2, 1, 0, 0]$으로 만들면 DCG@5 = IDCG@5 = 9.393이 되어
$$\text{nDCG@}5 = \frac{9.393}{9.393} = 1.000.$$
0.60에서 1.00으로. 리랭커는 후보 문서를 바꾼 것이 아니라 순서만 바로잡아 nDCG를 크게 높였다 — 이것이 6.4.2에서 말한 "리랭킹은 precision과 순위 품질을 올린다"의 정량적 모습이다. 현실의 리랭커가 늘 완벽하진 않으니 이것은 상한을 보인 것이고, 실제 이득은 자기 셋에서 리랭킹 전후로 nDCG를 재야 나온다.
import math
def dcg_at_k(rels, k):
return sum((2**rel - 1) / math.log2(i + 2) for i, rel in enumerate(rels[:k]))
def ndcg_at_k(rels, k):
ideal = sorted(rels, reverse=True)
idcg = dcg_at_k(ideal, k)
return dcg_at_k(rels, k) / idcg if idcg > 0 else 0.0
# ndcg_at_k([1, 0, 3, 0, 2], 5) → 0.603
# ndcg_at_k([3, 2, 1, 0, 0], 5) → 1.000
6.4.12 종단 평가 — 충실성·답변 적합성·문맥 적합성
검색 지표가 좋아도 최종 답이 나쁠 수 있다 — 좋은 문서를 꺼냈는데 LLM이 무시하거나 왜곡하는 경우다(6.4.13). 그래서 종단(end-to-end) 평가가 필요하다. 대부분 정답 키가 없는 개방형이라 LLM-as-judge(4.2)로 잰다. RAGAS류 프레임워크가 정형화한 세 지표가 핵심인데, 질문·컨텍스트·답변이라는 세 꼭짓점 사이의 세 변을 각각 재는 구조 — RAG 삼각형 — 로 기억하면 편하다.
6.4.12.1 충실성 — 답이 컨텍스트에 근거하나
충실성(faithfulness·groundedness)은 답변이 검색된 컨텍스트로 뒷받침되는가를 잰다 — RAG의 존재 이유에 가장 직결된 지표다. RAG가 환각을 없앤다고 흔히 말하지만 이것은 통념이다(6.4.13). RAG는 환각을 줄일 뿐이고, 모델은 컨텍스트에 없는 내용을 여전히 지어낼 수 있다. 충실성은 바로 그 지어냄을 검출한다. 측정은 이렇게 한다 — 답변을 개별 주장(claim)들로 분해하고, 각 주장이 컨텍스트에서 추론 가능한지 판정자로 검사해, 비율을 낸다.
$$\text{faithfulness} = \frac{\text{컨텍스트로 뒷받침되는 주장 수}}{\text{답변의 전체 주장 수}}$$
충실성 1.0이면 답의 모든 주장이 근거를 갖는다. 낮으면 환각 또는 컨텍스트 이탈이다. 4.2에서 다룬 "환각·충실성 축"을 RAG에 특화한 형태다.
6.4.12.2 답변 적합성 — 질문에 답했나
충실성이 높아도 질문을 빗나갈 수 있다 — 컨텍스트에는 충실하지만 잘못된 부분을 답하거나, 얼버무리거나, 장황하게 겉도는 경우다. 답변 적합성(answer relevance)은 답이 질문을 직접·완결하게 겨냥하는가를 잰다. 한 측정 방식은 답변으로부터 "이 답이 답하고 있는 질문"을 여러 개 역생성하고, 그것들이 원 질문과 얼마나 유사한지(임베딩 코사인)를 평균하는 것이다. 답이 질문에 집중돼 있으면 역생성된 질문들이 원 질문과 가깝고, 겉돌면 멀다 — 동어반복이나 회피를 벌하도록 설계된다.
6.4.12.3 문맥 적합성 — 꺼낸 것이 쓸모 있었나
문맥 적합성(context relevance)은 검색된 컨텍스트가 질문에 실제로 관련 있는가를 잰다 — 검색 품질을 종단 관점에서 다시 보는 것이다. 두 갈래다. 문맥 정밀도(context precision)는 꺼낸 청크 중 관련 있는 비율로, 낮으면 컨텍스트에 쓰레기가 많아 정답이 묻힌다. 문맥 재현율(context recall)은 정답에 필요한 정보가 컨텍스트에 다 들어왔는가로, 정답 라벨이 있어야 잴 수 있다. 이 지표들은 검색 지표(6.4.10)와 종단 지표를 잇는 다리다 — 문맥 재현율이 낮으면, 답이 나쁜 원인이 생성이 아니라 검색임을 가리킨다.
6.4.12.4 세 지표로 실패를 진단하기
세 지표를 따로 보면 숫자에 불과하지만, 조합해서 보면 어디가 고장 났는지를 알려 준다 — 이것이 종단 평가의 실제 값어치다.
| 충실성 | 답변 적합성 | 문맥 적합성 | 진단 | 처방 |
|---|---|---|---|---|
| 낮음 | 높음 | 높음 | 좋은 컨텍스트를 무시·왜곡(환각) | 생성 프롬프트·모델·"근거만 쓰라" 지시 |
| 높음 | 낮음 | 높음 | 근거는 충실하나 질문을 빗나감 | 프롬프트·질문 이해·재작성(6.4.5) |
| 높음 | 높음 | 낮음 | 답은 좋은데 검색이 노이즈·과다 | 리랭킹·필터·정제(6.4.2·6.4.7) |
| 낮음 | 낮음 | 낮음 | 검색이 관련 문서를 못 꺼냄(근본) | 하이브리드·쿼리변환·recall 개선 |
세 지표는 모두 LLM 판정자로 재는 대리지표라, 4.2의 함정을 그대로 상속한다. 판정자 자신이 틀리거나 편향되면(장황함 선호·자기선호·위치 편향) 지표가 오도한다. 그래서 판정자를 자기 골든셋에서 인간 일치도로 보정하고(4.2), 결정적으로 검출되는 것(형식·정확 일치)은 코드로 재고, 중요한 결정은 인간 표본으로 확인해야 한다. RAGAS류 자동 지표는 절대값을 과신하지 말고 추세·상대 비교·회귀 탐지로 쓰는 것이 정직하다 — "faithfulness 0.87" 같은 숫자를 진리로 옮기는 순간 평가가 오히려 눈을 가린다.
6.4.13 실패 모드 — 검색 실패·무시·충돌·주의 분산
기법과 평가를 갖췄으니 RAG가 어떻게 망가지는지를 유형으로 정리하자. 진단할 이름이 있어야 고칠 수 있다. 네 가지가 대부분을 덮는다.
검색 실패(retrieval miss). 관련 문서가 애초에 컨텍스트에 안 들어온 경우 — RAG 실패의 가장 흔하고 근본적인 원인이다. 어휘 격차(→하이브리드), 질문 표현 어긋남(→쿼리 변환), 청크 고립(→문맥적 검색), 순위 부정확(→리랭킹), 잘못된 필터가 원인이 된다. 종단 세 지표가 모두 낮고 특히 문맥 재현율이 매우 낮으면 이것이다. RAG 상한이 검색이므로, 여기서 실패하면 나머지가 다 무의미하다.
컨텍스트 무시(context ignoring). 관련 문서가 컨텍스트에 있는데도 모델이 무시하고 자기 파라미터 지식이나 환각으로 답하는 경우다. 증상은 충실성이 낮은데 문맥 적합성은 높은 조합이다. 프롬프트가 "근거를 쓰라"고 강제하지 않았거나, 컨텍스트가 너무 길어 정답이 묻혔거나(→주의 분산), 모델이 자기 사전 믿음을 고집할 때(prior가 셀 때) 일어난다. 처방은 명시적 근거 지시("제공된 문맥에만 근거하고, 없으면 모른다고 하라"), 인용 요구, 그리고 충실성 평가로 상시 감시하는 것이다.
정보 충돌(conflicting context). 검색된 청크들이 서로 모순되거나(옛 정책 대 새 정책), 컨텍스트가 모델의 파라미터 지식과 충돌하는 경우다. 모델이 어느 쪽을 믿을지 불안정해지고, 종종 틀린 쪽을 택하거나 둘을 뒤섞는다. 처방은 최신성 메타데이터로 필터·가중하고(6.4.6), 출처와 시점을 컨텍스트에 명시해 모델이 판단하게 하며, 충돌 자체를 답에 드러내게("문서에 따라 상충함") 하는 것이다.
주의 분산(lost in the middle). 컨텍스트가 길면 모델이 중간에 묻힌 정보를 덜 본다는 경향이다 — 맨 앞·맨 뒤는 잘 쓰지만 가운데를 놓친다(1.6). "많이 넣을수록 좋다"는 통념이 여기서 깨진다. 관련 없는 청크를 잔뜩 넣으면 정답이 노이즈에 묻히고 주의가 흩어진다. 처방은 적게·정밀하게(리랭킹으로 상위 소수만), 컨텍스트 정제·재정렬(관련 청크를 양 끝에, 6.4.7), 중복 제거다. 다만 이 경향은 연구에서 관찰된 것이고 모델·프롬프트에 의존하며 세대가 바뀌며 개선되고 있어, 자기 셋에서 확인할 일이다.
이 네 실패가 6.4.1의 지도와 맞물린다 — 진단(어느 실패인가)이 처방(어느 기법인가)을 정한다. 하나 더 잊지 말 것은 보안 실패다. 검색된 문서가 신뢰 불가한 출처(웹·사용자 업로드)라면, 문서 안의 텍스트가 간접 프롬프트 인젝션으로 모델을 조종할 수 있다 — "이전 지시를 무시하고…"가 검색 문서 안에 심겨 있는 경우다. 이것은 RAG 특유의 공격면이고 7.4에서 다룬다.
6.4.14 무엇을 언제 넣나
이 장의 기법들은 강력하지만 무엇도 무조건 좋지 않다. 실무에서 반복해 부딪히는 오해 몇 개를 정면으로 깨 두는 것이 결론을 대신한다.
RAG가 환각을 없앤다는 말은 틀렸다 — 줄일 뿐이다. 검색이 실패하면 모델은 여전히 지어내고, 컨텍스트가 있어도 무시할 수 있다. 충실성 평가로 감시하는 것이지 없애는 것이 아니다. 컨텍스트를 많이 넣을수록 좋다는 말도 틀렸다 — 노이즈·중복은 정답을 묻고 비용·지연을 키우며, 적게·정밀하게가 자주 이긴다. 롱컨텍스트 모델이 RAG를 대체한다는 말은 성급하다 — 컨텍스트가 길어져도 유효 활용은 별개이고(1.6), 전 코퍼스를 매번 넣는 것은 비용·지연·최신성에서 불리하다. 둘은 경쟁이 아니라 상보일 때가 많다(넓게 검색해 롱컨텍스트에 담기). 하이브리드·리랭킹·HyDE가 항상 낫다는 말은 도메인 의존이다 — 벤더·논문 벤치마크의 이득은 그 질의 분포에서의 것이고, 내 데이터에서 재기 전엔 가정할 수 없다.
무엇이 원리적으로 견고하고 무엇이 조건부인지를 가르는 것도 정직함의 일부다. 2단계 검색의 역할 분담(recall→precision), RRF가 스케일 무관하게 순위를 융합한다는 것, BM25가 정확 토큰·희귀어에 강한 이유(IDF), nDCG가 등급과 위치를 함께 본다는 것, "검색이 RAG의 상한"이라는 인과 — 이것들은 원리적으로 견고하다. 반면 HyDE·step-back·문맥적 검색·Graph RAG의 이득 폭, "lost in the middle"의 크기, RAGAS 자동 지표와 인간 판단의 일치도, 특정 리랭커의 개선치 — 이것들은 모두 도메인·데이터·시점에 의존하며 자기 셋에서 재검증해야 한다.
그래서 도입 순서는 "다 넣기"가 아니라 "측정하고 새는 곳부터"다. 기본 RAG를 막 넘어섰다면 평가 스위트부터 세운다 — 못 재면 못 고치고, 그 위에서 리랭킹은 투자 대비 효과가 큰 편이라 먼저 시도할 만하다. 고유명사·코드·ID 질의가 많으면 하이브리드를 밀집·어휘 단독과 비교해 본다. 대화형·모호·복합 질문이 많으면 쿼리 변환(재작성·분해)으로 검색이 질문 표현에 갇히는 문제를 푼다. 청크가 맥락을 잃는 전문 코퍼스면 문맥적 검색을 색인 비용을 감수하고 넣는다. 관계·다중홉·전역 이해가 핵심이면 단일 검색 틀을 벗어나 Graph RAG나 에이전틱으로 간다. 답이 길고 노이즈·비용이 문제면 정제·필터·재정렬로 좁힌다. 관통하는 규율은 하나다 — 먼저 평가로 어디서 손실되는지 재고(6.4.10·6.4.12), 그 실패를 겨냥한 기법을 하나씩 넣고, 다시 재서 이득을 확인한다. 감으로 다 켜면 지연·비용·복잡도만 늘고 어느 것이 도왔는지 영영 못 가른다.
이 장에서 검색을 하나의 도구로 보는 관점이 여러 번 고개를 들었다 — 에이전틱 RAG가 검색을 조건적으로 부르고, 분해가 하위 검색을 연결하고, 재검색이 루프를 반복한다. 다음 장(6.5)은 그 "도구"를 정식화한다 — 툴 스키마 정의, 모델이 도구를 부르는 법, 결과를 다시 대화에 통합하는 구조. 그 위에서 에이전트(6.6)가 검색·도구 호출을 루프로 결합해 에이전틱 RAG를 완성한다. RAG의 온라인 품질 모니터링과 회귀 탐지는 7.6이, 검색 문서를 통한 공격과 방어는 7.4이 이어 받는다.