자바 개발자의 AI 입문기 (4편) - RAG 실전, Knowledge Base 검색 챗봇 만들기
📚 시리즈: 자바 개발자의 AI 입문기
4 / 5편- 11편 - Python 환경세팅과 첫 API 호출
- 22편 - LangChain 기초
- 33편 - RAG 기초
- 44편 - RAG 실전지금 읽는 중
- 55편 - LangGraph
TL;DR
- 목표: 회사 문서 기반 Q&A 챗봇 완성
- 파이프라인: 질문 → 문서 검색 → 컨텍스트 주입 → 답변 생성
- 핵심: 검색된 문서를 프롬프트에 넣어서 AI가 답변하게 함
- 결과: “우리 회사 정책”에 대해 정확히 답변하는 챗봇
- 한계: 청킹 전략과 검색 k값 튜닝이 품질을 좌우, 최적값은 도메인마다 다름
이전 편 요약
3편에서는 다음을 다뤘습니다:
- Embedding으로 텍스트를 벡터로 변환
- ChromaDB에 벡터 저장
- 유사도 검색으로 관련 문서 찾기
이번 편에서 RAG 파이프라인을 완성합니다. 검색된 문서를 AI에게 전달해서 답변을 생성하는 거예요.
RAG 전체 흐름
다시 한번 정리하면:
graph LR
A["사용자 질문"] --> B["관련 문서 검색"]
B --> C["검색 결과 + 질문"]
C --> D["LLM이 답변 생성"]
D --> E["사용자에게 응답"]3편에서 B(검색)까지 했어요. 이번엔 C, D, E를 완성합니다.
1단계: 검색 + 답변 생성 연결
3편에서 만든 검색 함수에 LLM 답변 생성을 붙여봅시다.
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
load_dotenv()
# 모델 설정
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 3편에서 저장한 Vector DB 로드
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)
# RAG 프롬프트 템플릿
rag_prompt = ChatPromptTemplate.from_messages([
("system", """당신은 회사 정책 안내 도우미입니다.
아래 참고 문서를 바탕으로 질문에 답변하세요.
문서에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답변하세요.
참고 문서:
{context}"""),
("user", "{question}")
])
def answer_with_rag(question: str) -> str:
"""RAG 기반 질의응답"""
# 1. 관련 문서 검색
docs = vectorstore.similarity_search(question, k=2)
context = "\n".join([doc.page_content for doc in docs])
# 2. 프롬프트 + LLM 체인
chain = rag_prompt | llm | StrOutputParser()
# 3. 답변 생성
answer = chain.invoke({
"context": context,
"question": question
})
return answer
# 테스트
questions = [
"연차는 언제부터 쓸 수 있나요?",
"재택근무 하려면 뭐가 필요한가요?",
"회사 주차장은 어디 있나요?" # 없는 정보
]
for q in questions:
print(f"Q: {q}")
print(f"A: {answer_with_rag(q)}\n")결과:
Q: 연차는 언제부터 쓸 수 있나요?
A: 연차 휴가는 입사 1년 후부터 사용할 수 있으며, 15일이 부여됩니다.
3년차부터는 20일로 늘어납니다.
Q: 재택근무 하려면 뭐가 필요한가요?
A: 재택근무를 하려면 팀장 승인이 필요합니다. 주 2회까지 가능합니다.
Q: 회사 주차장은 어디 있나요?
A: 해당 정보를 찾을 수 없습니다.문서에 있는 내용은 정확히 답하고, 없는 건 모른다고 해요.
2단계: 실제 파일에서 문서 로드하기
지금까지는 코드에 문서를 하드코딩했어요. 실제로는 파일에서 로드해야겠죠.
텍스트 파일 로드
from langchain_community.document_loaders import TextLoader
# 텍스트 파일 로드
loader = TextLoader("./company_policy.txt", encoding="utf-8")
documents = loader.load()
print(f"로드된 문서 수: {len(documents)}")
print(f"첫 번째 문서 내용: {documents[0].page_content[:200]}...")PDF 파일 로드
from langchain_community.document_loaders import PyPDFLoader
# PDF 파일 로드
loader = PyPDFLoader("./휴가정책.pdf")
documents = loader.load()
print(f"로드된 페이지 수: {len(documents)}")PDF 로더를 쓰려면 추가 패키지가 필요해요:
pip install pypdf3단계: 청킹 (Chunking) - 문서 쪼개기
긴 문서를 그대로 Embedding하면 문제가 있어요:
- 검색 정확도 저하: 긴 문서는 여러 주제를 담고 있어서 검색이 부정확함
- 컨텍스트 낭비: LLM에게 불필요한 내용까지 전달됨
- 토큰 비용 증가: 긴 문서 = 많은 토큰 = 비용 증가
그래서 문서를 적절한 크기로 쪼갭니다. 이게 청킹(Chunking)이에요.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 문서 분할기 설정
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 청크 최대 길이
chunk_overlap=50, # 청크 간 중복 (문맥 유지용)
separators=["\n\n", "\n", ".", " "] # 분할 우선순위
)
# 문서 분할
chunks = text_splitter.split_documents(documents)
print(f"원본 문서 수: {len(documents)}")
print(f"분할 후 청크 수: {len(chunks)}")chunk_overlap이 중요해요. 청크 경계에서 문맥이 끊기지 않도록 약간 겹치게 합니다.
4단계: 전체 파이프라인 완성
지금까지 배운 걸 합쳐서 완전한 Knowledge Base 챗봇을 만들어봅시다.
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
import os
load_dotenv()
class KnowledgeBaseChatbot:
"""문서 기반 Q&A 챗봇"""
def __init__(self, persist_dir: str = "./kb_chroma"):
self.llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.persist_dir = persist_dir
self.vectorstore = None
# 프롬프트 템플릿
self.prompt = ChatPromptTemplate.from_messages([
("system", """당신은 친절한 회사 정책 안내 도우미입니다.
참고 문서:
{context}
규칙:
1. 참고 문서를 바탕으로 답변하세요.
2. 문서에 없는 내용은 "해당 정보를 찾을 수 없습니다"라고 답변하세요.
3. 친절하고 자연스럽게 답변하세요.
4. 한국어로 답변하세요."""),
("user", "{question}")
])
def load_and_index(self, file_path: str) -> int:
"""파일을 로드하고 벡터 인덱싱"""
# 1. 문서 로드
loader = TextLoader(file_path, encoding="utf-8")
documents = loader.load()
# 2. 청킹
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50
)
chunks = splitter.split_documents(documents)
# 3. 벡터 저장
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings,
persist_directory=self.persist_dir
)
return len(chunks)
def load_existing_db(self):
"""기존 DB 로드"""
if os.path.exists(self.persist_dir):
self.vectorstore = Chroma(
persist_directory=self.persist_dir,
embedding_function=self.embeddings
)
def ask(self, question: str) -> dict:
"""질문에 답변"""
if not self.vectorstore:
return {"answer": "먼저 문서를 로드해주세요.", "sources": []}
# 관련 문서 검색
docs = self.vectorstore.similarity_search(question, k=3)
context = "\n---\n".join([doc.page_content for doc in docs])
# 답변 생성
chain = self.prompt | self.llm | StrOutputParser()
answer = chain.invoke({
"context": context,
"question": question
})
return {
"answer": answer,
"sources": [doc.page_content[:100] + "..." for doc in docs]
}
# 사용 예시
if __name__ == "__main__":
# 챗봇 초기화
chatbot = KnowledgeBaseChatbot()
# 샘플 문서 생성 (실제로는 파일에서 로드)
sample_content = """회사 정책 안내
연차 휴가:
입사 1년 후 15일이 부여됩니다.
3년차부터는 20일로 늘어납니다.
연차는 1년 내 모두 소진해야 합니다.
병가:
연간 10일까지 사용 가능합니다.
3일 이상 사용 시 진단서가 필요합니다.
재택근무:
주 2회까지 가능합니다.
팀장 승인이 필요합니다.
코어타임(10시-4시)에는 연락 가능해야 합니다.
야근 수당:
오후 9시 이후 근무 시 지급됩니다.
시간당 기본급의 1.5배입니다.
사전 신청이 필요합니다.
"""
# 샘플 파일 저장
with open("company_policy.txt", "w", encoding="utf-8") as f:
f.write(sample_content)
# 문서 인덱싱
chunk_count = chatbot.load_and_index("company_policy.txt")
print(f"인덱싱 완료: {chunk_count}개 청크")
# 대화
questions = [
"연차 몇 개 받아요?",
"재택근무 하려면 뭐가 필요해요?",
"야근 수당은 어떻게 계산되나요?",
"회식비 지원되나요?" # 없는 정보
]
print("\n" + "="*50 + "\n")
for q in questions:
result = chatbot.ask(q)
print(f"Q: {q}")
print(f"A: {result['answer']}")
print()작동 확인
인덱싱 완료: 4개 청크
==================================================
Q: 연차 몇 개 받아요?
A: 입사 1년 후에 연차 15일이 부여됩니다. 3년차부터는 20일로 늘어나요.
참고로 연차는 1년 내에 모두 소진해야 합니다.
Q: 재택근무 하려면 뭐가 필요해요?
A: 재택근무를 하려면 팀장 승인이 필요합니다. 주 2회까지 가능하고,
코어타임(10시-4시)에는 연락 가능해야 해요.
Q: 야근 수당은 어떻게 계산되나요?
A: 오후 9시 이후 근무하시면 시간당 기본급의 1.5배가 지급됩니다.
사전 신청이 필요하니 참고해주세요.
Q: 회식비 지원되나요?
A: 해당 정보를 찾을 수 없습니다.완성이에요. 문서에 있는 내용은 정확히 답하고, 없는 건 “모른다”고 해요.
이게 RAG의 전부입니다
복잡해 보이지만 핵심은 간단해요:
- 문서를 벡터로 저장
- 질문이 오면 관련 문서 검색
- 검색 결과를 프롬프트에 넣어서 AI에게 전달
이게 RAG입니다. 사내 문서 검색 챗봇, 고객센터 FAQ 봇, 코드 문서 검색 등 다양한 곳에 적용할 수 있어요.
다음 편 예고
다음 편에서는 LangGraph를 다룹니다.
RAG로 단순 Q&A는 가능해졌는데, 복잡한 워크플로우는?
예를 들어:
- 질문 분석 → 카테고리 분류 → 해당 카테고리 문서 검색 → 답변 생성
- 사용자 의도 파악 → 필요한 도구 선택 → 실행 → 결과 정리
이런 “상태 기반 다단계 처리”가 필요하면 LangGraph를 씁니다.
(5편) LangGraph로 계속
실습 체크리스트
따라하고 나서 아래 항목들을 점검해보세요:
- 파일 로드 후 청킹 시 원본보다 청크 수가 많은지 확인
-
chunk_overlap값이chunk_size의 10% 내외로 설정됐는지 확인 - 존재하는 정보 질문 시 문서 기반 정확한 답변 확인
- 없는 정보 질문 시 “찾을 수 없습니다” 응답 확인
-
company_policy.txt파일 인코딩utf-8저장 확인
참고:
LangChain Document Loaders: https://python.langchain.com/docs/integrations/document_loaders
Text Splitters: https://python.langchain.com/docs/concepts/text_splitters
RAG 튜토리얼: https://python.langchain.com/docs/tutorials/rag
