
RAG를 처음부터 구축하여 AI가 내 문서에 대해 질문에 답하게 만드는 방법
요약
컨텍스트 윈도우의 한계를 극복하기 위한 RAG(검색 증강 생성)의 개념과 작동 원리를 설명합니다. 검색, 증강, 생성의 3단계 루프를 통해 모델이 외부 데이터를 근거로 정확한 답변을 생성하는 과정을 다룹니다.
핵심 포인트
- 컨텍스트 윈도우 제한 및 어텐션 성능 저하 문제 해결
- RAG의 3단계: 검색(Retrieval), 증강(Augmented), 생성(Generation)
- 모델이 학습 데이터가 아닌 실제 제공된 컨텍스트에 근거해 답변하도록 유도
- 기존 기업용 검색 시스템에 언어 모델을 결합한 형태
이전 포스트에서 우리는 컨텍스트 윈도우 (Context Window)에 대해 이야기했습니다. 모델은 고정된 크기의 책상을 가지고 있으며, 모든 것이 한 번에 그 위에 올라가야 합니다. 책상 위에 너무 많은 것이 올라가면, 중간에 있는 것들은 놓치게 됩니다.
저는 그 포스트를 다음과 같은 약속과 함께 마쳤습니다. 만약 당신이 한 번도 붙여넣은 적 없는 문서에서, 모델에게 딱 적절한 시점에 딱 적절한 조각만을 제공할 수 있는 방법이 있다면 어떨까요?
그것이 바로 이 포스트의 주제입니다. 우리는 모델에게 검색 시스템을 제공할 것입니다.
문제점: 당신의 문서는 너무 깁니다
당신에게 2,000페이지짜리 문서가 있다고 가정해 봅시다. 직원 핸드북, 제품 매뉴얼, 혹은 내부 문서일 수 있습니다. 당신은 그 안에서 단 하나의 구체적인 답변을 찾아야 합니다.
문서 전체를 모델의 컨텍스트 윈도우 (Context Window)에 붙여넣을 수는 없습니다. 설령 충분히 큰 윈도우를 가진 모델을 찾더라도, 우리는 어떤 일이 발생하는지 배웠습니다. 어텐션 (Attention) 성능이 저하되고, 중간에 있는 내용들은 누락되며, 모델은 잘못된 섹션에서 자신 있게 답변을 내놓습니다.
따라서 다른 무언가가 필요합니다. 모델이 무엇인가를 보기 (이전) 단계에서 일어나는 단계 말입니다. 질문에 실제로 답이 될 수 있는 2~3개의 문단을 찾아내어, 오직 그것들만을 모델에게 전달하는 무언가가 필요합니다.
그것이 바로 검색 (Retrieval)입니다. 전체 기술 명칭은 **RAG: 검색 증강 생성 (Retrieval-Augmented Generation)**이라고 불립니다. 먼저 검색하고, 그 다음에 생성하는 것입니다.
세 단어, 하나의 루프
이 이름을 하나씩 나누어 봅시다. 각 단어는 하나의 단계입니다.
검색 (Retrieval). 관련 정보를 찾아갑니다. 챕터에 몰입하기 전에 교과서의 색인을 확인하는 것과 같다고 생각하세요. 책 전체를 다시 읽는 것이 아니라, 먼저 올바른 페이지를 찾는 것입니다.
증강 (Augmented). 검색된 정보를 프롬프트 (Prompt)에 추가합니다. 모델의 내장된 지식에 신선하고 구체적인 컨텍스트 (Context)를 보충하는 것입니다. 마치 누군가가 질문에 답하기 직전에 커닝 페이퍼를 건네주는 것과 같습니다.
생성 (Generation). 모델은 대화 속에 바로 놓여 있는 검색된 컨텍스트 (Context)를 바탕으로 응답을 작성합니다. 모델은 단순히 학습된 내용이 아니라, 사용자의 실제 데이터에 근거하여 답변을 생성합니다. "근거를 둔 (Grounded)"이라는 말은 모델이 지시할 수 있는 실제 증거를 가지고 있다는 의미입니다. 기억에 의존해 추측하는 것이 아니라, 당신이 제공한 무언가를 바탕으로 답변하는 것입니다.
전체 루프를 한 문장으로 요약하면 다음과 같습니다: 적절한 정보 조각(Chunks)을 찾아 프롬프트 (Prompt)에 집어넣고, 모델이 그 컨텍스트를 사용하여 답변하게 만드는 것입니다. 그게 전부입니다. 그것이 바로 RAG입니다.
만약 "잠깐, 이거 그냥 기업용 검색 (Enterprise Search) 아닌가요?"라고 생각하신다면, 틀린 말이 아닙니다. Elasticsearch, Kendra, SharePoint 검색과 같은 도구들은 수십 년 동안 문서에서 관련 구절을 찾아왔습니다. 검색 (Retrieval) 부분은 새로운 것이 아닙니다. 새로운 것은 마지막 단계입니다. 사용자가 직접 읽어야 할 결과 페이지를 보여주는 대신, 파운데이션 모델 (Foundation Model)이 증거를 읽고 답변을 작성한다는 점입니다. 간단히 말해, RAG는 파이프라인 끝에 언어 모델 (Language Model)이 붙은 기업용 검색입니다.
설정: 가상의 회사를 위한 온보딩 문서
당신이 막 새로운 회사에 입사했고, 첫날에 수많은 문서를 건네받았다고 상상해 보세요. 직원 핸드북, 복리후생 가이드, 휴가 정책, 비용 규정, 엔지니어링 온보딩, IT 보안 등. 수천 줄에 달하는 6개의 문서입니다. 모든 정답은 그 어딘가에 들어있지만, 필요한 것을 찾으려면 그 모든 문서를 다 읽어야 할 것입니다.
여기 PineRidge Solutions라는 가상의 회사가 있습니다. 이것들은 이 회사의 온보딩 문서들입니다.
목표: 제가 "휴가는 며칠인가요?" 또는 _"육아 휴직 급여 보전은 어떻게 되나요?"_와 같은 질문을 입력하면, 시스템이 적절한 섹션을 찾아 그 내용을 바탕으로 답변하는 것입니다.
저는 이것을 Kiro IDE에서 구축하고 있으며, 모델로는 지난 네 개의 포스트에서 계속 사용해 온 도구인 Amazon Bedrock을 사용합니다. 다만 이번에는 AWS Console의 Playground 대신 코드를 통해 호출합니다.
참고로 여기서는 Bedrock을 사용하고 있지만, 이와 동일한 패턴은 로컬이나 클라우드에 있는 어떤 임베딩 (embeddings) 모델과도 작동합니다. 로컬의 Ollama, OpenAI, Cohere 등 무엇이든 상관없습니다. 파이프라인 (pipeline)은 동일하며, 모델은 그저 교체 가능한 플러그 (plug)일 뿐입니다.
이 포스트에서 언급된 모든 코드는 저의 GitHub 리포지토리 여기에서 확인할 수 있습니다.
구축을 위한 세 단계는 청킹 (Chunk), 임베딩 (embed), 검색 (retrieve)입니다. 시작해 봅시다.
1단계: 문서 청킹 (Chunk the document)
누군가 이 문서들을 검색하기 전에, 문서들은 더 작은 조각들로 나뉘어야 합니다. 이를 청크 (Chunks)라고 합니다. 보통 각 청크는 몇 개의 문단으로 구성됩니다.
[

왜 그럴까요? 목표는 모든 내용이 아니라 딱 필요한 관련 섹션만을 반환하는 것이기 때문입니다. 만약 각 문서를 하나의 거대한 블록으로 유지한다면, 단 한 문단만 필요할 때도 검색 결과로 파일 전체가 반환될 것입니다.
어떻게 나누느냐가 중요합니다. 너무 크면 다시
만약 중첩 (overlap) 없이 청킹 (chunking)을 한다면, 두 번째 문장 뒤에서 분할될 수도 있습니다. 다음 청크는 "이 휴가들은 다음 연도로 이월되지 않습니다."로 시작하게 됩니다. 이제 누군가 _"내 휴가 일수가 이월되나요?"_라고 질문하면, 시스템은 해당 청크를 검색합니다. 시스템은 "이 휴가들은 이월되지 않습니다."라고 답변할 것입니다. 하지만 어떤 휴가일까요? 표준 15일인가요? 1년 차의 10일인가요? "이(these)"라는 단어는 지칭 대상(referent)을 잃어버렸습니다. 해당 청크는 그 자체로는 의미가 없습니다.
중첩 (overlap)을 사용하면, 첫 번째 청크의 마지막 문장이 두 번째 청크의 시작 부분에서 반복됩니다. 두 청크 모두 독립적으로 의미를 갖게 됩니다.
여기에 코드가 있습니다. 각 파일을 읽고 조각으로 나눕니다:
def chunk_docs_paragraph(folder: str) -> list[dict]:
"""1개 문단의 중첩을 포함한 문단 기반 청킹 (paragraph-based chunking)."""
chunks = []
...
6개의 온보딩 (onboarding) 문서를 통해 약 150개의 청크를 얻었습니다. 각 청크는 대략 한두 문단 정도이며, 그 자체로 완결된 텍스트 조각입니다.
1단계가 완료되었습니다. 이제 이것들을 검색 가능하게 만들어야 합니다.
2단계: 청크를 임베딩 (embeddings)으로 변환하기
이 모든 것을 작동하게 만드는 개념이 여기 있습니다. 각 청크는 **임베딩 (embedding)**이라고 불리는 숫자 집합으로 변환됩니다.
이 이름은 말 그대로 수학적 용어입니다. 텍스트를 가져와 숫자로 이루어진 공간에 배치하는 것입니다. 그 공간에서 거리는 의미를 갖습니다. 비슷한 내용에 관한 두 청크는 서로 가까이 위치하게 됩니다. 서로 다른 주제에 관한 두 청크는 멀리 떨어지게 됩니다. "육아휴직 급여 보전 (Parental leave top-up)"과 "출산 휴가 중 급여 (salary during maternity leave)"는 실제 단어는 완전히 다르더라도 수치적으로 서로 가까이 있게 될 것입니다. 이것이 바로 임베딩이 유용한 이유입니다. 임베딩은 정확한 단어가 아닌 의미를 포착합니다.
이것을 도서관의 인덱스 카드 시스템이라고 생각해보세요. 카드는 책 전체를 담고 있지 않습니다. 누군가 질문했을 때 올바른 책을 찾을 수 있도록 내용에 대해 충분한 정보를 포착할 뿐입니다.
임베딩 모델 (embeddings model)이라고 불리는 특화된 모델이 우리를 대신해 이 변환을 수행합니다. 이는 답변을 생성하는 모델과 동일한 모델이 아닙니다. 서로 다른 작업을 수행하는 다른 모델입니다. 임베딩 모델은 작고 빠릅니다. 텍스트를 검색 가능한 숫자로 변환합니다.
import boto3, json
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
...
이제 각 청크 (chunk)는 수치적인 지문을 갖게 되었습니다. 이것이 바로 저의 검색 가능한 인덱스입니다.
이제 "벡터 (vector)"라는 용어를 자주 듣게 될 것입니다. 이는 단순히 방향을 가진 숫자 리스트를 의미합니다. 좌표라고 생각하면 됩니다. 임베딩 (embedding)이 개념이라면, 벡터 (vector)는 그것이 저장되는 형식입니다.
현재 이 벡터들은 제 노트북의 Python 리스트에 들어 있습니다. 이 스크립트를 닫으면 사라집니다. 이번 데모에서는 스크립트를 실행할 때마다 매번 다시 임베딩하지 않도록 로컬 파일에 캐싱(caching)하고 있습니다. 하지만 수천 개의 문서를 가진 프로덕션 (production) 시스템이라면, 적절한 어딘가에 저장해야 합니다. AWS는 최근 Amazon S3 Vectors를 출시했는데, 이는 말 그대로 벡터를 네이티브하게 저장하고 검색하기 위해 구축된 S3입니다. 또한 OpenSearch Serverless, Postgres를 원한다면 pgvector, 또는 전체 파이프라인을 관리형 서비스로 처리하는 Amazon Bedrock Knowledge Bases도 있습니다.
2단계 완료. 이제, 검색 단계입니다.
3단계: 검색 (Retrieve) 및 생성 (generate)
누군가가 질문을 합니다. 이 질문은 동일한 모델을 통해 임베딩 (embedding) 됩니다. 동일한 종류의 숫자들로 변환됩니다. 그런 다음 질문의 숫자들을 모든 청크 (chunk) 숫자들과 비교합니다. 가장 유사한 것들이 저의 검색 결과가 됩니다.
이것이 바로 시맨틱 검색 (semantic search) 입니다. 정확한 단어가 아닌 의미에 따라 일치 여부를 판단합니다.
만약 핸드북에 "원격 근무 정책 (remote work policy)"라고 적혀 있고, 제가 _"재택근무 규칙 (working from home rules)"_에 대해 묻는다면, 의미가 유사하기 때문에 이를 매칭하여 찾아냅니다.
import numpy as np
def retrieve(question: str, chunks: list[dict], embeddings: np.ndarray, top_k: int = 3):
...
상위 3개의 청크가 저의 근거 자료가 됩니다. 이제 이 자료들을 질문과 함께 생성 (generation) 모델에 전달합니다. Titan이 임베딩을 수행했고, Claude가 답변을 수행합니다.
def generate_answer(question: str, retrieved: list[dict]) -> str:
"""검색된 청크 + 질문을 Claude에게 전달합니다."""
context = "\n\n---\n\n".join(
...
저는 이렇게 물었습니다: "RRSP 매칭 정책은 무엇인가요?"
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기



