LangChain으로 나만의 RAG 챗봇 만들기: PDF 파일에 질문하는 AI 완성 코드


300페이지 PDF를 ChatGPT에 붙여넣다가 포기한 날, RAG를 만났다

팀 내 기술 문서 300페이지짜리 PDF가 있었다. 신입 팀원이 들어올 때마다 “이 중에서 배포 절차 부분 찾아봐”라고 말하는 게 일이었다. ChatGPT에 복붙해보려 했는데, 토큰 한도에 걸려 반도 못 넣고 잘렸다. “AI가 내 문서는 못 읽나?” 싶은 순간이었다.

그때 찾은 게 RAG(Retrieval-Augmented Generation)라는 방식이었다. 문서 전체를 AI에 욱여넣는 대신, 관련 있는 부분만 쏙 꺼내서 질문과 함께 보내는 구조다. LangChain은 그 파이프라인을 파이썬 코드 몇 줄로 만들 수 있게 해주는 프레임워크다. 직접 PDF 챗봇을 완성하기까지 부딪힌 에러와 완성 코드를 전부 공개한다.



1. RAG가 뭔지 3줄로 이해하고 시작하기

RAG는 단순하다. 문서를 잘게 쪼개 벡터 DB에 저장해 두고, 질문이 들어오면 가장 관련 있는 조각만 꺼내 AI에게 “이 내용 참고해서 답해줘”라고 건네주는 방식이다. 사람으로 치면 두꺼운 책을 통째로 외우는 대신, 색인을 보고 필요한 페이지만 펼치는 것과 같다.

LangChain은 이 파이프라인의 각 단계 — 문서 로딩, 청킹, 임베딩, 벡터 검색, LLM 호출 — 를 하나의 체인으로 묶어준다. 직접 구현하면 수백 줄이 넘는 코드가, LangChain을 쓰면 30줄 안에 끝난다. 그게 이 프레임워크를 쓰는 이유다.

2. 개발 환경 준비 및 필수 패키지 설치

별도 서버 없이 로컬 PC에서 전부 돌아간다. OpenAI API 키만 있으면 준비 완료다.

  • Python 버전: 3.11 이상 (3.12 권장)
  • 코드 편집기: VS Code 또는 Cursor AI
  • 필수 API 키: OpenAI API 키 (임베딩 및 LLM 호출용)
  • 핵심 패키지: langchain, langchain-openai, langchain-community, faiss-cpu, pypdf, python-dotenv

터미널에서 아래 명령어 한 줄로 필요한 패키지를 전부 설치한다. faiss-cpu는 벡터를 로컬에 저장하는 FAISS 라이브러리로, 별도 유료 벡터 DB 없이 내 PC 메모리를 그대로 쓴다.

pip install langchain langchain-openai langchain-community faiss-cpu pypdf python-dotenv

3. PDF 챗봇 전체 코드 단계별 구축

1단계: PDF 로딩 및 청킹

문서를 통째로 집어넣는 대신, 500자 단위로 쪼개고 각 조각이 50자씩 겹치도록 설정한다. 이 오버랩이 없으면 문장이 청크 경계에서 잘려 문맥이 끊기는 문제가 생긴다.

from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# PDF 파일 경로를 본인 파일에 맞게 수정하세요
loader = PyPDFLoader("my_document.pdf")
pages = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50
)
chunks = splitter.split_documents(pages)
print(f"총 {len(chunks)}개의 청크로 분할 완료")

2단계: 임베딩 생성 및 FAISS 벡터 DB 저장

각 청크를 숫자 벡터로 변환해 FAISS 인덱스에 저장한다. 이 단계가 끝나면 “의미적으로 가까운 청크”를 순식간에 검색할 수 있게 된다.

import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

load_dotenv()  # .env 파일에서 OPENAI_API_KEY 로드

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(chunks, embeddings)

# 로컬에 인덱스 저장 (다음 실행 시 재사용 가능)
vectorstore.save_local("faiss_index")
print("벡터 DB 저장 완료")

3단계: RAG 체인 완성 및 질문-답변 실행

저장된 벡터 DB를 불러와 리트리버(Retriever)로 만들고, GPT-4o-mini와 연결하면 PDF에 직접 질문할 수 있는 챗봇이 완성된다.

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA

# 저장된 벡터 DB 로드
vectorstore = FAISS.load_local(
    "faiss_index",
    embeddings,
    allow_dangerous_deserialization=True  # 로컬 파일 신뢰 옵션
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=retriever,
    return_source_documents=True
)

# 질문 던지기
query = "배포 절차 중 롤백 방법은?"
result = qa_chain.invoke({"query": query})

print("\n[AI 답변]")
print(result["result"])
print("\n[참조한 문서 페이지]")
for doc in result["source_documents"]:
    print(f" - 페이지 {doc.metadata.get('page', '?')}")

4. 직접 맞은 에러 2가지와 해결법

에러 ① allow_dangerous_deserialization 오류

저장한 FAISS 인덱스를 불러올 때 ValueError: allow_dangerous_deserialization 에러가 떴다. LangChain 최신 버전에서 보안상 로컬 직렬화 파일을 기본 차단한다는 걸 몰랐다. FAISS.load_local()allow_dangerous_deserialization=True를 명시적으로 추가하자 해결됐다. 본인이 직접 만든 신뢰할 수 있는 파일에만 이 옵션을 쓰면 된다.

에러 ② 한국어 PDF에서 글자 깨짐

한글 PDF를 로딩했더니 청크 안에 ????? 같은 깨진 문자가 잔뜩 섞여 나왔다. PyPDFLoader가 일부 한국어 폰트 임베딩 방식을 제대로 해석하지 못한 것이었다. PyPDFLoader 대신 pdfplumber 기반의 로더로 바꾸거나, PDF를 Adobe Acrobat으로 한 번 다시 저장(Print to PDF)해서 폰트를 재임베딩하면 깔끔하게 해결된다. 후자가 훨씬 빠르다.

5. 실제로 써보니 달라진 것들

300페이지 기술 문서로 RAG 챗봇을 완성한 뒤 팀 내에 공유했다. 신입 팀원이 “배포 실패 시 롤백 절차”를 물어봤을 때 챗봇이 해당 페이지를 정확히 찾아 3줄로 답했다. “페이지 142, 147 참조”라는 출처까지 붙여서.

  • 검색 시간: PDF에서 원하는 내용을 Ctrl+F로 뒤지던 평균 15분이 질문 하나로 10초 이내로 줄었다
  • 비용: text-embedding-3-small 모델로 300페이지 PDF 전체를 임베딩하는 비용은 약 50원 수준이었다. 한 번 인덱스를 만들면 이후 추가 비용은 LLM 호출 비용뿐이다
  • 확장성: PDF 파일 경로만 바꾸면 계약서, 논문, 매뉴얼 어디에든 즉시 적용된다. 파이프라인 코드는 그대로다

LangChain이 처음엔 버전 변경이 잦고 문서가 복잡해 보이지만, RAG 하나만 제대로 익히면 이후 에이전트, 멀티턴 대화, 도구 호출까지 같은 구조 위에서 확장된다. 오늘 만든 30줄짜리 코드가 그 출발점이다.