Mengapa RAG Dibutuhkan?
LLM seperti GPT-4 memiliki pengetahuan yang luas tapi terbatas pada data training mereka. Bagaimana jika kamu ingin AI Agent menjawab pertanyaan berdasarkan dokumen internal perusahaan kamu? Di sinilah RAG (Retrieval-Augmented Generation) hadir.
Dari pengalaman saya membangun AI Agent untuk beberapa perusahaan di Bandung, RAG adalah pattern yang paling sering dibutuhkan. Baik untuk knowledge base internal, dokumentasi produk, maupun FAQ customer.
Pipeline RAG yang Saya Gunakan
Ada tiga tahap utama dalam RAG pipeline: indexing, retrieval, dan generation. Berikut implementasinya dalam Go:
type RAGPipeline struct {
embedder Embedder
vectorDB VectorStore
llm LLMClient
chunker DocumentChunker
}
// Tahap 1: Indexing - Memproses dokumen menjadi embeddings
func (r *RAGPipeline) IndexDocument(ctx context.Context, doc Document) error {
// Chunk document menjadi bagian-bagian kecil
chunks := r.chunker.Chunk(doc.Content, 512, 50) // 512 tokens, 50 overlap
for i, chunk := range chunks {
embedding, err := r.embedder.Embed(ctx, chunk)
if err != nil {
return fmt.Errorf("failed to embed chunk %d: %w", i, err)
}
err = r.vectorDB.Upsert(ctx, Point{
ID: fmt.Sprintf("%s_%d", doc.ID, i),
Vector: embedding,
Payload: map[string]interface{}{
"content": chunk,
"doc_id": doc.ID,
"metadata": doc.Metadata,
},
})
if err != nil {
return err
}
}
return nil
}Retrieval yang Efektif
Retrieval adalah tahap kritis. Kita perlu mencari chunks yang paling relevan dengan query user. Saya menggunakan hybrid search (combining semantic + keyword search) untuk hasil yang lebih baik:
// Tahap 2: Retrieval
func (r *RAGPipeline) Retrieve(ctx context.Context, query string, topK int) ([]Chunk, error) {
queryEmbedding, err := r.embedder.Embed(ctx, query)
if err != nil {
return nil, err
}
results, err := r.vectorDB.Search(ctx, SearchRequest{
Vector: queryEmbedding,
TopK: topK,
Threshold: 0.7, // Minimum similarity score
})
if err != nil {
return nil, err
}
// Rerank results menggunakan cross-encoder
reranked := r.reranker.Rerank(query, results)
return reranked, nil
}Generation dengan Context
Tahap terakhir adalah generate jawaban menggunakan retrieved chunks sebagai context:
// Tahap 3: Generation
func (r *RAGPipeline) Query(ctx context.Context, question string) (string, error) {
chunks, err := r.Retrieve(ctx, question, 5)
if err != nil {
return "", err
}
// Build context dari retrieved chunks
context := ""
for i, chunk := range chunks {
context += fmt.Sprintf("[Sumber %d]: %s\n\n", i+1, chunk.Content)
}
prompt := fmt.Sprintf(`Berdasarkan sumber berikut, jawab pertanyaan user secara akurat.
Jika jawaban tidak ada di sumber, katakan bahwa kamu tidak memiliki informasi tersebut.
Sumber:\n%s\n\nPertanyaan: %s`, context, question)
return r.llm.Generate(ctx, prompt)
}Tips Optimization
Dari beberapa implementasi RAG yang sudah saya lakukan, ada tips penting: gunakan chunk size yang tepat (tidak terlalu kecil dan tidak terlalu besar), implement metadata filtering untuk domain-specific retrieval, dan jangan lupa untuk evaluate retrieval quality secara berkala menggunakan metrik seperti precision dan recall.