LangChain 독학 가이드 10 - 실전 프로젝트
드디어 마지막 글이에요! 지금까지 배운 프롬프트 템플릿, LCEL, 모델 연결, 출력 파서, 메모리, RAG, Agent를 모두 합쳐서 실제로 쓸 수 있는 PDF 문서 기반 QA 챗봇을 만들어볼 거예요.
회사에서 “이 문서에서 이거 찾아줘”라는 요청 많이 받으시죠? 이 프로젝트를 완성하면 PDF를 업로드하고 질문하면 답변해주는 나만의 AI 비서를 가질 수 있어요!
프로젝트 구조
우리가 만들 챗봇의 구조예요:
- PDF 업로드 → 텍스트 추출
- 텍스트 분할 → 적당한 크기로 나누기
- 벡터 저장소 → 임베딩하여 저장
- 질문 입력 → 관련 문서 검색
- 답변 생성 → LLM이 답변
- 대화 기록 → 이전 대화 기억
필요한 패키지 설치
pip install langchain langchain-openai langchain-community
pip install chromadb pypdf streamlit python-dotenv
1단계: PDF 로드 및 분할
먼저 PDF를 읽고 적당한 크기로 나누는 코드예요:
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
def load_and_split_pdf(pdf_path: str):
"""PDF를 로드하고 청크로 분할합니다."""
# PDF 로드
loader = PyPDFLoader(pdf_path)
documents = loader.load()
# 텍스트 분할
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 청크 크기
chunk_overlap=200, # 청크 간 겹침
separators=["\n\n", "\n", ".", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"총 {len(chunks)}개의 청크로 분할됨")
return chunks
# 테스트
chunks = load_and_split_pdf("sample.pdf")
print(chunks[0].page_content[:200])
실행 결과:
총 45개의 청크로 분할됨
제1장 서론
본 보고서는 2024년 상반기 실적을 분석하고...
2단계: 벡터 저장소 구축
분할된 텍스트를 벡터로 변환하고 저장해요:
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
def create_vector_store(chunks):
"""청크를 벡터화하여 저장소에 저장합니다."""
# 임베딩 모델
embeddings = OpenAIEmbeddings()
# 벡터 저장소 생성
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 로컬에 저장
)
print("벡터 저장소 생성 완료!")
return vector_store
# 실행
vector_store = create_vector_store(chunks)
실행 결과:
벡터 저장소 생성 완료!
3단계: RAG 체인 구성
이제 핵심인 RAG 체인을 만들어요:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
def create_rag_chain(vector_store):
"""RAG 체인을 생성합니다."""
# 검색기 설정
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 3} # 상위 3개 문서 검색
)
# 프롬프트 템플릿
prompt = ChatPromptTemplate.from_messages([
("system", """당신은 문서 기반 질의응답 AI입니다.
주어진 컨텍스트를 바탕으로 질문에 답변하세요.
컨텍스트에 없는 내용은 "문서에서 해당 정보를 찾을 수 없습니다"라고 답변하세요.
컨텍스트:
{context}"""),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}")
])
# 모델
model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# 문서 포맷팅 함수
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# RAG 체인 구성
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough(),
"chat_history": lambda x: x.get("chat_history", [])
}
| prompt
| model
| StrOutputParser()
)
return rag_chain
# 체인 생성
rag_chain = create_rag_chain(vector_store)
4단계: 대화 기록 관리
이전 대화를 기억하는 기능을 추가해요:
from langchain_core.messages import HumanMessage, AIMessage
class ChatBot:
def __init__(self, vector_store):
self.chain = create_rag_chain(vector_store)
self.chat_history = []
def ask(self, question: str) -> str:
"""질문하고 답변을 받습니다."""
# 체인 실행
response = self.chain.invoke({
"question": question,
"chat_history": self.chat_history
})
# 대화 기록에 추가
self.chat_history.append(HumanMessage(content=question))
self.chat_history.append(AIMessage(content=response))
# 기록이 너무 길어지면 오래된 것 삭제
if len(self.chat_history) > 10:
self.chat_history = self.chat_history[-10:]
return response
def clear_history(self):
"""대화 기록을 초기화합니다."""
self.chat_history = []
# 사용 예시
bot = ChatBot(vector_store)
print(bot.ask("이 문서의 주요 내용이 뭐야?"))
print(bot.ask("좀 더 자세히 설명해줘")) # 이전 대화 컨텍스트 유지
실행 결과:
[첫 번째 질문]
이 문서는 2024년 상반기 실적 보고서입니다. 주요 내용은...
[두 번째 질문 - 맥락 유지됨]
네, 더 자세히 설명드리겠습니다. 실적 보고서에 따르면...
5단계: Streamlit 웹 UI
이제 웹 인터페이스를 만들어요. app.py 파일을 만드세요:
import streamlit as st
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.messages import HumanMessage, AIMessage
import tempfile
import os
load_dotenv()
st.set_page_config(page_title="PDF 챗봇", page_icon="📚")
st.title("📚 PDF 문서 QA 챗봇")
# 세션 상태 초기화
if "chat_history" not in st.session_state:
st.session_state.chat_history = []
if "vector_store" not in st.session_state:
st.session_state.vector_store = None
# 사이드바 - PDF 업로드
with st.sidebar:
st.header("📁 PDF 업로드")
uploaded_file = st.file_uploader("PDF 파일을 선택하세요", type="pdf")
if uploaded_file and st.button("문서 처리 시작"):
with st.spinner("PDF 처리 중..."):
# 임시 파일로 저장
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
tmp.write(uploaded_file.getvalue())
tmp_path = tmp.name
# PDF 로드 및 분할
loader = PyPDFLoader(tmp_path)
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)
# 벡터 저장소 생성
embeddings = OpenAIEmbeddings()
st.session_state.vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings
)
# 임시 파일 삭제
os.unlink(tmp_path)
st.success(f"✅ {len(chunks)}개 청크 처리 완료!")
if st.button("대화 기록 초기화"):
st.session_state.chat_history = []
st.rerun()
# 메인 화면 - 채팅
if st.session_state.vector_store is None:
st.info("👈 왼쪽에서 PDF 파일을 업로드해주세요.")
else:
# 이전 대화 표시
for msg in st.session_state.chat_history:
if isinstance(msg, HumanMessage):
st.chat_message("user").write(msg.content)
else:
st.chat_message("assistant").write(msg.content)
# 질문 입력
if question := st.chat_input("질문을 입력하세요..."):
st.chat_message("user").write(question)
# RAG 체인 실행
retriever = st.session_state.vector_store.as_retriever(search_kwargs={"k": 3})
prompt = ChatPromptTemplate.from_messages([
("system", """주어진 컨텍스트를 바탕으로 질문에 답변하세요.
컨텍스트에 없는 내용은 "문서에서 해당 정보를 찾을 수 없습니다"라고 답변하세요.
컨텍스트:
{context}"""),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{question}")
])
model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
with st.spinner("답변 생성 중..."):
# 관련 문서 검색
docs = retriever.invoke(question)
context = format_docs(docs)
# 답변 생성
chain = prompt | model | StrOutputParser()
response = chain.invoke({
"context": context,
"question": question,
"chat_history": st.session_state.chat_history
})
st.chat_message("assistant").write(response)
# 대화 기록 저장
st.session_state.chat_history.append(HumanMessage(content=question))
st.session_state.chat_history.append(AIMessage(content=response))
실행하기
터미널에서 다음 명령어로 실행하세요:
streamlit run app.py
실행 결과:
You can now view your Streamlit app in your browser.
Local URL: http://localhost:8501
브라우저에서 http://localhost:8501로 접속하면 챗봇이 실행돼요!
전체 코드 (단일 파일)
전체 코드를 한 파일에 정리했어요. pdf_chatbot.py로 저장하세요:
"""
PDF 문서 기반 QA 챗봇 - LangChain 실전 프로젝트
실행: streamlit run pdf_chatbot.py
"""
import streamlit as st
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, AIMessage
import tempfile
import os
# 환경 변수 로드
load_dotenv()
# 페이지 설정
st.set_page_config(
page_title="PDF 문서 챗봇",
page_icon="📚",
layout="wide"
)
# 제목
st.title("📚 PDF 문서 QA 챗봇")
st.caption("PDF를 업로드하고 문서에 대해 질문하세요!")
# 세션 상태 초기화
if "messages" not in st.session_state:
st.session_state.messages = []
if "vector_store" not in st.session_state:
st.session_state.vector_store = None
# 사이드바
with st.sidebar:
st.header("설정")
# PDF 업로드
uploaded_file = st.file_uploader(
"PDF 파일 업로드",
type="pdf",
help="분석할 PDF 문서를 업로드하세요"
)
if uploaded_file:
if st.button("📄 문서 처리", use_container_width=True):
with st.spinner("문서를 처리하는 중..."):
# 임시 파일 저장
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as f:
f.write(uploaded_file.getvalue())
temp_path = f.name
# PDF 로드
loader = PyPDFLoader(temp_path)
docs = loader.load()
# 텍스트 분할
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(docs)
# 벡터 저장소 생성
embeddings = OpenAIEmbeddings()
st.session_state.vector_store = Chroma.from_documents(
chunks, embeddings
)
# 임시 파일 삭제
os.unlink(temp_path)
st.success(f"✅ {len(chunks)}개 청크 처리 완료!")
st.divider()
if st.button("🗑️ 대화 초기화", use_container_width=True):
st.session_state.messages = []
st.rerun()
# 메인 영역
if st.session_state.vector_store is None:
st.info("👈 왼쪽 사이드바에서 PDF를 업로드하세요.")
else:
# 이전 메시지 표시
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.write(msg["content"])
# 사용자 입력
if prompt := st.chat_input("문서에 대해 질문하세요..."):
# 사용자 메시지 표시
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.write(prompt)
# 답변 생성
with st.chat_message("assistant"):
with st.spinner("생각 중..."):
# 검색
retriever = st.session_state.vector_store.as_retriever(
search_kwargs={"k": 3}
)
docs = retriever.invoke(prompt)
context = "\n\n".join(d.page_content for d in docs)
# 대화 기록 변환
history = []
for m in st.session_state.messages[:-1]:
if m["role"] == "user":
history.append(HumanMessage(content=m["content"]))
else:
history.append(AIMessage(content=m["content"]))
# 프롬프트
chat_prompt = ChatPromptTemplate.from_messages([
("system", f"""문서 내용을 바탕으로 답변하세요.
문서에 없는 내용은 "문서에서 찾을 수 없습니다"라고 하세요.
문서 내용:
{context}"""),
MessagesPlaceholder(variable_name="history"),
("human", "{question}")
])
# 체인 실행
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
chain = chat_prompt | llm | StrOutputParser()
response = chain.invoke({
"history": history,
"question": prompt
})
st.write(response)
# 어시스턴트 메시지 저장
st.session_state.messages.append({"role": "assistant", "content": response})
다음 단계
이 프로젝트를 확장하고 싶다면:
- LangGraph: 더 복잡한 워크플로우 구현
- LangSmith: 디버깅과 모니터링
- 여러 문서 지원: 여러 PDF를 동시에 분석
- 배포: Streamlit Cloud나 AWS로 배포
시리즈를 마치며
10편에 걸쳐 LangChain의 핵심을 모두 다뤘어요:
- LangChain 소개
- 개발 환경 설정
- 프롬프트 템플릿
- LCEL 체인
- 다양한 LLM 연결
- 출력 파서
- 대화 메모리
- RAG 구현
- Agent와 Tools
- 실전 프로젝트 (지금!)
여기까지 따라오셨다면 이제 LangChain으로 웬만한 AI 애플리케이션은 만들 수 있어요. 직접 프로젝트를 만들어보면서 실력을 키워보세요!