1. RAG
1. RAG란?
RAG(Retrieval-Augmented Generation, 검색 증강 생성)는 사용자의 질문과 관련된 정보를 검색하여 LLM의 Context에 넣고, 해당 정보를 기반으로 답변을 생성하도록 하는 방식이다.
RAG라는 이름 그대로 다음 세 단계로 동작한다.
1. Retrieval
→ 질문과 관련된 문서를 검색
2. Augmented
→ 검색한 문서를 Context에 추가
3. Generation
→ LLM이 해당 문서를 기반으로 답변 생성
즉,
검색하고(R) → 넣고(A) → 답변을 생성한다(G).
2. RAG가 필요한 이유
LLM에는 몇 가지 한계가 있다.
지식의 단절(Knowledge Cutoff)
LLM은 학습된 시점 이후의 정보를 기본적으로 알 수 없다.
폐쇄 데이터(Private Data)
사내 문서나 내부 데이터처럼 학습 데이터에 포함되지 않은 정보는 알 수 없다.
할루시네이션(Hallucination)
모르는 내용에 대해서도 그럴듯한 답변을 만들어낼 수 있다.
예를 들어 사내 규정을 알려주는 챗봇을 만든다고 하자.
사용자
"우리 회사 연차는 며칠인가요?"
LLM
→ 회사 내부 규정을 알지 못함
이때 관련 사내 문서를 검색하여 질문과 함께 전달하면 된다.
사용자 질문
↓
관련 사내 문서 검색 ← Retrieval
↓
질문 + 검색된 문서 ← Augmented
↓
LLM
↓
문서를 기반으로 답변 ← Generation
RAG는 모델 자체를 학습시키는 것이 아니라 모델에게 답변에 필요한 자료를 제공하는 방식이다.
2. Embedding
1. 기존 문자열 검색의 한계
관련 문서를 찾는 가장 간단한 방법은 문자열 검색이다.
SELECT *
FROM documents
WHERE content LIKE '%병가%';
하지만 사용자가 반드시 문서와 같은 단어를 사용하는 것은 아니다.
문서
"병가 신청 규정"
사용자
"아파서 쉬고 싶은데 어떻게 해야 하나요?"
LIKE 검색은 문자열이 일치하는지 확인하기 때문에 두 문장의 의미가 비슷하다는 것을 판단할 수 없다.
따라서 RAG에서는 글자가 아니라 의미가 비슷한 문서를 검색할 방법이 필요하다.
2. Embedding이란?
Embedding(임베딩)은 텍스트의 의미를 숫자로 이루어진 Vector(벡터)로 변환하는 것이다.
"아파서 쉬고 싶어요"
↓
Embedding Model
↓
[0.021, -0.134, 0.892, ...]
의미가 비슷한 문장은 비슷한 위치의 벡터로 표현된다.
"아파서 쉬고 싶어요"
[0.021, -0.134, 0.892, ...]
"병가 신청 규정"
[0.019, -0.128, 0.885, ...]
→ 벡터가 가까움
→ 의미가 비슷함
반대로 의미가 다른 문장은 벡터 사이의 거리도 멀어진다.
즉,
텍스트의 의미를 숫자로 변환하여 의미가 비슷한지를 거리 계산 문제로 바꾸는 것
이라고 이해할 수 있다.
3. Dimension
벡터를 구성하는 숫자의 개수를 차원(Dimension)이라고 한다.
[0.021, -0.134, 0.892, ...]
↑
여러 개의 숫자로 구성
임베딩 모델마다 사용하는 차원이 다를 수 있다.
중요한 점은 문서를 저장할 때와 질문을 검색할 때 동일한 임베딩 모델과 동일한 차원을 사용해야 한다는 것이다.
서로 다른 임베딩 모델을 사용하면 좌표계 자체가 달라지기 때문에 벡터 간 거리를 비교하는 의미가 없어진다.
3. Vector DB
1. Vector DB란?
임베딩한 벡터를 저장하고 주어진 벡터와 가까운 벡터를 빠르게 검색하기 위한 저장소이다.
일반적인 관계형 DB와 검색 방식에서 차이가 있다.
| 일반 DB | Vector DB |
| 문자열, 숫자, 날짜 등을 저장 | 고차원 Vector 저장 |
| 정확한 값이나 문자열 검색 | 벡터 간 유사도 검색 |
| =/ LIKE 등 | 가장 가까운 Vector 검색 |
| B-Tree 등의 인덱스 | HNSW 등의 Vector Index |
일반적인 검색이
"이 글자가 포함되어 있는가?"
를 찾는다면 Vector Search는
"이 질문과 의미가 가장 비슷한 것은 무엇인가?"
를 찾는다.
2. pgvector
pgvector는 PostgreSQL에서 Vector를 저장하고 검색할 수 있도록 제공하는 Extension이다.
별도의 Vector DB를 구축하지 않고 기존 PostgreSQL에 Vector 검색 기능을 추가할 수 있다는 장점이 있다.
CREATE EXTENSION IF NOT EXISTS vector;
Spring AI에서는 VectorStore를 통해 pgvector와 같은 Vector DB를 추상화해서 사용할 수 있다.
vectorStore.add(documents);
vectorStore.similaritySearch(searchRequest);
VectorStore를 사용하면 저장소가 달라져도 애플리케이션에서 사용하는 인터페이스를 최대한 동일하게 유지할 수 있다.
4. RAG의 Indexing 과정
RAG는 크게 문서를 넣는 과정과 문서를 검색하는 과정으로 나눌 수 있다.
[문서를 넣을 때]
문서
↓
Chunking
↓
Embedding
↓
Vector DB 저장
문서를 등록하거나 변경할 때 수행하는 과정을 Indexing이라고 한다.
1. Chunking
긴 문서를 그대로 하나의 벡터로 저장하지 않고 작은 단위로 나누는 과정이다.
나누어진 각각의 조각을 Chunk라고 한다.
긴 사내 규정 문서
↓
Chunking
↓
┌──────────────────┐
│ 연차 관련 내용 │
├──────────────────┤
│ 병가 관련 내용 │
├──────────────────┤
│ 재택근무 관련 내용 │
└──────────────────┘
문서를 통째로 저장하면 여러 주제의 의미가 하나의 벡터에 섞여 검색 정확도가 떨어질 수 있다.
또한 검색에 성공하더라도 긴 문서 전체를 Context에 넣어야 하기 때문에 토큰도 낭비된다.
따라서 검색하고 싶은 단위에 맞게 문서를 적절하게 나누는 것이 중요하다.
Chunk가 너무 작으면
문장의 앞뒤 내용이 잘려 맥락이 사라질 수 있다.
Chunk가 너무 크면
여러 내용이 하나의 Chunk에 포함되어 검색 정확도가 떨어지고 Context에 불필요한 내용까지 포함될 수 있다.
따라서 Chunk Size는 문서의 특성에 맞게 조정해야 한다.
5. Retrieval & Generation
문서를 미리 Vector DB에 저장했다면 사용자가 질문할 때 다음 과정이 수행된다.
사용자 질문
↓
Embedding
↓
질문 Vector
↓
Vector DB 유사도 검색
↓
관련 Chunk 검색
↓
질문 + Chunk를 LLM에 전달
↓
답변 생성
여기서 중요한 점은 질문 역시 문서를 저장할 때 사용한 것과 동일한 Embedding Model로 Vector로 변환한다는 것이다.
1. Similarity Search
Spring AI에서는 similaritySearch()를 통해 질문과 유사한 문서를 검색할 수 있다.
SearchRequest request = SearchRequest.builder()
.query("아파서 쉬고 싶은데 어떻게 해요?")
.topK(5)
.similarityThreshold(0.7)
.build();
List<Document> results =
vectorStore.similaritySearch(request);
topK
질문과 가장 유사한 문서를 몇 개 가져올 것인지 결정한다.
topK = 3
→ 가장 유사한 Chunk 3개 검색
너무 크게 설정하면 관련 없는 문서까지 Context에 포함되어 토큰 사용량이 증가하고 답변에 잡음이 생길 수 있다.
similarityThreshold
검색 결과에 포함할 최소 유사도 기준이다.
기준보다 유사도가 낮은 문서는 제외한다.
따라서 topK와 similarityThreshold를 통해 얼마나 많은 문서를, 어느 정도의 관련성까지 가져올 것인지 조절할 수 있다.
6. Metadata
Vector DB에 문서를 저장할 때 본문뿐만 아니라 Metadata도 함께 저장할 수 있다.
Document {
text:
"재택근무는 주 2회까지 허용한다."
metadata:
filename: "working_hours.txt"
category: "근태"
}
Metadata를 저장하면 두 가지 용도로 활용할 수 있다.
출처 표시
"working_hours.txt에 따르면
재택근무는 주 2회까지 가능합니다."
답변이 어떤 문서를 기반으로 만들어졌는지 사용자에게 제공할 수 있다.
검색 범위 제한
.filterExpression("category == '근태'")
특정 카테고리나 사용자가 접근할 수 있는 문서만 검색하도록 제한할 수 있다.
특히 사내 문서를 RAG에 사용하는 경우에는 권한이 없는 문서가 검색되지 않도록 Retrieval 단계에서 검색 범위를 제한하는 것이 중요하다.
7. QuestionAnswerAdvisor
Spring AI에서는 RAG 과정을 직접 구현하지 않고 QuestionAnswerAdvisor를 사용할 수도 있다.
return chatClient.prompt()
.advisors(
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(
SearchRequest.builder()
.topK(5)
.similarityThreshold(0.7)
.build()
)
.build()
)
.user(question)
.call()
.content();
QuestionAnswerAdvisor는 LLM을 호출하기 전에
질문
↓
질문 Embedding
↓
VectorStore 검색
↓
관련 문서를 Prompt에 추가
↓
LLM 호출
과정을 처리한다.
즉, Retrieval과 Augmented 과정을 자동화하여 RAG를 쉽게 구성할 수 있도록 해준다.
8. RAG vs Fine-tuning
RAG와 Fine-tuning의 가장 큰 차이는 모델 자체를 변경하는지 여부이다.
RAG
모델의 가중치는 변경하지 않고 외부 데이터를 검색해서 Context로 제공한다.
LLM + 필요한 문서
Fine-tuning
추가 데이터를 이용하여 모델의 가중치 자체를 수정한다.
기존 Model
↓
추가 학습
↓
변경된 Model
| RAG | Fine-tuning |
| 모델 가중치 변경 X | 모델 가중치 변경 O |
| 외부 DB에 지식 저장 | 모델 내부에 반영 |
| 문서 변경으로 빠른 정보 갱신 | 변경하려면 다시 학습 필요 |
| 출처 제공 가능 | 출처 제공 어려움 |
| 최신 정보, 사내 문서 등에 적합 | 말투, 형식, 행동 패턴 등에 적합 |
따라서 선택 기준은 다음과 같이 생각할 수 있다.
모델이 몰라서 못 하는가, 알고 있지만 원하는 방식으로 행동하지 않는가?
최신 정보나 사내 규정처럼 지식이 부족한 것이 문제라면 RAG, 일정한 말투나 출력 형식을 학습시키고 싶다면 Fine-tuning을 고려할 수 있다.
9. RAG 문제 해결
RAG를 적용했는데 답변이 이상하다면 가장 먼저 검색된 Chunk를 확인해야 한다.
답변이 이상함
↓
검색 결과 확인
↓
┌──────────────────────┐
│ 정답 Chunk가 없음 │
│ → Retrieval 문제 │
└──────────────────────┘
┌──────────────────────┐
│ 정답 Chunk가 있음 │
│ → Generation 문제 │
└──────────────────────┘
검색 결과에 정답이 없다면
- Chunk Size 조정
similarityThreshold조정topK조정- Metadata Filter 확인
- Embedding 및 검색 방식 확인
등 Retrieval 과정을 확인한다.
검색 결과에는 정답이 있는데 답변이 틀리다면
검색 자체는 성공한 것이므로 Prompt나 Context 구성 등 Generation 과정을 확인한다.
즉, RAG의 문제를 해결할 때는 무작정 Prompt부터 수정하는 것이 아니라
Retrieval 문제인지 Generation 문제인지 먼저 구분하는 것이 중요하다.
