LangChain을 활용한 지역화폐 정책 RAG QA 챗봇 구축하기(1)
Introduction
ASAC 6기 기업 MATCH 프로젝트가 끝나고, 랭콘에서의 발표를 위해 지역화폐 프로젝트를 RAG 챗봇까지 확장했다.
우리가 구축한 지역화폐 데이터에 대한 Text-to-SQL 챗봇과 일반 정책 및 혜택에 대한 자연어 QA 챗봇을 함께 구축하고, 웹서비스에서 하나의 챗봇 UI를 통해 유저 인풋에 대해 자동 분기처리해서 답변하도록 구성했다.
전체적인 아키텍처는 다음과 같았다.
내 역할은 데이터 수집 + 파이프라인 구축 + 지도 대시보드 구축 + 프론트엔드(Streamlit) + 백엔드(연결, 배포) + RAG QA 챗봇 구축이었다. 그 중 프론트엔드, 백엔드, RAG 구축은 대부분 단독으로 수행했다. 물론 멘토님의 도움도 받고~
RAG 챗봇 구축을 위한 프레임워크는 LangChain을 활용했다.
테디노트님의 랭체인 노트가 많은 도움이 되었다.
<랭체인LangChain 노트> - LangChain 한국어 튜토리얼🇰🇷
**추천**은 공유할 수 있는 무료 전자책을 집필하는데 정말 큰 힘이 됩니다. **"추천"** 한 번씩만 부탁 드리겠습니다🙏🙏 ✅ **랭체인 한국어 튜토리얼 강의** …
wikidocs.net
지금 돌이켜보면 이 프로젝트에는 LangGraph를 사용하는게 더 효율적이었을 것 같다.
왜 이렇게 했지 싶은 구석도 많고 더 개선할 부분도 많아 보이지만 일단은 RAG를 활용한 첫 번째 프로젝트였다는 점에 의의를...!
RAG 아키텍처는 다음과 같다.
전체적인 프로세스나 프레임워크가 사용하기 어렵진 않고, 기획이나 성능 향상 측면에서 고민해야 할 부분이 많았다.
벡터스토어를 구축하자
데이터
RAG 챗봇의 첫 번째 스텝은 데이터를 수집하는 것이다.
직접적으로 답변에 필요한 만큼 품질 좋은 데이터가 필요하지만...?
나는 마땅치 않아서 일단 각 지자체 홈페이지를 돌며 지역화폐 정책을 PDF로 추출해왔다.
참고로 PDF는 텍스트 추출 가능한 형태인지 아닌지(텍스트 드래그가 가능한지)가 처리 난이도에 큰 영향을 주기 때문에 웬만하면 텍스트 추출 가능한 형태로 가져와야 한다.
최종적으로 사용한 데이터는 다음과 같다.
1. PDF: 전국 지역화폐 홈페이지 설명란 발췌
2: CSV: 정부24 보조금24 지역화폐 관련 정보 크롤링
여기서 신경쓴 부분은 추후 메타데이터 적용을 위해서 PDF 파일명을 구조화했다는 점이다.
모두 '시도_시군구_지역화폐명_파일정보'와 같은 형식으로 통일했다.(ex. 경북_청도_청도사랑상품권_소개)
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.dirname(current_d
pdf_directory = os.path.join(base_dir, "이용가이드")
csv_file_path = os.path.join(base_dir, "보조금24_혜택", "정부24_보조금24_지역화폐_크롤링_전처리.csv")
vector_store_path = os.path.join(base_dir, "vector_store", "text_chatbot_chromadb")
문서 로더
저장한 데이터를 문서로 사용하기 위해 처리해주는 과정을 거쳤다.
파일 형태에 따라 적절한 로더를 선택해야 한다.
이 과정에서 메타데이터 추출을 위한 처리도 진행했다.
1. PDF: PyMuPDFLoader
- 구조화된 파일명 인덱싱하여 메타데이터 구축
2. CSV: Pandas DataFrame → Document
- CSV 내부에 구조화된 메타데이터 입력하고 슬라이싱하여 구축
import pandas as pd
from langchain.document_loaders import PyMuPDFLoader
from langchain.schema import Document
# PDF 로드
def load_pdf_documents(pdf_directory):
documents = []
if not os.path.exists(pdf_directory):
print(f"🚨 PDF 디렉토리가 존재하지 않습니다: {pdf_directory}")
return documents
for filename in os.listdir(pdf_directory):
if filename.endswith(".pdf"):
file_path = os.path.join(pdf_directory, filename)
loader = PyMuPDFLoader(file_path)
doc_list = loader.load()
# 파일명에서 지역명, 지역화폐명 추출
parts = filename.replace(".pdf", "").split("_")
sido = parts[0] if len(parts) > 0 else "정보 없음"
sigungu = parts[1] if len(parts) > 1 else "정보 없음"
currency_name = parts[2] if len(parts) > 2 else "정보 없음"
# PDF 파일별 자동 메타데이터 추가
for doc in doc_list:
doc.metadata.update({
"시도": sido,
"시군구": sigungu,
"지역화폐명": currency_name,
"파일명": filename,
"문서유형": "database"
})
documents.extend(doc_list)
print(f"PDF 문서 로드 완료: {len(documents)} 개")
return documents
# CSV 로드
def load_csv_documents(csv_file_path):
if not os.path.exists(csv_file_path):
print(f"🚨 CSV 파일이 존재하지 않습니다: {csv_file_path}")
return []
df = pd.read_csv(csv_file_path, encoding="cp949")
# 첫 3개 컬럼은 메타데이터, 나머지는 본문
meta_columns = df.iloc[:, :3].columns.tolist()
content_columns = df.iloc[:, 3:].columns.tolist()
csv_documents = []
for _, row in df.iterrows():
metadata = {
"시도": row[meta_columns[0]],
"시군구": row[meta_columns[1]],
"지역화폐명": row[meta_columns[2]],
"문서유형": "database"
}
# 본문 내용 결합
text_content = "\n".join([f"{col}: {row[col]}" for col in content_columns])
# CSV 데이터를 `Document` 객체로 변환
csv_documents.append(Document(page_content=text_content, metadata=metadata))
print(f"CSV 문서 로드 완료: {len(csv_documents)} 개")
return csv_documents
문서 분할
문서 분할, 즉 청킹 방법도 문서의 특징, 전략에 따라 적절하게 선택해주면 된다.
나는 일반적인 텍스트에 활용할 수 있는 Recursive 방법을 선택했다.
RecursiveCharacterTextSplitter(chunk_size=700, chunk_overlap=150)
추가로, 분할 후 불필요한 메타데이터 필드를 제거하고 특정 필드로만 제한했다.
- 시도, 시군구, 지역화폐명, 파일명(pdf만), 문서유형 + 정보없음
from langchain.text_splitter import RecursiveCharacterTextSplitter
def split_documents(documents):
text_splitter = RecursiveCharacterTextSplitter(chunk_size=700, chunk_overlap=150)
split_docs = text_splitter.split_documents(documents)
print(f"문서 조각 생성 완료: {len(split_docs)} 개")
# 메타데이터 필드 제한
allowed_metadata_keys = {"시도", "시군구", "지역화폐명", "파일명", "문서유형"}
cleaned_docs = []
for doc in split_docs:
new_metadata = {key: doc.metadata.get(key, "정보 없음") for key in allowed_metadata_keys}
cleaned_docs.append(Document(page_content=doc.page_content, metadata=new_metadata))
print(f"메타데이터 정리 완료: {len(cleaned_docs)} 개")
return cleaned_docs
임베딩
임베딩은 OpenAI의 OpenAIEmbeddings를 사용했다.
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
벡터스토어
작은 프로젝트에서는 보통 FAISS와 ChromaDB 중 하나를 선택하는 것 같은데, 나는 메타데이터를 활용할 생각이었으므로 문서와 메타데이터를 함께 저장할 수 있는 ChromaDB를 선택했다.
ChromaDB의 파라미터인 collection_name으로는 벡터스토어를 분리할 수 있지만, 벡터스토어를 불러올 때 콜렉션 네임이 정확해야 불러와진다.
from langchain.vectorstores import Chroma
def save_to_chroma(split_docs, persist_directory):
os.makedirs(persist_directory, exist_ok=True)
vectorstore = Chroma.from_documents(
documents=split_docs,
embedding=embeddings,
persist_directory=persist_directory,
collection_name='text_db_0'
)
vectorstore.persist()
print(f"ChromaDB 저장 완료: {persist_directory}")
벡터DB스토어 구축을 위한 모든 파이프라인은 다음과 같다.
pdf_documents = load_pdf_documents(pdf_directory)
csv_documents = load_csv_documents(csv_file_path)
# 문서 통합
all_documents = pdf_documents + csv_documents
if len(all_documents) == 0:
print("🚨 로드된 문서가 없습니다. 프로세스를 종료합니다.")
else:
# 문서 분할
split_docs = split_documents(all_documents)
# ChromaDB 저장
save_to_chroma(split_docs, persist_directory=vector_store_path)
모든 코드는 깃허브에서 볼 수 있다.
GitHub - da-analysis/asac_6_dataanalysis: 전국 지역 화폐 데이터 현황판 구축 (with 챗봇) - ASAC 데이터 프로
전국 지역 화폐 데이터 현황판 구축 (with 챗봇) - ASAC 데이터 프로젝트. Contribute to da-analysis/asac_6_dataanalysis development by creating an account on GitHub.
github.com
다음 포스트에서는 본격적으로 RAG 모델을 구축하는 프로세스를 정리하고자 한다.