LLM을 실제 서비스에 붙이다 보면 곧 한계가 보인다.
모델이 학습하지 않은 사내 문서나 최신 데이터를 물어보면 제대로 답하지 못할 수 있고, 알고 있더라도 정확하지 않은 내용을 만들어낼 수 있다.
그렇다고 회사의 모든 문서를 LLM에 다시 학습시키는 것도 현실적인 방법은 아니다.
이때 자주 사용하는 구조가 RAG(Retrieval-Augmented Generation)다.
RAG를 이해하려면 먼저 한 가지 관점을 잡아두는 게 좋다.
LLM에게 모든 지식을 외우게 만드는 대신, 필요한 정보를 외부에서 찾아서 LLM에게 제공한다.

그리고 이때 데이터를 의미 기반으로 검색하기 위해 자주 사용하는 것이 Vector DB다.
LLM이 모든 정보를 가지고 있어야 할까
예를 들어 회사 내부 개발 문서를 기반으로 답변하는 챗봇을 만든다고 해보자.
문서에는 이런 내용이 있다고 하자.
InventorySystem은 아이템 데이터를 서버에서 관리한다.
아이템 변경 요청은 InventoryService를 통해 처리한다.
사용자가 질문한다.
아이템 변경 요청은 어디서 처리해?
LLM이 이 내용을 이미 학습했다면 답변할 수 있다.
문제는 이 문서가 LLM의 학습 데이터에 포함되어 있지 않은 경우다.
회사 내부 문서는 당연히 공개된 학습 데이터에 포함되어 있지 않을 가능성이 높고, 문서 내용도 계속 바뀐다.
이럴 때 질문할 때마다 관련 문서를 함께 전달하면 된다.
질문 + 관련 문서-> LLM-> 답변
LLM은 문서에서 필요한 정보를 참고해서 답변을 생성한다.
RAG의 기본 아이디어는 여기서 시작한다.
RAG는 무엇인가
RAG는 Retrieval-Augmented Generation의 약자다.
말 그대로 정보를 검색(Retrieval)한 뒤 생성(Generation)을 보강(Augmented)하는 구조다.
전체적인 흐름은 다음과 같다.

여기서 중요한 부분은 LLM이 직접 데이터베이스를 뒤지는 것이 아니라는 점이다.
별도의 검색 시스템이 관련 정보를 찾아주고, 그 결과를 LLM에게 전달한다.
따라서 RAG 시스템은 크게 두 부분으로 나눠서 생각할 수 있다.
Retrieval-> 필요한 정보를 찾는 부분
Generation-> 찾은 정보를 이용해 답변을 만드는 부분
이 둘을 분리해서 생각하면 RAG 구조가 훨씬 명확해진다.
먼저 문서를 저장해야 한다
사용자가 질문할 때 바로 검색하려면 사전에 문서를 검색하기 좋은 형태로 만들어두어야 한다.
원본 문서가 있다고 하자.
게임 서버 개발 문서.md
내용이 수백 페이지라면 이 전체를 하나의 검색 단위로 사용하기 어렵다.
그래서 문서를 적절한 크기의 Chunk로 나눈다.
원본 문서-> Chunk 1-> Chunk 2-> Chunk 3-> ...
예를 들어,
Chunk 1
InventorySystem의 역할과 구조
Chunk 2
InventoryService의 아이템 변경 처리
Chunk 3
아이템 저장 및 동기화 방식
같은 식으로 나눌 수 있다.
Chunk 크기를 너무 크게 만들면 검색했을 때 불필요한 내용이 많이 따라온다.
반대로 너무 작게 만들면 문맥이 잘려서 검색 결과만으로 내용을 이해하기 어려워질 수 있다.
그래서 Chunking은 RAG 성능에 영향을 주는 중요한 설계 요소다.
Vector는 왜 필요한가
문서를 Chunk로 나눴다고 바로 검색할 수 있는 것은 아니다.
일반적인 문자열 검색이라면 질문과 문서에 같은 단어가 들어 있는지를 기준으로 찾을 수 있다.
예를 들어,
질문:
아이템 변경 요청은 어디서 처리해?
문서에 정확히 “아이템 변경 요청”이라는 표현이 있어야 잘 찾을 가능성이 높다.
하지만 실제 사용자는 문서와 똑같은 표현을 사용하지 않을 수 있다.
질문: 인벤토리의 아이템을 수정하는 코드는 어디에 있어?
문서에는 “아이템 변경 요청”이라고 적혀 있다면 단순한 키워드 검색만으로는 원하는 문서를 놓칠 수도 있다.
이런 의미 기반 검색을 위해 사용하는 방법 중 하나가 Embedding이다.
Embedding으로 문서를 벡터로 바꾼다
Embedding은 텍스트를 숫자로 이루어진 벡터로 변환하는 방법이다.
예를 들어 문장을 벡터로 표현하면 개념적으로 다음과 같은 형태가 된다.
"아이템 변경 요청"-> [0.12, -0.34, 0.87, ...]
실제 벡터는 훨씬 더 많은 차원을 가질 수 있다.
중요한 것은 문장을 단순한 문자열이 아니라 의미를 어느 정도 반영한 숫자 공간의 좌표로 표현한다는 것이다.
예를 들어 의미가 비슷한 문장은 벡터 공간에서도 가까운 위치에 나타날 수 있다.

반대로 의미가 전혀 다른 문장은 상대적으로 멀리 위치할 수 있다.
이런 특성을 이용하면 정확히 같은 단어가 들어 있지 않더라도 의미적으로 가까운 문서를 찾을 수 있다.
Vector DB는 무엇을 저장하는가
이제 문서 Chunk를 Embedding으로 변환했다고 하자.
문서 Chunk-> Embedding Model-> Vector
이 벡터들을 저장하고 검색하기 위한 데이터베이스가 Vector Database다.
Vector DB에는 보통 벡터뿐만 아니라 원본 텍스트와 관련 메타데이터도 함께 저장한다.
개념적으로는 이런 형태다.
ID
Vector
Text
Metadata
예를 들어,
ID: 1024
Vector:
[0.12, -0.34, 0.87, ...]
Text:
"아이템 변경 요청은 InventoryService를 통해 처리한다."
Metadata:
document = inventory.md
section = InventoryService
사용자가 질문하면 질문도 같은 Embedding Model을 이용해 벡터로 변환한다.
사용자 질문-> Embedding Model-> Query Vector
그리고 이 Query Vector와 가까운 벡터를 Vector DB에서 검색한다.
Vector Search는 어떻게 동작하는가
개념적으로는 이런 구조다.

여기서 “비슷하다”는 것을 계산하기 위해 벡터 간 거리를 사용한다.
대표적으로 Cosine Similarity를 사용할 수 있다.
cosine similarity
= (A · B) / (||A|| ||B||)
두 벡터의 방향이 얼마나 비슷한지를 측정하는 방식이다.
벡터의 크기 자체보다 방향을 비교하기 때문에 텍스트 Embedding의 유사도를 계산할 때 자주 사용된다.
Vector DB는 이런 벡터 검색을 효율적으로 수행하기 위한 인덱싱과 검색 기능을 제공한다.
데이터가 적다면 모든 벡터를 직접 비교해도 되지만, 데이터가 수십만 개, 수백만 개로 늘어나면 매번 전부 비교하는 방식은 비효율적이다.
그래서 실제 Vector DB에서는 ANN(Approximate Nearest Neighbor) 같은 검색 방법과 다양한 인덱스 구조를 사용한다.
RAG 전체 과정을 연결해보면
문서를 저장하는 단계와 사용자가 질문하는 단계가 조금 다르다.
먼저 데이터를 준비한다.

이 과정을 보통 Indexing 단계라고 볼 수 있다.
그 다음 사용자가 질문한다.

이것이 RAG의 기본적인 구조다.
예를 들어 사용자가
아이템 변경 요청은 어디서 처리해?
라고 질문했다고 하자.
Vector DB에서 관련도가 높은 Chunk가 검색된다.
"아이템 변경 요청은 InventoryService를 통해 처리한다."
이 정보를 Prompt에 넣는다.
다음 문서를 참고해서 질문에 답변해라.
[검색된 문서]
아이템 변경 요청은 InventoryService를 통해 처리한다.
[질문]
아이템 변경 요청은 어디서 처리해?
그리고 LLM이 최종 답변을 생성한다.
아이템 변경 요청은 InventoryService를 통해 처리합니다.
RAG에서 검색 품질이 중요하다
RAG라고 해서 무조건 정확한 답변이 나오는 것은 아니다.
LLM에게 좋은 정보를 전달하려면 먼저 좋은 검색 결과가 나와야 한다.
예를 들어 질문에 대한 정답이 문서에 존재하더라도 검색 결과 상위에 엉뚱한 Chunk만 들어왔다면 LLM은 필요한 정보를 받지 못한다.

따라서 RAG 시스템의 문제를 무조건 LLM의 문제라고 생각하면 안 된다.
실제로는 여러 단계에서 문제가 발생할 수 있다.

RAG를 구축할 때 각각을 따로 평가해야 하는 이유다.
Chunking이 생각보다 중요하다
문서를 단순히 일정한 글자 수로 자르는 방법도 있지만, 문서의 구조를 고려하는 방법도 있다.
예를 들어 API 문서라면 클래스나 메서드 단위로 나누는 것이 더 적절할 수 있다.
InventorySystem-> 클래스 설명
AddItem()-> 메서드 설명
RemoveItem()-> 메서드 설명
반대로 긴 기술 문서라면 문단이나 제목 단위로 나누는 편이 더 적절할 수 있다.
또한 Chunk를 서로 조금 겹치게 만드는 Overlap을 사용할 수도 있다.
Chunk 1
[문장 1][문장 2][문장 3][문장 4]
Chunk 2
[문장 4][문장 5][문장 6][문장 7]
이렇게 하면 Chunk 경계에서 문맥이 끊기는 문제를 어느 정도 줄일 수 있다.
하지만 무조건 작은 Chunk나 큰 Chunk가 좋은 것은 아니다.
문서의 형태와 질문의 특성에 맞게 결정해야 한다.
검색 결과를 그대로 다 넣으면 될까
검색된 Chunk가 많다고 해서 전부 LLM에게 전달하는 것도 좋은 방법은 아니다.
LLM에 너무 많은 불필요한 정보를 넣으면 입력이 길어지고, 관련 없는 정보가 섞일 수 있다.
그래서 보통 상위 몇 개의 검색 결과를 사용하거나, 검색 결과를 다시 평가하는 단계를 추가하기도 한다.

여기서 Reranker는 검색된 후보들을 다시 평가해서 질문과 더 관련 있는 문서의 순서를 정리하는 역할을 한다.
이런 구조를 사용하면 단순 Vector Search만 사용하는 것보다 검색 품질을 개선할 수 있다.
Vector DB만 사용해야 하는 것은 아니다
Vector Search가 모든 검색 문제를 해결하는 것은 아니다.
예를 들어 다음과 같은 질문을 생각해보자.
InventoryService의 GetItem 함수가 있는 문서를 찾아줘.
여기서는 GetItem이라는 정확한 문자열이 중요할 수 있다.
이런 경우에는 키워드 기반 검색이 오히려 유리할 수 있다.
그래서 실제 검색 시스템에서는 Keyword Search와 Vector Search를 함께 사용하는 Hybrid Search를 사용하기도 한다.

키워드 검색은 정확한 이름이나 코드, 고유명사를 찾는 데 강하고 Vector Search는 표현이 달라도 의미가 비슷한 내용을 찾는 데 강하다.
둘을 조합하면 서로의 약점을 보완할 수 있다.
RAG와 Fine-tuning은 목적이 다르다
RAG를 공부하다 보면 Fine-tuning과 비교하게 된다.
둘은 해결하려는 문제가 다르다.
RAG는 외부 데이터를 검색해서 모델의 입력에 제공한다.
외부 데이터 -> 검색 -> Prompt에 추가 -> LLM
Fine-tuning은 기존 모델을 추가 학습시켜 특정 작업이나 행동 패턴에 맞게 조정한다.
기존 모델 -> 추가 학습 -> 조정된 모델
예를 들어 회사 내부 문서를 계속 참고해야 하는 시스템이라면 RAG가 적합할 가능성이 높다.
반면 특정한 출력 형식이나 작업 방식을 모델 자체가 잘 수행하도록 만들고 싶다면 Fine-tuning을 고려할 수 있다.
특히 자주 변경되는 정보를 모델의 파라미터에 직접 학습시키는 것은 관리하기 어렵다.
문서가 변경될 때마다 다시 학습해야 하기 때문이다.
RAG는 문서만 업데이트하면 검색 대상도 바뀐다.
RAG의 전체 구조
지금까지의 내용을 하나로 연결하면 다음과 같다.

여기서 Vector DB는 관련 정보를 찾는 역할을 담당하고, LLM은 찾아온 정보를 바탕으로 답변을 생성하는 역할을 담당한다.
둘을 같은 것으로 생각하면 RAG 구조가 헷갈린다.
Vector DB가 답변을 만들어주는 것도 아니고, LLM이 Vector DB에서 직접 검색하는 것도 아니다.
각각의 역할이 다르다.
실제 서비스에서는 더 복잡해진다
간단한 RAG는 구현하기 어렵지 않다.
하지만 실제 서비스에 넣으려면 생각해야 할 것이 많아진다.
문서가 업데이트되면 Embedding을 다시 생성해야 할 수도 있고, 같은 문서가 중복으로 들어가는 것도 관리해야 한다.
검색 결과가 적절한지도 평가해야 하고, 검색된 문서가 답변에 실제로 사용됐는지도 확인할 필요가 있다.
또한 권한 문제도 있다.
예를 들어 사용자가 접근할 수 없는 사내 문서를 Vector DB에서 검색해버리면 큰 문제가 된다.
따라서 실제 시스템에서는

처럼 접근 제어까지 고려해야 한다.
RAG는 단순히 Vector DB 하나 붙인다고 끝나는 기능이 아니다.
정리
RAG의 기본 아이디어는 단순하다.
LLM이 모든 정보를 가지고 있도록 만드는 대신, 필요한 정보를 검색해서 LLM의 입력으로 제공한다.
문서는 Chunk로 나누고 Embedding을 통해 벡터로 변환한다.
문서-> Chunk-> Embedding-> Vector DB
사용자가 질문하면 질문 역시 벡터로 변환하고 관련 문서를 검색한다.
질문-> Embedding-> Vector Search-> 관련 문서
검색된 문서를 LLM에게 전달하면 LLM이 해당 정보를 참고해서 답변을 생성한다.
검색 결과 + 질문-> LLM-> 답변
결국 RAG에서 중요한 것은 Vector DB 자체가 아니다.
어떤 데이터를 어떻게 나누고, 어떤 방식으로 검색하고, 검색 결과를 어떻게 LLM에게 전달할 것인가가 전체 품질을 결정한다.
그래서 RAG를 구현할 때도 “어떤 Vector DB를 사용할까?”부터 시작하기보다는
어떤 데이터를 가지고 있는가?
-> 사용자는 어떤 질문을 하는가?
-> 어떤 단위로 검색해야 하는가?
-> 검색 결과가 충분히 정확한가?
-> LLM에게 어떤 Context를 전달할 것인가?
순서로 문제를 보는 편이 낫다.
Vector DB는 그 구조 안에서 의미 기반 검색을 담당하는 하나의 구성 요소일 뿐이다.