LLM 앱 개발 실전 #8 RAG 파이프라인 구축
7편에서 질문과 관련된 문서를 찾는 법을 익혔습니다. 이제 찾은 문서를 Claude에게 건네, 그 문서에 근거해 답하게 하면 됩니다. 이 방식을 RAG(Retrieval-Augmented Generation, 검색 증강 생성)라고 합니다. 우리 문서에 답이 있는 챗봇, 사내 지식 검색 같은 앱의 핵심 구조입니다.
RAG의 흐름 #
RAG는 세 단계로 움직입니다.
- 검색(Retrieval) — 질문과 비슷한 문서를 벡터 검색으로 찾습니다.
- 증강(Augmented) — 찾은 문서를 질문과 함께 프롬프트에 담습니다.
- 생성(Generation) — Claude가 그 문서를 근거로 답합니다.
핵심 발상은 단순합니다. Claude는 우리 문서를 모르지만, 문서를 프롬프트에 함께 넣어 주면 그걸 읽고 답할 수 있습니다. 7편의 벡터 검색은 “프롬프트에 넣을 만한 관련 문서"를 고르는 1단계였던 셈입니다.
문서를 잘게 쪼개기 #
긴 문서를 통째로 넣으면 두 가지 문제가 있습니다. 토큰이 많이 들고, 질문과 무관한 부분까지 섞여 답이 흐려집니다. 그래서 문서를 의미 단위로 잘게 쪼갠 뒤, 그 조각(청크)들을 임베딩합니다. 검색은 문서 전체가 아니라 이 조각 단위로 이뤄집니다.
def chunk_text(text: str, size: int = 300, overlap: int = 50):
chunks = []
start = 0
while start < len(text):
end = start + size
chunks.append(text[start:end])
start = end - overlap # 조각 사이를 조금 겹쳐 문맥이 끊기지 않게 한다
return chunks조각 사이를 조금 겹치는 이유는, 마침 경계에서 잘린 문장이 양쪽 조각 어디서도 온전하지 않게 되는 일을 줄이기 위해서입니다. 조각 크기와 겹침은 문서 성격에 맞춰 조정합니다.
검색과 생성을 잇기 #
7편의 벡터 검색에 Claude 생성을 이으면 RAG가 완성됩니다. 찾은 조각들을 프롬프트에 담고, “이 자료만 근거로 답하라"고 지시합니다.
import numpy as np
import anthropic
from sentence_transformers import SentenceTransformer
client = anthropic.Anthropic()
embedder = SentenceTransformer("all-MiniLM-L6-v2")
# 미리 청크를 임베딩해 둔다 (chunks 는 chunk_text 로 만든 조각 목록)
chunk_vectors = embedder.encode(chunks)
def retrieve(query: str, top_k: int = 3):
q = embedder.encode([query])[0]
scores = chunk_vectors @ q
ranked = np.argsort(scores)[::-1][:top_k]
return [chunks[i] for i in ranked]
def answer(query: str) -> str:
found = retrieve(query)
context = "\n\n".join(f"<doc>{c}</doc>" for c in found)
prompt = f"""아래 <자료> 안의 내용만 근거로 질문에 답해줘.
자료에 답이 없으면 "자료에서 찾을 수 없습니다"라고 답해.
<자료>
{context}
</자료>
질문: {query}"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return next(b.text for b in response.content if b.type == "text")
print(answer("환불은 며칠 안에 신청해야 하나요?"))질문이 들어오면 관련 조각 세 개를 찾아 <자료> 태그(4편에서 본 방식)로 감싸 프롬프트에 넣습니다. Claude는 그 자료만 보고 답합니다. 자료에 없는 질문에는 지어내지 말고 “찾을 수 없다"고 답하라는 지시가 중요합니다. 이 한 줄이 환각을 크게 줄입니다.
왜 RAG를 쓰는가 #
전체 문서를 매번 프롬프트에 다 넣으면 안 될까 싶을 수 있습니다. 문서가 적으면 그래도 됩니다. 하지만 문서가 수백 개라면 토큰 비용이 감당이 안 되고, 무관한 내용까지 들어가 답 품질도 떨어집니다. RAG는 질문마다 관련된 조각만 골라 넣으므로, 문서가 아무리 많아도 프롬프트는 작게 유지됩니다.
또 하나, 문서가 바뀌면 해당 조각만 다시 임베딩하면 됩니다. 모델을 새로 학습시킬 필요가 없습니다. 최신 정보를 반영하기 쉽다는 것도 RAG의 큰 장점입니다.
검색 품질을 높이기 #
RAG의 답이 좋지 않을 때, 원인은 생성보다 검색에 있는 경우가 많습니다. 엉뚱한 조각을 가져오면 Claude가 아무리 잘 답해도 소용이 없습니다. 검색 품질을 높이는 방법 몇 가지를 짚어 두겠습니다.
- 조각 크기 조정 — 가장 먼저 손볼 부분입니다. 답이 한 문단에 담기는 문서라면, 조각을 그 문단 단위에 맞추는 편이 좋습니다.
- 가져오는 개수(
top_k) — 너무 적으면 정답 조각을 놓치고, 너무 많으면 무관한 내용이 섞입니다. 보통 3~5개에서 시작해 조정합니다. - 키워드 검색과 섞기 — 의미 검색은 “환불"과 “결제 취소"를 잇지만, 제품 코드나 고유 명사처럼 정확히 일치해야 하는 것에는 약합니다. 벡터 검색과 키워드 검색을 함께 쓰는 하이브리드 검색이 서로의 약점을 보완합니다.
또 답에 근거를 함께 보여 주면 신뢰도가 올라갑니다. 어떤 조각을 근거로 답했는지 출처로 표시하면, 사용자가 답을 검증할 수 있고 환각도 쉽게 잡힙니다.
흔히 걸려 넘어지는 곳 #
- 조각이 너무 크거나 작다 — 너무 크면 무관한 내용이 섞이고, 너무 작으면 문맥이 끊깁니다. 문서 성격을 보며 크기를 조정하고, 검색 결과가 엉뚱하면 먼저 조각 크기부터 의심합니다.
- 근거 지시를 빠뜨린다 — “자료만 근거로 답하라"는 지시가 없으면, Claude가 자료를 무시하고 학습 지식으로 답해 환각이 섞일 수 있습니다.
- 검색 품질을 점검하지 않는다 — 답이 이상하면 생성 단계가 아니라 검색 단계, 즉 애초에 엉뚱한 조각을 가져온 경우가 많습니다.
retrieve가 무엇을 가져왔는지 먼저 확인합니다.
마무리 #
이번 글에서는 검색과 생성을 이어 RAG 파이프라인을 만들었습니다.
- RAG는 검색, 증강, 생성의 세 단계로 우리 문서에 근거한 답을 만듭니다.
- 긴 문서는 조각으로 쪼개 임베딩하고, 질문마다 관련 조각만 골라 프롬프트에 넣습니다.
- “자료만 근거로 답하라"는 지시로 환각을 줄이고, 문서가 많아도 프롬프트를 작게 유지합니다.
지금까지는 한 번의 질문과 답이 중심이었습니다. 다음 글인 “LLM 앱 개발 실전 #9 대화 메모리와 컨텍스트 관리"에서는 대화가 길어질 때 쌓이는 히스토리를 어떻게 다룰지, 컨텍스트 한계 안에서 대화를 이어가는 방법을 다루겠습니다.