LangChain으로 RAG 챗봇 구축, 문서 검색 속도를 혁신하다
예전에 400페이지 가까운 매뉴얼 문서를 회사에서 검색하며, 원하는 정보를 찾는 데 30분 이상 걸리곤 했습니다. 비슷한 내용이 반복되는 데다, 문서에 검색 키워드가 여러 형태로 흩어져 있어 원하는 답변을 빠르게 찾기란 결코 쉽지 않았죠. 이런 문제를 해결하고자 LangChain으로 RAG(Retrieval-Augmented Generation) 챗봇을 직접 만들어보기로 했는데, 설치 단계부터 예상치 못한 의존성 오류가 기다리고 있었습니다. 특히 langchain-openai 설치 시 numpy 버전 충돌 오류가 발생해, pip uninstall numpy 후 지정 버전(numpy==1.23.5)으로 재설치하는 과정을 거쳤습니다. 덕분에 공식 문서만으로는 미처 설명되지 않은 복잡한 문제들을 어떻게 해결해야 하는지 감을 잡게 되었죠. 설치가 완료된 후, 400페이지 분량 PDF를 단 10분 만에 인덱싱하고, 문서 검색 시간이 30분에서 거의 실시간으로 줄어드는 경험은 직접 구현해본 사람만이 누릴 수 있는 값진 성과였습니다.
1. RAG와 LangChain의 핵심 메커니즘 살펴보기
LangChain의 가장 큰 매력은 방대한 문서를 적절한 크기로 쪼개어 검색에 최적화한다는 점입니다. ‘설정 > 검색 옵션’에서 이 기능을 활성화하면, 문서 내 의미 단위로 나누어진 청크를 빠르게 탐색할 수 있는데, 실제 업무에 적용하니 하루에 10건 이상의 문서를 5분 이내에 처리하는 일이 가능해졌습니다. 특히, 문서 청크 사이의 중첩(overlap)을 적절히 조절해 유사한 문맥이 끊기지 않도록 하는 점이 인상적입니다. 이 과정을 통해 검색 정확도가 높아졌고, 덕분에 자료를 뒤지는 데 들이던 시간을 획기적으로 줄일 수 있었습니다.
2. 개발 환경 구성: 필수 요소와 설치 팁
| 필수 요소 | 설명 |
|---|---|
| Python 버전 | 3.10 이상 권장 (특정 라이브러리 호환성 때문) |
| API 키 | OpenAI API 키 (임베딩 및 챗봇 응답 생성에 필수) |
| 필수 라이브러리 | langchain, langchain-openai, faiss-cpu, pypdf, python-dotenv |
라이브러리 설치 시 Permission Denied 오류가 발생했다면, 관리자 권한으로 터미널을 실행하는 것이 가장 빠른 해결책입니다. 가령 Windows에서는 ‘명령 프롬프트’ 아이콘을 우클릭하여 ‘관리자 권한으로 실행’을 선택한 후, 아래 명령어를 실행하세요.
pip install langchain langchain-openai faiss-cpu pypdf python-dotenv
또한, API 키는 프로젝트 루트에 .env 파일을 만들어 다음과 같이 입력합니다. 경로 문제로 인한 키 인식 오류를 방지하려면, load_dotenv() 호출 전 환경변수가 제대로 로드되는지 꼭 확인하세요.
OPENAI_API_KEY=발급받은_키
작업 중 .env 파일이 무시되는 경우, 환경 변수 로딩 코드를 아래처럼 명시적으로 경로를 지정해주면 문제가 해결되기도 했습니다.
from dotenv import load_dotenv
import os
env_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path=env_path)
3. PDF 문서 불러오기와 질문 답변 파이프라인 구축하기
1단계: PyPDFLoader로 PDF 파일 불러오기
PDF 파일 경로를 지정할 때 상대경로와 절대경로를 혼동하는 일이 많았습니다. 특히 Windows 환경에서는 역슬래시() 대신 슬래시(/)를 사용하거나, 경로 문자열 앞에 r을 붙여 raw string으로 처리하는 것이 중요합니다. 예를 들어 r"C:UsersuserDocumentsmanual.pdf"처럼 작성해야 오류를 줄일 수 있습니다. 파일이 로드되지 않는다면, 먼저 해당 파일을 PDF 뷰어로 열어 손상 여부를 확인하는 습관이 문제 해결에 큰 도움이 됩니다.
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
load_dotenv()
def load_and_split_pdf(pdf_path: str) -> list:
loader = PyPDFLoader(pdf_path)
documents = loader.load()
print(f"PDF 로드 완료: 총 {len(documents)} 페이지")
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["nn", "n", "。", ".", " ", ""]
)
chunks = splitter.split_documents(documents)
print(f"청킹 완료: {len(chunks)}개 덩어리 생성")
return chunks
이 코드에서 청크 크기(chunk_size=800)와 중첩 크기(chunk_overlap=100)는 문서 특성에 따라 조절해야 합니다. 예를 들어, 기술 문서처럼 문장이 짧고 자주 나뉘는 경우에는 청크 크기를 줄이고 중첩을 늘려 문맥이 끊기지 않도록 하는 것이 유리합니다. 반면, 일반 보고서 문서라면 청크 크기를 크게 해도 무방합니다.
2단계: 청크 임베딩 생성과 FAISS 인덱스 구축
문서를 청크로 나눈 뒤, OpenAI의 임베딩 모델을 활용해 각 청크를 벡터화합니다. 벡터화된 데이터는 FAISS 라이브러리로 인덱싱해 문서 내 빠른 검색을 가능하게 합니다. 처음에 벡터화 과정에서 API 호출 제한에 걸려 임베딩 생성이 중단된 적도 있었는데, 이럴 때는 호출 속도를 조절하거나, 청크 개수를 나누어 처리하는 방식을 추천합니다.
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
VECTORSTORE_PATH = "./faiss_index"
def build_or_load_vectorstore(chunks=None) -> FAISS:
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
if os.path.exists(VECTORSTORE_PATH) and chunks is None:
print("기존 FAISS 인덱스 로드 중...")
return FAISS.load_local(VECTORSTORE_PATH, embeddings, allow_dangerous_deserialization=True)
print("임베딩 생성 및 FAISS 인덱스 구축 중...")
vectorstore = FAISS.from_documents(chunks, embeddings)
vectorstore.save_local(VECTORSTORE_PATH)
print(f"인덱스 저장 완료: {VECTORSTORE_PATH}")
return vectorstore
FAISS 인덱스를 재사용하면 매번 임베딩을 새로 계산하지 않아도 되어 API 호출 비용과 시간을 크게 줄일 수 있습니다. 인덱스 파일이 손상될 경우, 다시 임베딩을 생성하는 옵션을 염두에 두고 백업을 주기적으로 관리하는 것이 좋습니다.
4. 실제 작업 중 마주친 오류와 대응법
PDF 파일이 로드되지 않는 경우가 있었습니다. 파일이 손상된 상태거나 암호화되어 있을 때 PyPDFLoader가 정상 작동하지 않았는데, 이런 문제는 Adobe Reader 같은 PDF 뷰어로 열어본 뒤 저장하거나, PDF 다시 생성 작업을 통해 해결했습니다. 또한, OpenAI API 호출 시 인증 오류가 발생하면, .env 파일에 API 키를 제대로 입력했는지, 키에 공백이 포함되어 있진 않은지 꼼꼼히 확인하는 게 필수입니다.
5. 응답 품질을 좌우하는 프롬프트 다듬기
초기에는 프롬프트가 지나치게 길고 복잡해, 챗봇의 응답 시간이 평균 5초 이상 걸렸습니다. 이를 간결하게 다듬고, 핵심 정보만 포함시키면서도 문맥을 명확히 하는 방향으로 수정하니, 응답 시간이 2초 내외로 줄었고, 사용자가 질문에 대한 답변을 훨씬 빠르게 받을 수 있었습니다. 프롬프트 설계에서 특히 주의할 점은 불필요한 중복 표현을 제거하고, 문장이 명확히 어떤 답변을 요구하는지 쉽게 파악할 수 있도록 하는 것입니다. 이런 작업은 반복해서 테스트하고 튜닝하는 과정에서 점점 나아졌습니다.
직접 LangChain과 FAISS를 활용해 RAG 챗봇을 구축해 보면서 문서 검색 속도가 눈에 띄게 개선된 것을 체감했습니다. 초기 세팅에서 마주친 의존성 문제, API 호출 제한, 경로 오류 등은 시간이 지나면서 노하우로 쌓였고, 프로젝트를 진행할수록 더 빠르고 안정적인 시스템을 만들 수 있었습니다. 다음에는 다양한 임베딩 모델과 추가적인 전처리 방식을 적용해, 더욱 복잡한 문서에서도 높은 정확도를 내는 챗봇을 만들어 볼 계획입니다.