अध्याय ५ · खण्ड II · 26 मिनेट
एक RAG pipeline निर्माण गर्ने
End-to-end, working RAG प्रणाली: कागजातहरू ingest गर्नुहोस्, तिनीहरूलाई chunk गर्नुहोस्, तिनीहरूलाई embed गर्नुहोस्, semantic search गर्नुहोस्, Claude लाई पास गर्नुहोस्, र ग्राउन्डेड जवाफहरू फिर्ता गर्नुहोस्। एक साना नेपाली FAQ बोटको लागि पूर्ण कोड।
पछिल्लो खण्डले RAG के हो र किन काम गर्दछ भन्ने बुझायो। यो खण्डले यसलाई निर्माण गर्दछ। कागजातहरूको संकलनबाट सुरु गरेर एक working assistant सम्म — प्रत्येक चरण कोड र व्याख्याको साथमा हिँडाइयो। एक घण्टामा, तपाईंको आफ्नो कागजातहरू माथि जवाफ दिने आफ्नै RAG प्रणाली हुनेछ। यो modern LLM app-building को सबैभन्दा उपयोगी patterns मध्ये एक हो।
Design
हामी नेपाल सरकारको पासपोर्ट कार्यालयको FAQ माथि जवाफ दिने assistant निर्माण गर्नेछौँ। कागजातहरू: 5-10 पाना Nepali PDF हरू जसले उम्मेदवारी, प्रक्रिया, र समयक्रम बारेको सामान्य प्रश्नहरू कभर गर्दछ। लक्ष्य: प्रयोगकर्ताले हिंदी वा नेपालीमा प्रश्न सोध्न सक्छन्, र सहायकले FAQ बाट grounded जवाफ फिर्ता गर्दछ।
Pipeline:
कागजातहरू
↓ (extract, clean)
पाठ
↓ (chunk)
Chunks
↓ (embed)
Chunk + vector database
↓ (search)
प्रयोगकर्ता प्रश्न → Top-K chunks
↓ (Claude)
ग्राउन्डेड जवाफ
चरण 1 — कागजातहरू
आफ्नो कागजातहरू संकलन गर्नुहोस्। यस उदाहरणको लागि, हामी दुई हातले-लेखिएको साना कागजातहरू प्रयोग गर्नेछौँ ताकि तपाईं pipeline सजिलै चलाउन सक्नुहुनेछ:
docs = [
{
"id": "passport-general",
"text": """
पासपोर्ट सामान्य जानकारी:
नेपाली नागरिकहरू 34 पेजको साधारण पासपोर्ट वा 66 पेजको बढी पेजको
पासपोर्ट रोज्न सक्दछन्। पासपोर्ट को अवधि 10 वर्ष हो। नाबालक
(18 वर्ष मुनि) को लागि, अवधि 5 वर्ष हो।
पासपोर्टहरू परराष्ट्र मन्त्रालयको पासपोर्ट विभाग (नारायणहिटी)
मा र प्रत्येक जिल्ला प्रशासन कार्यालयबाट जारी गरिन्छन्।
आवेदन प्रक्रिया पूर्णत: अनलाइन पंजीकरण संग शुरू हुन्छ।
फी: साधारण पासपोर्ट - रु 5,000; MRP (मशीन पठनीय) - रु 5,000;
Fast-track (5 कार्य दिन) - रु 12,000; बच्चा (5 वर्ष) - रु 2,500।
""",
},
{
"id": "passport-application",
"text": """
पासपोर्ट आवेदन प्रक्रिया:
चरण 1: nepalpassport.gov.np मा अनलाइन पंजीकरण गर्नुहोस्।
चरण 2: आफ्नो जिल्ला प्रशासन कार्यालय (DAO) वा पासपोर्ट विभाग (MOFA)
संग appointment book गर्नुहोस्।
चरण 3: आवश्यक कागजातहरू संग appointment मा उपस्थित हुनुहोस्:
- नागरिकता प्रमाणपत्रको सक्कल र प्रतिलिपि
- नागरिकता प्रमाणपत्रको साइज फोटो (2 प्रति)
- अनलाइन पंजीकरण फारमको प्रिन्टआउट
- फी बुझाइको रसिद (बैंक marfat)
चरण 4: fingerprint र फोटो लिइन्छ।
चरण 5: पासपोर्ट सामान्यतया 15 कार्य दिन (साधारण) वा
5 कार्य दिन (fast-track) मा तयार हुन्छ।
कागजातहरू लिन गइरहन नागरिक स्वयं आवश्यक छ।
""",
},
{
"id": "passport-lost-damaged",
"text": """
हराएको वा बिग्रिएको पासपोर्ट:
यदि तपाईंको पासपोर्ट हराएको छ, तपाईंले सर्वप्रथम प्रहरीलाई
report गर्नुपर्दछ र FIR प्राप्त गर्नुपर्दछ।
अनुरोध कागजातहरू:
- FIR को प्रति
- पुरानो पासपोर्टको प्रति (यदि उपलब्ध छ भने)
- नागरिकता प्रमाणपत्रको सक्कल र प्रतिलिपि
- लिखित निवेदन जसले हराएको वा बिग्रिएको स्थिति व्याख्या गरोस्
पुनरावृत्तिको फी नयाँ आवेदन भन्दा उच्च छ: साधारण - रु 10,000;
fast-track - रु 15,000। यो जरिवाना उद्देश्यसँग हो।
बिग्रिएको (जस्तै पानीले क्षति भएको) पासपोर्टको लागि, बिग्रिएको
पासपोर्ट पेश गर्नुपर्दछ। FIR आवश्यक छैन।
""",
},
]
Real world मा, तपाईं कागजातहरू PDFs, वेब पानाहरू, वा database बाट load गर्नुहुन्छ। Course 04 खण्ड 4.3 मा हामीले PDF ingestion हिडायौँ। यहाँको मूल कुरा: {"id": ..., "text": ...} जोडीहरूको list — जुन ingestion बाट आउँछ त्यो कुरा गर्दैन।
चरण 2 — Chunking
यो साना उदाहरणको लागि, हरेक FAQ पहिले नै ठ्याक्कै-राम्रो-आकारको छ (300-500 शब्द)। हामी अझै पनि cleanly chunk गर्नेछौँ ताकि तपाईंसँग जब कागजातहरू पछि ठूला हुन्छन् तब जान्नको लागि pattern छ:
def chunk_text(text: str, chunk_size: int = 300) -> list[str]:
"""Simple chunking by paragraphs, keeping ~chunk_size words per chunk."""
paragraphs = [p.strip() for p in text.split("\n") if p.strip()]
chunks = []
current = []
current_size = 0
for para in paragraphs:
word_count = len(para.split())
if current_size + word_count > chunk_size and current:
chunks.append(" ".join(current))
current = [para]
current_size = word_count
else:
current.append(para)
current_size += word_count
if current:
chunks.append(" ".join(current))
return chunks
# Convert docs to chunks
chunks = []
for doc in docs:
for i, chunk_text_content in enumerate(chunk_text(doc["text"])):
chunks.append({
"chunk_id": f"{doc['id']}#{i}",
"doc_id": doc["id"],
"text": chunk_text_content,
})
print(f"Total chunks: {len(chunks)}")
तीन कागजातहरू सामान्यतया 3-6 chunks दिन्छन्।
चरण 3 — Embed
Chunks लाई embed गर्नुहोस्:
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
openai_client = OpenAI()
def embed_batch(texts: list[str]) -> list[list[float]]:
response = openai_client.embeddings.create(
model="text-embedding-3-small",
input=texts,
)
return [d.embedding for d in response.data]
vectors = embed_batch([c["text"] for c in chunks])
for chunk, vector in zip(chunks, vectors):
chunk["vector"] = vector
print(f"Embedded {len(chunks)} chunks")
लागत: 3 कागजातहरू × ~500 tokens प्रत्येक ≈ 1500 tokens ≈ $0.00003 (0.003 सेन्ट)। लगभग नि: शुल्क।
चरण 4 — खोजी
Cosine similarity प्रयोग गरेर एक साधारण खोजी function:
import numpy as np
def cosine(v1: list[float], v2: list[float]) -> float:
a, b = np.array(v1), np.array(v2)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
def search(query: str, top_k: int = 3) -> list[dict]:
query_vector = openai_client.embeddings.create(
model="text-embedding-3-small",
input=query,
).data[0].embedding
scored = [
{**chunk, "score": cosine(query_vector, chunk["vector"])}
for chunk in chunks
]
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]
चरण 5 — Generate (Claude)
अन्तिम piece — Claude लाई retrieved chunks पास गर्ने र ग्राउन्डेड जवाफ प्राप्त गर्ने:
from anthropic import Anthropic
anthropic_client = Anthropic()
SYSTEM_PROMPT = """You are a helpful assistant for Nepal's passport office FAQs.
Answer questions using ONLY the documents provided below. Follow these rules:
1. Answer in the same language the user asked (Nepali or English).
2. If the answer is in the documents, provide it clearly with the document
name in parentheses at the end of relevant sentences, like: (Source:
passport-application).
3. If the answer is NOT in the documents, say exactly:
"मैले प्रदान गरिएका कागजातहरूमा त्यसको जवाफ भेटिनँ। कृपया आधिकारिक
पासपोर्ट विभागलाई सम्पर्क गर्नुहोस्।"
4. Do not invent facts, fees, timelines, or procedures. If a specific
number is not in the documents, say "मलाई त्यो specific संख्या थाहा छैन।"
5. Keep answers concise (2-4 sentences unless the question demands more).
"""
def rag_answer(question: str) -> str:
# Step 1: retrieve
hits = search(question, top_k=3)
# Step 2: build context from top hits
context_parts = []
for hit in hits:
context_parts.append(f"[Document: {hit['doc_id']}]\n{hit['text']}")
context = "\n\n---\n\n".join(context_parts)
# Step 3: ask Claude
user_message = f"Documents:\n\n{context}\n\n---\n\nQuestion: {question}"
response = anthropic_client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=500,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_message}],
)
return response.content[0].text
चरण 6 — यो प्रयास गर्नुहोस्
questions = [
"साधारण पासपोर्टको लागि कति फी लाग्छ?",
"मेरो पासपोर्ट हरायो। मैले के गर्नुपर्छ?",
"How long does it take to get a fast-track passport?",
"पासपोर्ट कति समय सम्म वैध रहन्छ?",
"म कहाँ खाना खान सक्छु?", # out of scope
]
for q in questions:
print(f"\n=== {q}")
answer = rag_answer(q)
print(answer)
Output:
=== साधारण पासपोर्टको लागि कति फी लाग्छ?
साधारण पासपोर्टको फी रु 5,000 हो। यो 34 पेज र MRP (मशीन पठनीय)
संस्करण दुबैको लागि लागू हुन्छ। यदि तपाईंलाई छिटो चाहिन्छ भने,
fast-track विकल्प (5 कार्य दिन) रु 12,000 मा उपलब्ध छ।
(Source: passport-general)
=== मेरो पासपोर्ट हरायो। मैले के गर्नुपर्छ?
पहिले प्रहरीलाई report गर्नुहोस् र FIR प्राप्त गर्नुहोस्। त्यसपछि
FIR को प्रति, नागरिकता प्रमाणपत्र (सक्कल र प्रतिलिपि), र लिखित
निवेदन पेश गर्नुहोस्। पुनरावृत्तिको फी नयाँ भन्दा उच्च हुन्छ: साधारण
रु 10,000, fast-track रु 15,000। (Source: passport-lost-damaged)
=== How long does it take to get a fast-track passport?
A fast-track passport is typically ready in 5 working days after your
appointment. Standard passports take 15 working days. (Source:
passport-application, passport-general)
=== पासपोर्ट कति समय सम्म वैध रहन्छ?
पासपोर्टको अवधि 10 वर्ष हो। नाबालक (18 वर्ष मुनि) को लागि,
अवधि 5 वर्ष हो। (Source: passport-general)
=== म कहाँ खाना खान सक्छु?
मैले प्रदान गरिएका कागजातहरूमा त्यसको जवाफ भेटिनँ। कृपया
आधिकारिक पासपोर्ट विभागलाई सम्पर्क गर्नुहोस्।
Notice के भयो: प्रत्येक जवाफ सान्दर्भिक कागजातहरूबाट फिर्ता आयो। मोडेलले भाषा मिलायो। यसले तथ्यहरू cite गर्यो। र scope बाहिरको प्रश्न ("म कहाँ खाना खान सक्छु?"), यसले अनुमान गर्न इन्कार गर्यो। यो नै तपाईं production RAG system बाट चाहानुहुन्छ।
Retrieval scores नोट गर्ने
तपाईंको प्रयोगकर्ताहरूको लागि debugging र quality monitoring को लागि, retrieval scores लगाउनुहोस्:
def rag_answer_with_scores(question: str) -> dict:
hits = search(question, top_k=3)
top_score = hits[0]["score"]
if top_score < 0.35:
return {
"answer": "मैले प्रदान गरिएका कागजातहरूमा त्यसको जवाफ भेटिनँ।",
"confidence": "low",
"top_score": top_score,
"sources": [],
}
context = "\n\n---\n\n".join(
f"[Document: {hit['doc_id']}]\n{hit['text']}" for hit in hits
)
response = anthropic_client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=500,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": f"Documents:\n\n{context}\n\n---\n\nQuestion: {question}"}],
)
return {
"answer": response.content[0].text,
"confidence": "high" if top_score > 0.6 else "medium",
"top_score": top_score,
"sources": [hit["doc_id"] for hit in hits],
}
अब तपाईं जान्नुहुन्छ प्रत्येक जवाफ को कति अच्छो ग्राउन्डेड छ, र तपाईं UI मा confidence तह अनुसार display गर्न सक्नुहुन्छ।
Debugging tips
जब तपाईंको RAG pipeline अपेक्षा जस्तै काम गर्दैन, यहाँ के जाँच गर्ने:
- Search hits print गर्नुहोस्। के top-3 hits वास्तवमै सान्दर्भिक छन्? यदि छैनन्, तपाईंको समस्या retrieval मा छ, generation मा होइन।
- Prompt छाप्नुहोस्। के तपाईंले Claude लाई पठाएको पूर्ण prompt अपेक्षा जस्तै देखिन्छ? कहिलेकाहीँ chunks को गलत formatting ले चुपचाप गुणस्तर हिर्काउँछ।
- Chunking जाँच गर्नुहोस्। के तपाईंको chunks उचित छन्, वा तपाईंले वाक्यहरू मध्य कट गर्दैहुनुहुन्छ? खराब chunk बाउन्ड्रीहरू एउटा embedding द्वारा एउटा-एउटा वाक्यहरू फैलाएर retrieval quality नष्ट गर्न सक्छ।
- किन प्रयासले Nepali जान्दैन। केही कागजातहरूमा किन Roman-लिपि प्रयोग तपाईंको जवाफलाई तपाईंको प्रयोगकर्ताहरूको प्रश्नहरू भन्दा फरक रहनु सक्छ। यसलाई पूर्व-प्रक्रिया (script-normalisation) गर्न विचार गर्नुहोस्।
लागत र latency
पूर्ण RAG कल (retrieve + generate):
- Embed the query: 1 कल, ~500 tokens = $0.00001
- Search: 100 chunks मा free (in-memory) मा linear scan
- LLM call: 3 chunks × 500 tokens context + 200 tokens output ≈ 1700 input + 200 output tokens। Claude 3.5 Sonnet मूल्यमा: 1700 × $3/M + 200 × $15/M = $0.008
प्रति RAG query कुल: ~1 सेन्ट। Latency: 1-3 सेकेन्ड। यसलाई आर्थिक रूपमा scale गर्न सक्नुहुन्छ।
अब के आउँछ
तपाईंसँग working RAG pipeline छ। अर्को खण्डले चरण 6 माथि निर्माण गर्दछ: एक चलिरहेको सहायक जुन नेपाली-भाषा FAQs माथि real prompts हरूमा जवाफ दिन्छ, ठोस मापन दक्षता संग, र UI मा packaged। तपाईं यसलाई एक साथी वा सहकर्मीलाई देखाउन र तिनीहरू प्रयोग गर्न सक्छन्।