Phase 4: 검색 품질 및 근거 기반 답변
요약
RAG 시스템의 검색 품질을 높이기 위해 벡터 검색과 리랭커를 결합한 2단계 검색(Two-Stage Retrieval) 전략을 설명합니다. 벡터 검색으로 후보군을 넓게 확보한 뒤, 크로스 인코더 기반의 리랭커로 정확도를 높이고 임계값을 설정해 환각을 방지하는 방법을 다룹니다.
핵심 포인트
- 벡터 검색은 넓은 그물 역할을 하며 근사치 검색에 적합함
- 리랭커(Cross-encoder)를 활용한 2단계 검색으로 검색 정확도 향상
- 리랭킹 전 후보군(Top-K)을 충분히 확보하는 것이 중요함
- 관련성 임계값(Threshold) 설정을 통해 근거 없는 답변 방지
'가장 유사한 것'에서 '실제로 신뢰할 수 있는 답변'으로
이야기의 시작: '어떻게 저에게 자신 있게 거짓말을 할 수 있었나요?'
👦 조카: 삼촌, 이번 주에 저희 RAG 시스템을 테스트해 봤어요. 저희 문서 어디에도 존재하지 않는 휴가 정책에 대해 질문했어요.
👦 조카: 잠깐만요 — 휴가 달력 청크 점수가 0.81이라고요? 이건 완전히 관련이 없잖아요!
👨🦳 삼촌: 이런 일은 초보자들이 예상하는 것보다 더 자주 발생해요. 임베딩 모델(Embedding models)은 의미를 1536개의 숫자로 압축하는데, 공유된 어휘나 형식, 문서 구조 같은 이유로 관련 없는 청크가 우연히 그 공간에서 '가까이' 위치하게 돼요. 심지어 사람이 보면 질문에 답하지 않는다는 것을 즉시 알 수 있음에도 말이죠. 벡터 검색(Vector search)은 넓은 그물이지, 정확한 판사는 아니에요.
해결책: 2단계 검색 (Two-Stage Retrieval)
👨🦳 삼촌: 실제 운영 패턴은 항상 두 단계로 이루어져요. 절대로 한 단계만 쓰지 않아요:
STAGE 1 — 검색 (Retrieve) (저렴하고, 넓고, 근사치)
─────────────────────────────────────────
벡터 검색(Vector search) → 상위 20개 후보군 (Top-20 candidates)
...
👦 조카: 이미 가지고 있던 임베딩 유사도보다 리랭커(reranker)가 더 정확한 이유는 무엇인가요?
👨🦳 삼촌: 여기 핵심적인 아키텍처 차이가 있어요. 그리고 이건 제대로 이해할 가치가 있습니다:
임베딩 모델 (bi-encoder):
질의 (Query) → [벡터 A] ─┐
├─ 비교는 나중에, 별도로 수행
...
👨🦳 삼촌: 크로스 인코더(cross-encoder)는 느려요. 스코어링 시점에 질의가 필요하기 때문에 어떤 것도 미리 계산할 수 없거든요. 이것이 바로 여러분이 수백만 개의 청크에 직접 실행하지 않는 이유예요. 벡터 검색이 이미 좁혀준 작은 상위 20개 후보군에 대해서만 실행하는 거죠. 저렴한 그물로 먼저, 비싼 판사로 나중에요.
코드: Cohere의 Rerank API를 이용한 리랭킹 (Reranking)
npm install cohere-ai
const { CohereClient } = require(
👨🦳 **삼촌:** 만약 진정으로 가장 좋은 청크가 근사 벡터 순위(approximate vector ranking)의 8번째 위치에 있는데 (기억하세요 — 근사!), 여러분이 항상 Top-5만 가져온다면, 리랭커는 그것을 볼 기회조차 얻지 못합니다. 따라서 리랭킹 전에 더 넓게 검색하는 것(Top-20 또는 Top-30)은 정확한 판단자에게 실제로 정답을 찾을 수 있는 충분한 후보군을 제공하며, 심지어 빠르지만 근사적인 첫 번째 통과 과정에서 그것이 불완전하게 순위가 매겨졌더라도 마찬가지입니다.
## 2단계: 관련성 임계값(Relevance Threshold) — '모른다'고 말할 시점을 아는 것
👨🦳 **삼촌:** 이제 여러분의 원래 문제, 즉 가짜 휴가 정책 답변에 대한 해결책이 나왔습니다. 리랭킹만으로는 이것을 해결하지 못합니다. 왜냐하면 리랭킹 후에도 시스템은 실제로 좋은 것이 하나도 없더라도 자신 있게
> **멘탈 모델 (Mental model) — 문 앞의 보안 요원:** 벡터 검색 (Vector search)은 그럴듯해 보이는 20명을 초대합니다. 리랭커 (Reranker)는 신분증을 제대로 확인합니다. 임계값 (Threshold)은 보안 요원의 규칙입니다: "만약 이 줄에 서 있는 사람 중 실제로 자격이 있는 사람이 단 한 명도 없다면, 아무도 들여보내지 마세요. 그냥 문을 닫고 그렇게 말하세요." 이 보안 요원이 없다면, 당신의 시스템은 자격 여부와 상관없이 줄 맨 앞에 있는 사람에게 정중하게 길을 비켜주며 들여보내게 됩니다.
## 3단계: 인용을 포함한 근거 기반 프롬프트 조립 (Grounded Prompt Assembly with Citations)
### 단순한 컨텍스트 덤프 (Context Dump)의 문제점
👨🦳 **삼촌:** 훌륭한 청크 (Chunks)가 있더라도, 그것을 LLM에 _어떻게_ 전달하느냐가 중요합니다. 게으른 프롬프트는 다음과 같습니다:
컨텍스트 (Context):
직원은 30일의 통지 기간을 받습니다.
통지 기간은 제출일로부터 시작됩니다.
...
👦 **조카:** 그게 뭐가 문제예요? 올바른 청크들이 들어있잖아요.
👨🦳 **삼촌:** 기술적으로 틀린 건 없어. 하지만 나중에 LLM의 답변에 의문이 제기될 때 ("30일이라는 근거가 어디에 있나요?"), 그 주장을 특정 출처로 추적할 **방법이 전혀** 없게 되지. 인사 정책, 법률, 의료, 금융처럼 규정 준수 (Compliance)가 민감한 분야에서는 "나를 믿으세요"라는 말만으로는 충분하지 않아. 추적 가능성 (Traceability)이 필요해.
### 해결책: 번호가 매겨진, 속성 부여가 가능한 컨텍스트 (Numbered, Attributable Context)
```javascript
function buildGroundedPrompt(query, chunks) {
const contextBlock = chunks
.map((c, i) => `[${i + 1}] (출처 (Source): ${c.metadata.source_file}, ${c.metadata.department})
${c.chunk_text}`)
...
이 프롬프트를 사용했을 때의 LLM 출력 예시:
"통지 기간은 제출일로부터 시작하여 30일입니다 [1][2].
사직은 확정되기 전에 관리자의 승인도 필요합니다 [3]."
👦 조카: 이제 [1], [2], [3]을 메타데이터 (Metadata)에 있는 실제 source_file과 department로 바로 연결할 수 있겠네요!
👨🦳 삼촌: 정확해. 그리고 바로 이 지점에서 3단계의 메타데이터 설계가 사용자용 제품에서 마침내 결실을 보게 되는 거야. 이 인용구들을 원본 문서로 연결되는 클릭 가능한 링크로 렌더링할 수 있는데, 이것이 "AI가 그렇게 말했습니다"와 "이 정보가 정확히 어디에서 왔는지 여기 있으니 직접 확인해 보세요"의 차이를 만든단다.
코드: 인용(Citations)을 소스로 다시 파싱하기
function attachCitationSources(llmAnswer, chunks) {
const citationPattern = /\[(\d+)\]/g;
const usedIndices = new Set();
...
4단계: 하이브리드 검색 (Hybrid Search) — 벡터 검색이 놓치는 것 포착하기
사각지대: 정확한 용어 (Exact Terms)
👨🦳 삼촌: 솔직한 약점이 하나 더 있단다. 예를 들어 누군가 이렇게 묻는다고 해보자:
"에러 코드 ERR_4521은 무엇을 의미하나요?"
👦 조카: 벡터 검색 (Vector search)이 그걸 잘 처리할 수 있지 않을까요? 그냥 텍스트잖아요.
👨🦳 삼촌: 2단계에서 배운 내용을 바탕으로 머릿속으로 직접 시도해 보렴. 임베딩 모델 (Embedding models)은 **의미 (meaning)**와 **개념 (concepts)**을 이해하도록 훈련되었단다. 예를 들어 "리더십 (leadership)"이 "팀 관리 (team management)"와 연관되는 식이지. 하지만 "ERR_4521"은 개념이 아니라, 정확하고 임의적인 식별자 (identifier)란다. 임베딩 모델은 특정 에러 코드 문자열에 대해 실질적인 의미론적 이해 (semantic understanding)를 가지고 있지 않아. 모델은 이를 단순히 "어떤 종류의 기술적 식별자"로 임베딩할 수도 있고, 특히 해당 청크(chunk)의 주변 언어가 질문과 의미론적으로 유사하지 않다면 ERR_4521을 구체적으로 기록하고 있는 단 하나의 청크를 놓칠 수도 있단다.
👦 조카: 그럼 제품 코드, 주문 번호, 정확한 이름 등에도 똑같은 약점이 적용되겠네요...
👨🦳 삼촌: 정확해. **의미 (meaning)**보다 **정확한 일치 (exact match)**가 더 중요한 모든 것이 그렇단다. 이것이 바로 구식 키워드 검색 (keyword search)이 여전히 승리하는 지점이며, 실제 운영 시스템 (production systems)에서 하나만 선택하지 않고 둘 다 실행하여 결과를 결합하는 이유란다. 그것이 바로 하이브리드 검색 (hybrid search)이지.
아키텍처: 두 검색을 함께 실행하기
사용자 질의 (User Query): "에러 코드 ERR_4521은 무엇을 의미하나요?"
↓
┌─────┴─────┐
...
코드: pgvector와 함께 사용하는 Postgres 전문 검색 (Full-Text Search)
-- 전문 검색 컬럼 추가 (테이블 설정 시 한 번만 수행)
ALTER TABLE document_chunks
ADD COLUMN chunk_tsv tsvector
...
Node.js: 두 결과 집합 결합하기
async function hybridSearch(query, queryEmbedding) {
const [vectorResults, keywordResults] = await Promise.all([
db.query(
...
👦 조카: 그러니까 하이브리드 검색 (Hybrid Search)이 재순위화 (Reranking)를 대체하는 게 아니라, 재순위화 단계에 더 좋고 완전한 후보군을 먼저 제공하는 역할인 거네요?
👨🦳 삼촌: 정확해. 오늘 다룬 네 단계는 네 개의 별개 옵션이 아니라 **하나의 파이프라인 (Pipeline)**을 형성해. 하이브리드 검색이 가장 넓고 똑똑한 그물(의미론적 검색과 정확한 검색 모두)을 던지면, 재순위화가 그 결합된 세트를 정확하게 판단하고, 관련성 임계값 (Relevance Threshold)이 그중 답변에 쓸 만큼 충분히 좋은 것이 있는지 결정하며, 근거 기반 프롬프팅 (Grounded Prompting)이 나오는 답변이 추적 가능한지 확인하는 거지.
전체 쿼리 타임 흐름 (End to End)
사용자 질문 (User Question)
↓
쿼리 임베딩 (Embed query) (Phase 2)
...
인터뷰 수준의 답변
Q1: 왜 실제 사용되는 것보다 더 많은 청크 (Chunks)를 검색하는 RAG 시스템이 프로덕션 환경에 존재하나요?
👨🦳 삼촌: "초기 검색 (ANN 인덱스에 대한 벡터 검색)은 빠르지만 근사치 (Approximate)이기 때문이야. 즉, 실제 최적의 매칭 결과가 최상위 순위에 위치한다는 보장이 없어. 따라서 더 넓은 후보군(예: Top-20)을 검색한 다음, 더 정확하지만 속도가 느린 재순위화 (Reranking) 모델을 적용함으로써, 초기 근사 검색에서 순위가 낮게 매겨졌을 수도 있는 최적의 매칭 항목들을 LLM에 실제로 전달될 최종 Top-K로 좁히기 전에 올바르게 식별할 기회를 얻는 거지."
Q2: 바이-인코더 (Bi-encoder)와 크로스-인코더 (Cross-encoder)의 차이점은 무엇이며, 왜 재순위화에서 이것이 중요한가요?
"원래의 임베딩에 사용되는 모델인 바이-인코더 (Bi-encoder)는 쿼리와 각 문서를 독립적으로 벡터로 인코딩하며, 이후 코사인 유사도 (Cosine Similarity) 같은 방식을 사용하여 비교해. 이는 빠르고 미리 계산(Precompute)할 수 있기 때문에 초기 대규모 검색에 사용되지. 반면 재순위화에 사용되는 크로스-인코더 (Cross-encoder)는 쿼리와 후보 문서를 하나의 입력으로 함께 받아 관련성 점수 (Relevance Score)를 직접 출력해. 이를 통해 두 요소 간의 관계를 더 정확하게 추론할 수 있지만, 수백만 개의 문서에 직접 실행하기에는 너무 느리다는 단점이 있어. 그래서 바이-인코더가 이미 걸러낸 작은 후보군 세트에만 적용되는 거야."
Q3: 관련 문서가 존재하지 않을 때 RAG 시스템의 환각 (Hallucination) 현상을 어떻게 방지하나요?
"검색 (Retrieval) 및 재순위화 (Reranking) 이후에 관련성 임계값 (Relevance threshold)을 적용함으로써 방지할 수 있습니다. 가장 높은 점수를 받은 검색된 청크 (Chunk)가 경험적으로 조정된 최소 관련성 점수 미만으로 떨어질 경우, 시스템은 약하거나 관련 없는 문맥 (Context)을 LLM에 전달하여 그럴듯해 보이지만 근거 없는 답변을 생성하게 만드는 대신, 명시적으로 답변을 거부하고 관련 정보가 없다고 응답해야 합니다. 이 임계값은 일반적으로 답변 가능한 질문과 답변 불가능한 질문에 대한 재순위화 점수 (Reranker score) 분포를 조사하여 조정합니다."
Q4: 임베딩 (Embeddings)에만 의존하는 대신 벡터 검색 (Vector search)과 키워드 검색 (Keyword search)을 결합하는 이유는 무엇인가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기