Reranking 개념부터 구현까지 — CrossEncoder로 RAG 검색 정확도 높이기

Reranking에 대해 다루고 있습니다. 검색 결과의 순위를 재조정하여 사용자의 쿼리 의도에 더 정확히 부합하는 문서를 상위로 올리는 것이 목적이며 RAG의 정확도를 올리기위해서 사용됩니다.

.

본 포스트는 클로드의 도움을 받아 작성되었습니다.

.

2026. 2. 24 최초작성

.

.

Reranking & CrossEncoder
Reranking이란?
Reranking이 필요한 이유
실제 문장으로 비교하는 예시
CrossEncoder의 학습 방법
Fine-tuning 없이 범용 모델을 사용할 수 있는 이유
점수 캘리브레이션 문제
RAG 파이프라인에서의 위치
sentence-transformers로 CrossEncoder 사용하기
LLM 기반 Reranking
입력 길이 제한
다국어 이슈

.

.

Reranking 개념부터 구현까지-CrossEncoder로 RAG 검색 정확도 높이기

Reranking이란?

정보 검색(Information Retrieval)에서는 보통 2단계 파이프라인을 사용합니다.

.

파이프라인(Pipeline)이란 데이터가 여러 단계를 순서대로 통과하며 처리되는 구조입니다. 공장의 조립 라인처럼 각 단계가 자기 역할을 하고 다음 단계로 결과를 넘깁니다. 예를 들어, 택배 배송 과정에서 “상품 주문 → 창고에서 포장 → 배송 트럭 적재 → 물류센터 분류 → 고객 배달”처럼 각 단계를 거쳐야 최종적으로 물건이 고객 손에 도착하는 것과 같습니다.

.

검색 엔진에 질문을 입력하면, 수백만 건의 문서 중에서 가장 관련 있는 결과를 찾아야 합니다. 이때 모든 문서를 하나하나 정밀하게 비교하면 너무 오래 걸리기 때문에, 검색 과정을 두 단계의 파이프라인으로 나눕니다. 먼저 1단계에서 빠르고 가볍게 후보를 추려내고, 2단계에서 추려낸 후보만 정밀하게 비교해 최종 결과를 선정합니다. 마치 서류 심사로 지원자를 먼저 걸러낸 뒤, 남은 소수만 면접을 보는 것과 같습니다.

.

1단계 — Retrieval (후보 추출)

전체 문서에서 질문과 관련 있을 만한 후보를 빠르게 골라냅니다. 예를 들어 “파이썬 리스트 정렬”이라고 검색하면, “파이썬”, “리스트”, “정렬”이라는 단어가 포함된 문서를 빠르게 찾아서 수백만 건을 수백 건 정도로 줄입니다. 이 단계에서는 정확도보다 속도가 중요하기 때문에, 비교적 단순한 방식(키워드 매칭, 벡터 유사도 검색 등)을 사용합니다.

이 단계에서 자주 사용되는 방식 중 하나가 Bi-Encoder입니다. Bi-Encoder는 질문과 문서를 각각 독립적으로 하나의 벡터(숫자 배열)로 변환한 뒤, 두 벡터가 얼마나 가까운지를 비교합니다.

.

예를 들어 보겠습니다. 우선 각각의 문장을 벡터로 변환합니다. 

질문: “파이썬 리스트 정렬”  →  Encoder  →  [0.8, 0.1, 0.5, …]

문서A: “파이썬에서 sort()로 리스트를 정렬하는 방법”  →  Encoder  →  [0.7, 0.2, 0.5, …]

문서B: “자바스크립트 DOM 조작 가이드”  →  Encoder  →  [0.1, 0.9, 0.2, …]

.

질문 벡터와 문서A 벡터는 숫자가 비슷하므로 “가깝다(유사하다)”고 판단하고, 문서B 벡터는 숫자 차이가 많이 나기 때문에 관련 없다고 판단합니다. 이 판단을 위해 코사인 유사도를 사용합니다. 

핵심은 질문과 문서를 따로따로 인코딩한다는 점입니다. 문서 벡터는 미리 계산해서 저장해 둘 수 있기 때문에, 검색 시점에는 질문만 인코딩하고 저장된 벡터와 비교하면 됩니다. 덕분에 수백만 건의 문서도 빠르게 검색할 수 있습니다.

.

이 단계에서 사용하는 코사인 유사도에 대해 다음 포스트에서 자세히 다루고 있습니다.

코사인 유사도의 개념과 원리

https://webnautes.com/%EC%A0%80%EC%9E%A5%EC%86%8C/157

.

2단계 — Reranking (재정렬)

1단계에서 추린 수백 건의 후보를 더 똑똑한 모델로 다시 정렬합니다. 이 모델은 질문과 각 문서를 함께 읽으면서 의미를 깊이 비교한 뒤, 질문과 가장 유사한 문서가 위로 오도록 순위를 재정렬합니다. 이 방식은 훨씬 정확하지만, 그만큼 연산 비용이 큽니다. 1단계에서 후보를 미리 줄여 놓았기 때문에 이 비용을 감당할 수 있습니다.

이 단계에서 주로 사용되는 방식이 CrossEncoder입니다. Bi-Encoder가 질문과 문서를 따로 인코딩했던 것과 달리, CrossEncoder는 질문과 문서를 하나의 입력으로 합쳐서 모델에 넣고 관련성 점수를 직접 출력합니다.

예를 들어, 1단계에서 아래 3개의 후보가 추출되었다고 합시다.

입력: [“파이썬 리스트 정렬”, “파이썬에서 sort()로 리스트를 정렬하는 방법”]  →  Cross-Encoder  →  0.95

입력: [“파이썬 리스트 정렬”, “파이썬 리스트 컴프리헨션 사용법”]              →  Cross-Encoder  →  0.42

입력: [“파이썬 리스트 정렬”, “파이썬 설치 가이드”]                          →  Cross-Encoder  →  0.15

.

CrossEncoder는 질문과 문서를 함께 읽기 때문에, 단순히 같은 단어가 들어 있는지가 아니라 의미적으로 얼마나 관련 있는지를 판단합니다. 위 예시에서 “파이썬 리스트 컴프리헨션 사용법”은 “리스트”와 “파이썬”이라는 단어를 포함하지만, 질문의 핵심인 “정렬”과는 관련이 적으므로 0.42로 낮게 평가됩니다. 반면 “파이썬에서 sort()로 리스트를 정렬하는 방법”은 질문의 의미와 정확히 일치하므로 0.95로 높게 평가됩니다. 이처럼 의미를 정밀하게 파악할 수 있지만, 매번 질문과 문서를 함께 모델에 넣어야 하므로 수백만 건에 적용하기엔 너무 느립니다.

.

.

Reranking이 필요한 이유

1단계 Bi-Encoder만으로는 충분하지 않은 이유가 있습니다. Bi-Encoder는 쿼리와 문서를 각각 독립적으로 벡터로 변환하기 때문에, 둘 사이의 세밀한 의미 관계를 파악하지 못합니다. 이로 인해 다음과 같은 문제가 발생합니다.

.

① 키워드는 비슷하지만 의미가 다른 문서가 상위에 올라옴

.

쿼리: “파이썬에서 리스트를 정렬하지 않는 방법”

.

Bi-Encoder 결과:

  1위: “sorted()로 리스트를 정렬하는 방법”  (키워드 유사도 높음)

  2위: “reverse=True로 역정렬하기”

  3위: “정렬 없이 원본 순서 유지하기”  (실제 정답)

.

→ “정렬하지 않는”이라는 부정어를 무시하고, “정렬” 키워드만 보고 순위를 매기는 문제가 발생합니다. 

.

.

② 문맥적 관련성을 판단하지 못함

Bi-Encoder는 쿼리와 문서를 별도로 처리하므로, 단어 수준의 유사성만 평가할 수 있습니다. “한국어를 영어로 번역”과 “영어를 한국어로 번역”처럼 방향이 반대인 경우도 구분하지 못합니다. CrossEncoder는 쿼리와 문서를 함께 읽으면서 이러한 뉴앙스를 파악합니다.

.

.

③ 검색 품질이 최종 답변 품질을 좌우함

RAG 시스템에서 LLM은 전달받은 문서만 보고 답변을 생성합니다. 만약 1단계에서 관련 없는 문서가 상위에 올라오면, LLM은 그 틀린 정보를 기반으로 답변하게 됩니다. 이는 할루시네이션(거짓 답변)이나 부정확한 답변의 원인이 됩니다.

.

▶ Reranking 없이 (1단계만 사용)

  쿼리: “파이썬 3.12의 새로운 기능”

  LLM에 전달된 문서: [파이썬 설치법, 파이썬 2.x 문법, 파이썬 vs 자바 비교]

  → 정답과 관련 없는 문서들이 포함되어 있어 부정확한 답변이 생성됩니다.

.

▶ Reranking 적용 후 (2단계 추가)

  LLM에 전달된 문서: [파이썬 3.12 릴리즈 노트, f-string 개선 사항, …]

  → 정확한 문서 기반으로 신뢰성 높은 답변이 생성됩니다.

.

Reranking은 속도(빠른 추출)와 정확도(정밀한 판단) 사이의 트레이드오프를 해결하는 핵심 전략입니다. 1단계에서 빠르게 후보를 모은 뒤, 2단계에서 정밀하게 걸러냄으로써 두 가지 장점을 모두 취할 수 있습니다.

.

.

실제 문장으로 비교하는 예시

아래 예시를 통해 같은 쿼리와 문서들에 대해 Bi-Encoder와 CrossEncoder가 어떻게 다른 결과를 내는지 살펴보겠습니다.

이해를 돕기위해 가상의 점수를 사용했습니다. 사용하는 모델에 따라 점수가 달라집니다. 

.

예시 1: 부정어가 포함된 쿼리

.

쿼리: “파이썬에서 예외 처리를 하지 않아도 되는 경우”

.

문서 A: “try-except문으로 예외를 처리하는 방법을 알아보겠습니다.”

문서 B: “LBYL 패턴으로 예외 없이 안전하게 코드를 작성하는 방법”

문서 C: “파이썬의 에러 유형과 디버깅 가이드”

.

문서 A문서 B문서 C판단 근거
Bi-Encoder0.91 (1위)0.72 (2위)0.68 (3위)“예외”, “처리” 등 핵심 토큰이 쿼리와 겹쳐 A가 1위. 부정어(“하지 않아도”)의 의미는 반영하지 못함
Cross-Encoder0.25 (3위)0.93 (1위)0.41 (2위)쿼리와 문서를 쌍으로 함께 읽어 의미를 파악. A는 예외 처리를 “하는” 방법이라 쿼리 의도와 반대, B는 “예외 없이 안전하게”가 쿼리의 “하지 않아도 되는”과 의미적으로 일치

.

→ 핵심: Bi-Encoder는 쿼리와 문서를 각각 독립적으로 임베딩하므로 “예외”, “처리” 등 겹치는 토큰만으로 문서 A에 높은 점수를 주고, 부정어(“하지 않아도”)의 의미를 반영하지 못합니다. 반면 Cross-Encoder는 쿼리와 문서를 하나의 쌍으로 함께 읽기 때문에, 문서 A는 쿼리 의도와 반대임을 파악하고, 문서 B의 “예외 없이 안전하게”가 쿼리의 “예외 처리를 하지 않아도 되는”과 의미적으로 일치한다고 정확히 판단합니다.

.

.

예시 2: 비슷한 키워드지만 의미가 다른 경우

쿼리: “파이썬으로 웹 서버를 만드는 방법”

.

문서 A: “Flask와 Django로 웹 애플리케이션을 개발하는 튜토리얼”

문서 B: “파이썬으로 웹 페이지를 크롤링(수집)하는 방법”

문서 C: “파이썬 HTTP 라이브러리(requests) 사용법”

.

문서 A문서 B문서 C판단 근거
Bi-Encoder0.85 (2위)0.89 (1위)0.78 (3위)B는 “파이썬”, “웹” 토큰이 모두 쿼리와 겹쳐 1위. A는 “웹”은 겹치지만 “파이썬”이 없어 2위. “서버를 만드는” vs “크롤링”의 의미 차이는 반영하지 못함
Cross-Encoder0.95 (1위)0.31 (3위)0.52 (2위)쿼리와 문서를 쌍으로 함께 읽어 의미를 파악. A의 “웹 애플리케이션을 개발”이 쿼리의 “웹 서버를 만드는”과 의미적으로 일치, B의 “웹 페이지를 크롤링”은 목적이 완전히 다르다고 판단

.

→ 핵심: Bi-Encoder는 쿼리와 문서를 각각 독립적으로 임베딩하므로 “파이썬”, “웹” 등 겹치는 토큰 수에 따라 B를 1위로 판단하고, “서버를 만드는”과 “크롤링”의 의미 차이를 구분하지 못합니다. 반면 Cross-Encoder는 쿼리와 문서를 하나의 쌍으로 함께 읽기 때문에, A의 “웹 애플리케이션을 개발”이 쿼리 의도와 일치하고, B의 “웹 페이지를 크롤링”은 목적이 다르다는 것을 정확히 판단합니다.

.

예시 3: 조건이 붙은 쿼리

쿼리: “파이썬 3.8 이상에서만 되는 문법”

.

문서 A: “파이썬 2에서 3으로 마이그레이션하는 방법”

문서 B: “Walrus operator(:=)는 파이썬 3.8부터 도입된 대입 표현식입니다.”

문서 C: “파이썬의 기본 문법: 변수, 조건문, 반복문”

.

문서 A문서 B문서 C판단 근거
Bi-Encoder0.82 (2위)0.80 (3위)0.86 (1위)“파이썬”, “문법” 등 핵심 토큰이 쿼리와 겹쳐 C가 1위. “3.8 이상에서만”이라는 버전 조건의 의미는 반영하지 못함
Cross-Encoder0.18 (3위)0.96 (1위)0.35 (2위)쿼리와 문서를 쌍으로 함께 읽어 의미를 파악. B의 “3.8부터 도입된”이 쿼리의 “3.8 이상에서만 되는”과 정확히 일치, C는 범용적인 기본 문법 설명이라 버전 조건을 충족하지 못한다고 판단

.

핵심: Bi-Encoder는 쿼리와 문서를 각각 독립적으로 임베딩하므로 “파이썬”, “문법” 등 겹치는 토큰에 따라 범용적인 문서 C를 1위로 판단하고, “3.8 이상에서만”이라는 버전 조건을 반영하지 못합니다. 반면 Cross-Encoder는 쿼리와 문서를 하나의 쌍으로 함께 읽기 때문에, B의 “3.8부터 도입된”이 쿼리의 버전 조건과 정확히 일치하고, C는 특정 버전과 무관한 기본 문법이라는 것을 정확히 판단합니다.

.

예시 4. 방향성 차이

쿼리: “한국어를 영어로 번역”

.

문서 A: “영어를 한국어로 번역하는 방법” → 단어 구성은 같지만 방향이 반대. CrossEncoder는 이 차이를 구분

.

예시 5. 다의어 구분

쿼리: “애플 주가”

.

문서 A: “사과(apple)의 시장 가격 동향” → Bi-Encoder는 헷갈릴 수 있지만, CrossEncoder는 “주가”라는 문맥과 함께 보고 구분

다만 학습 데이터에 없는 도메인이나 비꼬기/반어법 같은 표현은 여전히 틀릴 수 있습니다.

.

CrossEncoder의 학습 방법

CrossEncoder의 학습은 일반적인 딥러닝 분류 모델 학습과 본질적으로 같습니다. 비유하자면, 시험 채점관을 훈련시키는 것과 비슷합니다.

.

Step 1. 학습 데이터 준비

“질문-문서” 쌍에 정답 라벨을 붙여놓은 데이터가 필요합니다. 예를 들어 MS MARCO 데이터셋은 실제 검색 엔진에서 사용자가 클릭한 결과를 기반으로 만들어졌습니다.

.

(“파이썬 설치 방법”, “python.org에서 다운로드하세요”)  → 관련 있음 (1)

(“파이썬 설치 방법”, “자바는 객체지향 언어입니다”)  → 관련 없음 (0)

.

Step 2. 모델에 넣기

각 쌍을 하나로 합쳐서 BERT 같은 Transformer 모델에 넣습니다. 모델 마지막에 분류 헤드(Linear Layer + Sigmoid)가 붙어서 0~1 사이 점수를 출력합니다.

.

Step 3. 틀린 만큼 교정하기 (Backpropagation)

모델이 1이라고 답해야 하는데 0.3이라고 했으면, 오차(Loss)가 발생합니다. 이 오차를 줄이는 방향으로 모델 내부의 파라미터를 조금씩 조정합니다. 수많은 쌍을 반복 학습하면, 관련 있는 쌍은 1에, 관련 없는 쌍은 0에 가까워집니다.

.

Step 4. 결과

모델은 “어떤 패턴일 때 질문과 문서가 관련 있는가”를 스스로 터득하게 됩니다. 부정어, 방향성, 다의어 같은 뉴앙스도 이 과정에서 자연스럽게 배움니다.

.

Fine-tuning 없이 범용 모델을 사용할 수 있는 이유

CrossEncoder가 학습한 것은 “특정 문서의 내용”이 아니라 “관련성 판단 능력 자체”입니다.

국어 시험 채점관이 처음 보는 과학 시험 답안지를 채점할 수 있는 것과 같습니다. 채점관이 배운 것은 과학 지식이 아니라 “질문에 대해 답변이 얼마나 적절한가를 판단하는 능력”입니다.

.

파인튜닝(Fine-tuning)이란 이미 학습된 모델을 특정 목적에 맞게 추가로 학습시키는 것입니다. 영어를 배운 사람이 의학 영어를 추가로 공부하는 것과 비슷합니다.

.

Fine-tuning에서 구체적으로 학습된 언어적 패턴들

  • 질문의 키워드가 문서에 등장하는가
  • 질문의 의도와 문서의 내용이 방향이 맞는가
  • 부정어, 조건절 같은 뉴앙스가 일치하는가
  • “방법”을 묻는 질문에 문서가 실제로 “방법”을 설명하는가

이러한 패턴들은 도메인을 넘어서 보편적으로 통하기 때문에, 처음 보는 데이터에도 적용 가능합니다.

.

추가로 Fine-tuning이 필요한 경우

  • 전문 용어가 많은 도메인 (의료, 법률 등)
  • 도메인 특유의 관련성 기준이 있는 경우 (예: 판례 번호 매칭)
  • 영어로만 학습된 모델에 한국어 데이터를 넣는 경우

.

점수 캘리브레이션 문제

CrossEncoder의 점수는 “시험 점수”가 아니라 “줄 세우기용 번호표”에 가깝습니다. 점수의 절대값이 특정 의미를 갖지 않으며, 모델마다 쿼리마다 점수 분포가 다릅니다.

.

캘리브레이션(Calibration)은 “모델이 내놓는 점수가 실제 확률과 얼마나 일치하는가”를 말합니다. 달리기 시합에서 A가 12초, B가 13초로 들어오면 “A가 빠르다”는 알지만, 12초가 “빠른 것인지 느린 것인지”는 대회마다 다릅니다. CrossEncoder도 순서는 믿을 수 있지만 점수 자체의 의미는 믿기 어렵습니다.

.

우리가 기대하는 것

0.9 = 매우 관련 있음, 0.5 = 반반, 0.1 = 거의 관련 없음

이렇게 점수 자체에 의미가 있으면 좋겠지만, 실제로는 그렇지 않습니다.

.

실제로 일어나는 일

같은 쿼리에 대해 후보 문서 3개의 점수가 이렇게 나왔다고 합시다:

.

쿼리: “파이썬 설치 방법”

.

문서 A: “python.org에서 다운로드하세요”     → 0.92

문서 B: “pip install로 패키지를 설치하세요”  → 0.87

문서 C: “자바 설치는 oracle.com에서 합니다”  → 0.83

.

0.83이면 꽤 높아 보이죠? 하지만 문서 C는 사실 관련이 없습니다. 그런데 점수만 보면 “0.83이니까 꽤 관련 있겠지”라고 오해할 수 있습니다.

.

왜 이런 일이 생기나

CrossEncoder는 학습할 때 “A가 B보다 관련성이 높다”는 상대적 순서를 잘 맞추도록 최적화됩니다. 점수의 절대값이 특정 의미를 갖도록 학습되지는 않습니다. 그래서 모델마다, 쿼리마다 점수 분포가 다릅니다.

.

쿼리 1의 점수 분포: 0.83 ~ 0.95 사이에 몰려 있음

쿼리 2의 점수 분포: 0.10 ~ 0.40 사이에 몰려 있음

.

쿼리 2에서 0.40이 나온 문서가 쿼리 1에서 0.83이 나온 문서보다 실제로는 더 관련 있을 수도 있습니다. 쿼리마다 점수 스케일이 다르기 때문입니다.

.

.

대응 방법

방법핵심 아이디어장점단점
Top-K상위 K개만 선택단순함K를 미리 정해야 함
쿼리별 정규화점수를 0~1로 재조정쿼리 간 비교 공정전부 무관해도 1.0 발생
점수 차이(Gap)점수가 확 떨어지는 지점에서 컷오프쿼리마다 유연하게 적응갩이 고르면 판단 어려움

.

아래 상황을 공통으로 쓰겠습니다:

.

쿼리: “파이썬으로 CSV 읽는 법”

.

문서 A: “pandas의 read_csv()를 사용하세요”          → 0.93

문서 B: “csv 모듈의 reader()로 읽을 수 있습니다”     → 0.88

문서 C: “파이썬으로 JSON 파일을 읽으려면…”          → 0.84

문서 D: “엑셀 파일은 openpyxl로 처리합니다”          → 0.81

문서 E: “자바로 CSV를 파싱하는 방법은…”             → 0.79

.

점수만 보면 0.79~0.93으로 다 비슷비슷하고 높아 보입니다. 고정 임계값 0.7을 쓰면 5개 전부 통과해버립니다. 하지만 실제로 관련 있는 건 A, B 정도입니다.

.

1. Top-K — “상위 K개만 가져오기”

가장 단순합니다. 점수 절대값은 무시하고 순위만 봅니다.

K = 2로 설정

→ 문서 A (1등), 문서 B (2등) 선택

→ 나머지는 점수가 아무리 높아도 버림

장점은 간단하다는 것이고, 단점은 K를 몇으로 할지 미리 정해야 한다는 것입니다. 어떤 쿼리는 관련 문서가 1개뿐인데 K=5면 쓸데없는 문서가 섞이고, 관련 문서가 10개인데 K=3이면 좋은 문서를 놓칩니다.

.

2. 쿼리별 정규화 — “이 쿼리 안에서 상대적 위치로 변환”

해당 쿼리의 점수들을 기준으로 0~1 사이로 재조정합니다.

공식: (점수 – 최솟값) / (최댓값 – 최솟값)

최댓값 = 0.93 (문서 A)

최솟값 = 0.79 (문서 E)

문서 A: (0.93 – 0.79) / (0.93 – 0.79) = 1.00

문서 B: (0.88 – 0.79) / (0.93 – 0.79) = 0.64

문서 C: (0.84 – 0.79) / (0.93 – 0.79) = 0.36

문서 D: (0.81 – 0.79) / (0.93 – 0.79) = 0.14

문서 E: (0.79 – 0.79) / (0.93 – 0.79) = 0.00

.

원래는 0.79~0.93으로 다 비슷해 보였는데, 정규화하니까 차이가 명확해집니다. 여기서 임계값 0.5를 적용하면 A, B만 통과합니다.

다른 쿼리에서 점수가 0.2~0.5 범위로 나와도 같은 방식으로 정규화되니까, 쿼리 간 비교가 공정해집니다.

.

3. 점수 차이(Gap) 기반 — “점수가 확 떨어지는 지점에서 끊기”

순위별 점수 차이를 보고, 갭이 큰 곳에서 컷오프합니다.

A → B: 0.93 – 0.88 = 0.05

B → C: 0.88 – 0.84 = 0.04  ← 여기까지는 비슷비슷

C → D: 0.84 – 0.81 = 0.03

D → E: 0.81 – 0.79 = 0.02

.

이 경우 갭이 고르니까 잘 안 드러나는데, 다른 예시를 보면:

쿼리: “서울 날씨”

.

문서 A: 0.95

문서 B: 0.91

          ← 갭: 0.25 (여기서 확 떨어짐!)

문서 C: 0.66

문서 D: 0.62

문서 E: 0.59

.

B와 C 사이에서 점수가 크게 떨어지므로, **”A, B까지가 진짜 관련 있는 문서”**라고 자동으로 판단할 수 있습니다.

이 방법의 장점은 쿼리마다 관련 문서 개수가 달라도 유연하게 적응한다는 점입니다. 관련 문서가 1개면 1개만, 5개면 5개를 자동으로 잡아냅니다.

이 방법들을 조합해서 쓰는 경우가 많습니다. 예를 들어 “Top-10을 뽑되, 정규화 점수 0.3 이하는 제외” 같은 식입니다.

.

.

RAG 파이프라인에서의 위치

RAG에 대해서는 다음 포스트에서 다루고 있습니다.

.

RAG(Retrieval-Augmented Generation) 개념 및 구현

https://webnautes.com/%EC%A0%80%EC%9E%A5%EC%86%8C/136

.

.

RAG 파이프라인에서 Reranking의 위치는 다음과 같습니다.

.

사용자 질문

  → Embedding (Bi-Encoder)

  → Vector DB 검색 (상위 20개)

  → Reranking (CrossEncoder, 상위 3~5개로 압축)  ← 여기

  → LLM에 컨텍스트로 전달

  → 최종 답변 생성

.

Reranking이 빠지면 LLM에 관련 없는 문서가 섞여 들어가서 답변 품질이 떨어지거나 할루시네이션이 발생할 수 있습니다.

.

RAG(Retrieval-Augmented Generation)는 AI가 답변 전에 관련 문서를 먼저 찾아보고 참고해서 답하는 방식입니다. 시험 볼 때 책을 참고하는 것과 비슷합니다.

.

RAG와 관련된 주요 키워드를 살펴보겠습니다.

.

Vector DB는 텍스트를 벡터로 변환해 저장하고, 비슷한 벡터를 빠르게 찾아주는 DB입니다 (Pinecone, ChromaDB 등).

.

LLM(Large Language Model)은 ChatGPT, Claude 같은 AI 챗봇의 기반 기술입니다.

.

할루시네이션(Hallucination)은 AI가 없는 정보를 마치 사실처럼 자신 있게 말하는 현상입니다.

.

.

Reranking 유무에 따른 RAG 답변 차이

.

사용자 질문: “파이썬에서 비동기 HTTP 요청을 보내는 방법”

.

— Bi-Encoder만 사용 (Reranking 없음) —

Vector DB 결과:

  1위: “requests.get()으로 HTTP GET 요청 보내기”           (동기 방식)

  2위: “urllib을 사용한 HTTP 요청 처리”                    (동기 방식)

  3위: “aiohttp로 비동기 HTTP 클라이언트 만들기”           (정답)

.

→ LLM이 동기 문서(1,2위)를 주로 참고해 “비동기”와 무관한 답변 생성

.

.

— Reranking 적용 후 —

CrossEncoder 결과:

  1위: “aiohttp로 비동기 HTTP 클라이언트 만들기”    (0.94)

  2위: “httpx의 AsyncClient로 비동기 요청 처리”     (0.89)

  3위: “asyncio와 aiohttp 조합 튜토리얼”            (0.85)

.

→ LLM이 정확한 비동기 문서를 받아 신뢰성 높은 답변 생성

.

.

sentence-transformers로 CrossEncoder 사용하기

아래는 ms-marco-MiniLM-L-12-v2 모델을 사용하여 쿼리와 3개 문서의 관련성 점수를 계산하는 예제입니다.

.

다음 패키지 설치가 필요하며 실행하면 모델을 추가로 다운로드하게 됩니다.

pip install sentence_transformers

.

from sentence_transformers import CrossEncoder

model = CrossEncoder(“cross-encoder/ms-marco-MiniLM-L-12-v2”)

query = “파이썬으로 JSON 파일을 읽는 방법”

documents = [
    “json.load()를 사용하면 JSON 파일을 파이썬 딕셔너리로 변환할 수 있습니다.”,
    “CSV 파일은 pandas의 read_csv()로 쉽게 읽을 수 있습니다.”,
    “파이썬에서 XML을 파싱하려면 ElementTree 모듈을 사용합니다.”
]

pairs = [[query, doc] for doc in documents]
scores = model.predict(pairs)

print(query)
print()
for idx in scores.argsort()[::-1]:
    print(f”점수: {scores[idx]:.4f} | {documents[idx]}”)

.

실행 결과

파이썬으로 JSON 파일을 읽는 방법

점수: 7.6904 | json.load()를 사용하면 JSON 파일을 파이썬 딕셔너리로 변환할 수 있습니다.
점수: 6.1720 | 파이썬에서 XML을 파싱하려면 ElementTree 모듈을 사용합니다.
점수: 2.4767 | CSV 파일은 pandas의 read_csv()로 쉽게 읽을 수 있습니다..

→ JSON 관련 문서(7.69)가 1위

→ 같은 “파일 읽기”지만 CSV(2.47)보다 XML(6.17)이 더 관련 있다고 판단

.

.

LLM 기반 Reranking

GPT나 Claude 같은 LLM 자체를 Reranker로 쓰는 최신 트렌드입니다. “다음 문서들 중 질문에 가장 관련 있는 순서로 정렬해줘”라고 프롬프트를 주는 방식으로, 별도 학습 없이도 꽤 좋은 성능을 보여주지만 비용과 속도 문제가 있습니다.

.

프롬프트(Prompt)는 AI에게 주는 지시문/입력문입니다. “이 문서들을 관련성 순으로 정렬해줘”라고 지시하는 방식으로 Reranking을 수행합니다.

.

예제: LLM에게 Reranking을 시키는 프롬프트

프롬프트 

질문: “파이썬에서 멀티쓰레드 프로그래밍하는 방법”

아래 문서들을 질문과의 관련성 순서대로 정렬해주세요.

문서 1: “threading 모듈로 병렬 스레드를 생성하고 관리하는 방법”
문서 2: “multiprocessing으로 병렬 프로세스를 생성하는 방법”
문서 3: “파이썬 GIL(Global Interpreter Lock)의 원리와 한계”
문서 4: “asyncio로 비동기 프로그래밍하는 방법”

.

LLM 응답

관련성 순서: 1, 3, 2, 4
이유:
  문서 1 — 멀티쓰레드에 가장 직접적
  문서 3 — GIL은 멀티쓰레드의 핵심 배경지식
  문서 2 — 멀티프로세스는 쓰레드와 다른 개념
  문서 4 — asyncio는 비동기이지 멀티쓰레드가 아님

.

LLM은 개념 수준의 판단이 뛰어나지만, 문서 50개만 되어도 비용과 시간이 크게 증가합니다. 따라서 소수 후보(5~10개)의 최종 정밀 판단에 사용하는 경우가 많습니다.

.

.

입력 길이 제한

CrossEncoder는 BERT 기반이라 보통 512 토큰까지만 입력 가능합니다. 긴 문서는 단락(chunk) 단위로 쪼개서 각각 점수를 매기고, 그 중 최고 점수를 문서 점수로 쓰는 등의 처리가 필요합니다.

.

청킹(Chunking)은 긴 문서를 작은 조각으로 나누는 것입니다. 300페이지 책을 한 번에 읽을 수 없으니 챕터별로 나눠서 읽는 것과 같습니다. 512토큰은 대략 한국어 200~300자 정도입니다.

.

예제: 긴 문서를 청크로 나눠 Reranking

.

쿼리: “파이썬의 데코레이터 사용법”

문서: 파이썬 공식 문서 (50페이지, 약 15,000토큰)

  → 512토큰 제한으로 한 번에 넣을 수 없음

.

해결: 문서를 30개 청크로 분할 후 각각 점수 계산

.

  청크 1:  “파이썬 기초 문법 소개…”                → 0.12

  청크 2:  “함수와 클래스 정의 방법…”              → 0.34

  청크 8:  “람다 함수와 고차 함수…”                → 0.52

  청크 15: “데코레이터는 @기호로 정의하며…”        → 0.95  ← 최고

  청크 16: “데코레이터 실전 예제: @login_required…” → 0.91

  청크 24: “메타클래스와 디스크립터…”              → 0.28

.

→ 문서 점수 = max(0.95, 0.91, …) = 0.95 (청크 15의 점수 채택)

.

→ LLM에는 청크 15, 16을 컨텍스트로 전달하면 정확한 답변 가능

.

.

다국어 이슈

영어로 학습된 CrossEncoder는 한국어 문서에 성능이 떨어집니다. 다국어 지원이 필요하면 multilingual 모델(multilingual-e5, mMiniLM 등)을 쓰거나, 한국어 데이터로 fine-tuning해야 합니다.

.

예제: 영어 모델 vs 다국어 모델 성능 차이

.

쿼리: “파이썬으로 엑셀 파일을 읽는 방법”

.

문서 A: “openpyxl로 .xlsx 파일을 읽고 쓸 수 있습니다.”

문서 B: “pandas의 read_excel()로 엑셀 데이터를 DataFrame으로 변환”

.

— 영어 전용 모델 (ms-marco-MiniLM) —

  A: 0.45  |  B: 0.52

  → 점수가 전반적으로 낮음. 한국어를 제대로 이해하지 못함

  → “엑셀”과 “.xlsx”의 관계를 파악하지 못할 수 있음

.

— 다국어 모델 (mMiniLM-L12-v2) —

  A: 0.78  |  B: 0.92

  → 한국어 문맥을 이해하고 정확한 관련성 판단

.

댓글 남기기