logoRawon_Log
홈블로그소개

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

AI

RAG 활용 - Chroma를 활용한 RAG 구성

Rawon
2025년 10월 31일
목차
🤖 RAG 실전: 소득세법 챗봇 만들기
📋 프로젝트 개요
소득세법 챗봇 생성 준비
🛠️ 환경 설정
노트북 생성 및 패키지 설치
📖 1단계: 문서 불러오기
✂️ 2단계: 문서 분할 (Chunking)
패키지 설치 및 Import
Text Splitter 설정값
🧮 3단계: 임베딩 및 벡터 데이터베이스 저장
Embedding Model 설정
Vector Database 설정 (Chroma)
유사도 검색 테스트
💬 4단계: LLM 질의 및 답변 생성
기본 방법: 프롬프트 직접 작성
🔗 5단계: RAG Chain 구성하기
LangChain Hub의 프롬프트 활용
RetrievalQA Chain 구성
최신 방법 (create_retrieval_chain) 예시
💾 데이터 영속성 관리
🎯 RAG 시스템 동작 흐름 정리

목차

🤖 RAG 실전: 소득세법 챗봇 만들기
📋 프로젝트 개요
소득세법 챗봇 생성 준비
🛠️ 환경 설정
노트북 생성 및 패키지 설치
📖 1단계: 문서 불러오기
✂️ 2단계: 문서 분할 (Chunking)
패키지 설치 및 Import
Text Splitter 설정값
🧮 3단계: 임베딩 및 벡터 데이터베이스 저장
Embedding Model 설정
Vector Database 설정 (Chroma)
유사도 검색 테스트
💬 4단계: LLM 질의 및 답변 생성
기본 방법: 프롬프트 직접 작성
🔗 5단계: RAG Chain 구성하기
LangChain Hub의 프롬프트 활용
RetrievalQA Chain 구성
최신 방법 (create_retrieval_chain) 예시
💾 데이터 영속성 관리
🎯 RAG 시스템 동작 흐름 정리

🤖 RAG 실전: 소득세법 챗봇 만들기

이번 포스팅에서는 실제로 RAG를 구현하여 소득세법 전문을 학습한 챗봇을 만들어보겠습니다. 이론으로만 배웠던 RAG를 실제로 구현해보면서 각 단계를 자세히 살펴보겠습니다!

📋 프로젝트 개요

소득세법 챗봇 생성 준비

먼저 국가법령정보센터에서 소득세법을 Word 파일(.docx)로 저장합니다.

⚠️ 주의: 한글 파일(.hwp)은 Python으로 읽기 어렵고, 줄바꿈에 있는 단어가 하나의 단어일 경우 제대로 인식하지 못하는 문제가 있습니다. 반면 Word 파일은 이러한 문제가 없어 더 안정적입니다.

다운로드한 Word 파일을 프로젝트 폴더에 저장한 후, 다음과 같은 단계로 챗봇을 구현할 예정입니다:

구현 단계

  1. 📄 문서의 내용을 읽기
  2. ✂️ 문서를 적절한 크기로 구분
    • 토큰 수 초과로 답변을 생성하지 못할 수 있음
    • 문서가 길면 답변 생성 시간이 오래 걸림
  3. 🧮 구분된 문서를 임베딩 → 벡터 데이터베이스에 저장
  4. 🔍 질문이 있을 때 벡터 데이터베이스에서 유사도 검색
  5. 💬 유사도 검색으로 가져온 문서를 LLM에 질문과 함께 전달

🛠️ 환경 설정

노트북 생성 및 패키지 설치

문서를 읽을 때는 LangChain의 Document Loader를 사용합니다.

python
%pip install --upgrade --quiet langchain_community docx2txt

📖 1단계: 문서 불러오기

python
from langchain_community.document_loaders import Docx2txtLoader

loader = Docx2txtLoader("./tax.docx")
documents = loader.load()
print(documents)

실행 결과

python
[Document(metadata={'source': './tax.docx'}, page_content='소득세법\n\n소득세법\n\n[시행 2025. 10. 1.] [법률 제21065호, 2025. 10. 1., 타법개정]\n\n기획재정부(재산세제과(양도소득세)) 044-215-4312\n\n기획재정부(소득세제과(근로소득)) 044-215-4216\n\n...')]

출력 결과를 보면 문서의 내용이 한 줄로 모두 출력되는 것을 확인할 수 있습니다. 이를 적절한 크기로 구분해야 합니다!

✂️ 2단계: 문서 분할 (Chunking)

문서를 구분하는 방법은 LangChain의 Text Splitter를 활용합니다. 이 중 RecursiveCharacterTextSplitter는 여러 구분자를 기준으로 문서를 재귀적으로 구분할 수 있습니다.

패키지 설치 및 Import

python
%pip install -qu langchain-text-splitters
python
from langchain_community.document_loaders import Docx2txtLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1500, 
    chunk_overlap=200
)

loader = Docx2txtLoader("./tax.docx")
document_list = loader.load_and_split(text_splitter)
document_list

Text Splitter 설정값

RecursiveCharacterTextSplitter는 두 가지 중요한 설정값을 가집니다:

1. chunk_size 📏

  • 하나의 청크(chunk)가 가질 수 있는 최대 토큰 수입니다
  • 너무 크면 토큰 한도 초과, 너무 작으면 문맥 손실 가능성이 있습니다

2. chunk_overlap 🔗

  • 청크 간 겹침을 허용하는 토큰 수입니다
  • 청크 경계에서 문맥이 끊기는 것을 방지합니다
  • 유사도 검색 시 더 나은 결과를 얻을 수 있습니다

실행 결과는 다음과 같은 리스트 형태로 출력됩니다

python
[Document(metadata={'source': './tax.docx'}, page_content='소득세법\n\n소득세법\n\n[시행 2025. 10. 1.]...'),
 Document(metadata={'source': './tax.docx'}, page_content='1. 구성원 간 이익의 분배비율이...'),
 Document(metadata={'source': './tax.docx'}, page_content='⑤ 공동으로 소유한 자산에...'),
 ...]

각 Document는 적절한 크기로 분할되어 메타데이터(출처)와 함께 저장됩니다! 🎉

🧮 3단계: 임베딩 및 벡터 데이터베이스 저장

Embedding Model 설정

OpenAI의 Embedding 모델을 활용합니다. API Key가 필요하므로 환경변수를 먼저 로드합니다.

python
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings


# 환경변수를 불러옴

load_dotenv()


# OpenAI에서 제공하는 Embedding Model을 활용해서 chunk를 vector화

embedding = OpenAIEmbeddings(model='text-embedding-3-large')

Vector Database 설정 (Chroma)

이번 실습에서는 Chroma라는 벡터 데이터베이스를 사용합니다. Chroma는 인메모리 데이터베이스로 실습하기에 가장 간단하기 때문입니다.

python
%pip install langchain-chroma

Chroma에는 여러 메서드가 있는데, 지금은 Document가 있는 상황이므로 from_documents를 사용하여 데이터베이스를 생성합니다.

python
from langchain_chroma import Chroma


# 데이터를 처음 저장할 때 

database = Chroma.from_documents(
    documents=document_list, 
    embedding=embedding, 
    collection_name='chroma-tax', 
    persist_directory="./chroma"
)


# 이미 저장된 데이터를 사용할 때 database = Chroma(collection_name='chroma-tax', persist_directory="./chroma", embedding_function=embedding )

💡 참고: persist_directory 옵션을 사용하면 임베딩 결과가 디스크에 저장되어 노트북을 닫아도 데이터가 유지됩니다!

유사도 검색 테스트

잘 저장되었는지 간단한 쿼리로 유사도 검색을 테스트해봅니다.

python
query = "연봉이 5천만원인 직장인의 소득세는 얼마인가요?"
retrieved_docs = database.similarity_search(query, k=3)

retrieved_docs

similarity_search 메서드는 유사도 검색을 수행하여 가장 유사한 문서를 반환합니다. 기본값은 4개이며, k=3과 같이 파라미터를 수정하여 개수를 조절할 수 있습니다.

실행 결과

python
[Document(id='8246c897-9984-43a0-aaa4-b82518550059', metadata={'source': './tax.docx'}, page_content='③ 다음 각 호에 따른 소득의 금액은 종합소득과세표준을 계산할 때 합산하지 아니한다...'),
 Document(id='d9ec7197-6529-40c2-9a2f-4cc4e90e71aa', metadata={'source': './tax.docx'}, page_content='[전문개정 2009. 12. 31.]\n\n\n\n제10조(납세지의 변경신고)...'),
 Document(id='b637278a-f967-47bb-9023-780019b64791', metadata={'source': './tax.docx'}, page_content='2. 법인의 주주총회...')]

질문과 가장 관련 있는 문서 3개가 성공적으로 검색되었습니다! 🎯

💬 4단계: LLM 질의 및 답변 생성

기본 방법: 프롬프트 직접 작성

먼저 가장 기본적인 방법으로 프롬프트를 직접 작성하여 LLM에 전달해봅니다.

python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o")
python
prompt = f"""[Identity]
- 당신은 소득세 전문가입니다.
- [Context]를 참고해서 사용자의 질문에 답변해주세요.

[Context]
{retrieved_docs}

Question: {query}
"""

ai_message = llm.invoke(prompt)
print(ai_message.content)
python
실행 결과:
연봉 5천만 원인 직장인의 소득세를 계산하기 위해서는 연봉에서 소득 공제를 적용한 후 과세표준에 따라 세율을 적용해야 합니다. 일반적인 소득세 계산 과정을 설명하겠습니다...

이 방법도 작동하지만, 매번 프롬프트를 직접 작성하는 것은 번거롭습니다. LangChain에서는 더 효율적인 방법을 제공합니다!

🔗 5단계: RAG Chain 구성하기

LangChain Hub의 프롬프트 활용

Retrieval된 데이터를 효과적으로 활용하기 위해 LangChain Hub에서 제공하는 프롬프트("rlm/rag-prompt")를 사용할 수 있습니다.

python
from langchain import hub

prompt = hub.pull("rlm/rag-prompt")

RetrievalQA Chain 구성

RetrievalQA를 통해 검색과 답변 생성을 하나의 체인으로 연결합니다.

python
from langchain.chains import RetrievalQA

qa_chain = RetrievalQA.from_chain_type(
    llm, 
    retriever=database.as_retriever(),
    chain_type_kwargs={"prompt": prompt}
)


# 최신 LangChain에서는 .invoke() 사용을 권장

ai_message = qa_chain.invoke({"query": query})
ai_message

⚠️ 중요: LangChain의 RetrievalQA는 버전 0.1.17부터 deprecated되었으며, langchain 1.0에서 완전히 제거될 예정입니다. 실제 프로덕션 환경에서는 create_retrieval_chain을 사용하는 것을 권장합니다.

최신 방법 (create_retrieval_chain) 예시

python
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

system_prompt = (
    "Use the given context to answer the question. "
    "If you don't know the answer, say you don't know. "
    "Context: {context}"
)

prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{input}"),
])

question_answer_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(database.as_retriever(), question_answer_chain)

result = rag_chain.invoke({"input": query})
print(result["answer"])

💾 데이터 영속성 관리

Chroma는 기본적으로 인메모리 데이터베이스이기 때문에 노트북을 닫으면 임베딩 결과물이 사라집니다.

데이터를 영구적으로 저장하려면 from_documents 메서드에 다음 파라미터를 추가합니다:

python
database = Chroma.from_documents(
    documents=document_list,
    embedding=embedding,
    collection_name='chroma-tax',  
# 컬렉션 이름 지정

    persist_directory="./chroma"   
# 저장 경로 지정

)

이렇게 하면 지정한 이름의 폴더가 생성되며, 다음에 노트북을 다시 열었을 때 저장된 임베딩을 불러와 사용할 수 있습니다! 💪

저장된 데이터를 불러올 때는

python
database = Chroma(
    collection_name='chroma-tax',
    persist_directory="./chroma",
    embedding_function=embedding
)

🎯 RAG 시스템 동작 흐름 정리

완성된 RAG 시스템의 전체 동작 흐름을 정리하면 다음과 같습니다:

1. Retrieval (검색) 🔍

  • 사용자가 질문을 입력
  • 질문을 Embedding 모델로 벡터화
  • Vector Database에서 유사도 검색 수행
  • 가장 관련 있는 문서 청크를 k개 가져옴

2. Augmented (증강) 📚

  • 검색된 문서를 Context로 프롬프트에 추가
  • "이 정보를 바탕으로 답변해라"라는 지시사항 포함
  • LLM이 해당 정보를 아는 것처럼 동작하도록 설정

3. Generation (생성) ✨

  • 증강된 프롬프트를 LLM에 전달
  • LLM이 Context를 참고하여 답변 생성
  • 사용자에게 최종 답변 반환