Post

[LLM 앱 실전] Day 2: RAG - 모델이 모르는 것을 검색해 답하게 하기

[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 체크리스트

  1. RAG가 재학습 없이 외부 지식을 컨텍스트로 주입하는 패턴임을 이해했다.
  2. 청킹 크기·겹침의 트레이드오프를 파악했다.
  3. 색인·질의에 같은 임베딩 모델을 써야 함을 안다.
  4. 벡터DB의 ANN 검색과 코사인 유사도 개념을 이해했다.
  5. 하이브리드 검색·재정렬로 검색 품질을 높이는 법을 익혔다.

다음 편 예고

RAG는 모델에 “지식”을 줬다. 하지만 모델이 계산하거나, API를 호출하거나, 실제 행동을 하려면? Day 3에서는 함수 호출(tool use) — LLM이 외부 도구를 사용하게 만드는 메커니즘을 다룬다.

This post is licensed under CC BY 4.0 by the author.