1. Pengenalan RAG
Retrieval Augmented Generation (RAG) adalah teknik yang menggabungkan kemampuan retrieval (mengambil informasi dari dokumen) dengan generation (menghasilkan teks oleh LLM). Dengan RAG, LLM tidak hanya mengandalkan pengetahuan yang ada saat training, tetapi juga bisa mengakses data terbaru dan spesifik dari database Anda.
Mengapa RAG penting? Karena LLM seperti GPT-4 memiliki batasan kritis:
- Knowledge cutoff — pengetahuan LLM dibatasi oleh data training
- Hallucination — LLM bisa "mengarang" fakta yang tidak benar
- Tidak tahu data internal — LLM tidak tahu dokumen perusahaan Anda
- Sulit di-update — fine-tuning mahal dan butuh waktu lama
┌─────────────────────────────────────────────────────────────────────┐ │ LLM TANPA RAG │ │ │ │ User: "Berapa harga produk X bulan ini?" │ │ LLM: "Maaf, saya tidak memiliki data harga terbaru." │ │ (atau lebih buruk: mengarang jawaban palsu!) │ │ │ │═════════════════════════════════════════════════════════════════════│ │ LLM DENGAN RAG │ │ │ │ User: "Berapa harga produk X bulan ini?" │ │ ↓ │ │ [1] Retrieval → Cari di vector DB → Dokumen harga bulan Juni │ │ ↓ │ │ [2] Augment → Kirim context ke LLM │ │ ↓ │ │ [3] Generate → LLM jawab: "Harga produk X bulan Juni adalah │ │ Rp 250.000, naik 10% dari bulan lalu." │ │ (berdasarkan data Anda, bukan training!) ✅ │ └─────────────────────────────────────────────────────────────────────┘
LangChain — Framework untuk RAG
LangChain adalah framework open-source yang paling populer untuk membangun aplikasi LLM. LangChain menyediakan komponen siap pakai untuk setiap tahap RAG pipeline: document loader, text splitter, embedding, vector store, retriever, dan chain.
# Instalasi LangChain dan dependencies pip install langchain langchain-openai langchain-community pip install chromadb faiss-cpu tiktoken pip install pypdf docx2txt unstructured # Untuk OpenAI API pip install openai # Untuk environment variables pip install python-dotenv
2. Arsitektur RAG Pipeline
RAG pipeline terdiri dari dua fase utama: Indexing (offline) dan Retrieval + Generation (online).
┌─────────────────────────────────────────────────────────────────┐ │ RAG PIPELINE │ │ │ │ OFFLINE (Indexing): │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Document │──▶│ Text │──▶│ Embedding│──▶│ Vector │ │ │ │ Loading │ │ Splitting│ │ Model │ │ Store │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ PDF/Word/Web Chunk 500 OpenAI/HF ChromaDB/ │ │ CSV/JSON token each 1536 dim FAISS/Pinecone │ │ │ │ ONLINE (Query): │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ User │──▶│ Retrieval│──▶│ Prompt │──▶│ LLM │ │ │ │ Query │ │ Chain │ │ Template │ │ (GPT-4) │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ "Apa itu RAG?" Top-K docs Context + Menghasilkan │ │ dari store question jawaban akurat │ └─────────────────────────────────────────────────────────────────┘
Komponen Utama LangChain untuk RAG
| Komponen | Fungsi | Contoh Class |
|---|---|---|
| Document Loader | Load dokumen dari berbagai format | PyPDFLoader, TextLoader, CSVLoader |
| Text Splitter | Pecah dokumen jadi chunks | RecursiveCharacterTextSplitter, TokenTextSplitter |
| Embeddings | Ubah teks jadi vektor | OpenAIEmbeddings, HuggingFaceEmbeddings |
| Vector Store | Simpan & cari vektor | Chroma, FAISS, Pinecone |
| Retriever | Mengambil dokumen relevan | VectorStoreRetriever, MultiQueryRetriever |
| Chain | Rangkai semua komponen | RetrievalQA, ConversationalRetrievalChain |
3. Document Loading
LangChain mendukung lebih dari 100 format dokumen. Setiap loader mengembalikan objek Document yang berisi page_content (teks) dan metadata (info tambahan seperti sumber file).
# =============================================
# Document Loading dengan LangChain
# =============================================
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
CSVLoader,
DirectoryLoader,
WebBaseLoader,
UnstructuredMarkdownLoader
)
# ----- 1. Load PDF -----
loader = PyPDFLoader("dokumen/produk.pdf")
pages = loader.load()
print(f"Jumlah halaman: {len(pages)}")
print(f"Contoh halaman 1: {pages[0].page_content[:200]}")
print(f"Metadata: {pages[0].metadata}")
# {'source': 'dokumen/produk.pdf', 'page': 0}
# ----- 2. Load Text File -----
loader = TextLoader("dokumen/catatan.txt", encoding="utf-8")
docs = loader.load()
# ----- 3. Load CSV -----
loader = CSVLoader(
file_path="dokumen/data.csv",
csv_args={"delimiter": ",", "quotechar": '"'},
source_column="url"
)
docs = loader.load()
# ----- 4. Load dari Web -----
loader = WebBaseLoader([
"https://example.com/artikel-1",
"https://example.com/artikel-2"
])
web_docs = loader.load()
# ----- 5. Load seluruh direktori -----
loader = DirectoryLoader(
"./dokumen/",
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
)
all_docs = loader.load()
print(f"Total dokumen: {len(all_docs)}")
# ----- Metadata manipulation -----
for doc in all_docs:
doc.metadata["category"] = "produk"
doc.metadata["company"] = "BeebaneLabs"
- Gunakan
DirectoryLoaderuntuk batch loading seluruh folder - Tambahkan metadata kustom untuk filtering di vector store
- Untuk web scraping, gunakan
WebBaseLoaderatauSitemapLoader - Periksa encoding untuk file bahasa Indonesia (gunakan
utf-8)
4. Text Splitting & Chunking
Setelah dokumen di-load, langkah selanjutnya adalah memecah teks menjadi chunks yang lebih kecil. Chunking sangat penting karena embedding model punya batasan token, dan chunk kecil menghasilkan retrieval yang lebih presisi.
Parameter Penting Chunking
| Parameter | Deskripsi | Rekomendasi |
|---|---|---|
chunk_size | Jumlah karakter per chunk | 500-1000 karakter |
chunk_overlap | Overlap antar chunk | 50-200 karakter |
separators | Karakter pemisah | ["\n\n", "\n", " ", ""] |
length_function | Cara menghitung panjang | len() atau tiktoken |
# =============================================
# Text Splitting dengan LangChain
# =============================================
from langchain.text_splitter import (
RecursiveCharacterTextSplitter,
CharacterTextSplitter,
TokenTextSplitter,
MarkdownHeaderTextSplitter
)
# ----- 1. RecursiveCharacterTextSplitter (Paling Umum) -----
# Memecah berdasarkan separator secara rekursif
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)
print(f"Dokumen: {len(docs)} → Chunks: {len(chunks)}")
print(f"Contoh chunk: {chunks[0].page_content[:100]}")
# ----- 2. TokenTextSplitter (Berdasarkan Token) -----
# Lebih akurat untuk LLM karena LLM bekerja dengan token
splitter = TokenTextSplitter(
chunk_size=200, # 200 token per chunk
chunk_overlap=20, # 20 token overlap
encoding_name="cl100k_base" # GPT-4 encoding
)
token_chunks = splitter.split_documents(docs)
# ----- 3. MarkdownHeaderTextSplitter -----
# Memecah berdasarkan header markdown, mempertahankan struktur
headers_to_split = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split)
# Untuk markdown text (bukan Document object)
md_text = """
# Judul Utama
Ini adalah konten bagian 1.
## Sub Judul
Ini adalah konten bagian 2.
### Sub Sub
Detail lebih lanjut.
"""
md_splits = splitter.split_text(md_text)
for split in md_splits:
print(f"Content: {split.page_content[:50]}")
print(f"Metadata: {split.metadata}")
print("---")
# ----- 4. Semantic Chunking (Advanced) -----
# Chunk berdasarkan kemiripan semantik (butuh embedding)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
semantic_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
)
semantic_chunks = semantic_splitter.split_documents(docs)
┌─────────────────────────────────────────────────────────────┐ │ CHUNK OVERLAP │ │ │ │ Original Text: "A B C D E F G H I J K L M N O P" │ │ │ │ chunk_size = 6, chunk_overlap = 2 │ │ │ │ Chunk 1: [A B C D E F] │ │ Chunk 2: [E F G H I J] ← overlap "E F" │ │ Chunk 3: [I J K L M N] │ │ Chunk 4: [M N O P] │ │ │ │ Overlap memastikan tidak ada konteks yang hilang │ │ di batas antar chunk! │ └─────────────────────────────────────────────────────────────┘
5. Embedding Models
Setelah dokumen di-chunk, setiap chunk perlu diubah menjadi vektor embedding. Embedding model mengubah teks menjadi array angka berdimensi tinggi yang merepresentasikan makna teks tersebut.
# =============================================
# Embedding Models di LangChain
# =============================================
import os
from dotenv import load_dotenv
load_dotenv()
# ----- 1. OpenAI Embeddings -----
from langchain_openai import OpenAIEmbeddings
embeddings_openai = OpenAIEmbeddings(
model="text-embedding-3-small", # 1536 dimensi
# model="text-embedding-3-large", # 3072 dimensi (lebih akurat)
)
# Embed satu teks
vector = embeddings_openai.embed_query("Apa itu RAG?")
print(f"Dimensi: {len(vector)}") # 1536
print(f"5 elemen pertama: {vector[:5]}")
# Embed banyak teks sekaligus (batch)
texts = ["Apa itu RAG?", "Cara kerja LangChain", "Vector database"]
vectors = embeddings_openai.embed_documents(texts)
print(f"Jumlah vektor: {len(vectors)}") # 3
# ----- 2. HuggingFace Embeddings (Gratis, Lokal) -----
from langchain_huggingface import HuggingFaceEmbeddings
embeddings_hf = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2",
model_kwargs={"device": "cpu"}, # "cuda" jika punya GPU
encode_kwargs={"normalize_embeddings": True}
)
vector = embeddings_hf.embed_query("Apa itu RAG?")
print(f"Dimensi: {len(vector)}") # 384
# ----- 3. Multilingual Embeddings (Untuk Bahasa Indonesia) -----
embeddings_multi = HuggingFaceEmbeddings(
model_name="intfloat/multilingual-e5-large",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
# Dimensi: 1024, mendukung bahasa Indonesia dengan baik
Perbandingan Embedding Models
| Model | Dimensi | Harga | Bahasa Indonesia | Rekomendasi |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | Murah | ✅ Bagus | Produksi |
| text-embedding-3-large | 3072 | Sedang | ✅ Sangat bagus | Akurasi tinggi |
| all-MiniLM-L6-v2 | 384 | Gratis | ⚠️ Cukup | Prototyping |
| multilingual-e5-large | 1024 | Gratis | ✅ Bagus | Multi-bahasa |
6. Vector Store
Vector Store adalah database yang menyimpan vektor embedding dan mendukung similarity search. LangChain mendukung banyak vector store: ChromaDB (lokal), FAISS (lokal), Pinecone (cloud), Weaviate, dan lainnya.
# =============================================
# Vector Store dengan ChromaDB
# =============================================
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 1. Load & Split
loader = PyPDFLoader("dokumen/panduan.pdf")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=100
)
chunks = splitter.split_documents(docs)
# 2. Buat Vector Store (ChromaDB - persisted local)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db", # Simpan ke disk
collection_name="dokumen_produk"
)
print(f"Jumlah dokumen di store: {vectorstore._collection.count()}")
# 3. Similarity Search
query = "Apa fitur utama produk ini?"
results = vectorstore.similarity_search(query, k=3)
for i, doc in enumerate(results):
print(f"\n--- Hasil {i+1} ---")
print(f"Content: {doc.page_content[:150]}")
print(f"Source: {doc.metadata.get('source', 'N/A')}")
print(f"Page: {doc.metadata.get('page', 'N/A')}")
# 4. Similarity Search dengan Score
results_with_score = vectorstore.similarity_search_with_score(query, k=3)
for doc, score in results_with_score:
print(f"Score: {score:.4f} | Content: {doc.page_content[:100]}")
# 5. Load Vector Store yang sudah ada (persisted)
vectorstore_loaded = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
collection_name="dokumen_produk"
)
# =============================================
# Vector Store dengan FAISS (Facebook AI Similarity Search)
# =============================================
from langchain_community.vectorstores import FAISS
# Buat FAISS vector store
faiss_store = FAISS.from_documents(chunks, embeddings)
# Similarity search
results = faiss_store.similarity_search("apa itu machine learning?", k=5)
# Save & Load
faiss_store.save_local("./faiss_index")
faiss_store_loaded = FAISS.load_local(
"./faiss_index",
embeddings,
allow_dangerous_deserialization=True
)
# Merge dua vector store
faiss_store_2 = FAISS.from_documents(additional_chunks, embeddings)
faiss_store.merge_from(faiss_store_2)
7. Retrieval Chain
Setelah semua komponen siap, saatnya menyatukan semuanya menjadi retrieval chain yang bisa menjawab pertanyaan dari dokumen Anda.
# =============================================
# RAG Chain dengan LangChain LCEL
# =============================================
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Setup
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 4}
)
# Prompt Template
template = """Anda adalah asisten AI yang membantu menjawab pertanyaan
berdasarkan konteks yang diberikan. Jika jawaban tidak ada di konteks,
katakan "Maaf, saya tidak menemukan informasi tersebut di dokumen."
Konteks:
{context}
Pertanyaan: {question}
Jawaban:"""
prompt = ChatPromptTemplate.from_template(template)
# LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Helper function untuk format docs
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
# RAG Chain menggunakan LCEL (LangChain Expression Language)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# Invoke
answer = rag_chain.invoke("Apa saja fitur utama produk ini?")
print(answer)
# Streaming response
for chunk in rag_chain.stream("Bagaimana cara menggunakan fitur X?"):
print(chunk, end="", flush=True)
# =============================================
# Conversational RAG dengan History
# =============================================
from langchain.chains import create_history_aware_retriever
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import MessagesPlaceholder
# Contextualize question prompt (rewrite question with history)
contextualize_prompt = ChatPromptTemplate.from_messages([
("system", "Given a chat history and the latest user question, "
"reformulate the question to be standalone."),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_prompt
)
# Answer prompt
answer_prompt = ChatPromptTemplate.from_messages([
("system", "Answer based on context:\n\n{context}"),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
])
document_chain = create_stuff_documents_chain(llm, answer_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, document_chain)
# Conversation dengan history
chat_history = []
# Turn 1
response = rag_chain.invoke({
"chat_history": chat_history,
"input": "Apa itu produk X?"
})
print(response["answer"])
chat_history.append(HumanMessage(content="Apa itu produk X?"))
chat_history.append(AIMessage(content=response["answer"]))
# Turn 2 (follow-up question)
response2 = rag_chain.invoke({
"chat_history": chat_history,
"input": "Berapa harganya?" # Context-aware!
})
print(response2["answer"])
8. Advanced RAG Techniques
Basic RAG sudah bagus, tetapi ada banyak teknik advanced untuk meningkatkan kualitas retrieval dan generasi jawaban.
8.1 Multi-Query Retriever
# =============================================
# Multi-Query Retriever
# =============================================
from langchain.retrievers import MultiQueryRetriever
# Otomatis generate multiple query dari satu pertanyaan
multi_retriever = MultiQueryRetriever.from_llm(
retriever=vectorstore.as_retriever(),
llm=llm
)
# User: "Cara install aplikasi?"
# Generated queries:
# 1. "Langkah instalasi aplikasi"
# 2. "Panduan setup aplikasi"
# 3. "Tutorial deployment aplikasi"
# → Gabungkan hasil dari semua query!
results = multi_retriever.invoke("Cara install aplikasi?")
# =============================================
# Contextual Compression (Re-ranking)
# =============================================
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 10})
)
# Ambil 10 docs, lalu kompres hanya bagian yang relevan
8.2 Self-Query Retriever
# =============================================
# Self-Query: LLM generate filter dari pertanyaan natural
# =============================================
from langchain.chains.query_constructor.base import AttributeInfo
from langchain.retrievers.self_query.base import SelfQueryRetriever
metadata_field_info = [
AttributeInfo(name="category", description="Kategori dokumen", type="string"),
AttributeInfo(name="year", description="Tahun publikasi", type="integer"),
AttributeInfo(name="source", description="Sumber file", type="string"),
]
self_query_retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents="Dokumen produk",
metadata_field_info=metadata_field_info,
)
# User: "Dokumen tentang pricing dari tahun 2025"
# → Auto generate filter: category="pricing" AND year=2025
results = self_query_retriever.invoke(
"Dokumen tentang pricing dari tahun 2025"
)
9. Deployment & Best Practices
- Chunk size optimal — eksperimen dengan 200-1000 token, tergantung jenis dokumen
- Overlap 10-20% — cukup untuk menangkap konteks di batas chunk
- Metadata filtering — gunakan metadata untuk pre-filter sebelum vector search
- Hybrid search — gabungkan keyword search + vector search
- Re-ranking — gunakan cross-encoder untuk re-rank hasil retrieval
- Evaluation — ukur recall@k, precision, dan faithfulness
- Prompt engineering — instruksi yang jelas di system prompt
- Caching — cache embedding dan hasil query untuk efisiensi
# =============================================
# RAG API dengan FastAPI
# =============================================
# pip install fastapi uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Pre-load vectorstore & chain (di startup)
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
class QueryRequest(BaseModel):
question: str
top_k: int = 4
class QueryResponse(BaseModel):
answer: str
sources: list
@app.post("/ask", response_model=QueryResponse)
async def ask_question(req: QueryRequest):
# Retrieve relevant docs
docs = retriever.invoke(req.question)
# Generate answer
context = "\n\n".join(d.page_content for d in docs)
answer = rag_chain.invoke(req.question)
sources = [
{"source": d.metadata.get("source", ""),
"page": d.metadata.get("page", 0)}
for d in docs
]
return QueryResponse(answer=answer, sources=sources)
# Run: uvicorn main:app --reload --port 8000
10. Quiz Pemahaman
1. Apa tujuan utama RAG (Retrieval Augmented Generation)?
2. Mengapa chunk overlap penting dalam text splitting?
3. Apa fungsi dari vector store dalam RAG pipeline?
4. Apa yang dilakukan oleh Multi-Query Retriever?
5. Prompt template dalam RAG biasanya berisi apa?
Rangkuman
- RAG — menggabungkan retrieval + generation untuk jawaban akurat dari data Anda
- Document Loading — LangChain mendukung 100+ format dokumen
- Chunking — pecah dokumen jadi potongan kecil, gunakan overlap
- Embedding — ubah teks jadi vektor untuk similarity search
- Vector Store — ChromaDB (lokal), FAISS, Pinecone (cloud)
- Retrieval Chain — rangkai semua komponen dengan LCEL
- Advanced RAG — multi-query, re-ranking, self-query untuk kualitas lebih baik