RAG가 뭐야 (검색증강생성) - 원리부터 실패 원인까지 한 번에 정리 (2026)
회사 문서를 넣고 "우리 규정 알려줘"라고 물었더니, 모델이 아주 자신 있게 틀린 규정을 읽어준다. 모델을 욕하기 전에 확인할 것이 있다. 그 문서가 정말 프롬프트에 들어갔는지.
RAG는 어려운 기술이 아니다. 한 문장으로 줄이면 "질문이 들어오면 먼저 자료를 찾아서, 찾은 자료를 프롬프트에 붙여서 물어본다" 이다. 어려운 건 그 "먼저 찾는" 부분이 조용히 실패한다는 점이다.
핵심 원칙 한 줄: RAG의 품질은 모델이 아니라 "검색기"가 결정한다. 답이 틀렸다면 먼저 무엇이 검색됐는지 확인해라.
0) 30초 요약

| 궁금한 것 | 답 |
|---|---|
| RAG가 뭔가 | 검색(Retrieval) + 생성(Generation). 찾은 문서를 프롬프트에 붙여 답하게 하는 방식 |
| 왜 쓰나 | 모델이 모르는 사내 문서·최신 정보를 다루기 위해. 재학습 없이 지식 교체 가능 |
| 구성 요소 | 청킹 → 임베딩 → 벡터DB 저장 → 검색 → (리랭킹) → 프롬프트 조립 → 생성 |
| 가장 흔한 실패 | 검색 실패(엉뚱한 청크), 청크 절단, 인용 없는 답변 |
| 확인 순서 | 답변을 보지 말고 검색된 청크를 먼저 본다 |
이 다섯 줄이 전부다. 아래는 그 이유와 실제 구성이다.
1) 증상 - RAG를 붙였는데 답이 더 이상해졌다
RAG를 붙인 뒤 자주 나타나는 증상 네 가지.
# 1. 문서에 있는 내용을 "모른다"고 한다
"해당 내용은 확인할 수 없습니다." (문서에는 3장에 명확히 있음)
# 2. 문서에 없는 내용을 자신 있게 말한다
"연차는 15일입니다." (실제 규정은 12일, 문서에 15이라는 숫자 없음)
# 3. 최신 문서를 반영하지 않는다
개정된 2026년 규정을 물었는데 2024년 판본으로 답한다
# 4. 같은 질문에 매번 다른 답이 나온다
어제는 맞게 답하고 오늘은 틀린다
네 가지의 공통점은 모델이 아니라 파이프라인에 원인이 있다는 것. 1·2·4번은 대부분 검색 단계에서 이미 어긋났다.
2) 원인 - LLM은 "가중치에 저장된 것"만 안다
LLM은 학습이 끝난 시점의 지식을 파라미터(가중치) 에 담고 있다. 이걸 parametric memory라고 부른다. 그래서 두 가지 한계가 구조적으로 생긴다.
첫째, 학습 이후의 일은 모른다. 오늘 개정된 사내 규정은 가중치에 없다.
둘째, 없는 걸 물어도 그럴듯하게 답한다. 이게 할루시네이션(환각)이다. 모델은 "모른다"보다 "그럴듯한 다음 토큰"을 선택하도록 학습돼 있다.
RAG는 여기에 외부 저장소(non-parametric memory) 를 붙인다. 지식을 가중치에서 꺼내는 대신, 매 질문마다 문서를 찾아 프롬프트에 실어 보낸다. 2020년 Lewis 등이 제안한 원 논문의 표현이 정확히 이 구조다 — 파라미터 기억과 외부 문서 인덱스를 결합해 지식 집약적(knowledge-intensive) 과제를 푸는 방식이다. (arXiv:2005.11401)
정리하면 RAG의 본질은 "학습"이 아니라 "프롬프트 조립" 이다. 그래서 문서를 바꾸면 재학습 없이 바로 반영된다. 대신 프롬프트에 못 들어간 문서는 존재하지 않는 것과 같다.
3) 구조 - 인덱싱과 질의는 완전히 다른 단계다
RAG를 이해할 때 가장 중요한 구분은 오프라인(인덱싱) 과 온라인(질의) 이다. 이걸 섞으면 디버깅이 불가능해진다.

| 단계 | 시점 | 하는 일 | 실패하면 |
|---|---|---|---|
| ① 수집 | 오프라인 | PDF·위키·DB에서 텍스트 추출 | 표·이미지 내용이 사라짐 |
| ② 청킹 | 오프라인 | 긴 문서를 수백 토큰 조각으로 분할 | 문맥이 잘려 검색 불가 |
| ③ 임베딩 | 오프라인 | 조각을 벡터로 변환 | 의미 검색 자체가 어긋남 |
| ④ 저장 | 오프라인 | 벡터DB에 벡터+원문+메타데이터 저장 | 필터링·갱신 불가 |
| ⑤ 검색 | 온라인 | 질문과 가까운 조각 top-k 추출 | 엉뚱한 근거로 답변 |
| ⑥ 리랭킹 | 온라인 | 후보를 다시 정렬해 상위만 남김 | 노이즈가 프롬프트에 유입 |
| ⑦ 생성 | 온라인 | 근거 + 질문으로 답변 작성 | 출처 없는 단정 |
여기서 ②청킹과 ⑤검색이 실제 품질의 8할을 좌우한다. 모델을 바꿔도 안 되던 답이 청킹을 고치면 해결되는 일이 흔하다.
4) 청킹 - 여기서 절반이 갈린다
가장 흔한 실수는 고정 글자 수로 자르는 것이다.
# 나쁜 예: 500자마다 자르기
"...제3조(연차휴가) 1. 입사 1년 미만 직원은 매월 1일의 유급휴가를"
"받는다. 2. 3년 이상 근무자는 12일을 부여한다. 단, 다음 각 호의"
"경우에는..." ← 조항 중간에서 끊겨 주어와 조건이 사라짐
이렇게 자르면 "3년 이상 근무자 연차"라는 질문에 이 조각이 걸려도, 조건절이 앞 조각에 있어서 모델이 오독한다.
실무에서 통하는 규칙 네 가지.
- 구조 단위로 자른다. 마크다운 헤딩, 문서의 조/항, 함수 단위.
- 오버랩을 준다. 조각 사이 10~20% 겹치게 해서 경계 손실을 줄인다.
- 메타데이터를 붙인다. 파일명, 섹션 제목, 작성일, 권한 태그.
- 조각에 맥락을 덧붙인다. "이 조각은 ○○규정 3장 연차휴가 부분"을 앞에 붙이는 방식.
4번은 Anthropic이 Contextual Retrieval이라는 이름으로 정리한 방법이다. 각 청크를 임베딩하기 전에 문서 전체 맥락을 요약해 앞에 붙이고, 임베딩과 BM25 양쪽에 함께 쓴다. 공개된 결과로는 검색 실패가 49% 감소, 리랭킹까지 결합하면 67% 감소했다. (Anthropic - Introducing Contextual Retrieval)
즉 최신 기법의 방향은 "더 좋은 모델"이 아니라 "청크에 맥락을 얼마나 잘 붙이는가" 다.
5) 임베딩과 벡터 검색 - 최소 구현
임베딩은 문장을 숫자 배열로 바꿔, 의미가 비슷하면 거리가 가까워지게 만든 것이다. 벡터DB는 이 배열에서 가장 가까운 이웃을 빠르게 찾는 저장소다.
라이브러리 없이 개념만 확인하는 코드를 보자.
import numpy as np
# 실제로는 임베딩 모델 API를 호출한다 (예: 1536차원)
def embed(text: str) -> np.ndarray:
...
return vec
chunks = [
"제3조 연차휴가: 입사 1년 미만은 매월 1일 유급휴가",
"제3조 연차휴가: 3년 이상 근무자는 연 12일 부여",
"제7조 출장비: 국내 출장 일비 3만원",
]
M = np.array([embed(c) for c in chunks])
M = M / np.linalg.norm(M, axis=1, keepdims=True) # 정규화 → 내적=코사인
def search(q: str, k: int = 2):
qv = embed(q)
qv = qv / np.linalg.norm(qv)
scores = M @ qv # 코사인 유사도
idx = np.argsort(-scores)[:k]
return [(chunks[i], float(scores[i])) for i in idx]
print(search("3년 넘게 다녔는데 연차 며칠이야?"))
포인트는 마지막 줄이다. 질문에는 "연차"라는 단어가 없고 "3년", "며칠"만 있다. 키워드 검색으로는 못 찾고, 임베딩 검색이라야 찾는다. 반대로 사번 A-2026-0417 같은 고유 코드는 임베딩이 약하다. 그래서 둘을 섞는다.
6) 하이브리드 검색 + 리랭킹 - 실무 기본값
의미 검색(벡터)과 키워드 검색(BM25)은 실패하는 지점이 다르다.
| 질문 유형 | 벡터 검색 | BM25(키워드) |
|---|---|---|
| "연차 며칠이야" (동의어·구어체) | 강함 | 약함 |
| "A-2026-0417 계약서" (고유 코드) | 약함 | 강함 |
| "환불 규정" (정확한 용어) | 보통 | 강함 |
| "돈 돌려받는 조건" (완전 다른 표현) | 강함 | 실패 |
그래서 두 결과를 합치는 하이브리드 검색을 기본으로 둔다. 순위를 합칠 때는 RRF(Reciprocal Rank Fusion)가 흔히 쓰인다. 점수 스케일이 다른 두 검색기를 순위만으로 합치는 방식이라 튜닝이 적다.
def rrf(rank_lists, k: int = 60, top: int = 5):
score = {}
for ranks in rank_lists: # [벡터 결과, BM25 결과]
for r, doc_id in enumerate(ranks, start=1):
score[doc_id] = score.get(doc_id, 0) + 1 / (k + r)
return sorted(score.items(), key=lambda x: -x[1])[:top]
그다음 리랭커로 상위 30~50개를 다시 정렬해 5개만 남긴다. 리랭커는 질문과 문서를 한 쌍으로 보고 점수를 매기는 모델이라, 벡터 유사도보다 정밀하다. 대신 비용이 들기 때문에 후보 전체가 아니라 상위 후보에만 적용한다.
순서는 이렇게 기억하면 된다. 넓게 찾고(하이브리드) → 좁게 정렬한다(리랭커).
7) 생성 - 프롬프트에 "없으면 모른다고 하라"를 넣는다
검색이 끝났으면 생성 단계는 상대적으로 단순하다. 다만 프롬프트에 규칙을 명시해야 한다.
SYSTEM = """너는 사내 규정 안내 도우미다.
- 아래 <근거>에 있는 내용만 사용해 답한다.
- 근거에 없으면 "문서에서 확인되지 않습니다"라고만 답한다.
- 모든 문장 끝에 근거 번호 [1], [2]를 붙인다.
- 숫자·날짜·금액은 근거의 표기를 그대로 인용한다."""
def build_prompt(question, hits):
ctx = "\n\n".join(
f"[{i}] ({h['source']} / {h['updated']})\n{h['text']}"
for i, h in enumerate(hits, start=1)
)
return f"<근거>\n{ctx}\n</근거>\n\n질문: {question}"
세 가지가 핵심이다.
- "근거에만" — 없으면 모른다고 하게 만든다. 이 한 줄이 환각을 가장 크게 줄인다.
- 인용 번호 — 답변을 검증할 수 있게 된다. 사용자가 원문을 직접 확인할 수 있다.
- 출처·갱신일 노출 — 오래된 문서로 답하는 사고를 잡는다.
참고로 OpenAI의 Retrieval API도 같은 구조다. 파일을 벡터 스토어에 올려 조각으로 나누고 임베딩해 두고, 질문 시 의미 검색으로 관련 조각을 꺼내 응답 생성에 쓴다. (OpenAI - Retrieval) 직접 만들든 관리형을 쓰든 구조는 동일하다.
8) 실패 진단 - 답변 말고 검색 결과를 봐라
RAG가 이상할 때 볼 순서를 정리했다. 이 순서를 지키면 대부분 30분 안에 원인을 찾는다.
| 순서 | 확인할 것 | 판정 |
|---|---|---|
| 1 | 질문으로 검색된 top-5 청크를 출력 | 정답 청크가 없으면 → 검색 문제 |
| 2 | 정답 청크가 있는데 순위가 낮다 | → 리랭킹·가중치 문제 |
| 3 | 청크가 문장 중간에서 끊겼다 | → 청킹 문제 |
| 4 | 청크는 맞는데 답이 틀렸다 | → 프롬프트·모델 문제 |
| 5 | 문서 자체가 최신이 아니다 | → 인덱싱·갱신 문제 |
여기서 1번을 건너뛰는 것이 가장 흔한 실수다. 검색 결과를 보지 않고 프롬프트만 고치면, 원인이 검색인 경우 아무 효과가 없다.
자주 하는 실수 네 가지도 적어둔다.
# 1. top-k를 1로 두고 "왜 못 찾지" 고민한다 → 3~8로 올려라
# 2. PDF 표를 통째로 넣고 표가 깨진 걸 모른다 → 추출 결과를 눈으로 봐라
# 3. 오래된 문서와 새 문서를 함께 넣는다 → updated 메타데이터로 필터
# 4. 질문을 그대로 임베딩한다 → 질문이 길면 검색용으로 다시 써서 임베딩
4번은 실무에서 효과가 크다. "어제 말한 그거 환불 되나요? 3월에 산 건데요"를 그대로 임베딩하면 잡음이 섞인다. 검색 전에 "3월 구매 건 환불 조건"처럼 검색 질의로 재작성하면 정확도가 올라간다.
9) RAG가 답이 아닌 경우
RAG를 붙이면 안 되는 상황도 분명히 있다.
- 문서 양이 적을 때 — 규정 3장짜리면 그냥 프롬프트에 다 넣는 게 정확하고 싸다. 검색기는 그때부터 정확도를 깎기 시작한다.
- 문체·말투를 학습시킬 때 — 지식이 아니라 스타일은 검색으로 안 된다. 파인튜닝이나 예시 프롬프트의 영역이다.
- 실시간 계산·집계 — "이번 달 매출 합계"는 검색이 아니라 DB 쿼리·도구 호출로 풀어야 한다.
- 권한이 복잡할 때 — RAG에 넣는 순간 검색 결과가 곧 유출 경로가 된다. 문서 단위 접근제어를 먼저 설계해야 한다.
정리하면 판단 기준은 이렇다. 지식이 자주 바뀌고, 양이 많고, 출처를 보여줘야 하면 RAG. 형태(문체·형식)를 바꾸는 게 목적이면 파인튜닝.
10) 최소 체크리스트
처음 RAG를 붙인다면 이 여섯 개만 지켜도 절반은 성공한다.
- 청크에 출처·섹션·갱신일 메타데이터를 붙인다.
- 청킹은 구조 단위 + 오버랩 10~20%.
- 검색은 하이브리드(벡터+BM25), 상위 후보에 리랭커.
- 프롬프트에 "근거에 없으면 모른다" 와 인용 번호.
- 디버깅은 항상 검색된 청크 출력부터.
- 평가는 답변이 아니라 검색 적중률(retrieval hit rate) 로 먼저 측정한다.
6번이 특히 중요하다. 답변 품질만 보면 검색이 틀렸는지 모델이 틀렸는지 구분되지 않는다. 질문 50개를 만들고, 정답 청크가 top-5 안에 들어오는 비율을 세보면 개선이 되는지 숫자로 판정할 수 있다.
마무리 - 3줄 요약
- RAG는 모델을 다시 학습시키는 게 아니라, 질문마다 근거 문서를 찾아 프롬프트에 붙이는 방식이다.
- 답이 틀렸을 때 원인의 대부분은 모델이 아니라 청킹과 검색에 있다. 청크에 맥락을 붙이고(49% 개선), 하이브리드 검색과 리랭킹을 조합하라(67% 개선).
- 프롬프트에는 "근거에 없으면 모른다" 와 인용 번호를 반드시 넣고, 평가는 답변이 아니라 검색 적중률로 시작하라.
관련 글: 토큰(token)이란 - AI가 글자를 세는 방식 · LLM이란 무엇인가 · AI 기초 용어 20개 정리
'IT' 카테고리의 다른 글
| Aside(어사이드)란 - 로그인된 웹을 그대로 쓰는 AI 브라우저 (2026) (0) | 2026.10.24 |
|---|---|
| 토큰(token)이란 - AI가 글자를 세는 방식과 요금 계산 (2026) (0) | 2026.10.24 |
| 클로드 듀엣(Claude Duet) 나왔다 - 먼저 말 걸어오는 AI 비서, 뭐가 다른가 (2026) (0) | 2026.10.24 |
| AI 기초 용어 20개 정리 - 뉴스에 나오는 단어, 이제 다 알아듣기 (2026) (0) | 2026.10.23 |
| ModuleNotFoundError 해결 - 원인 6가지와 상황별 해결법 (2026) (0) | 2026.10.22 |














