목차
- RAG 시스템 개요
- RAG 아키텍처 구성요소
- RAG 시스템 구축 단계
- ChatGPT API 연동 방법
- 성능 최적화 및 평가
- 실제 구현 예시
- 자주 발생하는 문제 및 해결 방법
RAG 시스템 개요
RAG(Retrieval-Augmented Generation)는 대규모 언어 모델(LLM)의 지식 한계를 극복하고 최신 정보나 특정 도메인 지식을 제공하기 위한 기술입니다. RAG는 사용자 질문에 대해 관련 문서나 정보를 먼저 검색(Retrieval)한 후, 이 정보를 바탕으로 LLM이 응답을 생성(Generation)하는 방식으로 작동합니다.
RAG의 주요 이점
- 최신 정보 제공: LLM의 학습 데이터 이후의 최신 정보 활용 가능
- 도메인 특화 정보: 특정 분야나 기업 내부 자료에 대한 정확한 응답 생성
- 출처 인용: 응답의 근거가 되는 정보 출처 제공 가능
- 환각(Hallucination) 감소: 모델이 없는 정보를 지어내는 현상 최소화
RAG 아키텍처 구성요소
효과적인 RAG 시스템을 구성하기 위해서는 다음과 같은 핵심 구성요소가 필요합니다:
1. 문서 처리 파이프라인
- 문서 수집: 다양한 형식(PDF, HTML, DOCX 등)의 문서 수집
- 텍스트 추출: 구조화되지 않은 데이터에서 텍스트 추출
- 청소 및 전처리: 불필요한 요소 제거, 텍스트 정규화
2. 벡터 저장소(Vector Database)
- 임베딩 생성: 문서를 의미적 벡터로 변환
- 벡터 인덱싱: 효율적인 검색을 위한 벡터 저장 및 인덱싱
- 유사도 검색: 쿼리와 문서 간의 의미적 유사도 계산
3. 검색 엔진
- 쿼리 처리: 사용자 질문 분석 및 처리
- 관련성 랭킹: 검색된 문서의 관련성 점수 계산
- 필터링: 관련성 높은 문서만 선별
4. 생성 모델(LLM)
- 프롬프트 엔지니어링: 검색 결과와 사용자 질문을 효과적으로 조합
- 컨텍스트 관리: 제한된 컨텍스트 창에 중요 정보 포함
- 응답 생성: 제공된 정보를 바탕으로 응답 생성
RAG 시스템 구축 단계
1. 데이터 준비
import os
import re
import fitz # PyMuPDF
from bs4 import BeautifulSoup
# PDF에서 텍스트 추출
def extract_from_pdf(pdf_path):
doc = fitz.open(pdf_path)
text = ""
for page in doc:
text += page.get_text()
return text
# HTML에서 텍스트 추출
def extract_from_html(html_path):
with open(html_path, 'r', encoding='utf-8') as file:
soup = BeautifulSoup(file, 'html.parser')
return soup.get_text(separator=' ', strip=True)
# 텍스트 전처리
def preprocess_text(text):
# 불필요한 공백 제거
text = re.sub(r'\s+', ' ', text)
# 특수문자 처리
text = re.sub(r'[^\w\s가-힣]', ' ', text)
return text.strip()
2. 문서 청크 분할
def split_text_into_chunks(text, chunk_size=1000, overlap=200):
chunks = []
start = 0
while start < len(text):
end = min(start + chunk_size, len(text))
# 문장이나 단락의 경계에서 자르기 위한 로직
if end < len(text):
# 다음 공백이나 문장 종결 부호 찾기
next_period = text.find('.', end - 100, end + 100)
next_newline = text.find('\n', end - 100, end + 100)
if next_period != -1 and (next_newline == -1 or next_period < next_newline):
end = next_period + 1
elif next_newline != -1:
end = next_newline + 1
chunks.append(text[start:end].strip())
start = end - overlap
return chunks
3. 임베딩 생성 및 저장
import numpy as np
from openai import OpenAI
from pinecone import Pinecone
# OpenAI API 초기화
client = OpenAI(api_key="your_openai_api_key")
# 임베딩 생성
def create_embeddings(chunks):
embeddings = []
for chunk in chunks:
response = client.embeddings.create(
input=chunk,
model="text-embedding-3-large"
)
embeddings.append(response.data[0].embedding)
return embeddings
# Pinecone 초기화
pc = Pinecone(api_key="your_pinecone_api_key")
index = pc.Index("your_index_name")
# 벡터 저장소에 저장
def store_vectors(chunks, embeddings, metadatas):
vectors = []
for i, (chunk, embedding) in enumerate(zip(chunks, embeddings)):
vectors.append({
"id": f"chunk_{i}",
"values": embedding,
"metadata": {
"text": chunk,
"source": metadatas[i]["source"],
"page": metadatas[i].get("page", 0)
}
})
# 배치 처리로 업로드
for i in range(0, len(vectors), 100):
batch = vectors[i:i+100]
index.upsert(vectors=batch)
4. 쿼리 처리 및 관련 문서 검색
def query_processing(query):
# 쿼리 임베딩 생성
query_embedding_response = client.embeddings.create(
input=query,
model="text-embedding-3-large"
)
query_embedding = query_embedding_response.data[0].embedding
# 유사한 문서 검색
search_results = index.query(
vector=query_embedding,
top_k=5,
include_metadata=True
)
# 검색 결과에서 텍스트 추출
retrieved_texts = []
for match in search_results["matches"]:
retrieved_texts.append({
"text": match["metadata"]["text"],
"score": match["score"],
"source": match["metadata"]["source"]
})
return retrieved_texts
ChatGPT API 연동 방법
1. 프롬프트 템플릿 설계
def create_prompt(query, retrieved_texts):
context = "\n\n".join([f"출처: {text['source']}\n내용: {text['text']}" for text in retrieved_texts])
prompt = f"""
다음은 사용자의 질문과 관련된 문서 정보입니다:
{context}
위 정보를 참고하여 다음 질문에 답변해주세요:
질문: {query}
응답 시 알고 있는 정보만 사용하고, 제공된 문서에 없는 내용이면 '제공된 정보만으로는 답변하기 어렵습니다'라고 말해주세요.
답변에 사용된 정보의 출처를 명시해주세요.
"""
return prompt
2. ChatGPT API 호출
def generate_response(prompt):
response = client.chat.completions.create(
model="gpt-4-turbo", # 또는 gpt-3.5-turbo
messages=[
{"role": "system", "content": "당신은 정확하고 도움이 되는 AI 어시스턴트입니다. 제공된 문서 정보를 바탕으로 질문에 답변합니다."},
{"role": "user", "content": prompt}
],
temperature=0.3, # 일관된 응답을 위해 낮은.3
max_tokens=1000
)
return response.choices[0].message.content
3. 전체 RAG 파이프라인 통합
def rag_pipeline(query):
# 1. 쿼리 처리 및 관련 문서 검색
retrieved_texts = query_processing(query)
# 2. 검색 결과가 없는 경우 처리
if not retrieved_texts:
return "관련 정보를 찾을 수 없습니다. 다른 질문을 해주세요."
# 3. 프롬프트 생성
prompt = create_prompt(query, retrieved_texts)
# 4. 응답 생성
response = generate_response(prompt)
return response
성능 최적화 및 평가
1. RAG 시스템 성능 평가 지표
- 정확성(Accuracy): 응답이 사실과 일치하는지 여부
- 관련성(Relevance): 검색된 문서가 쿼리와 얼마나 관련되어 있는지
- 응답 품질(Response Quality): 생성된 답변의 일관성, 명확성, 완결성
- 검색 효율성(Retrieval Efficiency): 검색 속도 및 자원 사용량
2. 임베딩 모델 선택
모델 장점 단점 적합한 사용 사례
| OpenAI Ada-002 | 높은 품질, 한국어 지원 우수 | 비용 발생 | 전문 분야 문서 |
| SentenceBERT | 오픈소스, 한국어 모델 존재 | 비교적 큰 모델 크기 | 범용 검색 |
| KoSimCSE | 한국어 특화, 가벼움 | 영어 등 타언어 성능 제한 | 한국어 문서 전용 |
3. 청크 크기 최적화
청크 크기는 검색 정확도와 처리 효율성에 큰 영향을 미칩니다:
- 작은 청크 (200-500토큰): 정확한 정보 조각 검색 가능, 컨텍스트 손실 가능성
- 중간 청크 (500-1000토큰): 균형적인 접근법, 대부분의 경우 적합
- 큰 청크 (1000+ 토큰): 더 많은 컨텍스트 포함, 관련 없는 정보 포함 가능성
4. 하이퍼파라미터 튜닝
# 쿼리 확장 기법 적용
def expand_query(query):
expansion_prompt = f"다음 질문과 관련된 키워드를 5개 추출해주세요: {query}"
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "당신은 키워드 추출 전문가입니다."},
{"role": "user", "content": expansion_prompt}
],
temperature=0.3
)
keywords = response.choices[0].message.content
expanded_query = f"{query} {keywords}"
return expanded_query
실제 구현 예시
FastAPI를 활용한 RAG 챗봇 API 구현
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class Query(BaseModel):
text: str
@app.post("/api/chat")
def chat_endpoint(query: Query):
try:
# RAG 파이프라인 실행
response = rag_pipeline(query.text)
return {"response": response}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 서버 실행 명령어 (터미널에서)
# uvicorn app:app --reload
React를 활용한 프론트엔드 연동
import React, { useState } from 'react';
import axios from 'axios';
function ChatbotInterface() {
const [input, setInput] = useState('');
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim()) return;
const userMessage = { text: input, sender: 'user' };
setMessages([...messages, userMessage]);
setInput('');
setLoading(true);
try {
const response = await axios.post('http://localhost:8000/api/chat', {
text: userMessage.text
});
setMessages([...messages, userMessage, {
text: response.data.response,
sender: 'bot'
}]);
} catch (error) {
console.error('Error:', error);
setMessages([...messages, userMessage, {
text: '죄송합니다. 오류가 발생했습니다.',
sender: 'bot'
}]);
} finally {
setLoading(false);
}
};
return (
<div className="chatbot-container">
<div className="messages-container">
{messages.map((msg, idx) => (
<div key={idx} className={`message ${msg.sender}`}>
{msg.text}
</div>
))}
{loading && <div className="loading">응답 생성 중...</div>}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="질문을 입력하세요..."
/>
<button type="submit">전송</button>
</form>
</div>
);
}
export default ChatbotInterface;
자주 발생하는 문제 및 해결 방법
1. 환각(Hallucination) 문제
문제: LLM이 검색된 문서에 없는 정보를 생성하는 현상
해결 방법:
- 프롬프트에 명확한 지침 포함
- 높은 관련성의 문서만 선택하도록 임계값 설정
- 낮은 온도(temperature) 설정 사용
# 관련성 점수 필터링 적용
def filter_relevant_documents(retrieved_texts, threshold=0.7):
return [text for text in retrieved_texts if text["score"] > threshold]
2. 컨텍스트 길이 제한
문제: LLM의 컨텍스트 창 크기 제한으로 많은 문서를 포함할 수 없음
해결 방법:
- Re-ranking으로 가장 관련성 높은 문서만 선택
- 문서 요약 기법 적용
- 효율적인 청크 전략 활용
from sentence_transformers import CrossEncoder
# Cross-Encoder를 이용한 Re-ranking
def rerank_documents(query, retrieved_texts, top_k=3):
model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
pairs = [(query, text["text"]) for text in retrieved_texts]
scores = model.predict(pairs)
# 점수에 따라 문서 정렬
for i, score in enumerate(scores):
retrieved_texts[i]["score"] = score
ranked_texts = sorted(retrieved_texts, key=lambda x: x["score"], reverse=True)
return ranked_texts[:top_k]
3. 한국어 특화 문제
문제: 영어 중심의 임베딩 모델이 한국어에서 성능 저하
해결 방법:
- KoSimCSE 등 한국어 특화 임베딩 모델 사용
- 한국어 전처리 강화
- 형태소 분석기 활용
from konlpy.tag import Mecab
def preprocess_korean_text(text):
mecab = Mecab()
# 형태소 분석
morphs = mecab.morphs(text)
# 불용어 제거
stopwords = ["은", "는", "이", "가", "을", "를", "에", "의", "과", "와", "으로", "로"]
filtered_morphs = [word for word in morphs if word not in stopwords]
return " ".join(filtered_morphs)
4. 응답 생성 속도 개선
문제: 전체 파이프라인 처리 시간이 길어 사용자 경험 저하
해결 방법:
- 비동기 처리로 병렬화
- 벡터 인덱스 최적화
- 캐싱 전략 도입
import asyncio
from functools import lru_cache
# 임베딩 결과 캐싱
@lru_cache(maxsize=1000)
def get_embedding_cached(text):
response = client.embeddings.create(
input=text,
model="text-embedding-3-large"
)
return response.data[0].embedding
# 비동기 쿼리 처리
async def async_query_processing(query):
# 임베딩 생성 및 검색 비동기 처리
loop = asyncio.get_event_loop()
query_embedding = await loop.run_in_executor(
None, lambda: get_embedding_cached(query)
)
search_results = await loop.run_in_executor(
None, lambda: index.query(
vector=query_embedding,
top_k=5,
include_metadata=True
)
)
# 결과 처리
retrieved_texts = []
for match in search_results["matches"]:
retrieved_texts.append({
"text": match["metadata"]["text"],
"score": match["score"],
"source": match["metadata"]["source"]
})
return retrieved_texts
RAG 시스템 구축과 ChatGPT 연동을 통해 정확하고 최신 정보를 제공하는 챗봇을 만들 수 있습니다. 시스템의 성능은 문서 처리, 임베딩, 검색, 프롬프트 엔지니어링 등 여러 단계에서의 최적화를 통해 향상될 수 있습니다. 각 사용 사례에 맞게 적절한 기술과 모델을 선택하여 효과적인 챗봇 솔루션을 구축하시기 바랍니다.
반응형