RAG 지식 비서 구축하기: 청킹(Chunking), 임베딩(Embeddings), 벡터 검색(Vector Search) 및 출처를 인용하는
요약
RAG(Retrieval-Augmented Generation)는 LLM의 환각 현상과 컨텍스트 제한 문제를 해결하는 기술입니다. 이 시스템은 문서를 청킹하고 임베딩하여 벡터 인덱스에 저장하며, 질문이 들어오면 유사한 출처를 검색합니다. 이후 검색된 정보와 질문을 바탕으로 답변을 생성하고 원본 출처까지 제공합니다.
핵심 포인트
- RAG는 LLM에게 데이터를 '알게' 하는 대신 관련 텍스트를 찾아 전달하는 방식입니다.
- 인덱싱은 문서 파싱, 청킹, 임베딩 후 벡터 인덱스에 저장되는 과정으로 이루어집니다.
- 질문도 임베딩되어 유사도가 높은 상위 k개 청크가 검색되고 답변 생성의 근거가 됩니다.
- 단순 키워드 검색보다 시맨틱 검색이 어휘 불일치 문제를 해결합니다.
일반적인 목적의 LLM에게 회사 환불 정책에 대해 물어보면 자신 있게 답변할 것입니다. 하지만 그 정책을 본 적이 없기 때문에 틀릴 수도 있습니다.
바로 이 간극 때문에 Retrieval-Augmented Generation (RAG)이 존재합니다. 모델이 데이터를 "알기를" 기대하는 대신, 관련 텍스트를 먼저 찾아 질문과 함께 모델에 전달합니다. 그러면 모델은 추측을 멈추고 읽기 시작합니다.
Guidely는 제가 Zone01 Kisumu에서 구축하고 있는 내부 지식 비서입니다. FastAPI 백엔드와 React/Vite 프런트엔드를 사용합니다. 회사 문서를 업로드하면 이를 인덱싱하고, 질문을 하면 사용된 출처와 함께 짧은 답변을 반환합니다. 이 게시물에서는 파이프라인이 어떻게 작동하는지, 각 단계가 비용에 얼마만큼 영향을 미치는지, 그리고 RAG 시스템이 조용히 실패할 수 있는 지점들을 자세히 설명하겠습니다.
LLM에게 직접 질문할 때의 문제점
순수한 LLM은 이 작업에 두 가지 약점이 있습니다. 회사의 비공개 문서를 알지 못하며, 무언가를 모를 때 그럴듯한 답변을 만들어내려는 경향이 있기 때문입니다. 지원 도구의 경우, 아무 답변도 없는 것보다 자신감 있는 오답이 더 위험합니다. 왜냐하면 사람들이 그것에 기반하여 행동하기 때문입니다.
모든 문서를 프롬프트에 붙여넣는 것도 해결책이 아닙니다. 컨텍스트 윈도우(Context window)에는 제한이 있으며, 긴 프롬프트는 느리고 비용이 많이 들고, 모델은 작은 양의 정보 속에서 바늘을 찾는 것보다 거대한 더미 속에서 바늘을 찾는 데 더 취약합니다. 중요한 몇 가지 구절만 모델에 보내야 합니다.
왜 단순히 키워드 검색을 사용하지 않는가?
키워드 검색은 명백한 지름길이며, 작동하다가 실패할 때까지는 잘 작동합니다:
- 어휘 불일치가 문제를 일으킵니다. "비밀번호를 재설정하려면 어떻게 하나요?"와 "로그인 자격 증명을 잊었습니다"는 거의 공통 단어가 없지만, 같은 것을 질문하고 있습니다.
- 답변이 아닌 페이지를 반환합니다. 사용자는 여전히 관련 단락을 찾기 위해 결과들을 뒤져야 합니다.
- 자체적으로 명확하게 설명할 수 없습니다. 읽기 쉬운 답변 대신 링크 목록만 얻게 됩니다.
시맨틱 검색(Semantic search)은 첫 번째 문제를 해결하고, LLM은 나머지 두 가지 문제를 해결합니다. RAG는 이 둘을 결합한 것입니다.
아키텍처
이 설계는 두 단계로 이루어져 있습니다. 인덱싱(Indexing)은 문서를 추가하거나 변경할 때 오프라인으로 발생합니다. 쿼리링(Querying)은 질문이 들어올 때마다 온라인으로 발생합니다.
인덱싱 (Indexing):
- 업로드된 파일을 원본 텍스트로 파싱(Parse)합니다.
- 텍스트를 작고 중첩되는 조각(overlapping pieces)으로 청킹(Chunking)합니다.
- 각 청크를 벡터(vector)로 임베딩(Embed)합니다.
- 벡터와 함께 해당 청크 텍스트 및 파일 이름을 벡터 인덱스(vector index)에 저장합니다.
쿼리링 (Querying):
- 질문을 동일한 모델로 임베딩(Embed)합니다.
- 인덱스에서 가장 유사도가 높은 상위 k개 청크를 검색(Search)합니다.
- 해당 청크와 질문을 바탕으로 답변을 생성(Generate)합니다.
- 사용된 출처(sources)와 함께 답변을 반환(Return)합니다.
중요한 속성은 모델이 검색(retrieval)이 제공하는 정보만을 본다는 것입니다. 모든 후속 작업은 이 전달 과정에 달려 있습니다.
청킹(Chunking): 가장 과소평가된 결정
40페이지짜리 PDF를 하나의 벡터로 임베딩하면 의미가 모호해집니다(mush). 따라서 문서는 청크 단위로 분할되며, 일반적으로 500~1,000 토큰 크기이고 이웃 간에 약간의 중첩(overlap)을 두어 경계에서 잘린 문장이라도 다른 쪽에서 컨텍스트를 유지하도록 합니다.
청크 크기는 트레이드오프(trade-off)입니다. 너무 크면 하나의 청크가 여러 주제를 다루기 때문에 검색 결과가 모호해집니다. 반대로 너무 작으면 각 청크가 자체적으로 의미를 갖는 데 필요한 컨텍스트를 잃게 됩니다. 제목이나 단락 같은 자연스러운 경계(natural boundaries)를 기준으로 분할하는 것이 고정된 문자 수로 자르는 것보다 우수합니다. 왜냐하면 아이디어를 온전히 보존하기 때문입니다.
모든 청크는 또한 메타데이터(metadata)를 유지합니다. 즉, 해당 청크가 나온 파일과 그 파일 내의 위치입니다. 이 메타데이터 덕분에 나중에 사용자 인터페이스(UI)가 답변이 정확히 어디에서 왔는지 알려줄 수 있습니다.
임베딩(Embeddings) 및 벡터 스토어(Vector Store)
임베딩은 텍스트를 숫자의 목록으로 변환하여 유사한 의미가 가까운 벡터로 표현되게 합니다. 이것이 단어 공유가 전혀 없음에도 불구하고 비밀번호 질문이 로그인 질문과 일치하게 만드는 원리입니다.
저장소(storage)를 위해 Guidely는 FAISS를 사용합니다. 이는 로컬에서 실행되며, 계정이나 외부 서비스가 필요 없고 빠릅니다. 정규화된 벡터와 내적 검색(inner-product search)을 사용할 경우 점수는 코사인 유사도(cosine similarity)입니다. 수십만 개의 청크까지는 정확한 플랫 인덱스(exact flat index)로 충분합니다. 근사 인덱스(approximate indexes)는 나중에 중요해지며, 관리형 인프라가 필요할 때 Pinecone이나 Weaviate와 같은 호스팅 옵션이 의미를 갖습니다. 작동하는 가장 간단한 것부터 시작하세요.
여기서 지켜야 할 규칙이 하나 있습니다: 질문과 청크는 반드시 동일한 모델로 임베딩되어야 합니다. 서로 다른 모델에서 생성된 벡터는 각기 다른 공간에 존재하며 비교할 수 없습니다.
근거 기반 답변 생성하기 (Generating a Grounded Answer)
RAG의 'G'는 신뢰성(honesty)을 강제하는 부분입니다. 프롬프트는 모델에게 번호가 매겨진 청크와 엄격한 지침 세트를 제공합니다:
- 제공된 컨텍스트만을 사용하여 답변할 것.
- 출처를 번호로 인용할 것.
- 컨텍스트에 답이 포함되어 있지 않다면, 모른다고 말할 것.
이 모든 지침은 실제로 작동합니다. '컨텍스트만 사용'이라는 규칙은 환각(hallucination)을 줄입니다. 번호가 매겨진 출처는 모델이 인용하게 하고, UI가 각 인용을 실제 파일과 스니펫으로 매핑할 수 있게 합니다. '모른다고 말하기'라는 탈출구(escape hatch)는 검색에 실패했을 때 모델이 임의로 답변을 지어내는 것을 막아줍니다. 온도를 0으로 설정하면 답변이 안정적이고 재현 가능하게 유지됩니다.
API는 그 후 깨끗한 JSON을 반환합니다: 답변과 함께 파일 이름 및 스니펫 목록 형태의 출처 리스트입니다. 항상 출처를 보여주는 것은 '나를 믿어라'라는 것을 '스스로 확인해 보라'로 바꿉니다.
프로덕션 환경에서 주의할 점 (Things That Bite You in Production)
모든 내용을 재임베딩하기. 임베딩에는 비용과 시간이 들고, 문서는 거의 변경되지 않습니다. 인덱싱 시점에 각 파일의 콘텐츠를 해시(Hash)하고, 해시가 변경되지 않았다면 건너뛰세요. 새로운 임베딩에 대해 캐시 적중(cache hits) 횟수를 기록하세요. 질문을 다시 해도 임베딩 개수가 늘어난다면, 무언가 잘못된 것입니다.
정확한 용어 및 ID. 순수 벡터 검색(Pure vector search)은 오류 코드, 제품명, 식별자 같은 정확한 용어와 ID에 취약합니다. 이는 키워드 검색이 강점을 보이는 영역입니다. 하이브리드 검색(Hybrid search)은 이 두 가지를 모두 실행하고 랭킹을 병합하며, 종종 가장 큰 품질 향상을 가져올 수 있는 요소입니다.
거친 순위 지정 (Coarse ranking). 벡터 검색은 빠르지만 근사치에 가깝습니다. 일반적인 개선 방법은 넓은 범위(예: 상위 20개)를 검색한 다음, 질문과 각 청크를 함께 읽어 가장 적합한 몇 개를 선택하는 크로스 인코더 리랭커(cross-encoder reranker)를 사용하는 것입니다. 약간의 지연 시간(latency)을 희생하여 더 나은 정밀도(precision)를 얻는 트레이드오프입니다.
후속 질문 (Follow-up questions). 사용자가 “계약직 근로자에게는요?”라고 질문했지만, 검색할 내용이 없는 경우입니다. 대화 기록을 사용하여 후속 질문을 독립적인 질문으로 재작성(Rewrite)한 다음 임베딩하는 것이 좋습니다.
프롬프트 주입 (Prompt injection). 사용자의 문서가 프롬프트의 일부가 되기 때문에, “이전 지침 무시”와 같은 내용을 담은 문서는 공격 벡터가 될 수 있습니다. 시스템 지침(system instructions)을 검색된 콘텐츠와 분리하고, 컨텍스트를 데이터로 명확하게 라벨링하며, 검색된 텍스트가 검증 없이 어떤 행동을 유발하도록 두지 않아야 합니다.
실패 상태 (Failure states). 실제 시스템은 빈 쿼리(empty queries), 누락된 API 키, 손상된 파일, 모델 타임아웃, 아무것도 찾지 못한 검색 등 다양한 실패 상황에 직면합니다. 명확한 메시지를 담아 적절한 4xx 및 5xx 응답을 반환하고, UI에서 친근한 오류 상태를 보여주어야 합니다. “관련 문서가 없습니다”라는 답변이 좋습니다. 확신에 찬 환각(hallucination)은 최악의 경우입니다.
작동 여부를 어떻게 아는가?
“작동하는 것 같다”는 지표가 될 수 없습니다. Guidely는 백엔드에서 자동으로 기록되는 일부 확인 항목과 수동으로 검증되는 일부 항목을 포함하여 소수의 체크리스트를 추적합니다:
- 검색(Retrieval) 성능 측정 지표@3 (수동): 샘플 문서에 답이 존재하는 10개에서 20개의 질문에 대해 정답 구절이 상위 3개 청크에 얼마나 자주 나타나는지 확인합니다. 목표: 80% 이상.
- 답변 출처 커버리지(Answer reference coverage) (수동): 모든 답변이 최소한 하나의 유효 출처를 인용하는지 여부입니다. 목표: 90% 이상.
- 출처 정밀도(Source precision) (수동): 하이라이트된 스니펫(snippet)이 실제로 답변을 뒷받침하는지 여부입니다. 목표: 80% 이상.
- 지연 시간(Latency) (자동 기록): 질문부터 최종 JSON까지 걸리는 시간입니다. 목표: 따뜻한 캐시(warm cache) 상태에서 중앙값(median) 3초 미만, p95 5초 미만.
- 임베딩 캐시 효율성(Embedding cache effectiveness) (자동 기록): 변경되지 않은 문서에 대한 반복적인 쿼리 시 재임베딩이 발생하지 않아야 합니다.
가장 유용한 습관은 검색(retrieval)을 생성(generation)과 분리하여 평가하는 것입니다. 올바른 청크가 상위 3개 안에 없다면 어떤 프롬프트도 문제를 해결해 주지 못합니다. 먼저 검색을 확인하고, 그다음 답변을 확인하십시오. 품질이 떨어지는 지점을 알면 파이프라인의 어느 절반에 문제가 있는지 파악할 수 있습니다.
배운 교훈 (Lessons Learned)
- 검색 품질이 한계점입니다. LLM은 당신이 제공하는 컨텍스트(context)만큼만 좋을 수 있습니다.
- 모델 선택보다 청킹(chunking)과 프롬프트가 더 중요합니다. 모델을 업그레이드한다고 해서 나쁜 파이프라인이 고쳐지지는 않습니다.
- 사용자 인터페이스(UI) 이전에 임베딩 및 검색 기능을 테스트하십시오. 검색이 올바른 청크를 반환할 때 마지막에 인터페이스를 구성하세요.
- 첫날부터 캐싱을 구현하십시오. 해시 기반 건너뛰기(Hash-based skipping)는 초기에 구축하기 쉽고 나중에 추가하기 어렵습니다.
- 항상 출처를 보여주십시오. 인용문(Citations)이 이 도구를 실제 사용자가 사용할 수 있게 만듭니다.
- 저렴하게 모든 것을 기록하십시오. 지연 시간(Latency), 캐시 적중률(cache hits), 검색된 청크 ID, 오류 횟수 등을 기록하면 디버깅이 훨씬 쉬워집니다.
결론
RAG는 마법이 아닙니다. 그것은 검색(search)에 잘 지시된 리더가 더해진 것이며, 그렇게 이해하게 되면 모든 개선 사항은 평범한 엔지니어링 문제로 바뀝니다: 더 나은 청크(chunks), 하이브리드 검색(hybrid search), 재순위 지정(reranking), 쿼리 재작성(query rewriting) 등이 그것입니다. 첫 번째 시스템을 구축한다면, 작게 시작하세요. 즉, 5개의 문서, FAISS, 그리고 엄격한 프롬프트(prompt)를 사용하십시오. 다른 어떤 부분에 손대기 전에 검색(retrieval)이 작동하고 측정되도록 만드는 데 집중하면, 시스템의 나머지 부분은 훨씬 이해하기 쉬워집니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기