프로덕션 에이전트를 위한 RAG 리랭킹 (Reranking): 네 가지 접근 방식과 네 가지 실패 모드
요약
프로덕션 에이전트 환경에서 RAG의 성능을 결정짓는 리랭킹(Reranking)의 중요성과 네 가지 접근 방식에 따른 실패 모드를 분석합니다. 단순 벡터 검색의 한계와 에이전트가 잘못된 컨텍스트로 인해 잘못된 도구를 실행하는 '침묵하는 실패' 문제를 다룹니다.
핵심 포인트
- 리랭킹은 에이전트가 정답과 노이즈 중 무엇을 볼지 결정하는 핵심 계층임
- 단순 Bi-encoder 방식은 주제 유사성만 판단하여 쿼리 불일치 문제를 야기함
- 에이전트 워크로드에서는 벤치마크 점수보다 실패 모드 관리가 더 중요함
- 잘못된 리랭킹은 에이전트가 잘못된 도구를 실행하는 침묵하는 실패를 유발함
프로덕션 환경에서 "환각 (hallucinate)"을 일으키는 대부분의 에이전트는 실제로 환각을 일으키는 것이 아닙니다. 올바른 컨텍스트 (context)는 인덱스 (index)에 존재했습니다. 단지 검색 윈도우 (retrieval window)의 상단에 도달하지 못했을 뿐입니다.
리랭킹 (Reranking)은 에이전트가 정답을 볼 것인지 아니면 노이즈 (noise)를 볼 것인지를 결정하는 계층입니다. 그리고 리랭커 (reranker) 유형 사이의 선택은 당신이 다음 분기에 디버깅 (debugging)하며 시간을 보내게 될 실패 모드 (failure mode)를 결정합니다.
저는 팀들이 벡터 DB (vector DB)를 선택하는 방식처럼 리랭커를 선택하는 것을 계속 보게 됩니다. 즉, 공개 데이터셋으로 벤치마크 (benchmark)를 수행하고, 승자를 배포한 뒤, 다음 단계로 넘어가는 방식입니다. 이는 검색 증강 챗봇 (retrieval-augmented chatbots)에는 효과적일 수 있습니다. 하지만 에이전트에게는 작동하지 않습니다. 왜냐하면 실패 모드가 벤치마크가 드러내지 못하는 방식으로 다르기 때문이며, 또한 저희가 HiveIn을 구축하며 뼈저리게 배운 것처럼, 쿼리 (query)의 형태가 하나 이상이 되는 순간 모든 검색 호출에 적합한 단 하나의 리랭커는 존재하지 않기 때문입니다.
침묵하는 실패 (silent failure)의 형태:
- 사용자 → 에이전트: "내 구독을 취소해줘."
- 에이전트 → 검색 (Retrieval): 쿼리 임베딩 (query embedding)
- 검색 → 에이전트: top-5 = [가격 FAQ, 티어 비교, 업그레이드 흐름, …] (올바른 문서는 top-50에는 있었지만 top-5에는 도달하지 못함)
- 에이전트 → 도구 (Tool):
cancel_account(wrong_target_id) - 도구 → 사용자: "완료되었습니다." (잘못된 동작이 실행됨 — 아직 아무도 모름)
올바른 문서는 존재했습니다. 리랭커가 그것을 표면화하지 못했습니다. 에이전트는 어쨌든 행동했습니다. 이것이 바로 이 글에서 다루고자 하는 간극입니다.
네 가지 접근 방식과 각 방식이 무너지는 지점
1. Bi-encoder top-k, 리랭크 없음
단순한 벡터 검색 (vector search)입니다. 쿼리 임베딩 (query embedding)과 문서 임베딩 (document embeddings) 간의 코사인 유사도 (Cosine similarity)를 계산하여 top-k를 추출하고, 이를 모델에 전달합니다.
- P50 지연 시간 (latency): ~30ms
- 비용 (Cost): 쿼리당 거의 제로에 가까움
- 품질 상한선 (Quality ceiling): 낮음
실패 모드 (Failure mode): 주제는 유사하지만 쿼리와 불일치함 (topically similar but query-mismatched). Bi-encoders (양방향 인코더)는 쿼리와 답변의 적합성이 아니라 주제의 중첩(topic overlap)을 기준으로 점수를 매깁니다. "구독을 어떻게 취소하나요?"라는 질문에 대해 가격 FAQ, 요금제 비교 페이지, 업그레이드 절차 페이지를 가져올 수 있는데, 이들은 모두 주제는 관련이 있지만 질문에 대한 답은 아닙니다. 모델은 인접한 (adjacent) 문서들로 가득 찬 컨텍스트 윈도우 (context window)를 전달받게 되며, 그럴듯하게 들리는 답변을 지어내거나(confabulates), 만약 에이전트 (agent)라면 잘못된 대상에 대해 잘못된 도구 (tool)를 자신 있게 실행해 버립니다.
이것이 기본 설정이며, 에이전트 워크로드 (agent workloads)에서는 거의 항상 잘못된 방식입니다. 지연 시간 (latency)은 훌륭하지만, 그 외의 모든 것이 문제입니다.
2. Cross-encoder 리랭커 (Cohere Rerank, BGE-reranker, Voyage rerank-2)
Bi-encoder (양방향 인코더)가 뽑은 상위 50개를 (쿼리, 후보) 쌍을 공동으로 처리하며 양쪽 모두를 주의 깊게 살피는 (attending) Cross-encoder (교차 인코더)가 다시 점수를 매깁니다. 그중 상위 5개가 모델로 전달됩니다.
- P50 지연 시간 (latency): 100–300ms
- 비용 (Cost): 토큰당 비용 발생, 후보 개수 × 후보 길이에 따라 확장됨
- 품질 상한선 (Quality ceiling): 높음
실패 모드 (Failure mode): P99 지연 시간 및 제공자 드리프트 (P99 latency and provider drift). 평균값은 괜찮아 보일 수 있습니다. 하지만 꼬리 부분(tail)이 SLA (서비스 수준 협약)를 깨뜨리는데, 이는 Cross-encoder가 Bi-encoder처럼 쿼리 간에 배치 (batch) 처리를 근본적으로 할 수 없기 때문입니다. 각 쿼리+후보 쌍은 그 자체로 별도의 순전파 (forward pass) 과정을 거칩니다. 호스팅된 리랭커 (hosted rerankers)는 피크 시간대의 제공자 측 큐잉 (queueing) 문제로 인해 이 현상을 더욱 심화시킵니다.
그리고 아무도 말해주지 않는 또 다른 사실은, 제공자가 조용히 새로운 리랭커 버전을 배포할 때 여러분의 오프라인 평가 (offline eval) 스위트가 이를 잡아내지 못한다는 점입니다. Top-1 결과가 변하고, 에이전트의 행동이 변하며, 유일한 신호는 그다음 주 동안 서서히 늘어나는 사용자 불만뿐입니다. Cross-encoder는 여러분이 소유하지 않은 블랙박스 (black box)입니다.
3. Late-interaction 모델 (ColBERT, ColBERTv2, JaColBERT)
사전 계산된 토큰별 임베딩 (embeddings)을 사용하여 검색 시점에 토큰 수준의 유사도를 계산합니다. 품질/지연 시간 곡선상에서 Bi-encoder와 Cross-encoder의 중간에 위치합니다.
- P50 지연 시간 (latency): ~50ms
- 비용 (Cost): 쿼리 시점에는 저렴함. 저장 시점에는 비쌈.
- 품질 상한선 (Quality ceiling): 높음
실패 모드: 대규모 인덱스 저장 (index storage at scale). 토큰당 임베딩 (Per-token embeddings)은 바이-인코더 (bi-encoder) 대비 인덱스 크기를 10~30배 부풀립니다. 코퍼스 (corpus)가 작거나 인프라 예산이 넉넉할 때는 매우 잘 작동합니다. 하지만 문서 수가 1,000만 개(10M+) 정도에 도달하면 운영상 감당하기 어려워집니다. 인덱스가 원래 의도했던 서버에 담기지 않게 되며, 한 단계 높은 사양의 서버로 넘어가면 검색 계층 (retrieval-tier) 비용이 두 배로 뜁니다.
많은 팀이 프로토타이핑 단계에서 코퍼스가 작을 때 ColBERT를 채택했다가, 비용 곡선이 따라붙는 18개월 뒤에 조용히 다른 방식으로 전환합니다. 만약 이러한 궤적을 미리 예측할 수 있다면, 처음부터 건너뛰십시오.
4. LLM-as-reranker (리랭커로서의 LLM)
바이-인코더 (bi-encoder)에서 상위 N개의 후보를 가져와 프롬프트 (prompt)로 구성한 뒤, 작은 LLM에게 쿼리에 맞춰 순위를 매기도록 요청합니다. 때로는 GPT-4o-mini를 사용하고, 때로는 파인튜닝 (fine-tuned)된 1B 모델을 사용하며, 때로는 검색된 컨텍스트 (context)를 사용할 바로 그 모델을 사용하기도 합니다.
- P50 지연 시간 (latency): 500ms–2s
- 비용 (Cost): 토큰 수 × N, 여기에 추론 (inference) 호출 비용 추가
- 품질 상한선 (Quality ceiling): 가장 높음
실패 모드: 확률적 순서 결정 (stochastic ordering) 및 캐시 효율 저하 (cache hostility). 동일한 쿼리, 동일한 후보, 동일한 모델이라 하더라도 LLM은 반복 호출 시 다른 순서를 반환할 수 있습니다. 온도를 낮출 수는 있지만, LLM 리랭커를 선택하게 만든 근본적인 이유인 추론 (reasoning) 능력을 잃지 않으면서 이를 완전히 제거할 수는 없습니다. 또한 프롬프트에 쿼리와 후보가 모두 인코딩되어 있어 캐시 키 (cache keys)가 폭발적으로 증가하므로, 다른 방식보다 캐싱이 더 어렵습니다.
LLM 리랭커는 품질 상한선이 가장 높지만 운영 비용이 가장 많이 드는 옵션입니다. 기본 설정으로 선택하기에는 적절하지 않은 경우가 많습니다. 대신, 저렴한 리랭커들이 불확실해할 때 선택적으로 사용하는 에스컬레이션 (escalation, 단계적 격상) 용도로는 적절한 경우가 많습니다.
결정 매트릭스 (The decision matrix)
| 접근 방식 | P50 지연 시간 | 품질 상한선 | 한계점 |
|---|---|---|---|
| 바이-인코더 전용 (Bi-encoder only) | 30ms | 낮음 | 쿼리 의도 불일치 |
| ... |
오늘날 에이전트 스택 (agent stack)을 위한 합리적인 기본 구성은 다음과 같습니다: 저렴한 리콜 패스 (recall pass)를 위한 바이-인코더 (bi-encoder), 상위 50개에 대한 크로스-인코더 (cross-encoder), 그리고 크로스-인코더의 top-1 점수가 모호한 경우를 위해 예약된 LLM 리랭킹입니다.
"점수(score)"가 실제로 의미하는 것 (그리고 왜 당신을 괴롭히는가)
더 나아가기 전에, 이를 처음 구축하는 거의 모든 팀이 발을 헛디디는 부분이 있습니다. 바로 리랭커(reranker)가 반환하는 숫자는 벡터 검색(vector search)이 반환하는 숫자와 같은 종류가 아니며, 서로 다른 리랭커들이 반환하는 숫자들은 서로 비교할 수 없다는 점입니다.
바이-인코더 (bi-encoder) 점수는 코사인 유사도 (cosine similarity, 또는 정규화된 내적 (normalized dot product))입니다. 이 값은 대략 [-1, 1] 범위 내에 존재하며, 그 크기는 임베딩 모델 (embedding model)과 정규화 방식 (normalization scheme)에 따라 달라집니다. 또한 이는 해당 청크 (chunk)가 쿼리 (query)에 답할 확률이 아니라, _임베딩 공간 (embedding space)에서의 주제적 유사성 (topical similarity)_을 측정하는 값입니다.
크로스-인코더 (cross-encoder) 점수는 어떤 크로스-인코더를 사용하느냐에 따라 전적으로 달라집니다. Cohere는 여러 쿼리에 걸쳐 거의 추론 가능한 수준인 0~1 사이의 보정된 관련성 확률 (calibrated relevance probability)을 반환합니다. BGE-reranker는 절대적인 수치는 의미가 없는 로짓 (logits)을 출력합니다. 즉, 하나의 쿼리 내에서의 순위만이 중요하며, 두 개의 서로 다른 쿼리 간의 점수를 비교하는 것은 아무런 정보도 주지 못합니다. Voyage는 또 다른 방식으로 정규화합니다. ColBERT의 점수는 토큰 쌍 (token pairs) 간의 최대 유사도 (max-similarity)의 합이며, 이는 경계가 없고 쿼리 길이에 따라 확장됩니다. 따라서 4개의 토큰으로 구성된 쿼리의 8.4점은 20개의 토큰으로 구성된 쿼리의 8.4점과는 완전히 다른 의미를 갖습니다. 리랭커로서의 LLM (LLM-as-reranker) 점수는 대개 모델이 이미 선택한 순서를 정당화하기 위해 사후에 붙이는 허구적인 값입니다. 이를 기껏해야 서열적 (ordinal)인 값으로 취급하십시오.
다음은 동일한 개념을 참조용으로 정리한 내용입니다:
| 스코어러 (Scorer) | 범위 (Range) | 숫자의 실제 의미 |
|---|---|---|
| Bi-encoder 코사인 유사도 | [-1.0, 1.0] | 임베딩 공간에서의 주제 유사도 — 관련성에 대한 확률이 아님 |
| ... |
그리고 Reciprocal Rank Fusion (RRF) 또는 Distribution-Based Score Fusion을 통해 이미 밀집 (dense) 및 희소 (sparse) 점수를 결합하고 있는 하이브리드 검색 (hybrid retrieval)이 있습니다. 이 두 방식은 매우 다른 숫자 범위를 생성합니다. HiveIn의 검색 계층에서는 서로 다른 쿼리 형태에 따라 두 모드를 모두 사용하며, 한 모드에서 사용하는 "높은 신뢰도 (high confidence)" 임계값은 다른 모드의 임계값과 한 자릿수(order of magnitude) 이상의 차이가 납니다. 동일한 검색 파이프라인, 동일한 문서, 동일한 "모델이 확신하고 있다"는 개념을 사용함에도 불구하고, 두 숫자는 완전히 다릅니다.
제가 팀들이 빠지는 것을 계속 목격하는 함정은 이것입니다: 리랭커 (reranker)를 교체하면서 기존의 if score > 0.7 임계값을 그대로 가져오고, 이전 스코어링 공간에서의 0.7이 완전히 다른 의미를 가졌기 때문에 조용히 게이트(gate)의 절반을 잃어버리는 것입니다. 혹은 더 나쁜 경우, 기존 검색 파이프라인 위에 리랭킹을 계층화하고, 원시 검색 점수 (raw retrieval score)에 맞춰 보정된 임계값을 리랭킹 후의 점수와 비교하기 시작합니다.
점수의 절대적인 숫자보다 점수의 _분포 (distribution)_가 더 중요합니다. 분포는 (모델, 쿼리 클래스)별로 결정됩니다. 리랭커 간에 비교할 수 없으며, 퓨전 모드(fusion modes) 간에도 비교할 수 없습니다. 점수를 기반으로 구축하는 모든 것은 해당 점수를 생성하는 특정 파이프라인에 맞춰 보정되어야 합니다.
아무도 벤치마크하지 않는 에이전트 특유의 차원
챗봇의 경우, 리랭킹은 품질 대 지연 시간 (quality-vs-latency)의 트레이드오프이며 합리적인 기본값이 대부분 잘 작동합니다. 하지만 에이전트의 경우, 벤치마크가 측정하지 못하는 세 번째 축이 있습니다: 실패가 얼마나 조용하게 일어나는가 (how silent is the failure) 입니다.
잘못된 답변을 받은 챗봇 사용자는 다시 프롬프트를 입력합니다. 그 피해는 잠시 동안의 짜증에 그칩니다.
잘못된 검색(retrieval) 결과를 얻은 에이전트는 잘못된 대상을 향해 확신에 찬 도구 호출(tool call)을 수행합니다. 잘못된 고객에게 이메일을 발송합니다. 잘못된 레코드 ID(record ID)로 API를 호출합니다. 검색된 문서가 설명하고 있다고 생각한 워크플로우(workflow)를 실행하지만, 실제 검색된 문서는 다른 것을 설명하고 있었습니다. 검색 실패는 도구 실행 사고(tool-execution incident)로 이어지며, 누군가 이를 알아차렸을 때는 이미 조치가 완료된 후입니다.
제가 읽은 에이전트 사후 분석(post-mortems) 보고서와 저희가 직접 분석한 트레이스(traces)에서 반복적으로 나타나는 패턴은 대략 다음과 같습니다. Top-1 리랭커(reranker) 점수가 해당 쿼리 클래스(query class)에 대한 코퍼스(corpus)의 역사적 25백분위수(25th percentile) 미만일 때, 다음 도구 호출이 틀릴 확률은 급격히 상승하며, 종종 기본율(baseline rate)의 약 두 배에 달합니다. 리랭커는 이미 알고 있었습니다. 단지 시스템이 그 지식이 다음 결정에 반영되도록 허용하지 않았을 뿐입니다.
HiveIn의 검색 레이어를 구축하며 배운 점
제가 리랭킹(reranking)이 랭킹(ranking)의 문제가 아니라 정책(policy)의 문제라고 확신하는 이유는, 저희가 처음에는 이를 랭킹의 문제로 해결하려 시도했으나 단 하나의 리랭커만으로는 거의 즉시 작동을 멈췄기 때문입니다.
첫 번째 교훈은 어떠한 단일 리랭커도 우리가 수행하는 모든 검색 호출에 적합하지 않다는 것이었습니다. HiveIn의 플래너(planner)는 도구 정의(tool definitions), 이전 워크플로우 결정(prior workflow decisions), 정책 가이드라인(policy guidelines), 메모리 스냅샷(memory snapshots) 등 다양한 형태의 컨텍스트(context)를 위해 메모리를 쿼리(query)합니다. "이 의도에 맞는 올바른 도구를 찾기" 위해 튜닝된 리랭커는 "이 주제에 대한 가장 최근의 결정을 찾기"에는 부적절했고, "이 쿼리와 관련된 가이드라인의 모든 청크(chunk)를 찾기"에도 부적절했습니다. 저희는 하나를 선택하려 노력했고, 그다음에는 지배적인 케이스(dominant case)에 가장 적합한 것을 선택하려 했습니다. 하지만 두 방법 모두 튜닝되지 않은 케이스에서는 결과적으로 좋지 못했습니다.
우리가 최종적으로 도달한 결론은 검색 신뢰도(retrieval confidence)를 용어 커버리지(term coverage), 소스 아티팩트(source artifact) 내의 멀티 청크 존재 여부(multi-chunk presence), 쿼리 분해 범위(query-decomposition breadth), 그리고 최신성(recency)과 결합한 **멀티 시그널 리랭크 (multi-signal rerank)**입니다. 이때 가중치는 _쿼리의 형태(query shape) 자체에 따라 변화_합니다. 짧은 키워드 쿼리와 분해된 다중 문장 쿼리는 서로 다른 혼합 방식을 적용받는데, 이는 각 쿼리 유형에 따라 무엇이 "좋은" 결과인지에 대한 정의가 다르기 때문입니다.
두 번째 교훈 — 그리고 돌이켜봤을 때 가장 먼저 고려했어야 할 점 — 은 리랭크 게이트 (rerank gate)가 단일 숫자가 되어서는 안 된다는 것입니다. "검색 레이어가 리랭킹을 건너뛰기에 충분히 신뢰할 만하다"라고 결정하기 위해 사용하는 임계값(threshold)은 하위에서 실행 중인 퓨전 전략(fusion strategy)에 따라 절대값이 크게 달라지며, 우리는 이를 퓨전 모드별로 보정(calibrate)해야 했습니다. 만약 하나의 임계값을 하드코딩했다면, 모든 설정 변경 시 게이트가 소리 없이 고장 났을 것입니다. 동일한 하드코딩된 매직 넘버(magic number)가 한 모드에서는 "매우 신뢰할 만함"으로 읽히고, 다른 모드에서는 "노이즈를 겨우 벗어난 수준"으로 읽힐 수 있기 때문입니다.
세 번째 교훈은 이를 에이전트(agent)와 구체적으로 연결 짓는 지점입니다: 검색이 이미 충분히 신뢰할 만할 때 리랭킹이 오히려 해가 될 수 있다는 점입니다. 우리는 하위 검색 결과가 확실할수록 리랭커(reranker)의 영향력을 줄이는 신뢰도 인지 테이퍼 (confidence-aware taper)를 추가했습니다. 신뢰도가 완전히 높을 경우, 리랭크 가중치는 0으로 떨어지며 원본 검색 점수(raw retrieval score)가 승리하게 됩니다. 이것이 없었다면, 최신성(recency)이나 일관성(coherence) 시그널이 하위 하이브리드 검색(hybrid retrieval)이 이미 매우 확신하고 있던 청크를, 더 최신이지만 주제에서 약간 벗어난 청크를 위해 가끔씩 순위를 낮춰버렸을 것입니다. 이러한 종류의 소리 없는 순위 하락은 에이전트가 잘못된 컨텍스트를 바탕으로 확신을 가지고 행동하게 만드는 전형적인 실패 모드(failure mode)입니다. 즉, 올바른 문서가 검색되었고, 올바른 문서가 첫 번째로 검색되었음에도 불구하고, 리랭킹이 이를 세 번째 위치로 밀어내 버리는 상황 말입니다.
테이퍼(taper)의 모습은 대략 다음과 같습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기