MLOps
RAG
RAG 완전정복 시리즈 : LangChain이란? (2편)

포스팅 개요

지난 1편에서 RAG의 개념과 아키텍처를 살펴봤습니다. 이제 실제 구현을 위해 가장 중요한 도구인 LangChain에 대해 알아볼 차례입니다.

LangChain은 단순히 하나의 라이브러리가 아닙니다. LLM을 활용한 애플리케이션 개발을 위한 포괄적인 프레임워크로, RAG 시스템 구축에 필수적인 모든 구성 요소를 제공합니다. 문서 로딩부터 텍스트 분할, 벡터화, 검색, 그리고 최종 답변 생성까지의 전 과정을 체계적으로 관리할 수 있습니다.

본 포스팅에서는 LangChain이 무엇인지부터 시작해서 RAG 구현에 필요한 핵심 컴포넌트들을 하나씩 뜯어보고, 실제 코드를 통해 어떻게 활용하는지 단계별로 설명합니다.

LangChain 시작하기 (LangChain으로 RAG 구현 여정을 시작합니다)


1. LangChain이 대체 뭔가요?

LangChain은 대화형 AI 애플리케이션을 쉽게 만들 수 있도록 도와주는 Python/JavaScript 프레임워크입니다. 특히 RAG 시스템처럼 여러 단계가 복잡하게 연결된 AI 워크플로우를 구축할 때 빛을 발합니다.

일상 생활로 비유하면?

요리를 한다고 생각해보겠습니다.

LangChain 없이 RAG 만들기:

  • 직접 재료 하나하나 사서 오고
  • 도구도 하나씩 준비하고
  • 레시피도 직접 만들고
  • 매번 처음부터 다시 요리

LangChain 사용한 RAG 만들기:

  • 필요한 재료와 도구가 세트로 준비됨
  • 검증된 레시피(템플릿) 제공
  • 단계별로 가이드해줌
  • 다른 요리에도 응용 가능

LangChain은 AI 개발의 "요리 키트"라고 할 수 있습니다.

LangChain 개념 (LangChain이 제공하는 것들)


2. 왜 LangChain을 써야 할까요?

RAG 없이 직접 구현한다면...

# 문서 로딩 - 각각 다른 방법
pdf_text = extract_pdf("file.pdf")
word_text = extract_docx("file.docx") 
web_text = scrape_webpage("url")
 
# 텍스트 분할 - 직접 로직 작성
def chunk_text(text, size=1000):
    chunks = []
    for i in range(0, len(text), size):
        chunks.append(text[i:i+size])
    return chunks
 
# 임베딩 - 각 모델마다 다른 방식
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('model-name')
embeddings = model.encode(chunks)
 
# 벡터 DB - 직접 구축
import numpy as np
def similarity_search(query_vector, doc_vectors, top_k=5):
    # 코사인 유사도 계산 로직
    # 상위 k개 문서 반환 로직
    pass
 
# LLM 호출 - API별로 다른 방식
import openai
response = openai.ChatCompletion.create(...)

이렇게 하면 코드가 길어지고, 각 단계마다 다른 방식을 익혀야 합니다. 게다가 에러 처리, 최적화, 확장성까지 고려하면...😰

LangChain으로 구현하면?

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter  
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.llms import OpenAI
 
# 문서 로딩 - 통일된 방식
loader = PyPDFLoader("document.pdf")
documents = loader.load()
 
# 텍스트 분할 - 최적화된 알고리즘
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
texts = text_splitter.split_documents(documents)
 
# 벡터화 & 저장 - 한 줄로 완성
vectorstore = Chroma.from_documents(texts, OpenAIEmbeddings())
 
# 검색 & 답변 생성 - 체인으로 연결
from langchain.chains import RetrievalQA
qa = RetrievalQA.from_chain_type(
    llm=OpenAI(),
    retriever=vectorstore.as_retriever()
)
 
# 질문하기
result = qa.run("사용자 질문")

훨씬 간단하고 직관적이죠!


3. LangChain의 핵심 컴포넌트들

LangChain은 RAG 구현에 필요한 모든 요소를 컴포넌트 형태로 제공합니다. 각 컴포넌트의 역할과 사용법을 살펴보겠습니다.

3-1. Document Loaders: 문서 불러오기

Document Loader는 다양한 형태의 문서를 LangChain에서 처리할 수 있는 형태로 변환해주는 컴포넌트입니다.

from langchain.document_loaders import (
    PyPDFLoader,          # PDF 파일
    DirectoryLoader,      # 폴더 전체
    TextLoader,          # 텍스트 파일  
    WebBaseLoader,       # 웹페이지
    CSVLoader            # CSV 파일
)
 
# PDF 로딩
pdf_loader = PyPDFLoader("manual.pdf")
pdf_docs = pdf_loader.load()
 
# 폴더 전체 로딩 
dir_loader = DirectoryLoader(
    "./documents", 
    glob="**/*.txt",
    loader_cls=TextLoader
)
text_docs = dir_loader.load()
 
# 웹페이지 로딩
web_loader = WebBaseLoader("https://example.com")
web_docs = web_loader.load()

각 로더가 반환하는 Document 객체는 다음과 같은 구조를 가집니다:

Document(
    page_content="문서의 실제 내용",
    metadata={
        "source": "파일 경로 또는 URL",
        "page": 1,  # PDF의 경우 페이지 번호
        "title": "문서 제목"
    }
)

Document Loaders (LangChain의 다양한 Document Loader들)

3-2. Text Splitters: 텍스트 분할

큰 문서를 적절한 크기로 나누는 것은 RAG의 핵심입니다. LangChain은 다양한 분할 전략을 제공합니다.

from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,  # 범용적으로 사용
    CharacterTextSplitter,           # 단순 문자 기준 분할
    TokenTextSplitter,               # 토큰 기준 분할
    MarkdownTextSplitter            # 마크다운 구조 기준 분할
)
 
# 가장 많이 사용되는 RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        # 청크 크기
    chunk_overlap=200,      # 겹치는 부분
    separators=["\n\n", "\n", " ", ""]  # 분할 우선순위
)
 
chunks = text_splitter.split_documents(documents)

chunk_overlap이 중요한 이유: 청크 경계에서 정보가 잘려나가는 것을 방지합니다. 예를 들어, "파이썬은 프로그래밍 언어입니다. 파이썬의 특징은..."에서 첫 번째 청크가 "파이썬은 프로그래밍 언어입니다."로 끝나면, 두 번째 청크는 "파이썬의 특징은..."으로 시작하게 되어 맥락이 끊어집니다. overlap을 설정하면 "파이썬은 프로그래밍 언어입니다. 파이썬의 특징은..."이 두 번째 청크에도 포함되어 연속성을 유지합니다.

Text Splitting (텍스트 분할과 overlap의 역할)

3-3. Embeddings: 텍스트 벡터화

텍스트를 숫자 벡터로 변환하는 역할을 담당합니다. 다양한 임베딩 모델을 통일된 인터페이스로 사용할 수 있습니다.

from langchain.embeddings import (
    OpenAIEmbeddings,        # OpenAI 임베딩
    HuggingFaceEmbeddings,   # Hugging Face 모델들
    OllamaEmbeddings        # Ollama 로컬 모델
)
 
# OpenAI 임베딩 (유료)
openai_embeddings = OpenAIEmbeddings(
    model="text-embedding-ada-002"
)
 
# 무료 한국어 임베딩 모델
hf_embeddings = HuggingFaceEmbeddings(
    model_name="jhgan/ko-sroberta-multitask"
)
 
# 로컬 임베딩 (Ollama)
ollama_embeddings = OllamaEmbeddings(
    model="nomic-embed-text"
)
 
# 사용 방법은 모두 동일
vector = embeddings.embed_query("안녕하세요")
doc_vectors = embeddings.embed_documents(["문서1", "문서2"])

3-4. Vector Stores: 벡터 데이터베이스

임베딩된 벡터를 저장하고 유사도 검색을 수행하는 컴포넌트입니다.

from langchain.vectorstores import (
    Chroma,      # 로컬용 벡터 DB
    Pinecone,    # 클라우드 벡터 DB  
    FAISS,       # Facebook AI 검색
    Weaviate     # 오픈소스 벡터 DB
)
 
# Chroma 사용 (가장 간단)
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"  # 로컬 저장
)
 
# 검색 수행
similar_docs = vectorstore.similarity_search(
    "파이썬 설치 방법",
    k=5  # 상위 5개 문서 반환
)
 
# 유사도 점수와 함께 반환
similar_docs_with_score = vectorstore.similarity_search_with_score(
    "파이썬 설치 방법",
    k=5
)

Vector Store (벡터 데이터베이스의 유사도 검색 과정)

3-5. LLMs: 대화형 언어 모델

답변 생성을 담당하는 언어 모델 컴포넌트입니다.

from langchain.llms import (
    OpenAI,        # OpenAI GPT 모델들
    Ollama,        # 로컬 LLM
    HuggingFacePipeline  # Hugging Face 모델들
)
 
from langchain.chat_models import (
    ChatOpenAI,    # GPT-3.5, GPT-4
    ChatOllama     # Ollama 채팅 모델
)
 
# OpenAI 사용
openai_llm = ChatOpenAI(
    model_name="gpt-3.5-turbo",
    temperature=0.7
)
 
# Ollama 로컬 모델 사용  
ollama_llm = ChatOllama(
    model="llama3.1:8b",
    temperature=0.7
)
 
# 직접 호출
response = llm.predict("안녕하세요, 파이썬에 대해 설명해주세요.")

4. 실제 코드로 보는 LangChain 활용

이제 각 컴포넌트를 조합해서 간단한 RAG 시스템을 만들어보겠습니다.

4-1. 기본적인 RAG 파이프라인

from langchain.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
from langchain.llms import Ollama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
 
# 1단계: 문서 로딩
print("📁 문서 로딩 중...")
loader = DirectoryLoader(
    "./documents",
    glob="**/*.txt",
    loader_cls=TextLoader
)
documents = loader.load()
print(f"로딩된 문서 수: {len(documents)}개")
 
# 2단계: 텍스트 분할
print("✂️ 텍스트 분할 중...")
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ".", "!", "?", ",", " ", ""]
)
splits = text_splitter.split_documents(documents)
print(f"분할된 청크 수: {len(splits)}개")
 
# 3단계: 임베딩 & 벡터 저장
print("🔢 벡터화 중...")
embeddings = HuggingFaceEmbeddings(
    model_name="jhgan/ko-sroberta-multitask"
)
 
vectorstore = Chroma.from_documents(
    documents=splits,
    embedding=embeddings,
    persist_directory="./chroma_db"
)
print("벡터 DB 구축 완료!")
 
# 4단계: LLM 설정
print("🤖 LLM 설정 중...")
llm = Ollama(model="llama3.1:8b")
 
# 5단계: RAG 체인 구성
retriever = vectorstore.as_retriever(
    search_kwargs={"k": 3}  # 상위 3개 문서 검색
)
 
# 커스텀 프롬프트 설정
prompt_template = """다음 문서들을 참고하여 질문에 답변해주세요:
 
{context}
 
질문: {question}
 
답변: 위 문서들을 바탕으로 정확하고 자세한 답변을 제공해주세요. 
문서에 명시되지 않은 내용은 추측하지 마시고, "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 말씀해주세요.
"""
 
PROMPT = PromptTemplate(
    template=prompt_template,
    input_variables=["context", "question"]
)
 
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT},
    return_source_documents=True
)
 
# 6단계: 질문하기
def ask_question(question):
    print(f"\n🔍 질문: {question}")
    print("💭 답변 생성 중...")
    
    result = qa_chain({"query": question})
    
    print(f"📝 답변: {result['result']}")
    print(f"\n📚 참고 문서:")
    for i, doc in enumerate(result['source_documents'], 1):
        print(f"{i}. {doc.metadata.get('source', '알 수 없음')}")
        print(f"   내용 미리보기: {doc.page_content[:100]}...")
 
# 사용 예시
ask_question("파이썬 설치 방법을 알려주세요")
ask_question("라이브러리를 설치하는 방법은 무엇인가요?")

RAG 파이프라인 실행 (LangChain으로 구성한 RAG 파이프라인 실행 과정)

4-2. 고급 기능: 메모리와 대화 기록

단순한 일회성 질답이 아닌, 이전 대화를 기억하는 RAG 시스템도 만들 수 있습니다.

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain
 
# 대화 기록을 저장할 메모리
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True,
    output_key="answer"
)
 
# 대화형 RAG 체인
conversational_chain = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory,
    return_source_documents=True
)
 
# 연속적인 대화
def chat():
    print("💬 대화형 RAG 시스템 시작! (종료하려면 'quit' 입력)")
    
    while True:
        question = input("\n사용자: ")
        if question.lower() == 'quit':
            break
            
        result = conversational_chain({"question": question})
        print(f"AI: {result['answer']}")
 
chat()

4-3. 다양한 문서 형식 처리

from langchain.document_loaders import (
    PyPDFLoader,
    Docx2txtLoader,
    CSVLoader,
    UnstructuredHTMLLoader
)
 
def load_various_documents():
    all_documents = []
    
    # PDF 파일들
    pdf_loader = DirectoryLoader(
        "./documents",
        glob="**/*.pdf",
        loader_cls=PyPDFLoader
    )
    pdf_docs = pdf_loader.load()
    all_documents.extend(pdf_docs)
    
    # Word 문서들
    docx_loader = DirectoryLoader(
        "./documents", 
        glob="**/*.docx",
        loader_cls=Docx2txtLoader
    )
    docx_docs = docx_loader.load()
    all_documents.extend(docx_docs)
    
    # CSV 파일들
    csv_loader = DirectoryLoader(
        "./documents",
        glob="**/*.csv", 
        loader_cls=CSVLoader
    )
    csv_docs = csv_loader.load()
    all_documents.extend(csv_docs)
    
    return all_documents
 
# 사용
mixed_documents = load_various_documents()
print(f"총 {len(mixed_documents)}개 문서 로딩 완료")

다양한 문서 형식 (LangChain이 지원하는 다양한 문서 형식들)


5. LangChain 체인의 이해

LangChain의 핵심 개념 중 하나는 **체인(Chain)**입니다. 체인은 여러 단계의 처리 과정을 하나로 연결하는 방식입니다.

5-1. 기본 체인들

from langchain.chains import (
    LLMChain,           # 기본 LLM 체인
    RetrievalQA,        # 검색 기반 QA
    StuffDocumentsChain, # 문서 요약
    MapReduceDocumentsChain  # 대용량 문서 처리
)
 
# LLMChain: 가장 기본적인 체인
from langchain.prompts import PromptTemplate
 
prompt = PromptTemplate(
    input_variables=["topic"],
    template="{topic}에 대해 간단히 설명해주세요."
)
 
llm_chain = LLMChain(llm=llm, prompt=prompt)
result = llm_chain.run(topic="파이썬")

5-2. 커스텀 체인 만들기

from langchain.chains.base import Chain
from typing import Dict, List
 
class CustomRAGChain(Chain):
    """커스텀 RAG 체인"""
    
    vectorstore: object
    llm: object
    
    @property
    def input_keys(self) -> List[str]:
        return ["query"]
    
    @property  
    def output_keys(self) -> List[str]:
        return ["result", "sources"]
    
    def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
        query = inputs["query"]
        
        # 1. 문서 검색
        docs = self.vectorstore.similarity_search(query, k=3)
        
        # 2. 컨텍스트 구성
        context = "\n".join([doc.page_content for doc in docs])
        
        # 3. 프롬프트 생성
        prompt = f"""
        다음 문서를 참고하여 질문에 답하세요:
        
        {context}
        
        질문: {query}
        답변:
        """
        
        # 4. LLM 호출
        result = self.llm.predict(prompt)
        
        return {
            "result": result,
            "sources": [doc.metadata for doc in docs]
        }
 
# 사용
custom_chain = CustomRAGChain(
    vectorstore=vectorstore,
    llm=llm
)
 
result = custom_chain({"query": "파이썬이란?"})

LangChain 체인 구조 (LangChain 체인의 연결 구조)


6. LangChain 활용 팁과 주의사항

6-1. 성능 최적화

# 1. 임베딩 캐싱
from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import LocalFileStore
 
fs = LocalFileStore("./cache/")
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
    underlying_embeddings=embeddings,
    document_embedding_cache=fs
)
 
# 2. 벡터스토어 최적화
vectorstore = Chroma.from_documents(
    documents=splits,
    embedding=cached_embedder,
    persist_directory="./chroma_db",
    # 컬렉션 메타데이터 설정
    collection_metadata={"hnsw:space": "cosine"}
)
 
# 3. 검색 설정 최적화
retriever = vectorstore.as_retriever(
    search_type="mmr",  # MMR (Maximal Marginal Relevance)
    search_kwargs={
        "k": 5,
        "lambda_mult": 0.25  # 다양성 vs 관련성 균형
    }
)

6-2. 에러 처리와 로깅

import logging
from langchain.callbacks import get_openai_callback
 
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
def safe_rag_query(query: str):
    try:
        with get_openai_callback() as cb:
            result = qa_chain({"query": query})
            
            logger.info(f"토큰 사용량: {cb.total_tokens}")
            logger.info(f"비용: ${cb.total_cost:.4f}")
            
            return result
            
    except Exception as e:
        logger.error(f"RAG 처리 중 오류 발생: {e}")
        return {"result": "죄송합니다. 처리 중 오류가 발생했습니다."}
 
# 사용
result = safe_rag_query("파이썬이란 무엇인가요?")

6-3. 메타데이터 활용

# 문서에 메타데이터 추가
documents_with_metadata = []
for doc in documents:
    doc.metadata.update({
        "document_type": "manual",
        "language": "korean",
        "created_date": "2025-01-01"
    })
    documents_with_metadata.append(doc)
 
# 메타데이터 기반 필터링
retriever = vectorstore.as_retriever(
    search_kwargs={
        "k": 5,
        "filter": {"document_type": "manual"}  # 매뉴얼만 검색
    }
)

LangChain 최적화 기법 (LangChain 성능 최적화를 위한 다양한 기법들)


마무리

이번 포스팅에서는 RAG 구현의 핵심 도구인 LangChain에 대해 상세히 알아봤습니다.

LangChain의 진정한 가치는 복잡한 AI 워크플로우를 표준화된 컴포넌트로 단순화한다는 점입니다. 문서 로딩부터 최종 답변 생성까지의 전 과정을 일관된 인터페이스로 관리할 수 있으며, 각 단계별로 다양한 옵션을 제공하여 요구사항에 맞게 유연하게 조합할 수 있습니다.

특히 RAG 시스템 구축에 있어서 LangChain은 단순히 개발 시간을 단축시켜주는 것을 넘어서, 검증된 아키텍처와 최적화 기법들을 제공하여 안정적이고 성능 좋은 시스템을 만들 수 있게 해줍니다.

다음 3편에서는 RAG의 또 다른 핵심 요소인 벡터 데이터베이스에 대해 깊이 있게 다뤄보겠습니다. 벡터 데이터베이스가 무엇인지, 어떻게 작동하는지, 그리고 실제로 어떻게 활용하는지 알아보겠습니다.


이미지 URL 참고자료:

  • img_000.png: LangChain 공식 로고 및 RAG 연결 개념도