Skip to content
This page has been auto-translated and may contain errors.View in English

RAG

docs.scrimba.com

पहले, नाम की बात। RAG का पूरा रूप है retrieval-augmented generation: आप स्वयं प्रासंगिक टेक्स्ट लाते हैं और प्रश्न के समय उसे मॉडल को सौंप देते हैं, ताकि वह जमी हुई स्मृति की बजाय आपके दिए गए तथ्यों से उत्तर दे। यह अध्याय बताता है कि यह fetch-then-answer लूप कैसे काम करता है और कहाँ गलत हो जाता है।

Retrieve-then-generate पैटर्न

एक मॉडल को आपकी कंपनी के help docs, आपके उत्पाद की जानकारी, या प्रशिक्षण के बाद की कोई भी घटना पता नहीं होती। उनके बारे में पूछें तो वह या तो स्वीकार करेगा कि वह मदद नहीं कर सकता, या इससे भी बुरा, कुछ ऐसा गढ़ लेगा जो सही लगे। RAG इसे ठीक करता है — प्रश्न के समय मॉडल को प्रासंगिक टेक्स्ट सौंपकर।

यह विचार तब स्पष्ट होता है जब आपके पास embeddings हों — वह तरकीब जो टेक्स्ट को vectors में बदलती है जिन्हें निकटता से तुलना किया जा सकता है (cosine similarity मापती है कि दो vectors कितने करीब इशारा करते हैं)। Retrieval-augmented generation एक सामान्य model call पर जोड़े गए दो चरण हैं:

  1. Retrieve। उपयोगकर्ता का प्रश्न लें, अपने दस्तावेज़ों में सबसे प्रासंगिक टुकड़े खोजें, और शीर्ष मिलान एकत्र करें।
  2. Generate। उन टुकड़ों को prompt में context के रूप में डालें, फिर मॉडल से केवल उस context का उपयोग करके प्रश्न का उत्तर देने को कहें।

मॉडल की सामान्य भाषा क्षमता लेखन करती है; आपका retrieved टेक्स्ट तथ्य प्रदान करता है। आपको ऐसे प्रवाहमय उत्तर मिलते हैं जो आपकी सामग्री में निहित हों और अद्यतन रह सकें, क्योंकि आप नियंत्रित करते हैं कि क्या जाता है।

यह समझना उपयोगी है कि यह prediction loop के संदर्भ में इतना अच्छा क्यों काम करता है। RAG के बिना, आप मॉडल से उसके parameters में जमे patterns से एक तथ्य याद करने को कह रहे हैं, जो उसके पास नहीं भी हो सकता, जिससे एक आत्मविश्वासपूर्ण अनुमान मिलता है। RAG के साथ, तथ्य context में सामने ही रखा होता है, इसलिए मॉडल का काम "इसे याद करो" से बदलकर "सामने जो है उसे पढ़ो और उससे उत्तर दो" हो जाता है। Retrieval काम को स्मृति से पठन की ओर ले जाती है। साफ तस्वीर एक परीक्षा की है: बंद-पुस्तक परीक्षा अनुमान लगाने को आमंत्रित करती है, जबकि RAG इसे खुली-पुस्तक परीक्षा में बदल देता है जहाँ प्रासंगिक पृष्ठ मेज पर खुला होता है।

JunoRetrieve-then-generate पैटर्न RAG एक सामान्य model call में दो चरण जोड़ता है: प्रश्न के लिए सबसे प्रासंगिक दस्तावेज़ retrieve करें, फिर उन्हें context के रूप में उपयोग करते हुए उत्तर generate करें। मॉडल भाषा प्रदान करता है, आपका retrieved टेक्स्ट तथ्य प्रदान करता है। इससे वह स्मृति की बजाय आपकी सामग्री से उत्तर दे सकता है, जो उसे उत्तर गढ़ने से रोकता है। मुझे इस पर भरोसा करने में थोड़ा समय लगा, लेकिन खुली-पुस्तक मॉडल वास्तव में बंद-पुस्तक से कम अनुमान लगाता है।

एक मॉडल आपके help docs, उत्पाद विवरण, या training cutoff के बाद की कोई भी चीज़ नहीं रखता। फिर भी पूछें तो आपको या तो अस्वीकृति मिलती है या एक गढ़ा हुआ उत्तर जो ठीक पढ़ता है। Retrieval-augmented generation उस अंतर को request के समय प्रासंगिक टेक्स्ट लाकर और उसे prompt में रखकर पाटता है, ताकि मॉडल तथ्य को याद करने की बजाय पढ़े। तंत्र embeddings पर टिका है, जहाँ टेक्स्ट का प्रत्येक टुकड़ा एक vector बन जाता है और similarity vectors के बीच की दूरी होती है (cosine similarity, उनके बीच के कोण की cosine)।

लूप दो चरणों में है:

  1. Retrieve। प्रश्न को अपने दस्तावेज़ों के समान vector space में embed करें, फिर similarity द्वारा निकटतम chunks खींचें।
  2. Generate। उन chunks को prompt में context के रूप में जोड़ें, और मॉडल को केवल उन्हीं से उत्तर देने का निर्देश दें।

Retrieval चरण में एक dial होती है जिसे आप सेट करते हैं: top-k — आप वास्तव में कितने निकटतम chunks रखते हैं। k एक संतुलन है। बहुत छोटा हो तो आप वह chunk चूक जाते हैं जिसमें उत्तर था, इसलिए मॉडल के पास काम करने के लिए कुछ नहीं। बहुत बड़ा हो तो आप context में कमज़ोर रूप से संबंधित टेक्स्ट भर देते हैं, जो signal को पतला करता है, लागत बढ़ाता है, और lost-in-the-middle समस्या को बदतर बनाता है। 3 से 5 का k शुरुआत के लिए उचित है, और आप इसे वास्तविक प्रश्नों के विरुद्ध tune करते हैं।

यहाँ तरीका है: index को records की एक list के रूप में बनाएं, जिनमें से प्रत्येक में chunk टेक्स्ट और उसका precomputed vector हो, और prompt को इस तरह बनाएं कि retrieved context प्रश्न के ऊपर एक स्पष्ट रूप से सीमांकित block में हो। मॉडल की भाषा क्षमता लिखती है; आपका retrieved टेक्स्ट तय करता है कि क्या सत्य है।

python
def retrieve(question, index, k=4):
    query_vector = embed(question)
    scored = [
        {"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])}
        for item in index
    ]
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
JunoRetrieve-then-generate पैटर्न RAG है retrieve फिर generate: प्रश्न embed करें, निकटतम chunks खींचें, उन्हें prompt में डालें, और उनसे उत्तर दें। यहाँ जो dial मायने रखती है वह है top-k, आप कितने chunks रखते हैं: बहुत कम हों तो उत्तर चूक जाता है, बहुत अधिक हों तो वह शोर में दब जाता है और आप उसकी कीमत चुकाते हैं। लगभग 3 से 5 से शुरू करें और वास्तविक प्रश्नों के विरुद्ध tune करें, न कि अनुमान से।

एक मॉडल आपका private corpus या cutoff के बाद की कोई भी चीज़ नहीं रखता, और वह आपको यह नहीं बताएगा कि कौन सी है; वह दोनों में समान आत्मविश्वासी स्वर में उत्तर देता है। Retrieval-augmented generation request के समय प्रासंगिक टेक्स्ट लाने और उत्तर को उसमें आधारित करने का पैटर्न है, ताकि शुद्धता एक ऐसे स्रोत पर निर्भर हो जिसे आप नियंत्रित करते हैं, न कि parametric recall पर। यह embeddings पर चलता है: टेक्स्ट vectors में mapped, cosine similarity द्वारा ranked (दो vectors के बीच के कोण की cosine, अर्थात लंबाई की परवाह किए बिना meaning की निकटता)।

retrieve-then-generate का दो-चरणीय ढाँचा सही है लेकिन छुपाता है कि काम कहाँ है। Retrieval एक search समस्या है जो generation टोपी पहने हुई है, और पूरे system की गुणवत्ता वहाँ तय होती है, model call में नहीं। इसलिए retrieve-then-generate को शुरू से ही retrieve-rank-then-generate के रूप में मानें, क्योंकि naive संस्करण — query embed करें, निकटतम k लें, उन्हें paste करें — उन failure modes पर टूट जाता है जिनसे निचले स्तर अभी नहीं मिले हैं: exact-match misses, near-duplicate chunks जो coverage को बाहर कर देते हैं, और stale टेक्स्ट जो आत्मविश्वास से उत्तर देता है।

शुरू में wire करने लायक पहली आदत यह है कि यह एक measurable system है, न कि अनुभव। वास्तविक प्रश्नों का एक सेट hold out करें उन chunks के साथ जिन्हें उनका उत्तर देना चाहिए, फिर recall@k track करें: उन chunks का अंश जिन्हें retrieve होना चाहिए था, जो आपके top k में पहुँचे। Recall@k आपकी सीमा है। यदि सही chunk context में नहीं है, तो कोई भी prompt craft उसे recover नहीं कर सकती, और generate चरण वह नहीं दे सकता जो retrieval ने कभी सौंपा ही नहीं। हर downstream चीज़ इस संख्या से bounded है, इसलिए यह पहली चीज़ है जिसे measure करना है और पहली चीज़ है जिसे बदलना है।

python
def retrieve(question, index, k=8):
    query_vector = embed(question)
    scored = [
        {"text": it["text"], "score": cosine_similarity(query_vector, it["vector"]), "meta": it["meta"]}
        for it in index
    ]
    # यहाँ over-fetch करें, फिर एक छोटे final set तक re-rank करें (नीचे देखें)
    return sorted(scored, key=lambda x: x["score"], reverse=True)[:k]
JunoRetrieve-then-generate पैटर्न RAG generation टोपी पहने एक search समस्या है: गुणवत्ता retrieval में तय होती है, model call में नहीं, इसलिए naive embed-take-k-paste संस्करण exact matches और stale टेक्स्ट पर निराश करेगा। वह संख्या जो सब कुछ bound करती है वह है recall@k, उन chunks का अंश जो retrieve होने चाहिए थे जो वास्तव में आपके top k में पहुँचे। यदि सही chunk context में कभी नहीं पहुँचा, तो कोई prompt आपको नहीं बचाती। कुछ और tune करने से पहले उसे measure करें, वरना आप गलत छोर को चमका रहे हैं।

Chunking

आप पूरे दस्तावेज़ embed नहीं करते। आप पहले उन्हें छोटे chunks में विभाजित करते हैं — एक paragraph या कुछ वाक्य प्रत्येक — और उन्हें embed करते हैं। दो कारण। पहला, retrieval अधिक सटीक हो जाती है: आप एक 40-पृष्ठ की मैनुअल की बजाय वह एक प्रासंगिक paragraph खींचते हैं। दूसरा, chunks context window में फिट होते हैं, जहाँ एक विशाल दस्तावेज़ शायद न हो।

python
# एक छोटा chunker: paragraphs पर विभाजित करें
def chunk(text):
    return [c.strip() for c in text.split("\n\n") if c.strip()]

Chunk size एक संतुलन है, और इसका कारण embeddings से जुड़ता है। प्रत्येक chunk को एक single vector में बदला जाता है जो उसके पूरे meaning को सारांशित करता है। Chunk बहुत बड़ा बनाएं और वह कई विषयों को कवर करता है, इसलिए उसका एक vector उन सभी का एक धुंधला औसत है जो किसी विशिष्ट प्रश्न से अच्छी तरह match नहीं करता। बहुत छोटा बनाएं और वह उस surrounding meaning खो देता है जो उसे समझने के लिए चाहिए थी, इसलिए vector एक टुकड़े की ओर इशारा करता है। Paragraph-sized chunks में प्रत्येक एक सुसंगत विचार होता है। आप वहाँ से अपनी retrieval के प्रदर्शन के आधार पर समायोजित करते हैं।

JunoChunking Embedding से पहले, दस्तावेज़ों को छोटे chunks में विभाजित करें — लगभग paragraph-sized — और उन्हें embed करें। छोटे chunks retrieval को अधिक सटीक बनाते हैं और context window में बेहतर fit होते हैं। Chunk size एक संतुलन है: बहुत बड़ा धुंधला और बर्बाद है, बहुत छोटा surrounding meaning खो देता है। जब मेरे उत्तर अस्पष्ट आने लगे, chunks लगभग हमेशा बहुत बड़े थे।

आप chunks embed करते हैं, पूरे दस्तावेज़ नहीं, क्योंकि प्रत्येक chunk एक vector बन जाता है जिसे अपने पूरे meaning को सारांशित करना होता है। एक दस्तावेज़ को paragraph-sized टुकड़ों में विभाजित करें और प्रत्येक को embed करें। बहुत बड़ा chunk कई विषयों को एक vector में औसत करता है जो किसी चीज़ से तीखी match नहीं करता; बहुत छोटा chunk वह context खो देता है जिसने उसे meaningful बनाया था।

इसलिए chunk size को एक fixed rule नहीं बल्कि एक tuning dial मानें। कुछ सौ tokens का एक chunk एक विचार को साफ रूप से रखता है। Dial top-k के साथ interact करती है: बड़े chunks का मतलब है उनमें से कम आपके context budget में fit होते हैं, इसलिए आप प्रति request कम coverage रखते हैं। छोटे chunks precision बढ़ाते हैं लेकिन एक बड़े k की ज़रूरत होती है ताकि एक विचार जो उनमें फैला हो उसे फिर से जोड़ा जा सके।

दूसरी dial है overlap: consecutive chunks को उनकी boundaries पर टेक्स्ट का एक टुकड़ा साझा करने दें, मान लीजिए 10 से 20 प्रतिशत। Overlap के बिना, एक वाक्य जो प्रश्न का उत्तर देता है एक boundary पर split हो सकता है, एक chunk में आधा और अगले में आधा, इसलिए न तो chunk अच्छी तरह retrieve होता है। Overlap आपको कुछ duplication और कुछ और vectors की लागत देती है, और यह उन उत्तरों को वापस खरीदती है जिन्हें boundaries अन्यथा दो में काट देतीं।

python
def chunk(text, size=800, overlap=120):
    # size और overlap यहाँ characters में; real systems के लिए tokens में budget करें
    out, start = [], 0
    while start < len(text):
        out.append(text[start:start + size])
        start += size - overlap
    return out
JunoChunking Chunk size और overlap tuning dials हैं, न कि ऐसी settings जो आप एक बार सही कर लेते हैं। एक विचार रखने वाले chunks का लक्ष्य रखें, कुछ सौ tokens, और याद रखें dial top-k के विरुद्ध trade करती है: बड़े chunks, कम fit होते हैं। 10 से 20 प्रतिशत overlap जोड़ें ताकि एक boundary पर फैला उत्तर कम से कम एक chunk में पूरा रहे। Boundaries वह जगह हैं जहाँ retrieval चुपचाप उत्तर खो देती है।

Chunking वह जगह है जहाँ retrieval recall जीती या हारी जाती है इससे पहले कि एक भी query चले। प्रत्येक chunk एक vector बन जाता है जिसे अपने पूरे meaning का प्रतिनिधित्व करना होता है, इसलिए split तय करता है कि क्या findable है। Paragraph या section boundaries fixed character windows को तब beat करती हैं जब दस्तावेज़ में structure हो, क्योंकि एक सुसंगत section पर vector दो sections के आधे हिस्से पर vector की तुलना में एक cleaner query target है।

Chunk size और overlap आपके पूरे budget से coupled हैं, free parameters नहीं। Size top-k के विरुद्ध context window के विरुद्ध trade करती है: बड़े chunks प्रत्येक में अधिक context रखते हैं लेकिन कम fit होते हैं, इसलिए coverage घटती है; छोटे chunks precision बढ़ाते हैं लेकिन विचारों को fragment करते हैं और reassemble करने के लिए एक उच्च k की ज़रूरत होती है, जो tokens की लागत और lost-in-the-middle को फिर से प्रस्तुत करता है। लगभग 10 से 20 प्रतिशत overlap उन उत्तरों को बचाती है जो boundary पर फैले हैं, आपके index में duplication की लागत पर। Duplication का एक downstream bite है: near-identical overlapping chunks आपके कई top-k slots को same content से भर सकते हैं, coverage भूखा रखते हुए। यही एक कारण है कि re-ranking और de-duplication बाद में अपना स्थान अर्जित करते हैं।

वह लागत जिसे अधिकांश लोग तब तक skip करते हैं जब तक कि यह दर्दनाक न हो: chunking strategy आपके index में baked है, इसलिए इसे बदलने का मतलब है पूरे corpus को re-embed और re-index करना। वह real compute है, real पैसा है, और एक migration window है जहाँ पुराने और नए vectors को मिलाना नहीं चाहिए। Chunk size को अपने actual दस्तावेज़ों और queries के विरुद्ध जल्दी तय करें, held-out set पर recall@k measure करें, और re-chunk को एक deliberate re-index मानें, न कि एक config tweak जिसे आप शुक्रवार को ship करते हैं।

JunoChunking Structure पर chunk करें जहाँ आपके पास हो, क्योंकि प्रत्येक chunk एक vector है जो अपने पूरे meaning का प्रतिनिधित्व करता है, और size एक साथ top-k के विरुद्ध window के विरुद्ध trade करती है। 10 से 20 प्रतिशत overlap boundary-straddling उत्तरों को बचाती है लेकिन near-duplicates पैदा करती है जो आपके top-k को crowd करते हैं, जो partly वह कारण है कि re-ranking मौजूद है। और याद रखें chunking choice index में baked है: इसे बदलना एक full re-embed और re-index है, real लागत और एक migration, न कि config flip।

Vectors कहाँ रहते हैं

कुछ chunks के लिए, आप vectors को memory में रख सकते हैं और उन्हें embeddings अध्याय की cosine-similarity function से compare कर सकते हैं, जो बाद के उदाहरण में किया गया है। हज़ारों या लाखों chunks के लिए, यह धीमा पड़ जाता है, और आप एक vector store की ओर जाते हैं: एक database जो scale पर निकटतम vectors को तेज़ी से खोजने के लिए बना है।

RAG सीखने के लिए आपको इसकी ज़रूरत नहीं है, और आपको अभी कोई चुनने की ज़रूरत नहीं है। कई hosted services और libraries के रूप में मौजूद हैं; concept ही मायने रखता है: embeddings store करने और उन्हें तेज़ी से search करने की कोई जगह। Memory में शुरू करें, जब collection बड़ी हो जाए तो store की ओर बढ़ें।

JunoVectors कहाँ रहते हैं कुछ chunks के लिए, vectors को memory में रखें और उन्हें सीधे compare करें। बड़े collections के लिए, एक vector store एक database है जो scale पर निकटतम vectors को तेज़ी से खोजता है। RAG सीखने के लिए आपको इसकी ज़रूरत नहीं है: memory में शुरू करें और vector store की ओर जाएं जब collection बड़ी हो जाए। पहले दिन ही database खोजने की ज़रूरत नहीं।

कुछ सौ chunks के लिए, vectors memory में रहते हैं और आप हर एक को query के विरुद्ध score करते हैं। यह एक linear scan है: छोटे scale पर ठीक है, एक बार जब collection हज़ारों या लाखों तक पहुँचती है तो धीमा, क्योंकि आप हर query पर हर vector के विरुद्ध compare करते हैं। उस point पर आप एक vector store की ओर जाते हैं, एक database जो fast nearest-neighbor search के लिए vectors को index करता है।

जो asymmetry ध्यान में रखने लायक है वह है embed-once बनाम embed-per-query। आप हर document chunk को ठीक एक बार embed और index करते हैं, पहले से, और वह काम हर भविष्य के प्रश्न के लिए reuse होता है। आप query को प्रति request एक बार embed करते हैं, request के समय, और यही hot path पर एकमात्र embedding लागत है। इसलिए भारी indexing लागत upfront चुकाई जाती है और amortised होती है; per-query लागत एक embedding call और एक search है। यही बात RAG को एक बार बनने के बाद serve करना सस्ता बनाती है।

Pattern सीखने के लिए आपको store चुनने की ज़रूरत नहीं है। Records की एक in-memory list से शुरू करें, जब scale या persistence की ज़रूरत हो तो vector store में swap करें, और interface वही रखें: query embed करें, search करें, top-k लौटाएं।

JunoVectors कहाँ रहते हैं Memory में आप प्रति query हर vector scan करते हैं, जो ठीक है जब तक collection बड़ी न हो, फिर एक vector store indexed nearest-neighbor search देता है। Asymmetry को ध्यान में रखें: documents एक बार embed और index होते हैं और हमेशा के लिए reuse होते हैं, जबकि query हर request पर fresh embed होती है। Upfront लागत एक बार चुकाई और amortised होती है, इसलिए serving सस्ती रहती है। Memory में शुरू करें, जब scale या persistence मजबूर करे तो store में swap करें।

Memory में आप एक linear scan करते हैं, query को हर chunk vector के विरुद्ध score करते हुए, जो exact है लेकिन प्रति query O(n) है और दसियों हज़ार में कहीं scaling रोक देता है। एक vector store scan को एक approximate nearest-neighbor index (ANN) से replace करता है: यह हर vector को check न करके थोड़े recall के बदले एक बड़ा speedup trade करता है। वह trade एक knob है, और production scale पर आप स्वीकार करते हैं कि index कभी-कभी एक true nearest neighbor miss कर सकता है, जो सीधे आपके recall@k में fold हो जाता है।

Embed-once asymmetry आपकी लागत shape सेट करती है। Indexing पूरे corpus को एक बार embed करता है, एक बड़ी upfront batch लागत; serving प्रति request एक query embed और एक search run करता है। इसलिए आपका steady-state latency budget लगभग है: query embed करें, index search करें, फिर generate call। Search आमतौर पर सबसे छोटा slice है; generate call latency dominate करती है, और query embedding एक fixed tax है जो आप retrieve करने से पहले चुकाते हैं। प्रत्येक leg measure करने लायक है, क्योंकि लोग faster vector store की ओर पहुँचते हैं जब real latency हर समय generation में बैठी थी।

फिर staleness है, वह failure mode जो in-memory toy कभी नहीं दिखाता। आपका index एक snapshot है; source documents drift करते हैं। एक policy बदलती है, एक doc edit होती है, एक पुराना chunk अभी भी index में बैठा है अतीत की ओर इशारा करते हुए, और retrieval उसे पूरे confidence के साथ serve करती है। इसलिए एक index write-once नहीं है: आपको एक re-indexing path चाहिए जो changed documents को re-embed करे, और ideally प्रत्येक chunk पर metadata (source id, version, timestamp) ताकि आप stale entries को invalidate या filter कर सकें। Refresh cadence को तय करें इस हिसाब से कि आपका truth कितनी तेज़ी से बदलता है, और "index current है" को कुछ ऐसा मानें जो आप verify करते हैं, न कि assume।

JunoVectors कहाँ रहते हैं एक vector store exact linear scan को approximate nearest-neighbor index से swap करता है: तेज़, लेकिन यह एक true neighbor miss कर सकता है, जो आपके recall@k में वापस आता है। Embedding corpus के लिए एक बार और प्रति query एक बार है, इसलिए steady-state latency है embed-query plus search plus generate, और generate call आमतौर पर dominate करती है, इसलिए store को blame करने से पहले measure करें। Toy जो trap कभी नहीं दिखाता वह है staleness: index एक snapshot है, sources drift करते हैं, इसलिए re-indexing path बनाएं और chunks को version और timestamp के साथ stamp करें बजाय index के current होने पर भरोसा करने के।

Grounding और citing

Generate चरण एक prompt है, और आप इसे कैसे लिखते हैं यह तय करता है कि RAG वास्तव में गढ़े हुए उत्तरों को कम करता है या नहीं। दो निर्देश भारी काम करते हैं: मॉडल को बताएं कि दिए गए context से केवल उत्तर दे, और उसे बताएं कि जब context में उत्तर न हो तो ऐसा कहे।

python
context_text = "\n\n".join(retrieved_chunks)

system_prompt = f'''नीचे दिए गए context का उपयोग करके ONLY प्रश्न का उत्तर दें।
यदि context में उत्तर नहीं है, तो कहें "I don't have that information."
अपने उत्तर में context के प्रासंगिक भाग को उद्धृत करें।

Context:
"""
{context_text}
"""'''

यह दो पहले के पाठों को एक साथ लाता है। Delimiters और explicit "answer only from this" prompting से आते हैं। "I don't have that information" कहने की अनुमति hallucination defense है: एक grounded मॉडल जिसके पास escape hatch हो वह अनुमान लगाने वाले की तुलना में बहुत कम गढ़ता है। Grounding plus escape hatch ही गढ़े हुए उत्तरों को काटता है। स्रोत उद्धृत करने के लिए कहना users को verify करने के लिए कुछ देता है।

JunoGrounding और citing Generate चरण एक prompt है, और यह तय करता है कि RAG काम करता है या नहीं। मॉडल को बताएं कि दिए गए context से केवल उत्तर दे और जब उत्तर वहाँ न हो तो ऐसा कहे। वह grounding, plus यह स्वीकार करने की अनुमति कि उसे नहीं पता, ही गढ़े हुए उत्तरों को कम करती है। स्रोत उद्धृत करने के लिए कहना users को check करने के लिए कुछ देता है, और जब retrieval चूक जाए तो आपको एक सुराग देता है।

Retrieval मॉडल को perfect context दे सकती है और मॉडल फिर भी उसे ignore कर सकता है। Generate prompt ही उत्तर को context से बाँधता है, इसलिए इसे जान-बूझकर लिखें: मॉडल को दिए गए context से केवल उत्तर देने, जब context में उत्तर न हो तो स्वीकार करने, और उसने जो span उपयोग किया उसे उद्धृत करने का निर्देश दें।

python
context_text = "\n\n".join(f"[{i}] {c}" for i, c in enumerate(retrieved_chunks))

system_prompt = f'''नीचे दिए गए context का उपयोग करके ONLY उत्तर दें।
यदि उत्तर context में नहीं है, तो बिल्कुल reply करें: "I don't have that information."
आपने जो chunk number(s) उपयोग किए उन्हें cite करें, जैसे [2]।

Context:
"""
{context_text}
"""'''

Chunks को नंबर देना "स्रोत उद्धृत करो" को एक ऐसी citation में बदलता है जिसे आप programmatically check कर सकते हैं: मॉडल chunk [2] की ओर इशारा करता है, और आप confirm कर सकते हैं कि chunk [2] वास्तव में claim को support करता है। यह RAG में सबसे ज़्यादा miss होने वाले failure mode पर आपका handle है — सही उत्तर गलत chunks के साथ retrieved, जहाँ मॉडल एक correct-sounding उत्तर produce करता है जिसे supplied context support नहीं करता। Citations के बिना आप एक grounded उत्तर को एक lucky guess से अलग नहीं कर सकते।

Escape hatch उतना ही मायने रखता है जितना instruction। एक मॉडल जिसे केवल context से उत्तर देने के लिए कहा गया है लेकिन fail करने की कोई अनुमति नहीं दी गई है वह खराब retrieval से भी कुछ जोड़ लेगा। उसे एक explicit out दें — exact "I don't have that information" string — और एक retrieval miss एक confident invention की बजाय एक clean gap के रूप में सामने आता है। यहाँ message shape OpenAI-style है; system-then-context व्यवस्था provider के अनुसार भिन्न होती है, लेकिन सिद्धांत बना रहता है।

JunoGrounding और citing Generate prompt उत्तर को context से बाँधता है: केवल उससे उत्तर दें, स्वीकार करें जब वह वहाँ न हो, और chunk cite करें। Chunks को नंबर दें ताकि citation checkable हो, जो उस sneaky failure को पकड़ती है जहाँ उत्तर सही है लेकिन retrieved chunks ने उसे कभी support नहीं किया। मॉडल को exact escape string दें, वरना एक weak retrieval एक confident गलत उत्तर में जुड़ जाती है। Shape provider के अनुसार भिन्न होती है, लेकिन system-then-context structure आगे जाता है।

Grounding retrieval और मॉडल के बीच का contract है, और यह सबसे सस्ता reliability lever है जो आपके पास है, इसलिए prompt उस पर खर्च करें। मॉडल को दिए गए context से केवल उत्तर देने, एक fixed abstain string return करने जब context उत्तर support नहीं करता, और उसने उपयोग किए chunk ids cite करने का निर्देश दें।

python
context_text = "\n\n".join(f"[{c['id']}] {c['text']}" for c in retrieved)

system_prompt = f'''नीचे दिए गए context का उपयोग करके ONLY उत्तर दें।
यदि context उत्तर support नहीं करता, तो बिल्कुल reply करें: "I don't have that information."
प्रत्येक claim के लिए, brackets में chunk id(s) cite करें, जैसे [doc12]।

Context:
"""
{context_text}
"""'''

Citations सजावट नहीं हैं; वे grounding को measurable बनाने का तरीका हैं। Output में chunk ids के साथ आप automatically check कर सकते हैं कि क्या एक cited chunk वास्तव में claim contain करता है, जो एक grounded उत्तर को एक fluent guess से अलग करता है। यही वह failure भी detect करता है जो naive RAG को defeat करता है: उत्तर plausibly correct है लेकिन retrieved context ने उसे कभी support नहीं किया, इसलिए retrieval miss हुई और मॉडल ने parametric memory से gap fill किया। Ids के बिना आप spot-checking कर रहे हैं; उनके साथ आप अपने evals में grounding score कर सकते हैं (known-good उत्तरों के साथ प्रश्नों का एक held-out set, automatically scored) और faithfulness को एक number के रूप में देख सकते हैं।

दो production notes। Abstain path सस्ता और sanctioned होना चाहिए, क्योंकि fail करने की कोई अनुमति नहीं मिला मॉडल हमेशा कुछ produce करेगा, और एक खराब retrieval तब एक clean gap की बजाय एक confident गलत उत्तर बन जाती है। और यहाँ message structure OpenAI-SDK shape है; system-versus-user split और context कैसे attach होता है provider के अनुसार भिन्न होता है, इसलिए arrangement को portable मानें, exact field names को नहीं। नीचे का durable rule: मॉडल को कभी source न बनने दें, केवल एक ऐसे source का phraser जिसे आप cite और check कर सकते हैं।

JunoGrounding और citing Grounding contract है: केवल context से उत्तर दें, एक fixed string से abstain करें, और chunk ids cite करें ताकि faithfulness evals में score करने योग्य हो, न कि eyeball। Citations वह तरीका है जिससे आप वह उत्तर पकड़ते हैं जो सही है लेकिन unsupported है, जहाँ retrieval miss हुई और मॉडल ने memory से gap patch किया। Abstain path को सस्ता और sanctioned रखें वरना एक weak retrieval एक confident गलत उत्तर बन जाती है। Shape provider के अनुसार भिन्न होती है; structure ports करता है, field names नहीं।

Nearest-neighbor से परे: hybrid search और re-ranking

Pure vector search meaning पर match करती है, जो अधिकांश समय आप यही चाहते हैं। लेकिन इसका एक blind spot है: यह एक ऐसे exact word को miss कर सकती है जो मायने रखता हो। E_4021 जैसे error code या किसी specific product name की खोज करें, और meaning-based search उन chunks को वापस दे सकती है जो उसी topic के बारे में हैं जबकि उसे skip कर देती है जिसमें exact string है।

Fix का एक नाम है, hybrid search: meaning-based search को पुराने ज़माने की keyword search के साथ मिलाएं, ताकि exact terms और general meaning दोनों को vote मिले। RAG शुरू करने के लिए आपको यह बनाने की ज़रूरत नहीं है। इसे तब reach करें जब exact terms मायने रखते हों और pure vectors उन्हें miss करते रहें।

JunoNearest-neighbor से परे: hybrid search और re-ranking Vector search meaning match करती है, जो तब तक अच्छा है जब तक कोई exact code या product name search न करे और सही chunk skip न हो जाए। Hybrid search plain keyword search मिलाता है ताकि exact terms भी count हों। आप RAG इसके बिना शुरू कर सकते हैं और एक बार exact-match misses ढेर होने पर इसे जोड़ सकते हैं।

Nearest-neighbor vector search meaning पर match करती है, और यही उसकी कमज़ोरी भी है। यह एक ऐसे chunk को rank कर सकती है जो topically close है उस एक chunk से ऊपर जिसमें exact token है जो आपको चाहिए — error code, SKU, function name, rare proper noun। Embeddings उन्हें "roughly this area of meaning" में smear करती हैं, और exact strings smear में खो जाती हैं।

Hybrid search दो retrievers चलाता है और उन्हें merge करता है: meaning के लिए vector search, plus एक keyword search (classic term matching, अक्सर BM25, जो documents को score करता है इस आधार पर कि उनमें query के exact words कितने हैं और वे words कितने rare हैं)। आप दोनों से candidates लेते हैं और rankings combine करते हैं, ताकि exact terms या semantic closeness में से किसी एक पर जीतने वाला chunk surface हो सके। Practical payoff उन exactly queries पर recall है जो pure vectors के साथ fumble होती हैं।

दूसरी technique है re-ranking: एक दूसरा pass जो आपके first-pass candidates को एक अधिक accurate, अधिक expensive model से re-score करता है। Pattern है over-fetch फिर narrow। सस्ते में एक generous set retrieve करें, मान लीजिए top 20, फिर एक re-ranker run करें जो प्रत्येक candidate को query के विरुद्ध पढ़ता है और उन्हें reorder करता है, और actually send करने के लिए top 4 रखें। First-pass retrieval fast और rough है; re-ranker slow और sharp है, इसलिए आप इसे केवल short list पर run करते हैं। Result है बेहतर final k बिना re-ranker की लागत को पूरे index पर चुकाए।

JunoNearest-neighbor से परे: hybrid search और re-ranking Pure vector search error codes और SKUs जैसी exact strings miss करती है क्योंकि embeddings उन्हें meaning के एक region में blur कर देती हैं। Hybrid search keyword matching (BM25) जोड़ता है ताकि exact terms को semantics के साथ-साथ vote मिले। Re-ranking एक cheap-then-sharp pattern है: fast retrieval के साथ wide set over-fetch करें, फिर short list को एक धीमे, अधिक accurate model से re-score करें और best few रखें। आपको बेहतर final results मिलते हैं बिना expensive scorer को हर चीज़ पर run किए।

Pure approximate nearest-neighbor एक strong default और एक known failure surface है। यह semantic proximity के लिए optimize करती है, इसलिए यह exactly वहाँ underperform करती है जहाँ query एक exact token पर टिकी हो — error code, identifier, SKU, rare proper noun — क्योंकि embedding उस token को meaning के एक neighborhood में compress करती है जहाँ exact strings blur हो जाती हैं। यह एक tuning समस्या नहीं है; यही dense vectors करते हैं।

Hybrid search इसे dense retrieval (vectors, meaning) के साथ-साथ sparse retrieval (keyword term matching, typically BM25, जो exact term frequency पर score करता है term rarity से weighted) चलाकर और result lists fuse करके address करता है, commonly reciprocal rank fusion के साथ, जो दो rankings को incomparable raw scores की बजाय प्रत्येक item की position से combine करता है। जीत exact-match queries पर recall@k के रूप में measurable है, जो slice है जिसे pure dense quietly drop करता है। लागत एक दूसरा index और एक fusion step है, इसलिए यह precisely तब pay off करता है जब आपके traffic में identifiers और rare terms शामिल हों, और कम जब यह सब natural-language paraphrase हो।

Re-ranking एक अलग miss पर attack करता है। First-pass retrieval (dense, sparse, या hybrid) एक large index पर speed के लिए built है, इसलिए यह coarsely rank करता है। एक re-ranker एक cross-encoder है (एक model जो query और एक candidate chunk को एक साथ पढ़ता है और उनकी relevance jointly score करता है, दो precomputed vectors compare करने की बजाय) जो अधिक accurate और पूरे corpus पर run करने के लिए बहुत slow है। इसलिए आप cheap retriever से over-fetch करते हैं, top 20 से 50, फिर उस short list को re-rank करते हैं और final few रखते हैं।

यह अधिकांश production RAG में सबसे बड़े payoff वाला recall fix है: यह directly right-answer-wrong-chunks failure पर attack करता है उस chunk को promote करके जो वास्तव में उत्तर support करता है merely topical ones से ऊपर, और आप gain recall@k के रूप से measure करते हैं re-rank stage से पहले बनाम बाद में। इसे budget करें: re-ranker hot path पर latency और per-query लागत जोड़ता है, इसलिए over-fetch को अपने latency target के विरुद्ध size करें, न कि abstract में आप कितने candidates score कर सकते हैं।

JunoNearest-neighbor से परे: hybrid search और re-ranking Dense vectors exact tokens blur करते हैं, इसलिए hybrid search sparse keyword retrieval (BM25) dense के बगल में चलाता है और reciprocal rank fusion के साथ lists fuse करता है, identifiers और rare terms पर recall@k वापस खरीदता है। Re-ranking बड़ी जीत है: सस्ते में over-fetch करें, फिर short list को एक cross-encoder से re-score करें जो query और chunk को एक साथ पढ़ता है, और यह directly right-answer-wrong-chunks fix करता है उस chunk को promote करके जो उत्तर support करता है। Recall@k पहले और बाद में measure करें, और re-ranker की latency और per-query लागत को अपने hot-path target के विरुद्ध budget करें।

व्यवहार में

embeddings अध्याय से search और embed helpers का पुनः उपयोग करते हुए, एक minimal RAG उत्तर:

python
def answer_from_docs(question, index):
    # 1. retrieve: प्रश्न के लिए शीर्ष matching chunks
    query_vector = embed(question)
    scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
    top = sorted(scored, key=lambda x: x["score"], reverse=True)[:3]

    # 2. generate: केवल उन chunks का उपयोग करके उत्तर दें
    context = "\n\n".join(t["text"] for t in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": f'Answer using ONLY this context. If it is not here, say you don\'t know.\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

तीन निकटतम chunks retrieve करें, उन्हें prompt में डालें, और मॉडल को उनसे उत्तर देने दें। यही पूरी RAG pipeline है: embeddings search एक model call को feed करती है, एक grounded prompt के साथ जो इसे एक साथ रखती है। लगभग सारी गुणवत्ता सही chunks retrieve करने में होती है।

Junoव्यवहार में एक minimal RAG उत्तर है embeddings search एक model call को feed करती है: निकटतम chunks retrieve करें, उन्हें prompt में डालें, और मॉडल से केवल उनसे उत्तर देने को कहें। Retrieval तथ्य प्रदान करती है और grounded prompt उत्तर को उनसे anchored रखती है। लगभग सारी गुणवत्ता सही chunks retrieve करने में है, इसलिए वहाँ अपना समय लगाएं।

एक working RAG call है retrieve, ground, generate, पहले से wired citation handle के साथ:

python
def answer_from_docs(question, index, k=4):
    top = retrieve(question, index, k)  # pattern section से
    context = "\n\n".join(f"[{i}] {t['text']}" for i, t in enumerate(top))
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'Answer using ONLY this context. If it is not here, reply '
                f'"I don\'t have that information." Cite chunks like [1].\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content

पूरी चीज़ है embeddings search एक model call को feed करती है, एक grounded prompt के साथ एक साथ रखी। chat.completions shape OpenAI-style है; message roles और call signature provider के अनुसार भिन्न होती है, लेकिन retrieve-then-ground-then-generate flow हर जगह same है।

जब कोई उत्तर गलत हो, stage order में debug करें। पहले retrieved chunks check करें: क्या प्रासंगिक टेक्स्ट top में पहुँचा? यदि नहीं, तो fix retrieval में है — k बढ़ाएं, chunking fix करें, hybrid या re-ranking जोड़ें — prompt में नहीं। यदि सही chunk retrieve हुआ और उत्तर अभी भी गलत है, तो यह prompt problem है। यह order आपको एक retrieval bug के विरुद्ध prompt wording tune करने से बचाता है, जो कभी converge नहीं होता।

Junoव्यवहार में एक working RAG call है retrieve, citations के साथ ground करें, generate करें, और OpenAI-style message shape structure में दूसरे providers पर port होती है यदि field names में नहीं। एक fixed order में debug करें: पहले retrieved chunks देखें। यदि सही टेक्स्ट कभी नहीं पहुँचा, retrieval fix करें (k, chunking, hybrid, re-rank), prompt नहीं। केवल एक बार जब सही chunk context में हो तभी गलत उत्तर prompt problem है।

व्यवहार में retrieve-rank-generate pipeline एक function है, और आप जो parts instrument करते हैं वे stages के बीच के joints हैं:

python
def answer_from_docs(question, index, k_fetch=20, k_final=4):
    candidates = retrieve(question, index, k=k_fetch)   # dense (या hybrid)
    top = rerank(question, candidates)[:k_final]        # cross-encoder pass
    context = "\n\n".join(f"[{c['id']}] {c['text']}" for c in top)
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content":
                f'Answer using ONLY this context. If unsupported, reply '
                f'"I don\'t have that information." Cite ids like [doc12].\n\n"""{context}"""'},
            {"role": "user", "content": question},
        ],
    )
    return response.choices[0].message.content, [c["id"] for c in top]

उत्तर के साथ chunk ids return करना वह move है जो system को observable बनाता है: आप log करते हैं क्या retrieve हुआ, क्या cite हुआ, और क्या cited ids वास्तव में उत्तर support करते हैं, जो evals में आपका faithfulness signal है। chat.completions shape OpenAI-SDK है; flow को portable मानें और field names को provider-specific।

इस pipeline का latency और cost map ध्यान में रखने लायक है, क्योंकि optimization वहाँ जाता है जहाँ समय है:

  • Query embedding एक fixed small tax है।
  • First-pass retrieval सस्ता है।
  • Re-ranking आपके over-fetch size के अनुसार scaled एक real per-query hit जोड़ता है।
  • The generate call latency और dollar दोनों लागत dominate करती है।

इसलिए faster vector store chase करने से पहले, confirm करें कि search कभी bottleneck था; आमतौर पर वह generation में था।

और failures को stage order में debug करें: retrieved set, फिर re-ranked set, फिर citations, फिर prompt। एक गलत उत्तर जिसका सही chunk कभी candidates में नहीं गया वह एक retrieval bug है, और कोई prompt change उस chunk को fix नहीं करता जो कभी वहाँ था ही नहीं।

Junoव्यवहार में Pipeline है retrieve, re-rank, ground, generate, और chunk ids को उत्तर के बगल में return करना ही इसे observable बनाता है: retrieved बनाम cited log करें और आप evals में faithfulness score कर सकते हैं अनुमान लगाने की बजाय। Cost map देखें, generation latency और dollars dominate करती है, इसलिए vector stores swap करने से पहले confirm करें कि search bottleneck था। Stage order में debug करें — retrieved फिर re-ranked फिर cited फिर prompt — क्योंकि कोई prompt उस chunk को fix नहीं करता जो candidate set में कभी नहीं गया। Shape OpenAI-SDK है; flow ports करता है, field names नहीं।

जब आपको RAG की ज़रूरत न हो

RAG मुफ़्त नहीं है, और यह हमेशा उत्तर नहीं है। इसे skip करें जब:

  • Data छोटा है और prompt में fit होता है। यदि आपका पूरा knowledge base कुछ paragraphs है, तो उसे सीधे paste करें। RAG तब के लिए है जब एक बार में सब भेजने के लिए बहुत अधिक हो।
  • एक long-context model सब कुछ hold कर सकता है। कुछ models बहुत बड़े inputs accept करते हैं, एक पूरा दस्तावेज़ एक साथ लेने के लिए पर्याप्त, जो retrieval बनाने से कम काम हो सकता है।

और असली सीमा याद रखें: RAG quality ज़्यादातर retrieval quality है। यदि search step गलत chunks return करता है, तो एक perfect prompt भी उत्तर नहीं बचा सकता। Real RAG systems में अधिकांश प्रयास सही टेक्स्ट retrieve करने में जाता है, final model call में नहीं।

Embeddings और RAG मॉडल को प्रश्न के समय ज्ञान देते हैं, बिना मॉडल को बदले। अगला अध्याय, Fine-tuning, दूसरे option को cover करता है: अपने उदाहरणों से मॉडल बदलना, और prompting या RAG की तुलना में यह कब worth it है।

Junoजब आपको RAG की ज़रूरत न हो RAG skip करें जब आपका data prompt में paste करने के लिए पर्याप्त छोटा हो, या जब एक long-context model सब कुछ एक बार में hold कर सके। और असली सीमा ध्यान में रखें: RAG quality ज़्यादातर retrieval quality है, इसलिए यदि search गलत chunks return करता है, तो कोई prompt उत्तर नहीं बचा सकती। अधिकांश real काम सही टेक्स्ट retrieve करने में है, final call में नहीं।

RAG infrastructure है — एक index, एक embedding step, एक retrieval step, एक re-index path — और यह overhead तभी earn करता है जब data भेजने के लिए बहुत बड़ा हो और इतनी बार बदलता हो कि retraining एक option नहीं है। इसे skip करें जब एक सस्ती shape काम करे:

  • Corpus prompt में fit होता है। कुछ paragraphs या एक छोटा policy doc सीधे context में जाता है। RAG तभी अपना खर्च earn करता है जब एक बार में भेजने से अधिक हो।
  • एक long-context model पूरे source को hold करता है। यदि document एक large window में fit होता है, तो उसे पूरा भेजना एक retrieval pipeline से बेहतर हो सकता है जो आपको बनाना और tune करना पड़े, हालाँकि आप उन tokens की लागत हर call पर चुकाते हैं और अभी भी lost-in-the-middle से टकराते हैं।

governing fact hold करें: RAG quality retrieval quality से bounded है। गलत chunks पर एक perfect prompt अभी भी गलत उत्तर देता है, इसलिए index और retrieval step वहाँ हैं जहाँ काम और जीत है, final model call में नहीं। जब आपको तथ्य supply करने की बजाय behavior बदलने की ज़रूरत हो, Fine-tuning अगला अध्याय है, और यह RAG की तुलना में एक अलग समस्या solve करता है।

Junoजब आपको RAG की ज़रूरत न हो RAG real overhead के साथ infrastructure है, इसलिए इसे skip करें जब corpus prompt में fit हो या एक long-context model पूरे source को निगल सके, यह याद रखते हुए कि long-context route हर call पर tokens चुकाता है और अभी भी middle में drift करता है। Governing fact बना रहता है: quality retrieval से bounded है, इसलिए गलत chunks पर एक perfect prompt अभी भी गलत उत्तर देता है। Index और retrieval पर खर्च करें, final call पर नहीं।

RAG आपको एक ऐसे retrieval system की कीमत पर currency और grounding खरीदता है जिसे आप build, tune, monitor, और re-index करते हैं। इसलिए decision एक cost-benefit है, default नहीं। इसे skip करें जब एक lighter shape bar clear करे:

  • छोटा, stable corpus। यदि यह prompt में fit होता है और rarely बदलता है, तो इसे context में डालें, और prompt caching पर lean करें ताकि fixed block repeat calls पर सस्ते serve हो। यहाँ एक retrieval pipeline बिना payoff के overhead है।
  • Long-context, modest reuse। यदि source एक large window में fit होता है, तो उसे पूरा भेजना build skip करता है, लेकिन आप उन input tokens की लागत हर request पर चुकाते हैं और middle में recall अभी भी सड़ता है, इसलिए यह RAG से एक बार volume या document size climb करने पर हार जाता है।

Governing constraint वही है जो पूरे अध्याय को चलाती है: end-to-end quality recall@k से bounded है। कोई prompt, कोई model upgrade, कोई re-rank उस fact को recover नहीं करता जो retrieval ने कभी context में नहीं डाला। इसलिए engineering investment retrieval में जाती है — chunking, hybrid, re-ranking, evals, re-indexing — और model call सही करने के लिए सस्ता हिस्सा है। और RAG और fine-tuning को उनकी अलग lanes में रखें: retrieval supply करता है क्या सत्य है और sources बदलने पर current रहता है, जबकि fine-tuning shape करता है कि मॉडल कैसे respond करता है और facts inject करने का एक stale, unsourced तरीका है। Knowledge के लिए retrieval use करें, form के लिए fine-tuning, और कभी एक से दूसरे का काम करने को न कहें।

Junoजब आपको RAG की ज़रूरत न हो RAG currency और grounding खरीदता है एक ऐसे system की कीमत पर जो आप build और re-index करते हैं, इसलिए इसे skip करें जब corpus छोटा और stable हो (paste करें, prompt caching पर lean करें) या modest reuse के साथ long window में fit हो। वह constraint जो सब कुछ चलाती है वह है recall@k: कुछ भी downstream उस fact को recover नहीं करता जो retrieval ने surface नहीं किया, इसलिए retrieval में invest करें, model call में नहीं। Lanes clean रखें: क्या सत्य है उसके लिए retrieval, यह कैसे sounds है उसके लिए fine-tuning, और कभी एक से दूसरे का काम करने को न कहें।