रीट्राइवल-एग्मेंटेड जनरेशन (आरएजी)

Genkit, ऐसे एब्स्ट्रैक्शन उपलब्ध कराता है जिनकी मदद से, रीट्रिवल-ऑगमेंटेड जनरेशन (आरएजी) फ़्लो बनाए जा सकते हैं. साथ ही, इससे मिलते-जुलते टूल के साथ इंटिग्रेशन करने वाले प्लग इन भी उपलब्ध होते हैं.

आरएजी क्या है?

रीट्रिवल-ऑगमेंटेड जनरेशन एक ऐसी तकनीक है जिसका इस्तेमाल, एलएलएम के जवाबों में जानकारी के बाहरी सोर्स को शामिल करने के लिए किया जाता है. ऐसा करना ज़रूरी है, क्योंकि आम तौर पर एलएलएम को ज़्यादा से ज़्यादा कॉन्टेंट पर ट्रेनिंग दी जाती है. हालांकि, एलएलएम का व्यावहारिक इस्तेमाल करने के लिए, अक्सर किसी खास डोमेन की जानकारी ज़रूरी होती है. उदाहरण के लिए, हो सकता है कि आप अपनी कंपनी के प्रॉडक्ट के बारे में ग्राहकों के सवालों के जवाब देने के लिए, एलएलएम का इस्तेमाल करना चाहें.

इसका एक तरीका यह है कि ज़्यादा सटीक डेटा का इस्तेमाल करके, मॉडल को बेहतर बनाया जाए. हालांकि, यह प्रोसेस, कंप्यूट की लागत और ज़रूरत के मुताबिक ट्रेनिंग डेटा तैयार करने के लिए ज़रूरी मेहनत, दोनों के लिहाज़ से महंगी हो सकती है.

इसके उलट, आरएजी, मॉडल को पास किए जाने के समय प्रॉम्प्ट में बाहरी डेटा सोर्स को शामिल करके काम करता है. उदाहरण के लिए, "बार्ट और लिसा के बीच का रिश्ता क्या है?" प्रॉम्प्ट को कुछ काम की जानकारी जोड़कर बड़ा ("बढ़ाया") किया जा सकता है. इससे, "होमर और मार्ज के बच्चों के नाम बार्ट, लिसा, और मैगी हैं" प्रॉम्प्ट बन जाएगा. लिसा और बार्ट के बीच क्या संबंध है?"

इस तरीके के कई फ़ायदे हैं:

  • यह ज़्यादा किफ़ायती हो सकता है, क्योंकि आपको मॉडल को फिर से ट्रेन नहीं करना पड़ता.
  • अपने डेटा सोर्स को लगातार अपडेट किया जा सकता है. साथ ही, एलएलएम, अपडेट की गई जानकारी का तुरंत इस्तेमाल कर सकता है.
  • अब आपके पास एलएलएम के जवाबों में रेफ़रंस देने का विकल्प है.

दूसरी ओर, आरएजी का इस्तेमाल करने का मतलब है कि आपको लंबे प्रॉम्प्ट मिलेंगे. साथ ही, कुछ एलएलएम एपीआई सेवाएं, आपके भेजे गए हर इनपुट टोकन के लिए शुल्क लेती हैं. आखिर में, आपको अपने ऐप्लिकेशन के लिए, लागत के बदले मिलने वाले फ़ायदों का आकलन करना होगा.

आरएजी एक बहुत बड़ा क्षेत्र है और सबसे अच्छी क्वालिटी का आरएजी पाने के लिए, कई अलग-अलग तकनीकों का इस्तेमाल किया जाता है. RAG करने में आपकी मदद करने के लिए, Genkit का मुख्य फ़्रेमवर्क दो मुख्य एब्स्ट्रैक्शन उपलब्ध कराता है:

  • इंडेक्स करने वाले: "इंडेक्स" में दस्तावेज़ जोड़ें.
  • एम्बेड करने वाले: दस्तावेज़ों को वेक्टर में बदलता है
  • दस्तावेज़ पाने वाले: किसी क्वेरी के हिसाब से, "इंडेक्स" से दस्तावेज़ पाएं.

ये परिभाषाएं जान-बूझकर व्यापक हैं, क्योंकि Genkit को इस बात से कोई फ़र्क़ नहीं पड़ता कि "इंडेक्स" क्या है या उससे दस्तावेज़ों को कैसे वापस पाया जाता है. Genkit सिर्फ़ Document फ़ॉर्मैट उपलब्ध कराता है. बाकी सभी चीज़ें, डेटा वापस लाने वाले टूल या इंडेक्सर लागू करने वाली कंपनी तय करती है.

इंडेक्स करने वाले टूल

इंडेक्स, आपके दस्तावेज़ों को इस तरह से ट्रैक करता है कि किसी खास क्वेरी के हिसाब से, काम के दस्तावेज़ तुरंत खोजे जा सकें. आम तौर पर, ऐसा करने के लिए वेक्टर डेटाबेस का इस्तेमाल किया जाता है. यह डेटाबेस, एम्बेडिंग नाम के कई डाइमेंशन वाले वेक्टर का इस्तेमाल करके आपके दस्तावेज़ों को इंडेक्स करता है. टेक्स्ट एम्बेडिंग, टेक्स्ट के किसी पैसेज में मौजूद कॉन्सेप्ट को (अस्पष्ट रूप से) दिखाता है. इन्हें खास मकसद के लिए बनाए गए एमएल मॉडल का इस्तेमाल करके जनरेट किया जाता है. एम्बेडिंग का इस्तेमाल करके टेक्स्ट को इंडेक्स करके, वेक्टर डेटाबेस, कॉन्सेप्ट के हिसाब से मिलते-जुलते टेक्स्ट को क्लस्टर कर सकता है. साथ ही, टेक्स्ट की नई स्ट्रिंग (क्वेरी) से जुड़े दस्तावेज़ों को भी वापस ला सकता है.

दस्तावेज़ जनरेट करने के लिए, उन्हें अपने दस्तावेज़ इंडेक्स में डालना होगा. डेटा डालने का सामान्य फ़्लो ये काम करता है:

  1. बड़े दस्तावेज़ों को छोटे दस्तावेज़ों में बांटें, ताकि आपके प्रॉम्प्ट को बेहतर बनाने के लिए सिर्फ़ काम के हिस्सों का इस्तेमाल किया जा सके – "चंकिंग". ऐसा करना ज़रूरी है, क्योंकि कई एलएलएम में कॉन्टेक्स्ट विंडो सीमित होती है. इसलिए, प्रॉम्प्ट में पूरे दस्तावेज़ शामिल करना मुमकिन नहीं होता.

    Genkit में, चंक करने की सुविधा वाली लाइब्रेरी पहले से मौजूद नहीं होतीं. हालांकि, Genkit के साथ काम करने वाली ओपन सोर्स लाइब्रेरी उपलब्ध हैं.

  2. हर चंक के लिए एम्बेड जनरेट करें. इस्तेमाल किए जा रहे डेटाबेस के आधार पर, एम्बेड करने के लिए, एम्बेड जनरेशन मॉडल का इस्तेमाल किया जा सकता है. इसके अलावा, डेटाबेस से मिले एम्बेड जनरेटर का इस्तेमाल भी किया जा सकता है.

  3. डेटाबेस में टेक्स्ट का हिस्सा और उसका इंडेक्स जोड़ें.

अगर डेटा के किसी स्थिर सोर्स का इस्तेमाल किया जा रहा है, तो डेटा डालने का फ़्लो कम बार या सिर्फ़ एक बार चलाया जा सकता है. दूसरी ओर, अगर आपको ऐसे डेटा के साथ काम करना है जो अक्सर बदलता रहता है, तो डेटा डालने का फ़्लो लगातार चलाया जा सकता है. उदाहरण के लिए, जब भी कोई दस्तावेज़ अपडेट किया जाता है, तो Cloud Firestore ट्रिगर में.

एम्बेड करने वाले

एम्बेडर एक ऐसा फ़ंक्शन है जो कॉन्टेंट (टेक्स्ट, इमेज, ऑडियो वगैरह) लेता है और एक न्यूमेरिक वेक्टर बनाता है. यह वेक्टर, ओरिजनल कॉन्टेंट के सिमेंटिक मतलब को कोड में बदलता है. जैसा कि ऊपर बताया गया है, इंडेक्स करने की प्रोसेस के हिस्से के तौर पर, एम्बेड करने वालों का इस्तेमाल किया जाता है. हालांकि, इंडेक्स के बिना एम्बेड बनाने के लिए, इनका इस्तेमाल अलग से भी किया जा सकता है.

रिट्रीवर

रीट्रिवर एक ऐसा कॉन्सेप्ट है जिसमें किसी भी तरह के दस्तावेज़ को वापस पाने से जुड़ा लॉजिक शामिल होता है. आम तौर पर, डेटा वापस पाने के सबसे लोकप्रिय मामलों में, वैक्टर स्टोर से डेटा वापस पाना शामिल है. हालांकि, Genkit में रीट्रिवर कोई भी ऐसा फ़ंक्शन हो सकता है जो डेटा दिखाता हो.

डेटा वापस लाने वाला टूल बनाने के लिए, दिए गए टूल में से किसी एक का इस्तेमाल किया जा सकता है या खुद का टूल बनाया जा सकता है.

इंडेक्स करने, डेटा वापस लाने, और एम्बेड करने वाले टूल

Genkit, अपने प्लग इन सिस्टम के ज़रिए इंडेक्सर और रीट्रिवर की सुविधा देता है. यहां दिए गए प्लग-इन, आधिकारिक तौर पर काम करते हैं:

  • Pinecone क्लाउड वेक्टर डेटाबेस

इसके अलावा, Genkit पहले से तय किए गए कोड टेंप्लेट की मदद से, इन वेक्टर स्टोर के साथ काम करता है. इन टेंप्लेट को अपने डेटाबेस कॉन्फ़िगरेशन और स्कीमा के हिसाब से बनाया जा सकता है:

मॉडल को एम्बेड करने की सुविधा, इन प्लग इन की मदद से मिलती है:

प्लग इन मॉडल
Google का जनरेटिव एआई टेक्स्ट एम्बेड करना

आरएजी फ़्लो तय करना

यहां दिए गए उदाहरणों में बताया गया है कि रेस्टोरेंट के मेन्यू के PDF दस्तावेज़ों के कलेक्शन को वेक्टर डेटाबेस में कैसे डाला जा सकता है. साथ ही, उन्हें उस फ़्लो में इस्तेमाल करने के लिए कैसे वापस लाया जा सकता है जिससे यह तय होता है कि कौनसे खाने के आइटम उपलब्ध हैं.

डिपेंडेंसी इंस्टॉल करना

इस उदाहरण में, हम langchaingo की textsplitter लाइब्रेरी और ledongthuc/pdf PDF पार्सिंग लाइब्रेरी का इस्तेमाल करेंगे:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

इंडेक्सर तय करना

यहां दिए गए उदाहरण में, PDF दस्तावेज़ों के कलेक्शन को डालने और उन्हें स्थानीय वेक्टर डेटाबेस में सेव करने के लिए, इंडेक्सर बनाने का तरीका बताया गया है.

यह स्थानीय फ़ाइल पर आधारित वेक्टर मिलते-जुलते आइटम ढूंढने वाले टूल का इस्तेमाल करता है. Genkit, आसान जांच और प्रोटोटाइप बनाने के लिए, इस टूल को पहले से उपलब्ध कराता है. इसका इस्तेमाल प्रोडक्शन में न करें.

इंडेक्सर बनाना

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(g, "menuQA",
      localvec.Config{Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004")})
if err != nil {
    log.Fatal(err)
}

चंक करने का कॉन्फ़िगरेशन बनाना

इस उदाहरण में textsplitter लाइब्रेरी का इस्तेमाल किया गया है. यह दस्तावेज़ों को ऐसे सेगमेंट में बांटने के लिए, टेक्स्ट को आसानी से अलग करने की सुविधा देती है जिन्हें वेक्टर में बदला जा सकता है.

नीचे दी गई परिभाषा, चंक करने वाले फ़ंक्शन को कॉन्फ़िगर करती है, ताकि दस्तावेज़ के 200 वर्णों के सेगमेंट दिखाए जा सकें. साथ ही, 20 वर्णों के चंक के बीच ओवरलैप भी हो.

splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithChunkSize(200),
    textsplitter.WithChunkOverlap(20),
)

इस लाइब्रेरी के लिए, डेटा को अलग-अलग हिस्सों में बांटने के ज़्यादा विकल्प, langchaingo दस्तावेज़ में देखे जा सकते हैं.

इंडेक्सर फ़्लो तय करना

genkit.DefineFlow(
    g, "indexMenu",
    func(ctx context.Context, path string) (any, error) {
        // Extract plain text from the PDF. Wrap the logic in Run so it
        // appears as a step in your traces.
        pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
            return readPDF(path)
        })
        if err != nil {
            return nil, err
        }

        // Split the text into chunks. Wrap the logic in Run so it appears as a
        // step in your traces.
        docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
            chunks, err := splitter.SplitText(pdfText)
            if err != nil {
                return nil, err
            }

            var docs []*ai.Document
            for _, chunk := range chunks {
                docs = append(docs, ai.DocumentFromText(chunk, nil))
            }
            return docs, nil
        })
        if err != nil {
            return nil, err
        }

        // Add chunks to the index.
        err = ai.Index(ctx, menuPDFIndexer, ai.WithDocs(docs...))
        return nil, err
    },
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
    f, r, err := pdf.Open(path)
    if f != nil {
        defer f.Close()
    }
    if err != nil {
        return "", err
    }

    reader, err := r.GetPlainText()
    if err != nil {
        return "", err
    }

    bytes, err := io.ReadAll(reader)
    if err != nil {
        return "", err
    }

    return string(bytes), nil
}

इंडेक्सर फ़्लो चलाना

genkit flow:run indexMenu "'menu.pdf'"

indexMenu फ़्लो चलाने के बाद, वेक्टर डेटाबेस में दस्तावेज़ों को डाला जाएगा. साथ ही, इसे Genkit फ़्लो में, दस्तावेज़ों को वापस पाने के चरणों के साथ इस्तेमाल किया जा सकेगा.

डेटा वापस पाने के लिए फ़्लो तय करना

यहां दिए गए उदाहरण में, RAG फ़्लो में रीट्रिवर का इस्तेमाल करने का तरीका बताया गया है. इंडेक्सर के उदाहरण की तरह, इस उदाहरण में भी Genkit के फ़ाइल-आधारित वेक्टर रीट्रिवर का इस्तेमाल किया गया है. इसका इस्तेमाल प्रोडक्शन में नहीं किया जाना चाहिए.

ctx := context.Background()

g, err := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
if err != nil {
    log.Fatal(err)
}

if err = localvec.Init(); err != nil {
    log.Fatal(err)
}

model := googlegenai.VertexAIModel(g, "gemini-1.5-flash")

_, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
    g, "menuQA", localvec.Config{Embedder: googlegenai.VertexAIEmbedder(g, "text-embedding-004")},
)
if err != nil {
    log.Fatal(err)
}

genkit.DefineFlow(
  g, "menuQA",
  func(ctx context.Context, question string) (string, error) {
    // Retrieve text relevant to the user's question.
    resp, err := ai.Retrieve(ctx, menuPdfRetriever, ai.WithTextDocs(question))


    if err != nil {
        return "", err
    }

    // Call Generate, including the menu information in your prompt.
    return genkit.GenerateText(ctx, g,
        ai.WithModelName("googleai/gemini-2.0-flash"),
        ai.WithDocs(resp.Documents),
        ai.WithSystem(`
You are acting as a helpful AI assistant that can answer questions about the
food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question. If you don't know, do not
make up an answer. Do not add or change items on the menu.`)
        ai.WithPrompt(question),
  })

इंडेक्स करने और डेटा वापस लाने वाले अपने प्रोग्राम लिखना

अपना रिट्रिवर भी बनाया जा सकता है. यह तब काम आता है, जब आपके दस्तावेज़ किसी ऐसे दस्तावेज़ स्टोर में मैनेज किए जाते हैं जो Genkit के साथ काम नहीं करता. जैसे, MySQL, Google Drive वगैरह. Genkit SDK टूल, दस्तावेज़ों को फ़ेच करने के लिए, कई तरह के तरीके उपलब्ध कराता है. इनकी मदद से, अपनी ज़रूरत के हिसाब से कोड दिया जा सकता है.

Genkit में मौजूदा retriever के आधार पर, कस्टम retriever भी बनाए जा सकते हैं. साथ ही, उन पर RAG की बेहतर तकनीकें (जैसे, फिर से रैंकिंग या प्रॉम्प्ट एक्सटेंशन) भी लागू की जा सकती हैं.

उदाहरण के लिए, मान लें कि आपके पास फिर से रैंकिंग करने का कोई कस्टम फ़ंक्शन है और आपको उसका इस्तेमाल करना है. यहां दिए गए उदाहरण में, एक कस्टम रीट्रिवर के बारे में बताया गया है. यह रीट्रिवर, पहले से तय किए गए मेन्यू रीट्रिवर पर आपका फ़ंक्शन लागू करता है:

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}

advancedMenuRetriever := genkit.DefineRetriever(
    g, "custom", "advancedMenuRetriever",
    func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
        // Handle options passed using our custom type.
        opts, _ := req.Options.(CustomMenuRetrieverOptions)
        // Set fields to default values when either the field was undefined
        // or when req.Options is not a CustomMenuRetrieverOptions.
        if opts.K == 0 {
            opts.K = 3
        }
        if opts.PreRerankK == 0 {
            opts.PreRerankK = 10
        }

        // Call the retriever as in the simple case.
        resp, err := ai.Retrieve(ctx, menuPDFRetriever,
            ai.WithDocs(req.Query),
            ai.WithConfig(ocalvec.RetrieverOptions{K: opts.PreRerankK}),
        )
        if err != nil {
            return nil, err
        }

        // Re-rank the returned documents using your custom function.
        rerankedDocs := rerank(response.Documents)
        response.Documents = rerankedDocs[:opts.K]

        return response, nil
    },
)