목표: AI 분야에서 주목받고 있는 RAG(Retrieval-Augmented Generation) 기술을 학습하고, 이를 기반으로 챗봇 서비스 만들어보기
일단 나는 Spring JPA 중심의 백엔드 개발자로,
AI 경험은 간단한 프롬프트 기반 API 연동이 전부이다. 그래서 AI관련 용어도 잘 모른다.
그래서 이 글은 AI 초심자의 눈높이에서, RAG의 개념을 아주 쉽게 풀어가며 공부하려 한다.
🧠 RAG란?
RAG (Retrieval-Augmented Generation)는 생성형 AI가 모르는데 아는 척 하는 것(hallucination)을 줄이기 위해 등장한 구조이다.
일반적인 GPT 같은 생성형 모델은 자신이 학습한 데이터만 가지고 답을 한다.
그래서 GPT에게
“우리 회사 사내 문서 기반으로 질문에 답해줘”
라고 해도, 그 문서를 직접 본 적이 없기 때문에 엉뚱하거나 틀린 내용을 그럴듯하게 지어낸다.
RAG는 여기에 외부 지식 검색 기능(Retrieval)을 추가한 구조이다.
문서를 직접 검색해서, 그 내용을 참고한 답변을 생성할 수 있다.
즉 “검색 + 생성”의 구조를 가지는 것이다.
→ 따라서 회사 내부 문서, 논문, 제품 가이드 등 다양한 도메인 지식에 대해 대응할 수 있다는 강점이있다.
🔧 RAG의 구조
- 사용자의 질문이 들어오면
- Retriever가 질문과 관련된 문서를 벡터 검색을 통해 찾아낸다.
- Generator (GPT 등)가 찾아낸 문서를 기반으로 답변을 생성한다.
엑,,, Retriever..? Generator ..? 벡터 검색..? 어려운 용어가 너무 많다.
그럼 지금부터 용어 하나하나를 쉽게 풀어서 알아보자.
이걸 이해하고 나면 위 구조 설명도 훨씬 잘 들어올것이다.
🔎 Retriever
질문에 관련있는 문서를 찾음
Rag 서비스에 “퇴직금 정산 기준이 뭐야?” 라고 물었을 때,
수백 개 사내 문서 중에서 “퇴직금 관련 규정.pdf”의 12페이지를 찾아서 꺼내주는 게 Retriever이다.
작동 방식은 다음과 같다.
- 문서를 embedding(=벡터화)해서 벡터DB에 저장해 둠 (예: FAISS, Chroma, Pinecone 등)
- 사용자의 질문도 embedding함
- 벡터끼리 유사도(cosine similarity) 계산해서 가장 가까운 문서 몇 개를 골라냄
🔎 Generator
찾아온 문서를 바탕으로 자연어로 설명
Retriever가 퇴직금 규정 문서를 찾아왔다면, LLM에 질문과 함께 넘긴다.
그걸 바탕으로 LLM이 “퇴직금은 근속 연수에 따라 계산되며...” 라고 알기 쉽게 말로 풀어주는 것이 generator이다.
[사용자 질문]
→ Retriever: “관련 문서 여기 있음~”
→ Generator: “그 문서 보니까 이렇대~ 요약해서 말해줄게”
→ [답변 반환]
대충 느낌은 알았다. 근데 또 어려운 용어가 많이나왔다.
retrever가 문서를 embedding해서 벡터DB에 저장해 두고, 사용자의 질문도 embedding한 후 벡터끼리 유사도(cosine similarity) 계산해서 가장 가까운 문서 몇 개를 골라낸다는데,,
embedding은 뭐고 vectorDB는 뭐고, 코사인 유사도 계산은 어떻게 하는걸까
🛏️ 임베딩(Embedding)
AI는 사람처럼 말의 의미를 느끼는능력이 없다. 우리가 쓰는 문장도 AI에게는 단순한 문자 나열일 뿐이다.
예를 들어,
"퇴직금은 언제 받을 수 있나요?"
라는 문장은 사람에겐 의미가 있지만,
AI에게는 그냥 "ㄸ, ㅊ, ㄱ, ㅇ, ㅈ..." 같은 텍스트일 뿐이다.
그래서 AI가 문장의 의미를 이해하게 만들려면, 이 문장이 갖고있는 뜻을 수치로 표현해야 한다.
그래서 자연어 문장을 벡터(방향과 크기를 가진 수학적 값)로 바꾸는 것이고, 이 과정을 임베딩이라고 한다.
“퇴직금은 언제 받을 수 있나요?”
→ [0.12, -0.45, 0.77, ...] (768차원 등 숫자 벡터)
즉, 문장을 “위치 정보”처럼 벡터 공간에 뿌리는 것.
비슷한 뜻이면 가까운 위치에,
다른 뜻이면 멀리 떨어지게.
이걸 해주는 게 바로 OpenAIEmbeddings, HuggingFaceEmbeddings 같은 임베딩 모델들이다.
이 블로그에서 자세하게 설명하고 있는것 같다.
이떄, 긴 문서를 한 번에 LLM에 넘기면 부담이 크다.
이들이 한번에 처리할 수 있는 컨텍스트에는 한계(token 제한)가 있기 때문이다.
ex)
GPT-3.5 turbo → 약 16,345 tokens
GPT-4 → 약 128,000 tokens
그래서 보통 문서를 LangChain의 TextSplitter등을 사용하여 적절한 크기로 나눠서(Chunk) 임베딩하고 저장한다.
근데,, 문장 길이와 상관없이 항상 고정 차원 (384, 768, 1536...) 을 쓰는걸까?
어떤건 한문장에 두단언데 384개 다쓰고, 어떤건 막 엄청긴데 384개 쓰고 이러면 정확성에 차이가 생기는거 아닌가 하는 궁금증이 생겼다.
-> 맞다.문장 길이에 상관없이 항상 384개 (또는 768, 1536...) 짜리 벡터가 나온다.
고정 길이여야 코사인 유사도, 거리 계산, 인덱싱이 효율적으로 가능하기 때문이다.
그리고 그 때문에 긴 문장은 정보가 압축되기 때문에, 짧고 간결한 문장이 더 정확한 임베딩을 만들 수 있다.
이도 chunk size를 나눠주는 이유 중 하나다.
문장을 임베딩해서 나온 벡터 값을 직접 출력해서
"벡터가 실제로 어떤 숫자들로 구성되어 있는지" 확인해보자.
from sentence_transformers import SentenceTransformer
import numpy as np
# 1. 모델 로드
model = SentenceTransformer('all-MiniLM-L6-v2')
# 2. 문장
sentence = "퇴직금은 언제 받을 수 있나요?"
# 3. 임베딩 (벡터화)
vector = model.encode(sentence)
# 4. 벡터 출력
print(f"문장: {sentence}")
print(f"벡터 차원 수: {len(vector)}")
print("앞쪽 10개 요소만 출력:")
print(vector[:10]) # 전체 벡터는 너무 기니까 앞에 몇 개만
# 5. 벡터 전체 출력 원한다면 아래도 가능
# print("전체 벡터:")
# print(vector)
결과
문장: 퇴직금은 언제 받을 수 있나요?
벡터 차원 수: 384
앞쪽 10개 요소만 출력:
[ 0.01882134 0.00819211 -0.01318623 0.04036659 0.05520485
0.0169004 -0.00709183 -0.01310762 0.03287694 -0.032233 ]
구체적인 임베딩 과정은 다음과 같다.
- 문장을 → 여러 토큰(token) 으로 나눔
(예: “퇴직금은 언제 받나요?” → [퇴직, 금, 은, 언제, 받, 나, 요]) - 각 토큰을 → 각각 벡터로 변환
→ [토큰 수 × 차원 수]짜리 행렬이 됨 - 이걸 하나의 벡터로 줄이기 위해
👉 압축(=pooling) 을 함 → 최종 벡터 [384]
압축 방식에는 주로 3가지 방식이 쓰인다.
(각 토큰 벡터를 평균내는 mean Pooling, 문장의 대표 토큰만 사용하는 CLS, 각 차원별로 가장 큰 값을 사용하는 max pooling 등)
mean pooling이 가장 많이 쓰지만, 임베딩 방식에 대한 다양한 논문과 견해들이 있으니 찾아보면 공부가 될것같다.
↗️ Vector search
사용자가 입력한 문장을 임베딩해서,
벡터 공간에서 가장 가까운 문서들을 찾는 것!
벡터가 의미를 수치로 표현한거라고 했으니.
벡터값을 기반으로 방향이 가까운 문서들을 찾으면 의미가 비슷한 문장을 찾을 수 있을 것이다.
실제 AI는 문장 하나를 384, 768,1536등 다차원 벡터로 표현한다.
우리가 머릿속으로는 상상 못하지만, 수학적으로는 고차원 공간이 가능하기 때문이다.
768차원 공간: [0.5, -0.2, ...(768개 반복).., 0.003] → (문장의 위치)
앗.. 그럼 784차원이나 되는데 ,, 가까운걸 어떻게 찾지? 하고 궁금할 수 있다..
이는 코사인 유사도를 기반으로 검색한다.
📐 Cosine Similarity
벡터 공간에서 두 벡터가 이루는 각도를 기반으로 유사도를 계산. 각도가 작을수록 비슷하다
Cosine Similarity=∥A∥×∥B∥ / A⋅B
A⋅B: 두 벡터의 내적 (각 요소의 곱)
∥A∥, ∥B∥ : 벡터 A, B의 크기 (각 요소 제곱의 합의 제곱근)
- 두 벡터가 같은 방향이면 → 코사인 유사도 ≈ 1
- 완전히 반대 방향이면 → 코사인 유사도 ≈ -1
- 서로 직각이면 → 코사인 유사도 ≈ 0 → 관련 없음
벡터 임베딩 + 코사인 유사도 기반 유사 문서 검색과정을 예시로 보자
❔질문 : “퇴직금은 언제 받나요?”
→ 벡터 A = [0.11, -0.42, 0.78]
📚 벡터 DB
문서1: 퇴직금 계산 기준 [0.10, -0.40, 0.80]
문서2: 휴가 규정 [0.90, 0.22, -0.30]
문서3: 퇴직금 지급 시점 [0.12, -0.45, 0.77]
📐코사인 유사도 검색
CosineSim(A,문서1)≈0.999
CosineSim(A,문서2)≈0.25
CosineSim(A,문서3)≈0.9989
❕결과
문서3이 가장 유사도 점수가 높음! 의미가 가장 비슷
문서 2가 가장 유사도 점수가 낮음! 의미가 가장 다름
-> 문서 3을 꺼내옴
실제 LangChain에서 사용하는 예시를 보자
LangChain : LLM(AI 모델)을 이용한 앱을 쉽게 만들 수 있게 도와주는 파이썬 프레임워크
문서 검색, 외부 API 연결, 메모리 관리, 체이닝(연속 흐름) 같은걸 한줄 코드로 연결해준다.
LangChain 의 주요 기능들
LLM 연결 : OpenAI, HuggingFace 등 LLM 쉽게 붙이기
Embedding : 처리문장을 벡터로 변환하는 작업 자동 처리
Retriever : 구성벡터 DB에 저장하고, 유사 문서 찾는 기능 제공
PromptTemplate : 프롬프트 포맷 관리
Memory 관리 : 대화 이력 기억하는 챗봇 구현도 가능
코드 예시
embeddings = OpenAIEmbeddings()
vector_db = Chroma.from_documents(docs, embeddings)
retriever = vector_db.as_retriever() # 내부적으로 cosine similarity로 가장 가까운 문서 추출
+α)
이제 임베딩 성능 최적화랑 chunk 쪼개는 기준을 실무 관점에서 알아보자!
임베딩 성능 최적화
어떻게 해야 문서를 임베딩했을 때 검색 정확도가 높아질까?
① 좋은 문장 단위로 자르기
한 문장이 너무 길면 압축이 일어나서 정보가 손실,
반대로 너무 짧으면 의미 단편화돼서 유사도가 떨어진다.
따라서 chunk size를 적절하게 설정해주어야 한다.
일반적으로 500~800 tokens 사이가 적당하다.
또 이전 청크와 다음 청크 사이에 끝과 시작 글자들을 overlap 시켜주는 것이 좋다.
중요한 문장이 중간에서 끊기더라도 다음 chunk에서 다시 등장하니까 검색할 때 의미가 끊기지 않고 더 부드럽게 이어진다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50, # 문맥 유지용
)
chunks = splitter.split_documents(documents)
② 데이터 전처리
- 중요한 문장은 강조, 불필요한 문장은 제거
- 목차, 페이지 번호, “이하 동일함” 같은 텍스트는 제거
- 표제어, 문단 제목은 chunk 앞에 덧붙여서 넣어주면 정확도 ↑
[Title: 퇴직금 지급 기준]
퇴직금은 근속연수에 따라 지급되며, 평균임금을 기준으로...
LLM이 문맥이 뭔지 모른 채 문장만 해석하는 것을 방지한다.
③ 임베딩 모델 성능
- 임베딩 모델 자체의 성능도 중요하다.
- 모델 성능이 높을수록 속도나 리소스 소비도 증가하기 때문에,사용 환경에 맞게 적절한 모델을 선택해야 한다.
이 외 검색할때도 reranking, top-k, 프롬포트 설정, metadata 지정 등 다양한 방법을 사용하여 검색의 정확도를 높일 수 있으니,
이를 적절히 활용하여 정확한 RAG를 만드는것이 기술의 핵심 아닐까 싶다.
활용 사례들
'etc' 카테고리의 다른 글
LangServe 사용해보기 (4) | 2025.06.25 |
---|---|
[RAG] 민법 챗봇 만들기 (with LangChain) (11) | 2025.06.23 |
EC2환경에서 환경변수 적용하기(ubuntu) (3) | 2024.03.28 |
Github Actions를 통한 배포 자동화 3 (8) | 2024.03.04 |
Github Actions를 통한 배포 자동화 (1) | 2023.07.28 |