RAG 기초 (1) - Why RAG: 검색을 붙이는 이유
모델이 모르는 것을 확인하고 문서를 전부 프롬프트에 넣는 방식과 비교해 RAG가 필요한 조건과 색인, 질의 두 단계의 구조를 정리합니다.
RAG 기초 시리즈의 1편입니다. 전체 목차는 0편에 있습니다.
모델이 모르는 것
0편에서 만든 정책 문서를 주지 않고 그냥 물어봅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
from anthropic import Anthropic
client = Anthropic()
def text_of(message):
return "".join(b.text for b in message.content if b.type == "text")
message = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "제주도 배송비가 얼마인가요?"}],
)
print(text_of(message))
돌아오는 답은 “쇼핑몰마다 다르며 보통 3,000원에서 5,000원의 추가 요금이 붙습니다” 같은 일반론입니다. 모델은 우리 쇼핑몰의 정책을 학습한 적이 없으므로 이것이 최선입니다.
모델이 막히는 상황은 세 가지입니다.
- 비공개 정보 사내 위키, 계약서, 제품 매뉴얼은 학습 데이터에 없습니다
- 최신 정보 학습 시점 이후에 바뀐 정책이나 가격은 모릅니다
- 환각 모른다고 말하는 대신 그럴듯한 숫자를 지어내는 경우가 있습니다
세 번째가 가장 위험합니다. “제주도 추가 배송비는 3,000원입니다”라고 단정해 버리면 우연히 맞았는지 지어낸 것인지 구분할 수 없습니다.
문서를 넣으면 해결된다
해법 자체는 단순합니다. 답에 필요한 문서를 질문과 함께 넣어주면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
policy = open("docs/shipping.md", encoding="utf-8").read()
message = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
system="아래 <문서>만 근거로 답한다. 문서에 없으면 '자료에 없습니다'라고 답한다.\n\n"
f"<문서>\n{policy}\n</문서>",
messages=[{"role": "user", "content": "제주도 배송비가 얼마인가요?"}],
)
print(text_of(message))
# 기본 배송비 3,000원에 제주 및 도서산간 추가 3,000원이 붙습니다.
이제 정확합니다. RAG는 이 방식을 자동화한 구조일 뿐이고, 자동화해야 하는 이유는 “어떤 문서를 넣을지” 를 사람이 매번 고를 수 없기 때문입니다.
전부 넣으면 안 되나
Claude 5 계열의 context window는 100만 토큰입니다. 문서가 웬만큼 많아도 다 들어갑니다. 그래서 검색 없이 전부 넣는 선택지가 실제로 존재합니다.
문서를 전부 넣는 방식이 더 나은 경우가 있습니다.
- 문서 전체가 수만 토큰 이하로 고정되어 있다
- 질문이 문서 여러 곳을 동시에 참조한다 (검색은 조각만 가져오므로 불리합니다)
- 문서가 자주 바뀌지 않아 prompt caching으로 앞부분을 재사용할 수 있다
이 조건이면 검색 파이프라인을 만들 이유가 없습니다. 프롬프트 엔지니어링 기초 7편의 caching을 걸고 통째로 넣는 편이 간단하고 정확합니다.
반대로 RAG가 필요한 조건은 이렇습니다.
| 상황 | 이유 |
|---|---|
| 문서가 수백만 토큰 이상 | 넣을 수 없습니다 |
| 문서가 자주 바뀐다 | 캐시가 매번 깨져 비용 이점이 사라집니다 |
| 질문마다 필요한 문서가 다르다 | 관련 없는 내용이 많으면 정확도가 떨어집니다 |
| 호출량이 많다 | 매 호출 수십만 토큰의 입력 비용이 누적됩니다 |
“context window가 커졌으니 RAG는 필요 없다”도, “무조건 RAG를 붙여야 한다”도 아닙니다. 문서 양과 변경 빈도와 호출량으로 정하는 문제입니다.
파이프라인 구조
RAG는 두 단계로 나뉩니다. 문서를 미리 검색 가능한 형태로 준비하는 색인 단계와, 질문이 들어올 때마다 도는 질의 단계입니다.
flowchart LR
A[문서] --> B[chunk 분할]
B --> C[embedding]
C --> D[(vector store)]
Q[질문] --> E[embedding]
E --> F[유사 chunk 검색]
D --> F
F --> G[프롬프트에 삽입]
G --> H[LLM 답변]
위쪽 줄이 색인이고 아래쪽 줄이 질의입니다. 색인은 문서가 바뀔 때만 돌리고, 질의는 질문마다 돕니다.
각 단계가 이 시리즈의 각 편에 대응합니다.
| 단계 | 하는 일 | 편 |
|---|---|---|
| chunk 분할 | 긴 문서를 검색 단위로 자릅니다 | 2편 |
| embedding | 텍스트를 의미가 담긴 벡터로 바꿉니다 | 3편 |
| vector store | 벡터를 저장하고 가까운 것을 찾습니다 | 4편 |
| 생성 | 찾아온 조각을 근거로 답하게 합니다 | 5편 |
어려운 지점은 검색이다
RAG를 붙였는데 답이 부실할 때, 원인은 대부분 생성이 아니라 검색에 있습니다. 모델은 받은 근거 안에서는 대체로 잘 답합니다. 문제는 엉뚱한 근거를 받았을 때입니다.
이 시리즈가 chunk 분할과 embedding에 두 편을 쓰고, 검색 품질 개선에 한 편을 더 쓰는 이유입니다. 그리고 어느 단계가 문제인지 판단하려면 검색과 생성을 나눠 채점해야 합니다. 그 방법이 7편에 있습니다.
다음 편에서 문서 세 개를 실제로 chunk로 자르고, 자르는 방식에 따라 결과가 어떻게 달라지는지 확인합니다.