RAG(Retrieval-Augmented Generation, 검색 증강 생성)를 처음 접하면 벡터 데이터베이스, 임베딩, 유사도 검색 같은 단어가 먼저 쏟아집니다. 구조가 복잡해 보입니다. 그런데 한 겹만 벗겨 보면 핵심 아이디어는 한 문장으로 끝납니다.
답하는 데 필요한 자료를, 질문을 보내기 직전에 질문 옆에 복사해 붙인다.
이 글은 그 한 문장에서 출발해, 왜 거기서 청킹·하이브리드 검색·리랭킹 같은 기법들이 파생되는지, 그리고 실제로 만들려고 할 때 무엇이 어려운지를 순서대로 정리한 기록입니다.
RAG가 푸는 문제
생성형 LLM은 학습 과정에서 가중치에 흡수한 지식만 가지고 있습니다. 이걸 파라메트릭 지식이라고 부릅니다. 이 경계 바깥에서 네 가지 문제가 생깁니다.
- 전문 지식의 공백 — 학습 데이터에 얕게 담긴 좁고 깊은 주제에는 피상적인 답만 내놓습니다.
- 오래된 정보 — 학습 컷오프 이후에 벌어진 일은 알지 못합니다.
- 사유 데이터 접근 불가 — 사내 위키, 고객 DB, 개인 문서는 애초에 학습에 포함될 수 없습니다.
- 환각 — 모르는 것을 모른다고 말하는 대신, 그럴듯한 문장으로 지어내 설득력 있게 제시합니다.
RAG는 여기에 외부 저장소의 비파라메트릭 지식을 결합합니다. 시험으로 비유하면, 외운 것만으로 푸는 시험이 아니라 자료 반입이 허용된 오픈북 시험을 치르게 하는 셈입니다.
중요한 건 모델의 가중치를 전혀 건드리지 않는다는 점입니다. 재학습 없이, 자료만 갈아 끼우면 답변이 바뀝니다.
"프롬프트에 끼워 넣는다"의 실체
RAG를 설명할 때 가장 자주 흐릿하게 넘어가는 부분이 증강(Augmentation) 단계입니다. 실체는 생각보다 훨씬 단순합니다. 문자열 조립입니다.
우리가 착각하기 쉬운 게, LLM이 질문을 받고 어딘가를 뒤져서 답한다고 생각하는 것입니다. 아닙니다. LLM은 그 순간 프롬프트에 들어 있는 텍스트만 봅니다. 그게 전부입니다.
RAG 없이 보내는 프롬프트는 이렇습니다.
우리 회사 연차 이월 규정이 어떻게 돼?
모델은 사내 규정을 알 리 없으니 일반론을 말하거나 지어냅니다. 반면 RAG가 조립해서 보내는 프롬프트는 이렇습니다.
아래는 컨텍스트 정보입니다.
---------------------
[취업규칙 제32조] 미사용 연차는 익년도 3월 31일까지 이월할 수 있으며,
이월 가능 일수는 최대 5일로 한다. 이월분 미사용 시 수당으로 정산한다.
[인사 FAQ] 이월 신청은 12월 말까지 사내 포털에서 처리한다.
---------------------
주어진 컨텍스트 정보만 사용하고 사전 지식은 사용하지 말고,
다음 질문에 답하세요.
질문: 우리 회사 연차 이월 규정이 어떻게 돼?
답변:
가운데 구분선 사이의 덩어리가 방금 벡터 DB에서 검색해 온 조각들입니다. 사용자는 이걸 보지 못합니다. 사용자가 타이핑한 건 마지막 한 줄뿐이고, 나머지는 시스템이 실시간으로 끼워 넣은 것입니다.
여기서 세 가지 사실이 따라 나옵니다.
모델은 검색을 하지 않습니다. 검색은 RAG 시스템이 미리 끝내고 모델에게는 결과만 넘깁니다. 모델 입장에서는 그냥 긴 프롬프트를 읽고 추론하는 평범한 작업입니다.
검색이 틀리면 회복이 불가능합니다. 엉뚱한 문서를 끼워 넣으면 모델은 그 엉뚱한 문서를 성실하게 요약합니다. 그래서 RAG 평가는 검색기(retriever)와 생성기(generator)를 나눠서 측정합니다. 틀린 답이 나왔을 때 잘못 가져온 것인지, 잘 가져왔는데 잘못 읽은 것인지를 구분해야 하기 때문입니다.
프롬프트에는 정원이 있습니다. 컨텍스트 윈도우가 유한하니 아무거나 다 넣을 수 없습니다. RAG의 기법들이 대부분 이 제약에서 파생됩니다.
두 단계로 나뉘는 흐름
RAG는 시점이 다른 두 단계로 구성됩니다.
인제스천(Ingestion) 은 질문이 오기 전에 미리 지식 베이스를 만들어 두는 준비 과정입니다. 원본 데이터를 임베딩 모델에 통과시켜 벡터화하고, 그 결과를 벡터 데이터베이스에 저장합니다. 임베딩은 텍스트를 다차원 벡터로 표현한 것으로, 의미가 가까운 내용일수록 벡터 공간에서 가까이 놓입니다. 한 번 구축한 지식 베이스는 이후 모든 질의에 재사용됩니다.
추론(Inference) 은 질문이 들어온 순간 실행되는 세 걸음입니다.
| 단계 | 하는 일 |
|---|---|
| Retrieval (검색) | 질문을 같은 벡터 공간에 임베딩하고, 거리 계산으로 가장 가까운 조각들을 찾습니다 |
| Augmentation (증강) | 검색된 데이터를 프롬프트 템플릿에 끼워 넣습니다 |
| Generation (생성) | 증강된 컨텍스트와 모델의 내부 지식을 종합해 답변을 만듭니다 |
그림으로 정리하면 이렇습니다.
[인제스천] 원본 문서 → 청킹 → 임베딩 → 벡터 DB 저장
[추론] 사용자 질문
↓ 임베딩
유사도 검색 ──→ 관련 청크 top-k
↓
프롬프트 템플릿에 삽입
↓
LLM 생성 → 근거가 붙은 답변
파인튜닝과는 목적이 다르다
RAG와 파인튜닝은 경쟁 관계처럼 이야기되지만, 실제로는 겨냥하는 곳이 다릅니다.
| 기준 | RAG | 파인튜닝 |
|---|---|---|
| 모델 변경 | 가중치를 수정하지 않음 | 재학습으로 가중치를 갱신 |
| 비용과 시간 | 상대적으로 낮고 즉시 적용 가능 | 높고 학습·검증에 시간이 필요 |
| 실시간성 | 외부 소스를 동적으로 조회 | 학습 시점에 고정된 정적 지식 |
| 적합한 목적 | 정확도 향상, 환각 감소, 최신·사유 데이터 반영 | 말투·문체·출력 형식 커스터마이징 |
| 지식 갱신 | 문서를 추가·삭제하고 다시 색인 | 데이터셋을 만들어 다시 학습 |
기억하기 쉬운 기준은 이렇습니다. 무엇을 아는가는 RAG, 어떻게 말하는가는 파인튜닝입니다. 정확도를 높이고 환각을 줄이는 방법 중 RAG가 가장 낮은 곳에 달린 열매이고, 파인튜닝은 검색으로 해결되지 않는 문제가 남았을 때 그 위에 얹는 편이 경제적입니다.
자료를 직접 주는 것과 무엇이 다른가
여기서 자연스럽게 드는 의문이 있습니다. 그냥 참고 문서를 통째로 프롬프트에 붙여 놓고 물어보면 되는 것 아닌가?
맞습니다. 그리고 모델이 답을 만드는 순간에는 아무 차이가 없습니다. 둘 다 결국 "프롬프트에 자료가 들어 있는 상태"로 수렴합니다. 차이는 그 자료를 누가, 언제, 몇 개 중에서 골랐는가에 있습니다.
| 문서를 직접 줌 | RAG 구성 | |
|---|---|---|
| 문서 선택 | 사람이 매번 고름 | 유사도 검색이 자동으로 고름 |
| 다룰 수 있는 양 | 컨텍스트 윈도우까지 | 사실상 무제한 |
| 비용 | 매 질문마다 전체 문서 토큰 지불 | 관련 청크 top-k만 지불 |
| 재현성 | 세션에 묶임, 끝나면 소멸 | 인제스천 한 번 → 모든 질의에 재사용 |
문서가 하나뿐이면 직접 주는 게 낫습니다. 더 빠르고, 더 정확하고, 검색 실패가 없습니다. RAG를 구성할 이유가 없습니다.
문제는 문서가 3만 페이지일 때 생깁니다. 물리적으로 컨텍스트에 들어가지 않습니다. 그래서 "전부 다 준다"를 포기하고 "질문마다 필요한 것만 골라서 준다"로 전략을 바꾸는 것 — 그게 RAG입니다.
그리고 이 전환에는 대가가 따릅니다. 검색 실패라는 새로운 실패 지점이 생깁니다. 사람이 직접 자료를 고를 때는 존재하지 않던 문제입니다. RAG를 구성한다는 건 편의와 규모를 얻는 대신 "자료 고르기"라는 새 문제를 떠안는 일입니다.
"무제한"의 정체
앞의 표에 "사실상 무제한"이라고 썼는데, 정확히는 저장·검색 범위가 무제한이지 모델이 읽는 양이 무제한인 게 아닙니다. 구조를 보면 깔때기입니다.
벡터 DB 3,000,000 청크 ← 여기가 "무제한"
↓ 유사도 검색
후보 top-k 5~100 청크
↓ 리랭킹
프롬프트 삽입 3~5 청크 ← 여기는 여전히 좁음
↓
LLM
문서가 3만 개든 300만 개든 모델이 실제로 읽는 건 항상 몇 개입니다. 늘어나는 건 "고를 대상"이지 "읽을 양"이 아닙니다. 컨텍스트 윈도우 제약은 하나도 완화되지 않았고, 그 앞단에 필터를 하나 세운 것뿐입니다.
그럼 300만 개를 매번 다 뒤지는가 하면, 그것도 아닙니다. 순진하게 하면 질문 벡터와 300만 개 벡터의 거리를 전부 계산해야 하고, 이러면 데이터가 2배가 될 때 시간도 2배가 되어 무제한이 성립하지 않습니다.
실제 벡터 DB는 ANN(Approximate Nearest Neighbor, 근사 최근접 이웃) 을 씁니다. 벡터들을 미리 그래프처럼 엮어 두고, 검색할 때 전체를 훑지 않고 가까워 보이는 방향으로만 몇 단계 점프합니다. 정확한 답 대신 거의 정확한 답을 받아들이는 대가로, 탐색 시간이 데이터 양에 로그 비례로 증가합니다.
도서관에서 책을 찾을 때 첫 칸부터 훑지 않고 분류 기호를 따라 몇 번 방향만 꺾어 도착하는 것과 같습니다. 장서가 10배 늘어도 발걸음 수는 몇 걸음만 늘어납니다.
다만 용량은 버텨도 품질은 버티지 못합니다. 규모가 커질수록 비슷한 문서가 많아져 진짜 정답이 후보 밖으로 밀리고, 엉뚱한 부서나 연도의 문서가 섞입니다. 이 지점에서 이른바 고급 기법들이 등장합니다.
| 기법 | 어느 단계 | 푸는 문제 |
|---|---|---|
| 메타데이터 필터링 | 검색 이전 | 날짜·부서·문서 유형으로 검색 범위를 미리 좁힘 |
| 청킹 | 검색 이전 | 문서를 검색 단위로 분할. 크면 잡음, 작으면 맥락 소실 |
| 하이브리드 검색 | 검색 | 의미 검색이 놓치는 고유명사·코드·약어를 키워드 매칭으로 보완 |
| 리랭킹 | 검색 이후 | 진짜 관련 있는 조각을 상위로 재배치 |
전부 규모가 커졌을 때 무너지는 검색 정밀도를 떠받치는 장치입니다. 문서 100개짜리 RAG에는 리랭커가 필요 없지만, 300만 개가 되면 없이는 쓰기 어렵습니다.
리랭킹이 기술적으로 하는 일
리랭킹은 "순서를 다시 매긴다"고만 설명되곤 하는데, 그 안을 열어 보면 애초에 왜 순서가 틀리는가에 답이 있습니다.
인제스천 시점에 문서를 임베딩합니다. 그런데 그 시점에 질문이 뭔지 모릅니다.
[인제스천 시점] 문서 → 임베딩 → 벡터 저장 (질문이 아직 존재하지 않음)
[검색 시점] 질문 → 임베딩 → 거리 계산
즉 검색은 질문 벡터 하나와 문서 벡터 하나의 거리 비교입니다. 두 벡터는 서로를 본 적이 없습니다. 이 방식을 바이인코더(bi-encoder)라고 부릅니다. 문서 한 페이지를 벡터 하나로 압축하는 순간 정보가 대량으로 손실되고, "어떤 질문이 올지 모르니 평균적으로 쓸 만한 요약"에 가까운 것이 만들어집니다. 그래서 주제는 맞는데 정작 답이 없는 문서가 상위에 올라옵니다.
리랭커는 크로스인코더(cross-encoder)입니다. 벡터 거리를 재는 대신, 질문과 문서를 하나의 입력으로 이어 붙여 모델에 통과시킵니다.
바이인코더 (1차 검색)
embed("연차 이월 규정?") → [0.2, -0.7, ...]
embed("취업규칙 제32조...") → [0.3, -0.6, ...] ← 따로 계산, 미리 저장됨
cosine similarity → 0.81
크로스인코더 (리랭킹)
model("연차 이월 규정? [SEP] 취업규칙 제32조...") ← 같이 입력
→ 0.94 ← 관련성 점수 하나
차이가 결정적입니다. 크로스인코더 안에서는 어텐션이 질문의 토큰과 문서의 토큰을 직접 연결합니다. "이월"이라는 단어가 문서의 어느 문장과 맞물리는지를 실제로 계산합니다. 바이인코더는 구조상 이게 불가능합니다. 문서를 벡터로 만들 때 질문이 없었기 때문입니다.
그렇다면 왜 처음부터 크로스인코더를 쓰지 않을까요. 쓸 수가 없습니다. 미리 계산해 둘 수 없으니 300만 개 문서에 적용하려면 질문 하나당 300만 번 모델을 돌려야 합니다. 그래서 2단계로 나눕니다.
| 단계 | 모델 | 대상 | 목표 | 비용 |
|---|---|---|---|---|
| 1차 검색 | 바이인코더 + ANN | 300만 | 재현율 — 정답을 후보에 포함시키기 | 매우 낮음 |
| 리랭킹 | 크로스인코더 | 50~100 | 정밀도 — 그중 진짜를 1등으로 | 높음 |
1차는 넉넉하게 건지고, 2차는 깐깐하게 고릅니다. 리랭킹을 전제하면 1차 검색을 top-5가 아니라 top-100으로 넓게 잡을 수 있게 되는데, 실은 이쪽이 더 큰 이득입니다. 1차에서 놓친 문서는 리랭커가 되살릴 방법이 없습니다. 후보에 없으면 끝입니다.
순서가 중요한 이유도 둘입니다. 하나는 자를 수 있게 되는 것입니다. top-100 중 상위 3~5개만 프롬프트에 넣고 나머지는 버려 토큰과 잡음을 함께 줄입니다. 다른 하나는 위치가 성능을 바꾼다는 점입니다. LLM은 프롬프트의 처음과 끝을 잘 보고 가운데를 흘리는 경향이 있어서, 정답 조각이 10개 중 7번째에 묻혀 있으면 분명히 프롬프트 안에 있는데도 모델이 못 찾는 일이 생깁니다.
정리하면 리랭킹은 빠르지만 대충 보는 검색이 뽑은 후보를, 느리지만 제대로 읽는 모델이 다시 채점하는 것입니다. 싼 방법으로 범위를 좁히고 비싼 방법을 좁은 범위에만 쓰는 전형적인 2단계 전략입니다.
직접 만들면 무엇이 필요한가
여기까지 이해했다면 자연스럽게 "그래서 직접 만들 수 있나"라는 질문이 따라옵니다.
필요한 부품은 넷입니다. 언어와 무관합니다.
| 부품 | 역할 | 조달 |
|---|---|---|
| 임베딩 모델 | 텍스트 → 벡터 | API 또는 로컬 모델 |
| 벡터 저장소 | 벡터 보관 + 유사도 검색 | 전용 DB 또는 기존 DB의 벡터 확장 |
| LLM | 최종 답변 생성 | API 또는 로컬 모델 |
| 접착 코드 | 위 셋을 잇는 파이프라인 | 직접 작성 |
프레임워크가 대신해 주는 건 사실상 네 번째뿐입니다. 앞의 셋은 프레임워크를 써도 어차피 직접 고르고 붙여야 합니다.
그리고 그 네 번째, 즉 핵심 로직은 놀랍도록 짧습니다.
[인제스천] (한 번만)
1. 원본 문서 로드
2. 청킹 chunks = split(text, size=800, overlap=100)
3. 임베딩 vectors = embed(chunks)
4. 저장 store.insert(chunks, vectors, metadata)
[추론] (질문마다)
5. 질문 임베딩 qv = embed(question)
6. 유사도 검색 top = store.search(qv, k=5)
7. 프롬프트 조립 prompt = template(context=join(top), query=question)
8. 생성 answer = llm(prompt)
대략 50줄 안쪽입니다. 문서 수백 개 규모의 내부 도구라면 이걸로 충분하고, 프레임워크를 얹으면 오히려 손해입니다.
그런데 진짜 일은 빈칸에 있다
위 8단계는 뼈대일 뿐이고, 실무 시간을 잡아먹는 건 각 줄 안쪽입니다.
문서 파싱 — 가장 과소평가되는 단계입니다. PDF·오피스 문서·HTML을 텍스트로 바꾸는 일인데, 표가 깨지고 2단 편집이 뒤섞이고 스캔본은 OCR이 필요합니다. 실무 시간의 절반이 여기서 사라진다고 해도 과장이 아닙니다.
청킹 전략 — 크면 잡음, 작으면 맥락 소실이라는 딜레마에 더해, 정답이 문서 종류마다 다릅니다. 법률 조문은 조 단위로, 매뉴얼은 섹션 단위로, 대화 로그는 턴 단위로 잘라야 합니다. 고정 길이로 자르는 게 기본이지만 거의 항상 최선이 아닙니다.
대량 임베딩의 운영 — 수만 청크를 넣으면 반드시 중간에 실패합니다. 레이트 리밋, 타임아웃, 비용 상한. 어디까지 처리했는지 기록하고 이어가는 로직이 필요합니다.
인덱스 갱신 — 직접 구현에서 가장 흔한 함정입니다. 원본 문서가 수정되면 어떤 청크를 지우고 무엇을 다시 넣을지 추적해야 합니다. 이걸 만들지 않으면 최신 문서와 낡은 문서가 벡터 DB에 공존하고, 검색기가 둘 다 가져와 모순된 답이 나옵니다. 초기엔 보이지 않다가 몇 달 뒤에 터집니다.
평가 — 없으면 개선이 불가능합니다. 청킹 크기를 800에서 500으로 바꿨을 때 좋아졌는지 나빠졌는지, 평가셋 없이는 감으로 판단하게 됩니다. 직접 구현의 가장 치명적인 공백입니다.
관측 — 이 답변에 어떤 청크가 왜 뽑혔는지를 로깅하지 않으면 디버깅이 점집이 됩니다.
한 줄로 요약하면 이렇습니다. 만드는 데 하루, 쓸 만하게 만드는 데 몇 달. 데모는 첫날 동작합니다. 문제는 "왜 이 질문엔 엉뚱한 답을 하지?"가 시작된 뒤이고, 위 항목을 하나씩 직접 구현하다 보면 결국 프레임워크를 다시 발명하는 경로로 들어섭니다.
덧붙여, 벡터 저장소는 직접 만들 영역이 아닙니다. 초기엔 배열 전수 비교로 충분하지만 수만 건을 넘기면 ANN 인덱스가 필요하고, 이건 직접 구현할 성질의 것이 아닙니다.
그럼에도 직접 만들 만한 경우는 있습니다. 파이프라인을 완전히 이해하고 통제하고 싶을 때(학습 목적으로는 오히려 권장합니다), 요구사항이 단순하고 고정적일 때, 프레임워크의 추상화가 오히려 방해될 만큼 특수한 흐름일 때입니다.
프레임워크 고르기
널리 쓰이는 선택지는 대부분 파이썬 생태계에 있습니다. 우연이 아니라 임베딩 모델, 리랭커, 평가 도구, 파서가 파이썬 우선으로 나오기 때문입니다. 특별한 이유가 없다면 파이썬이 가장 마찰이 적습니다.
| 프레임워크 | 강점 | 적합한 경우 | 주의점 |
|---|---|---|---|
| LlamaIndex | 데이터 연결·인덱싱 특화. 파싱·청킹·재색인 기본 제공 | 문서 Q&A가 목적일 때 | 복잡한 제어 흐름에는 표현력 부족 |
| LangChain / LangGraph | 범용 조립 + 방대한 통합. 그래프 기반 제어 흐름 | 검색→판단→재검색 같은 에이전틱 흐름 | 추상화가 두꺼워 단순 RAG엔 학습비용 과다 |
| DSPy | LLM과 검색 모델을 함께 최적화 | 품질을 체계적으로 끌어올릴 때 | 사고방식이 달라 러닝커브가 높음 |
| Elysia | 설정 없이 곧바로 에이전틱 RAG 체험 | 프로토타이핑, 감 잡기 | 프로덕션 설계용은 아님 |
기본값은 LlamaIndex로 두는 편이 무난합니다. RAG를 만든다는 말의 대부분은 "문서 Q&A를 만든다"는 뜻이고, 직접 구현에서 가장 아픈 부분인 파싱·청킹·재색인이 정확히 LlamaIndex가 잘하는 영역입니다.
흐름이 복잡해지면 LangGraph로 갑니다. "한 번 검색하고 한 번 답한다"로 안 되는 순간 — 재검색, 분기, 다단계 추론 — 이 지점에서 값어치가 생깁니다. 처음부터 이걸로 시작할 필요는 없습니다.
파이썬이 아닌 언어라면 파이썬 프레임워크를 억지로 끌어오기보다 벡터 DB SDK와 LLM SDK를 직접 호출하는 편이 낫습니다. 위 8단계는 어느 언어에서든 짧게 구현되고, 언어 생태계가 얇을수록 프레임워크의 이점보다 제약이 커집니다.
프레임워크보다 먼저 정할 것
마지막으로, 프레임워크 선택은 사실 우선순위가 높지 않습니다. 그보다 먼저 정해야 할 것이 셋 있습니다.
- 임베딩 모델 — 검색 품질의 상한을 결정합니다. 대상 언어에서 검증된 모델인지가 성패를 가릅니다.
- 벡터 저장소 — 이미 쓰는 RDB에 벡터 확장이 있다면 그게 가장 저렴한 선택입니다.
- 평가셋 — 질문 30개와 기대 답변. 검색기와 생성기를 나눠 평가하라는 원칙이 여기서 구체화됩니다.
프레임워크는 나중에 갈아탈 수 있습니다. 하지만 3번이 없으면 무엇을 갈아타야 하는지조차 알 수 없습니다.
정리
RAG를 한 문장으로 줄이면 "모델을 가르치는 대신, 시험 직전에 컨닝페이퍼를 책상에 올려 주는 것" 입니다. 그래서 재학습이 필요 없고, 자료만 바꾸면 답변이 즉시 바뀝니다.
동시에 RAG는 컨텍스트 윈도우 문제를 해결한 것이 아니라 회피한 것입니다. "다 읽는 건 포기하고, 잘 고르는 데 걸겠다"는 거래입니다. 그래서 RAG 시스템의 성패는 거의 전부 검색기 품질에 달려 있고, 청킹·하이브리드 검색·리랭킹 같은 기법들이 전부 그 한 곳을 향합니다.
만들기는 쉽고 잘 만들기는 어렵다는 말이, 이만큼 정확하게 들어맞는 주제도 드뭅니다.
