Skip to content

자바 개발자의 AI 입문기 (4편) - RAG 실전, Knowledge Base 검색 챗봇 만들기





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 pypdf




3단계: 청킹 (Chunking) - 문서 쪼개기

긴 문서를 그대로 Embedding하면 문제가 있어요:

  1. 검색 정확도 저하: 긴 문서는 여러 주제를 담고 있어서 검색이 부정확함
  2. 컨텍스트 낭비: LLM에게 불필요한 내용까지 전달됨
  3. 토큰 비용 증가: 긴 문서 = 많은 토큰 = 비용 증가

그래서 문서를 적절한 크기로 쪼갭니다. 이게 청킹(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의 전부입니다

복잡해 보이지만 핵심은 간단해요:

  1. 문서를 벡터로 저장
  2. 질문이 오면 관련 문서 검색
  3. 검색 결과를 프롬프트에 넣어서 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




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site