[LLM 앱 실전] Day 2: RAG - 모델이 모르는 것을 검색해 답하게 하기
이 글은 AI(Claude)의 도움을 받아 작성하고, 작성자가 검토·편집했습니다.
서론: 모델의 지식 한계를 넘기
LLM은 학습 시점까지의 지식만 안다. 오늘 뉴스도, 사내 위키도, 어제 작성한 문서도 모른다. 게다가 모르는 것을 그럴듯하게 지어낸다(환각). RAG(Retrieval-Augmented Generation)는 질문과 관련된 문서를 검색해 프롬프트에 주입함으로써, 모델이 “근거를 보고” 답하게 한다. 오늘날 가장 중요한 LLM 앱 패턴이다.
1. RAG의 기본 흐름
1
2
3
4
5
6
[색인 단계 - 미리]
문서 → 청킹(분할) → 임베딩(벡터화) → 벡터DB에 저장
[질의 단계 - 실시간]
질문 → 임베딩 → 벡터DB에서 유사 청크 검색 →
검색된 청크를 프롬프트 컨텍스트에 주입 → LLM이 근거 기반 답변
핵심 직관: 모델을 재학습하지 않고, 답에 필요한 지식을 그때그때 컨텍스트로 넣어준다(Day 1의 컨텍스트 관리의 연장).
2. 청킹: 문서를 어떻게 자를까
문서를 통째로 임베딩하면 검색 정확도가 떨어진다. 적절한 크기로 잘라야 한다.
1
2
3
4
5
6
7
8
청크가 너무 크면: 한 청크에 여러 주제 → 검색 정밀도 하락, 컨텍스트 낭비
청크가 너무 작으면: 문맥 단절 → 의미 파편화
전략:
- 고정 크기 + 겹침(overlap): 예) 500토큰 청크, 50토큰 겹침
→ 경계에서 잘린 문맥을 겹침이 보완
- 의미 단위: 문단·섹션·헤더 경계로 분할 (구조 보존)
- 문서 유형별 조정: 코드는 함수 단위, 대화는 턴 단위
1
2
3
4
5
6
def chunk_text(text, size=500, overlap=50):
chunks, start = [], 0
while start < len(text):
chunks.append(text[start:start + size])
start += size - overlap # 겹침만큼 뒤로 물러나 다음 청크
return chunks
3. 임베딩: 의미를 벡터로
각 청크를 임베딩 모델로 벡터화한다(Transformer Day 1의 임베딩과 같은 원리). 의미가 비슷한 텍스트는 벡터 공간에서 가까워진다.
1
2
3
4
5
6
7
# 임베딩 API로 청크를 벡터로
def embed(texts):
resp = client.embeddings.create(model="text-embedding-3-small", input=texts)
return [d.embedding for d in resp.data] # 각 청크 → 예: 1536차원 벡터
# 질문도 같은 모델로 임베딩해야 같은 공간에서 비교 가능
query_vec = embed([question])[0]
중요: 색인과 질의에 같은 임베딩 모델을 써야 벡터가 같은 공간에 놓인다.
4. 벡터 검색: 유사도로 찾기
질문 벡터와 가장 가까운 청크 벡터들을 찾는다. 유사도는 보통 코사인 유사도다.
1
2
3
4
5
6
7
8
코사인 유사도: 두 벡터의 방향이 얼마나 같은가 (-1 ~ 1)
높을수록 의미가 유사
벡터DB의 역할:
수백만 벡터 중 가장 가까운 K개를 빠르게 찾음 (ANN: 근사 최근접 탐색)
완전 탐색은 느리므로 HNSW 같은 인덱스로 근사 (정확도↔속도 트레이드오프)
도구: Pinecone, Weaviate, Qdrant, pgvector(Postgres 확장) 등
1
2
3
# 개념적 검색 (실제론 벡터DB가 ANN으로 수행)
results = vector_db.search(query_vec, top_k=5) # 가장 유사한 5개 청크
context = "\n\n".join(r.text for r in results)
5. 생성: 검색 결과를 근거로
검색한 청크를 프롬프트에 넣고, “이 근거만 사용하라”고 지시한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
prompt = f"""다음 문서를 근거로만 질문에 답하라.
문서에 없는 내용은 "제공된 문서에서 찾을 수 없습니다"라고 답하라.
[문서]
{context}
[질문]
{question}"""
answer = client.chat.completions.create(
model="...",
messages=[{"role": "user", "content": prompt}],
temperature=0.1, # 사실 답변이므로 낮게 (Transformer Day 4)
)
“근거만 사용하고, 없으면 모른다고 하라”는 지시가 환각을 크게 줄인다. 출처(청크 메타데이터)를 함께 보여주면 신뢰도가 오른다.
6. 검색 품질 높이기: 하이브리드와 재정렬
순수 벡터 검색은 의미는 잘 잡지만 정확한 키워드(제품명·코드·고유명사)에 약하다.
1
2
3
4
5
6
7
8
9
10
11
하이브리드 검색:
벡터 검색(의미) + 키워드 검색(BM25, 정확 일치) 결합
→ 두 점수를 합쳐 의미와 정확성을 모두 잡음
재정렬(Re-ranking):
1차로 벡터DB가 후보 20개를 빠르게 뽑음
2차로 더 정밀한 reranker 모델이 질문-청크 관련성을 재채점
→ 상위 5개만 컨텍스트에 (정밀도↑)
쿼리 변환:
질문을 여러 형태로 확장·재작성해 검색 (HyDE, 멀티쿼리)
7. RAG의 흔한 실패와 대응
1
2
3
4
5
6
7
8
9
10
11
12
13
"검색은 됐는데 답이 틀림":
→ 청킹 점검 (관련 정보가 한 청크에 온전히 있나?)
"관련 문서를 못 찾음":
→ 임베딩 모델·하이브리드 검색·쿼리 재작성 검토
"근거 무시하고 환각":
→ 프롬프트 강화, temperature 낮춤, 출처 인용 강제
"느림/비쌈":
→ top_k 조정, 캐싱(대규모 시스템 Day 1), 청크 수 최적화
원칙: RAG 품질 = 검색 품질 × 생성 품질. 검색이 나쁘면 좋은 모델도 못 구한다.
8. Day 2 체크리스트
- RAG가 재학습 없이 외부 지식을 컨텍스트로 주입하는 패턴임을 이해했다.
- 청킹 크기·겹침의 트레이드오프를 파악했다.
- 색인·질의에 같은 임베딩 모델을 써야 함을 안다.
- 벡터DB의 ANN 검색과 코사인 유사도 개념을 이해했다.
- 하이브리드 검색·재정렬로 검색 품질을 높이는 법을 익혔다.
다음 편 예고
RAG는 모델에 “지식”을 줬다. 하지만 모델이 계산하거나, API를 호출하거나, 실제 행동을 하려면? Day 3에서는 함수 호출(tool use) — LLM이 외부 도구를 사용하게 만드는 메커니즘을 다룬다.