RAG(Retrieval-Augmented Generation, 검색 증강 생성)를 한 문장으로 줄이면 “질문에 답하기 전에 관련 문서를 먼저 찾아서 프롬프트에 넣어 준다” 입니다. 개념은 이렇게 단순한데, 실제로 만들어 보면 정확도가 60%에서 더 안 올라가는 지점에 반드시 부딪힙니다.
이 글은 그 지점들을 단계별로 짚습니다. 가장 먼저 새겨야 할 명제는 이것입니다.
검색이 실패하면 생성은 절대 복구하지 못합니다. 프롬프트를 아무리 다듬어도, 컨텍스트에 정답이 없으면 모델은 지어냅니다. RAG 품질 개선의 80%는 검색 쪽에 있습니다.
1. 전체 구조
[색인 시점 — 오프라인]
원본 문서 → 파싱 → 청킹 → 임베딩 → 벡터 저장소
[질의 시점 — 온라인]
질문 → (질의 변환) → 검색 → 재순위화 → 컨텍스트 조립 → LLM → 답변 + 출처
└─ 여기가 품질의 8할
각 단계가 어디서 깨지는지 순서대로 보겠습니다.
2. 파싱 — 가장 과소평가되는 단계
“PDF에서 텍스트를 뽑는다”는 말은 쉽지만, 실제로 뽑아 보면 이렇게 나옵니다.
원본 (표)
┌────────┬────────┬────────┐
│ 과목 │ 학점 │ 이수구분│
├────────┼────────┼────────┤
│ 자료구조│ 3 │ 전공필수│
└────────┴────────┴────────┘
단순 텍스트 추출 결과
과목 학점 이수구분 자료구조 3 전공필수
표의 구조가 사라지면 “자료구조는 몇 학점인가”에 답할 수 없습니다. 숫자 3이 무엇에 붙는 값인지 모르기 때문입니다.
실무에서 신경 쓸 것들입니다.
| 문제 | 대응 |
|---|---|
| 표 구조 소실 | 표를 마크다운 표나 HTML로 변환해 보존. 표 단위로 별도 청크 |
| 다단 조판(2단 편집) | 좌→우 순서로 읽히는지 확인. 안 되면 레이아웃 인식 파서 사용 |
| 머리말·꼬리말·쪽번호 | 모든 청크에 반복 삽입되어 임베딩을 오염시킴. 제거 |
| 이미지 속 텍스트 | OCR 또는 멀티모달 모델로 캡션 생성 후 텍스트화 |
| 각주·참고문헌 | 본문과 섞이면 문맥이 끊김. 분리하거나 제거 |
색인 전에 추출된 텍스트를 눈으로 100건쯤 읽어 보는 것이 가장 값싼 품질 개선입니다. 이 단계를 건너뛰고 임베딩 모델부터 바꾸는 경우가 많은데, 순서가 거꾸로입니다.
3. 청킹 — 크기보다 경계
왜 쪼개나
두 가지 이유입니다.
- 임베딩 모델의 입력 길이 제한 — 대부분 512~8192 토큰
- 더 중요한 이유: 희석(dilution) — 긴 문서 전체를 벡터 하나로 만들면 여러 주제가 평균되어 뭉개집니다. 10페이지짜리 학사 규정 문서의 벡터는 “휴학”에도 “졸업요건”에도 애매하게 가깝습니다. 어느 질문에도 뚜렷하게 걸리지 않습니다.
전략 비교
| 방식 | 내용 | 장단점 |
|---|---|---|
| 고정 크기 | N 토큰마다 자름 | 구현이 쉽지만 문장·문단 한가운데를 자름 |
| 재귀적 문자 분할 | \n\n → \n → . → ` ` 순으로 시도 |
실무 기본값. 문단 경계를 최대한 지킴 |
| 문서 구조 기반 | 제목(#, ##) 단위로 분할 |
구조화된 문서라면 가장 좋음. 제목이 곧 주제 경계 |
| 의미 기반 | 인접 문장 임베딩 유사도가 급락하는 지점에서 분할 | 주제 전환을 잘 잡지만 색인 비용이 큼 |
교육 도메인처럼 문서가 잘 구조화되어 있다면 구조 기반이 압도적으로 낫습니다. 학사 규정, 교육과정 편람, 강의계획서 모두 절·항 체계가 이미 있습니다. 그 경계를 쓰지 않을 이유가 없습니다.
크기와 겹침
- 크기 — 짧으면(200토큰) 정밀하지만 문맥이 잘리고, 길면(1500토큰) 문맥은 살지만 노이즈가 섞입니다. 실무 출발점은 400~800 토큰입니다.
- 겹침(overlap) — 청크 경계에 걸친 문장을 살리기 위해 앞뒤를 10~20% 겹칩니다.
겹침이 필요한 이유를 보여 주는 예입니다.
겹침 없음
청크 1: "... 휴학은 통산 6학기를 초과할 수 없다."
청크 2: "다만 군 복무로 인한 휴학은 이에 포함하지 않는다. ..."
↑ "다만"이 무엇에 대한 단서인지 알 수 없음
겹침 있음
청크 2: "휴학은 통산 6학기를 초과할 수 없다. 다만 군 복무로 인한
휴학은 이에 포함하지 않는다. ..."
청크에 무엇을 함께 넣을 것인가
청크 본문만 임베딩하면 맥락을 잃습니다. 3항만 떼어 놓으면 그게 무슨 규정의 3항인지 알 수 없습니다.
# 나쁨 — 본문만
text = "3. 신청 기간은 매 학기 개시일로부터 30일 이내로 한다."
# 좋음 — 상위 구조를 헤더로 붙여 함께 임베딩
text = """[학사운영규정 > 제4장 휴학 및 복학 > 제21조 휴학의 신청]
3. 신청 기간은 매 학기 개시일로부터 30일 이내로 한다."""
이 헤더 한 줄이 리콜을 크게 올립니다. “휴학 신청 언제까지”라는 질문의 임베딩과 훨씬 가까워지기 때문입니다.
또 하나 흔한 기법이 부모-자식 분리입니다.
검색은 작은 청크로 (정밀도 ↑)
↓ 맞으면
LLM 에는 그 청크가 속한 큰 문단을 전달 (문맥 ↑)
검색 정밀도와 생성 문맥을 동시에 챙기는 방법으로, 실무에서 효과가 확실합니다.
4. 검색 — 벡터만으로는 부족합니다
여기는 Elasticsearch BM25 와 kNN 벡터 검색 글에서 자세히 다뤘으므로 RAG 관점에서만 짚습니다.
순수 벡터 검색이 실패하는 전형적인 질문들입니다.
| 질문 | 왜 실패하나 |
|---|---|
| “GE1023 과목 뭐예요?” | 과목 코드는 임베딩 공간에서 의미가 없음. 다른 코드와 뭉쳐 있음 |
| “2024학년도 요건이요” | 연도 숫자가 벡터에서 구별되지 않아 2023년 문서가 섞임 |
| “장학금 말고 대출” | 임베딩은 부정을 거의 표현하지 못함. 장학금 문서가 상위에 옴 |
그래서 실무 RAG는 거의 항상 하이브리드입니다. BM25가 코드·연도·고유명사를 잡고, 벡터가 의역·동의어를 잡습니다. 두 결과를 RRF로 섞습니다.
메타데이터 필터도 검색만큼 중요합니다.
질문: "2024학년도 컴퓨터공학과 졸업요건"
→ 필터: year=2024, department=컴퓨터공학과 ← 먼저 좁히고
→ 그 안에서 하이브리드 검색 ← 그다음 검색
연도·학과·문서종류 같은 축은 벡터 유사도에 맡기지 말고 필터로 못 박아야 합니다. 이걸 안 하면 작년 규정이 답변에 섞여 들어가는 사고가 반드시 납니다.
질의 변환
사용자 질문을 그대로 검색에 쓰면 잘 안 될 때가 많습니다.
| 기법 | 내용 | 언제 |
|---|---|---|
| 질의 재작성 | 대화 맥락을 반영해 독립적인 질문으로 바꿈 | 멀티턴 챗봇에서 필수 |
| 질의 분해 | “A와 B의 차이”를 A 검색 + B 검색으로 나눔 | 복합 질문 |
| HyDE | LLM에게 가상의 답변을 쓰게 하고 그 답변을 임베딩해 검색 | 질문과 문서의 어투가 크게 다를 때 |
멀티턴에서 재작성이 왜 필수인지 보면 명확합니다.
User: 휴학 신청 언제까지예요?
Bot: 매 학기 개시일로부터 30일 이내입니다.
User: 그럼 복학은요?
↑ 이걸 그대로 검색하면 "그럼 복학은요?" 로 검색하게 됨
재작성 후: "복학 신청 기간은 언제까지인가?"
5. 재순위화 — 가장 가성비 좋은 개선
검색이 상위 50개를 가져왔습니다. 이걸 그대로 LLM에 넣으면 컨텍스트가 넘칩니다. 상위 5개만 쓰자니 진짜 정답이 12등일 수 있습니다.
재순위화(reranking) 는 검색이 넓게 건진 후보를 더 정확한 모델로 다시 정렬합니다.
1단계 검색 (bi-encoder) 2단계 재순위화 (cross-encoder)
질문과 문서를 따로 인코딩 질문+문서를 함께 인코딩
↓ ↓
미리 계산 가능 → 빠름 매번 계산 → 느림
정확도 보통 정확도 높음
전체 문서 대상 상위 50개만 대상
핵심 차이는 상호작용입니다. bi-encoder는 문서를 미리 벡터로 만들어 두므로 질문을 못 봅니다. cross-encoder는 질문과 문서를 한 번에 읽어 “이 질문에 대해 이 문서가 답이 되는가” 를 직접 판정합니다. 그래서 훨씬 정확하고, 대신 미리 계산할 수 없어 느립니다.
검색 top-50 → 재순위화 → 상위 5개만 LLM 에
(빠름) (정확함) (비싸니 적게)
검색 파이프라인 손볼 곳이 하나뿐이라면 여기가 가장 효과가 큽니다. 임베딩 모델을 바꾸는 것보다 재순위화 한 단계 얹는 쪽이 대체로 더 오릅니다.
6. 컨텍스트 조립 — 순서가 정확도를 바꿉니다
찾은 문서를 프롬프트에 넣을 때 배치 순서가 실제로 영향을 줍니다.
LLM은 긴 컨텍스트에서 앞과 뒤는 잘 보고 가운데는 흘립니다. “Lost in the Middle” 로 알려진 현상입니다.
컨텍스트 10개를 넣었을 때 정답 활용률
위치: 1 2 3 4 5 6 7 8 9 10
활용: 높음 ↘ 최저 ↗ 높음
그래서 가장 관련성 높은 문서를 맨 앞과 맨 뒤에 배치하는 전략을 씁니다.
[1등 문서] ← 앞
[3등] [5등] [4등] ← 가운데 (덜 중요한 것)
[2등 문서] ← 뒤
그 외 실무 규칙입니다.
- 출처를 각 청크에 붙입니다.
[출처: 학사운영규정 제21조]형태로. 답변에 근거를 달게 하려면 필수입니다. - 개수를 무작정 늘리지 않습니다. 컨텍스트가 길수록 비용·지연이 늘고 정확도는 오히려 떨어질 수 있습니다. 3~5개에서 시작합니다.
- 중복을 제거합니다. 같은 내용이 여러 청크에 겹쳐 들어가면 그만큼 자리를 낭비합니다.
프롬프트
당신은 학사 안내 도우미입니다. 아래 [문서]에만 근거하여 답변하세요.
규칙:
- [문서]에 없는 내용은 답하지 마세요. 모르면 "제공된 자료에서 확인할 수 없습니다"라고 하세요.
- 답변의 각 문장 끝에 근거 문서 번호를 [1] 형태로 표기하세요.
- 추측하거나 일반 상식으로 보완하지 마세요.
[문서]
[1] (출처: 학사운영규정 제21조) 휴학 신청은 ...
[2] (출처: 2024 학사 안내 p.34) ...
[질문]
휴학 신청은 언제까지 하나요?
“모르면 모른다고 하라” 는 지시가 환각을 눈에 띄게 줄입니다. 그리고 실제로 답을 못 하는 비율(기권율)을 지표로 관리해야 합니다. 기권율이 0%면 그건 잘하는 게 아니라 모르는 걸 지어내고 있다는 신호입니다.
7. 평가 — 검색과 생성을 따로 재야 합니다
“답변이 좋아졌다”는 감상으로는 개선이 안 됩니다. 두 단계를 분리해서 재야 어디를 고칠지 알 수 있습니다.
검색 평가
정답 문서가 표시된 질문 세트(골든셋)를 만들어 둡니다.
| 지표 | 의미 |
|---|---|
| Recall@k | 정답 문서가 상위 k개 안에 들어왔는가 |
| MRR | 정답 문서가 평균 몇 등에 있는가 (1/등수의 평균) |
| nDCG@k | 순위와 관련도 등급을 함께 반영 |
Recall@k 가 낮으면 생성 쪽은 손댈 필요가 없습니다. 컨텍스트에 정답이 아예 없으니까요. 여기부터 고쳐야 합니다.
생성 평가
검색이 정답을 가져왔다는 전제에서, 답변 자체를 봅니다.
| 지표 | 의미 | 왜 필요한가 |
|---|---|---|
| 충실성(Faithfulness) | 답변의 모든 주장이 컨텍스트에서 뒷받침되는가 | 환각 탐지. 가장 중요 |
| 답변 관련성 | 질문에 실제로 답했는가 | 문서 내용을 요약만 하고 답은 안 하는 경우가 잦음 |
| 컨텍스트 정밀도 | 넣은 문서 중 실제로 쓸모 있던 비율 | 낮으면 재순위화가 필요 |
충실성은 LLM-as-a-judge 로 자동화하는 게 일반적입니다. 답변을 문장 단위로 쪼개고, 각 문장이 컨텍스트에서 추론 가능한지 별도 모델에 묻습니다.
골든셋 만들기
이게 제일 귀찮지만 가장 중요합니다.
- 실제 사용자 질문 로그에서 100~300건을 뽑습니다(가상 질문보다 훨씬 낫습니다)
- 각 질문에 대해 정답이 든 문서(청크) 를 사람이 표시합니다
- 모범 답변도 함께 적어 둡니다
- 파이프라인을 바꿀 때마다 전체를 돌려 지표를 비교합니다
골든셋 없이 RAG를 개선하는 것은 계기판 없이 비행하는 것과 같습니다. 청킹 크기를 바꿨을 때 좋아졌는지 나빠졌는지 알 방법이 없습니다.
8. 자주 밟는 함정
| 증상 | 원인 | 대응 |
|---|---|---|
| 작년 규정으로 답함 | 버전 관리 없음 | 메타데이터에 year·effective_date, 필터로 강제 |
| 문서에 있는데 못 찾음 | 청크 경계가 답을 갈라 놓음 | 겹침 확대, 구조 기반 청킹, 부모-자식 분리 |
| 그럴듯하게 지어냄 | 검색 실패 + 프롬프트에 기권 지시 없음 | Recall 먼저 확인, “모르면 모른다” 지시, 충실성 측정 |
| 코드·숫자 질문에 약함 | 순수 벡터 검색 | 하이브리드(BM25 + kNN) |
| 멀티턴에서 헤맴 | 질의 재작성 없음 | 대화 맥락 반영한 독립 질의 생성 |
| 답변이 장황함 | 컨텍스트 과다 | top-k 축소, 재순위화 |
| 문서 업데이트가 반영 안 됨 | 증분 색인 파이프라인 부재 | 문서 해시 비교 후 변경분만 재색인 |
9. 운영에서 잊기 쉬운 것
증분 색인. 문서가 바뀔 때마다 전체를 다시 임베딩하면 비용이 감당이 안 됩니다. 문서·청크 단위 해시를 저장해 두고 바뀐 것만 갱신합니다.
임베딩 모델 교체는 전체 재색인입니다. 벡터 공간이 달라지므로 기존 벡터와 섞을 수 없습니다. 모델 선택은 초기에 신중히 하고, 교체할 땐 새 인덱스를 만들어 무중단 전환(별칭 스위칭)을 준비해야 합니다.
출처 링크는 기능입니다. 답변만 주는 챗봇은 신뢰를 못 얻습니다. “원문 보기”가 붙어 있으면 사용자가 직접 확인할 수 있고, 틀렸을 때 피드백도 정확해집니다.
답변 불가를 정상 동작으로 설계합니다. 검색 최고 점수가 임계값 미만이면 LLM을 아예 호출하지 않고 “해당 내용은 자료에 없습니다. 학사지원팀(내선 1234)으로 문의해 주세요”로 빠지게 합니다. 지어내는 것보다 훨씬 낫습니다.
정리
| 질문 | 답 |
|---|---|
| 무엇부터 고치나 | 검색. Recall@k 를 먼저 재고, 낮으면 생성은 손대지 않는다 |
| 청킹은 어떻게 | 크기보다 경계. 문서 구조를 쓰고, 상위 제목을 헤더로 붙여 임베딩 |
| 검색은 벡터만으로 되나 | 안 된다. 코드·연도·부정어는 BM25가 잡는다. 하이브리드 + 메타데이터 필터 |
| 가장 가성비 좋은 개선은 | 재순위화(cross-encoder) 한 단계 추가 |
| 컨텍스트는 많이 넣을수록 좋나 | 아니다. 가운데는 흘린다. 3~5개, 중요한 것을 앞뒤에 |
| 환각을 어떻게 줄이나 | 검색 품질 + “모르면 모른다” 지시 + 충실성 측정. 기권율 0%는 경고 신호 |
| 평가는 어떻게 | 골든셋 100~300건. 검색 지표와 생성 지표를 분리해서 측정 |
참고 문헌
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (2020): https://arxiv.org/abs/2005.11401
- Liu et al., Lost in the Middle: How Language Models Use Long Contexts (2023): https://arxiv.org/abs/2307.03172
- Gao et al., Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE, 2022): https://arxiv.org/abs/2212.10496
- Es et al., RAGAS: Automated Evaluation of Retrieval Augmented Generation (2023): https://arxiv.org/abs/2309.15217