मुख्य सामग्री पर जाएँ

मल्टीमोडल RAG: दस्तावेज़ों से सीखने के लिए हैंड्स-ऑन गाइड

OpenAI API और ChromaDB वेक्टर डेटाबेस का उपयोग करके IKEA असेंबली निर्देशों पर एक विजुअल Q&A सिस्टम के लिए मल्टीमोडल RAG पाइपलाइन बनाना सीखें।
· 11 मि॰ पढ़ें

AI के साथ खोजें

कल्पना कीजिए आप IKEA का फर्नीचर असेंबल कर रहे हैं। आप निर्देशों के स्टेप 14 को घूर रहे हैं, लेकिन समझ नहीं आ रहा कि कौन-सा स्क्रू कहाँ लगेगा। डायग्राम कुछ दिखा रहा है, पर बात समझ नहीं बन रही। 

अगर आप पूछें: "स्टेप 14 में छोटा धातु का पार्ट किस काम आता है?", तो एक स्टैंडर्ड टेक्स्ट-आधारित रिट्रीवल-ऑग्मेंटेड जेनरेशन (RAG) सिस्टम यहाँ काम नहीं आएगा। जवाब खुद डायग्राम में है, पेज के किसी टेक्स्ट में नहीं। IKEA के निर्देश मशहूर हैं कि वे बिना शब्दों के होते हैं।

यहीं पर मल्टीमोडल RAG काम आता है।

इस ट्यूटोरियल में, मैं आपको ऐसा मल्टीमोडल RAG सिस्टम बनाना सिखाऊंगा जो ऐसी डॉक्युमेंट्स पर सवालों के जवाब दे सके जिनमें इमेज, डायग्राम और मिक्स्ड कंटेंट हो। हम IKEA असेंबली निर्देशों को टेस्ट केस के रूप में इस्तेमाल करेंगे क्योंकि वे एक अच्छा स्ट्रेस टेस्ट हैं: ज़्यादातर विज़ुअल, बहुत कम टेक्स्ट, और वही चीज़ जिसमें लोगों को सचमुच मदद चाहिए होती है।

हम पूरे सेटअप में OpenAI के मॉडल्स का उपयोग करेंगे: इमेज समझने और जवाब बनाने के लिए GPT-4o, और सर्चेबल वेक्टर्स बनाने के लिए text-embedding-3-small। किसी अलग फ्रेमवर्क के साथ RAG पाइपलाइन बनाना सीखने के लिए, मैं हमारा RAG with LangChain कोर्स सुझाता हूँ।

आखिर में, आपके पास एक वर्किंग सिस्टम होगा जिसे आप लोकली चला सकते हैं और अपनी खुद की डॉक्युमेंट्स के लिए अनुकूलित कर सकते हैं।

मल्टीमोडल RAG क्या है?

मल्टीमोडल RAG मानक RAG पैटर्न को केवल टेक्स्ट से आगे बढ़ाकर अन्य माध्यमों तक ले जाता है। मूल विचार वही रहता है, लेकिन अब "कॉन्टेक्स्ट" में इमेज, टेबल, डायग्राम और अन्य गैर-टेक्स्ट कंटेंट भी शामिल हैं।

यह क्यों मायने रखता है? क्योंकि असली डॉक्युमेंट्स सिर्फ टेक्स्ट नहीं होते।

सोचिए आप वास्तव में किन चीज़ों पर काम करते हैं: 

  • वायरिंग डायग्राम वाले टेक्निकल मैनुअल 
  • एक्सपेरिमेंटल प्लॉट्स वाले रिसर्च पेपर्स 
  • रेवेन्यू चार्ट्स वाली फाइनेंशियल रिपोर्ट्स
  • कंपोनेन्ट इमेज वाली प्रोडक्ट स्पेक्स 

सिर्फ टेक्स्ट प्रोसेस करने वाला सिस्टम अधूरी जानकारी पर काम करता है। यह वैसा ही है जैसे किसी रेसिपी को समझने के लिए केवल सामग्री सूची पढ़ना और उन फ़ोटो को नज़रअंदाज़ कर देना जो दिखाती हैं कि "धीरे-धीरे फोल्ड करें" वास्तव में कैसा दिखता है।

पारंपरिक RAG किसी PDF को कैरेक्टर्स की स्ट्रीम की तरह लेता है। यह टेक्स्ट निकालता है, उसे चंक्स में बाँटता है, एम्बेड करता है, और काम पूरा मान लेता है। कोई भी इमेज पूरी तरह स्किप हो जाती हैं, या अधिकतम [IMAGE] जैसे प्लेसहोल्डर टोकन में बदल दी जाती हैं। टेबल्स बेसिर-पैर के टेक्स्ट की पंक्तियों में बदल जाती हैं और उनकी संरचना खो जाती है।

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

पहलू

सिर्फ-टेक्स्ट RAG

मल्टीमोडल RAG

इनपुट प्रकार

सादा टेक्स्ट, संभवतः मार्कडाउन

टेक्स्ट, इमेज, टेबल, डायग्राम, मिक्स्ड डॉक्युमेंट्स

क्या एम्बेड होता है

टेक्स्ट चंक्स

टेक्स्ट चंक्स के साथ इमेज विवरण या इमेज एम्बेडिंग्स

जेनरेशन मॉडल

कोई भी LLM

विज़न-लैंग्वेज मॉडल (GPT-4o, Claude, Gemini)

सबसे उपयुक्त

टेक्स्ट-हेवी डॉक्युमेंट्स

मैनुअल, रिपोर्ट्स, स्लाइड्स, कोई भी कंटेंट जिसमें विज़ुअल्स हों

यदि "मल्टीमोडल" अभी भी एक धुंधला-सा छतरी शब्द लगता है, तो इस ब्लॉग Multimodal AI में ठोस उदाहरणों के साथ संक्षिप्त व्याख्या है।

मल्टीमोडल RAG कैसे काम करता है?

टेक्स्ट-ओनली RAG की तुलना में पाइपलाइन में ज़्यादा हिस्से होते हैं, पर संरचना मिलती-जुलती है। डेटा प्रोसेस होकर एम्बेड होता है, वेक्टर स्टोर में जाता है, और जब कोई सवाल पूछता है तो रिट्रीव होता है।

Multimodal RAG Pipeline

फ्लो इस प्रकार है:

डॉक्युमेंट इन्गेशन और पार्सिंग

कच्चे डॉक्युमेंट्स (PDFs, इमेज, स्लाइड्स) को उनके घटक हिस्सों में तोड़ा जाता है। आप टेक्स्ट निकालते हैं, इमेज पहचानते हैं, टेबल्स पार्स करते हैं, और यह समझते हैं कि सब कुछ स्पेशियली कैसे जुड़ा है। पेज 5 पर कोई डायग्राम हो सकता है जिसके नीचे कैप्शन है और पेज 4 के टेक्स्ट में उसका संदर्भ। आपको उन रिश्तों को कैप्चर करना होगा।

कॉन्टेंट प्रीप्रोसेसिंग

अलग-अलग कंटेंट टाइप्स को अलग ट्रीटमेंट चाहिए। इमेज के लिए दो मुख्य विकल्प हैं: विज़न मॉडल (जैसे GPT-4o) से टेक्स्ट विवरण जनरेट करना, या मल्टीमोडल एनकोडर (जैसे CLIP) से सीधे इमेज एम्बेड करना। विवरण वाला तरीका सरल है और स्टैंडर्ड टेक्स्ट एम्बेडिंग्स के साथ काम करता है। टेबल्स को स्ट्रक्चर्ड फ़ॉर्मैट में बदला जाता है या नैचुरल लैंग्वेज में समरी किया जाता है।

इसके बाद, सब कुछ वेक्टर्स में बदल दिया जाता है। अगर आप विवरण वाला तरीका अपनाते हैं, तो आप अपने सारे टेक्स्ट (मूल टेक्स्ट + जनरेटेड इमेज विवरण) को स्टैंडर्ड टेक्स्ट एम्बेडिंग मॉडल से एम्बेड कर देते हैं। कुंजी यह है कि अब विज़ुअल कंटेंट पर आने वाली क्वेरीज़ उन विवरणों से मैच हो सकेंगी।

इंडेक्सिंग और स्टोरेज 

वेक्टर्स को एक डेटाबेस में मेटाडेटा के साथ स्टोर किया जाता है, जिसमें सोर्स डॉक्युमेंट, पेज नंबर, कंटेंट टाइप, और इमेज के लिए ओरिजिनल फ़ाइल का पाथ शामिल होता है। जेनरेशन के समय मॉडल को इमेज दिखाने के लिए आपको बाद में वही पाथ चाहिए होगा।

रिट्रीवल

जब कोई क्वेरी मिलती है, तो उसी मॉडल से उसे एम्बेड करें, निकटतम पड़ोसियों को खोजें, और टॉप मैचेज़ लौटाएँ। रिट्रीव किए गए चंक्स टेक्स्ट, इमेज, या दोनों को रेफर कर सकते हैं।

जेनरेशन

क्वेरी को, रिट्रीव्ड कॉन्टेक्स्ट के साथ, किसी विज़न-लैंग्वेज मॉडल को पास करें। अगर रिट्रीव्ड चंक्स इमेज का संदर्भ देते हैं, तो वे इमेज लोड करें और प्रॉम्प्ट में शामिल करें। मॉडल टेक्स्ट कॉन्टेक्स्ट और असल इमेज दोनों देखता है, फिर जवाब जनरेट करता है।

यही स्टेप है जहाँ GPT-4o चमकता है। यह IKEA के स्क्रू प्लेसमेंट दिखाने वाले डायग्राम को देख सकता है और सीधे-सादे शब्दों में समझा सकता है कि क्या हो रहा है। यह चार्ट पढ़कर ट्रेंड समरी कर सकता है। यह टेक्निकल ड्रॉइंग्स में ऐसे डीटेल्स पकड़ सकता है जो टेक्स्ट विवरण से छूट सकते हैं।

मल्टीमोडल RAG को इम्प्लीमेंट करना

आइए इसे बनाते हैं। हम ऐसा सिस्टम तैयार करेंगे जो IKEA असेंबली निर्देशों पर सवालों के जवाब दे सके—सेटअप से लेकर एक काम करने वाले क्वेरी इंटरफ़ेस तक, स्टेप-बाय-स्टेप।

स्टेप 0: फ्रेमवर्क और टूल्स चुनना

इस ट्यूटोरियल के लिए, हम इसे सरल रख रहे हैं:

घटक

चॉइस

क्यों

एम्बेडिंग्स

text-embedding-3-small

सस्ता, अच्छी गुणवत्ता, वही API

वेक्टर स्टोर

ChromaDB

लोकली चलता है, कोई सेटअप नहीं, पर्सिस्टेंट

विज़न मॉडल

GPT-4o

इमेज समझने में बेस्ट-इन-क्लास

जेनरेशन

GPT-4o

वही मॉडल, दोनों काम संभालता है

आप विकल्प बदल सकते हैं। स्केल की ज़रूरत हो तो वेक्टर स्टोर के लिए Pinecone या Weaviate। जेनरेशन के लिए Claude या Gemini अच्छे अल्टरनेटिव हैं। उन मामलों में भी जनरल आर्किटेक्चर वही रहता है।

एम्बेडिंग्स के लिए CLIP या ColPali क्यों न लें? ले सकते हैं। वे आपको बिना टेक्स्ट विवरण जनरेट किए सीधे इमेज एम्बेड करने देते हैं। लेकिन वे जटिलता बढ़ाते हैं और अक्सर GPU की ज़रूरत होती है। विवरण वाला तरीका ऐसे डॉक्युमेंट-स्टाइल कंटेंट के लिए अच्छा काम करता है जहाँ आप केवल विज़ुअल समानता नहीं, बल्कि सेमांटिक डीटेल्स कैप्चर करना चाहते हैं।

स्टेप 1: एन्वायरनमेंट सेट करना

आइए डिपेंडेंसीज़ और एन्वायरनमेंट वेरिएबल्स से प्रोजेक्ट सेटअप शुरू करें।

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

सबसे पहले, PDF प्रोसेसिंग संभालने के लिए आपको poppler इंस्टॉल करना होगा।

  • Ubuntu/Debian: sudo apt-get install poppler-utils

  • macOS ( Homebrew के ज़रिए): brew install poppler

  • Windows: रिलीज़ पेज से डाउनलोड करें और PATH में जोड़ें

अब Python पैकेज इंस्टॉल करें। ChromaDB के अलावा, हमें PDFs को इमेज में बदलने के लिए pdf2image, इमेज प्रोसेसिंग के लिए pillow, और PDF फाइलें डाउनलोड करने के लिए requests चाहिए।

pip install openai chromadb pdf2image pillow requests

API कुंजी और फ़ाइलें सेट करें

मॉडल्स का उपयोग करने के लिए, अपनी OpenAI API कुंजी को एक एन्वायरनमेंट वेरिएबल के रूप में सेट करें।

Unix/Linux/macOS:

export OPENAI_API_KEY="your-api-key-here"

Windows: 

setx OPENAI_API_KEY="your-api-key-here"

अब, प्रोजेक्ट संरचना सेट करें और सैंपल डेटा खींचें। स्क्रिप्ट में पहले से डाउनलोड URLs शामिल हैं, लेकिन अगर आप पहले PDFs पर नज़र डालना चाहें, तो यहाँ सीधे लिंक हैं— MALM डेस्क और MALM बेड फ्रेम। 

step1_setup.py नाम की फ़ाइल बनाइए:

"""
Step 1: Setup and Download Sample Data
"""
import os
import requests

# Create directories
for d in ["data", "data/images", "data/cache"]:
    os.makedirs(d, exist_ok=True)
print("✓ Created directories")

# Download IKEA assembly instructions
SAMPLE_PDFS = {
    "malm_desk.pdf": "https://www.ikea.com/us/en/assembly_instructions/malm-desk-white__AA-516949-7-2.pdf",
    "malm_bed.pdf": "https://www.ikea.com/us/en/assembly_instructions/malm-bed-frame-low__AA-75286-15_pub.pdf",
}

for filename, url in SAMPLE_PDFS.items():
    filepath = f"data/{filename}"
    if not os.path.exists(filepath):
        print(f"Downloading {filename}...")
        response = requests.get(url, timeout=30)
        with open(filepath, "wb") as f:
            f.write(response.content)
        print(f"  ✓ Saved ({len(response.content) / 1024:.1f} KB)")
    else:
        print(f"✓ {filename} already exists")

प्रारंभिक सेटअप पूरा करने के लिए, स्क्रिप्ट चलाएँ:

python step1_setup.py

आपको IKEA की PDFs अपने data/ फ़ोल्डर में डाउनलोड होती दिखनी चाहिए।

स्टेप 2: डॉक्युमेंट्स का प्रीप्रोसेसिंग

IKEA निर्देश मुश्किल होते हैं। वे अधिकतर डायग्राम होते हैं, टेक्स्ट बहुत कम। हमें PDF पेजों को इमेज में बदलना होगा, फिर हर पेज पर क्या दिख रहा है, यह बताने के लिए GPT-4o का इस्तेमाल करना होगा।

step2_preprocess.py बनाइए:

"""
Step 2: Preprocess Documents
Convert PDFs to images and generate descriptions with GPT-4o
"""
import os
import json
import base64
from pdf2image import convert_from_path
from openai import OpenAI

client = OpenAI()

DATA_DIR = "data"
IMAGES_DIR = "data/images"
CACHE_FILE = "data/cache/descriptions.json"


def convert_pdf_to_images(pdf_path):
    """Convert a PDF to page images."""
    pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]
    images = convert_from_path(pdf_path, dpi=150)
    
    image_paths = []
    for i, image in enumerate(images):
        image_path = f"{IMAGES_DIR}/{pdf_name}_page_{i+1:03d}.png"
        image.save(image_path, "PNG")
        image_paths.append(image_path)
    
    return image_paths


def describe_page(image_path):
    """Use GPT-4o to describe an instruction page."""
    with open(image_path, "rb") as f:
        base64_image = base64.b64encode(f.read()).decode("utf-8")
    
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """Describe this IKEA assembly instruction page in detail.
Include: step numbers, parts shown, tools needed, actions demonstrated, 
quantities (like "2x"), warnings, and part numbers if visible.
This description will help people find this page when they have questions."""
            },
            {
                "role": "user",
                "content": [
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}},
                    {"type": "text", "text": "Describe this instruction page."}
                ]
            }
        ],
        max_tokens=500
    )
    return response.choices[0].message.content


def main():
    # Load cache if exists
    cache = {}
    if os.path.exists(CACHE_FILE):
        with open(CACHE_FILE, "r") as f:
            cache = json.load(f)
    
    all_pages = []
    
    # Process each PDF
    for pdf_file in os.listdir(DATA_DIR):
        if not pdf_file.endswith(".pdf"):
            continue
        
        print(f"\nProcessing {pdf_file}...")
        pdf_path = f"{DATA_DIR}/{pdf_file}"
        
        # Convert to images
        print("  Converting to images...")
        image_paths = convert_pdf_to_images(pdf_path)
        print(f"  ✓ {len(image_paths)} pages")
        
        # Generate descriptions
        print("  Generating descriptions with GPT-4o...")
        for i, image_path in enumerate(image_paths):
            if image_path in cache:
                description = cache[image_path]
                status = "cached"
            else:
                description = describe_page(image_path)
                cache[image_path] = description
                # Save cache after each page
                with open(CACHE_FILE, "w") as f:
                    json.dump(cache, f, indent=2)
                status = "generated"
            
            all_pages.append({
                "source_pdf": pdf_file,
                "page_number": i + 1,
                "image_path": image_path,
                "description": description
            })
            print(f"    Page {i+1}/{len(image_paths)} [{status}]")
    
    # Save all page data
    with open("data/cache/all_pages.json", "w") as f:
        json.dump(all_pages, f, indent=2)
    
    print(f"\n✓ Processed {len(all_pages)} pages total")


if __name__ == "__main__":
    main()

ये फ़ंक्शंस क्या कर रहे हैं:

  • convert_pdf_to_images(pdf_path): pdf2image का उपयोग करके हर PDF पेज को 150 DPI पर PNG इमेज में रेंडर करता है। यह उन डायग्राम्स को एक्सट्रैक्ट करता है जिनमें सेलेकेबल टेक्स्ट नहीं होता, और उन्हें malm_desk_page_001.png जैसे सुसंगत नामों से सेव करता है।

  • describe_page(image_path): इमेज को base64 में एन्कोड करता है और एक टेलर्ड सिस्टम प्रॉम्प्ट के साथ GPT-4o को भेजता है। मॉडल स्टेप्स, पार्ट्स और क्वांटिटीज़ कैप्चर करते हुए एक डिटेल्ड टेक्स्ट विवरण लौटाता है ताकि सेमांटिक सर्च हो सके।

  • main(): पूरा ऑर्केस्ट्रेशन: यह रीप्रोसेसिंग से बचने के लिए कैश्ड विवरण लोड करता है, PDFs को इमेज में बदलता है, ज़रूरत पर नए विवरण जनरेट करता है, इन्क्रिमेंटल कैश सेव करता है, और इंडेक्सिंग के लिए मेटाडेटा सहित all_pages.json आउटपुट करता है।

स्क्रिप्ट ऐसे चलाएँ:

python step2_preprocess.py

यह स्टेप कुछ मिनट लेता है। GPT-4o हर पेज का विश्लेषण करता है और डिटेल्ड विवरण जनरेट करता है। विवरण कैश हो जाते हैं, इसलिए दोबारा चलाने पर पहले से प्रोसेस हुए पेज स्किप हो जाते हैं।

यहाँ क्या हो रहा है? हम विज़ुअल कंटेंट को ऐसे टेक्स्ट में बदल रहे हैं जिसे खोजा जा सके। जब कोई पूछे, "मैं पैर कैसे लगाऊँ?", तो रिट्रीवर इन विवरणों को खोजेगा और वे पेज ढूंढेगा जो पैर जोड़ने का ज़िक्र करते हैं, भले ही ओरिजिनल पेज पर बिलकुल टेक्स्ट न हो।

एक बार जब आप बेसिक पाइपलाइन बना लें, तो अगली जीतें रिट्रीवल ट्रिक्स से आती हैं, जैसे हाइब्रिड सर्च, रीरैंकिंग, या क्वेरी री-राइटिंग। उनसे परिचित होने के लिए यह ब्लॉग पढ़ें Advanced RAG Techniques।

स्टेप 3: मल्टीमोडल डेटा स्टोर करना और इंडेक्सिंग

अब हम विवरणों को एम्बेड करेंगे और उन्हें ChromaDB में स्टोर करेंगे।

step3_index.py बनाइए:

"""
Step 3: Create Embeddings and Index
"""
import os
import json
from openai import OpenAI
import chromadb

client = OpenAI()

PAGES_FILE = "data/cache/all_pages.json"
CHROMA_DIR = "data/chroma_db"
COLLECTION_NAME = "ikea_instructions"


def get_embedding(text):
    """Get embedding from OpenAI."""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding


def main():
    # Load page descriptions
    with open(PAGES_FILE, "r") as f:
        pages = json.load(f)
    print(f"Loaded {len(pages)} pages")
    
    # Initialize ChromaDB with persistent storage
    chroma_client = chromadb.PersistentClient(path=CHROMA_DIR)
    
    # Delete existing collection if present
    try:
        chroma_client.delete_collection(name=COLLECTION_NAME)
    except:
        pass
    
    collection = chroma_client.create_collection(
        name=COLLECTION_NAME,
        metadata={"hnsw:space": "cosine"}
    )
    print(f"Created collection: {COLLECTION_NAME}")
    
    # Index each page
    print("\nCreating embeddings...")
    for i, page in enumerate(pages):
        # Combine source info with description
        text_to_embed = f"Source: {page['source_pdf']}, Page {page['page_number']}\n\n{page['description']}"
        
        embedding = get_embedding(text_to_embed)
        
        collection.add(
            ids=[f"page_{i}"],
            embeddings=[embedding],
            documents=[page["description"]],
            metadatas=[{
                "source_pdf": page["source_pdf"],
                "page_number": page["page_number"],
                "image_path": page["image_path"]
            }]
        )
        
        if (i + 1) % 5 == 0 or i == len(pages) - 1:
            print(f"  Indexed {i + 1}/{len(pages)} pages")
    
    print(f"\n✓ Index created with {collection.count()} documents")
    
    # Quick retrieval test
    print("\nTesting retrieval...")
    test_query = "How do I attach the legs?"
    query_embedding = get_embedding(test_query)
    results = collection.query(query_embeddings=[query_embedding], n_results=2)
    
    print(f"Query: '{test_query}'")
    for meta in results["metadatas"][0]:
        print(f"  → {meta['source_pdf']}, Page {meta['page_number']}")


if __name__ == "__main__":
    main()

यहाँ get_embedding(text) फ़ंक्शन विवरण और सोर्स जानकारी से आए टेक्स्ट को OpenAI के text-embedding-3-small मॉडल से डेंस वेक्टर में बदलता है। 1536-डायमेंशनल होने के कारण, ये वेक्टर्स समानता खोज के लिए सेमांटिक अर्थ कैप्चर कर पाते हैं।

main() पिछले स्टेप के all_pages.json से प्रोसेस्ड पेज लोड करता है, पर्सिस्टेंट स्टोरेज के साथ ChromaDB इनिशियलाइज़ करता है, कलेक्शन को डिलीट/रीक्रिएट करता है, हर पेज के लिए एम्बेडिंग्स जनरेट करता है, उन्हें समृद्ध मेटाडेटा (सोर्स, पेज नंबर, इमेज पाथ) के साथ स्टोर करता है, और इंडेक्सिंग वेरीफाई करने के लिए एक त्वरित रिट्रीवल टेस्ट चलाता है।

इसे चलाएँ:

python step3_index.py

यहाँ मेटाडेटा अहम है। हम image_path स्टोर करते हैं ताकि बाद में असल इमेज रिट्रीव करके जेनरेशन के दौरान GPT-4o को दिखा सकें। सर्च विवरण पर चलता है, पर दिखाया इमेज जाता है।

स्टेप 4: RAG पाइपलाइन बनाना

यह है पूरी मल्टीमोडल RAG पाइपलाइन। एक क्वेरी एम्बेड होती है, प्रासंगिक पेज रिट्रीव होते हैं, इमेज लोड होती हैं, और GPT-4o टेक्स्ट विवरणों और विज़ुअल्स के आधार पर जवाब जनरेट करता है।

step4_query.py बनाइए:

"""
Step 4: Query the Multimodal RAG System
"""
import os
import sys
import base64
from openai import OpenAI
import chromadb

client = OpenAI()

CHROMA_DIR = "data/chroma_db"
COLLECTION_NAME = "ikea_instructions"
TOP_K = 3

# Initialize ChromaDB
chroma_client = chromadb.PersistentClient(path=CHROMA_DIR)


def get_embedding(text):
    """Get embedding from OpenAI."""
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding


def retrieve(query, top_k=TOP_K):
    """Find relevant instruction pages."""
    collection = chroma_client.get_collection(name=COLLECTION_NAME)
    query_embedding = get_embedding(query)
    
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
        include=["documents", "metadatas"]
    )
    return results


def generate_answer(query, retrieved_results):
    """Generate answer using GPT-4o with retrieved images."""
    if not retrieved_results["ids"][0]:
        return "No relevant pages found.", []
    
    # Build image content for GPT-4o
    image_content = []
    sources = []
    
    for doc, metadata in zip(retrieved_results["documents"][0], retrieved_results["metadatas"][0]):
        image_path = metadata["image_path"]
        
        if not os.path.exists(image_path):
            continue
        
        with open(image_path, "rb") as f:
            base64_image = base64.b64encode(f.read()).decode("utf-8")
        
        image_content.append({
            "type": "image_url",
            "image_url": {"url": f"data:image/png;base64,{base64_image}"}
        })
        image_content.append({
            "type": "text",
            "text": f"[Page {metadata['page_number']} from {metadata['source_pdf']}]"
        })
        
        sources.append(f"{metadata['source_pdf']}, Page {metadata['page_number']}")
    
    # Generate with GPT-4o
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": """You answer questions about IKEA assembly based on instruction pages.
Be specific, reference page numbers when helpful, and say if something isn't clear from the images."""
            },
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": f"Question: {query}\n\nRelevant instruction pages:"},
                    *image_content,
                    {"type": "text", "text": "Answer based on these pages."}
                ]
            }
        ],
        max_tokens=600
    )
    
    return response.choices[0].message.content, sources


def answer_question(query):
    """Full RAG pipeline."""
    print(f"\nQuery: {query}")
    print("-" * 50)
    
    print("Retrieving relevant pages...")
    results = retrieve(query)
    print(f"  Found {len(results['ids'][0])} pages")
    
    print("Generating answer...")
    answer, sources = generate_answer(query, results)
    
    return answer, sources


def main():
    # Check index exists
    try:
        collection = chroma_client.get_collection(name=COLLECTION_NAME)
        print(f"Connected to index ({collection.count()} documents)")
    except:
        print("ERROR: Index not found. Run step2_index.py first.")
        return
    
    # Single query from command line
    if len(sys.argv) > 1:
        query = " ".join(sys.argv[1:])
        answer, sources = answer_question(query)
        print(f"\nAnswer:\n{answer}")
        print(f"\nSources: {', '.join(sources)}")
        return
    
    # Interactive mode
    print("\nAsk questions about IKEA assembly. Type 'quit' to exit.\n")
    
    while True:
        try:
            query = input("Question: ").strip()
        except KeyboardInterrupt:
            break
        
        if not query or query.lower() in ["quit", "exit"]:
            break
        
        answer, sources = answer_question(query)
        print(f"\nAnswer:\n{answer}")
        print(f"\nSources: {', '.join(sources)}\n")


if __name__ == "__main__":
    main()

मुख्य फ़ंक्शंस की व्याख्या:

  • get_embedding(text): text-embedding-3-small से क्वेरी का वेक्टर एम्बेडिंग जनरेट करता है, जिससे ChromaDB में सेमांटिक समानता खोज संभव होती है.​

  • retrieve(query, top_k=TOP_K): क्वेरी को एम्बेड करता है, कलेक्शन से टॉप-k निकटतम पड़ोसी मांगता है, और include=["documents", "metadatas"] के ज़रिए डॉक्युमेंट्स और मेटाडेटा (जैसे इमेज पाथ) लौटाता है.​

  • generate_answer(query, retrieved_results): टॉप इमेजेज़ को base64 में लोड करता है, पेज लेबल और इमेज को इंटरलीव करते हुए GPT-4o के लिए मल्टीमोडल प्रॉम्प्ट बनाता है, और IKEA-विशेष निर्देशों के साथ डिटेल्ड रिस्पॉन्स जनरेट करता है.​

  • answer_question(query): रिट्रीवल और जेनरेशन का ऑर्केस्ट्रेशन करता है, प्रोग्रेस लॉगिंग के साथ।

  • main(): जांचता है कि इंडेक्स मौजूद है, CLI सिंगल क्वेरीज़ या इंटरैक्टिव चैट मोड को सपोर्ट करता है ("quit", "exit" या Ctrl+C से रुकें)।

स्टेप 5: RAG पाइपलाइन चलाना

आप इसे चलाकर अपना सवाल टाइप कर सकते हैं, या इस तरह किसी खास सवाल के साथ सीधे चला सकते हैं:

python step4_query.py "How do I attach the legs to the desk?"
Retrieving relevant pages...
Found 3 pages
Generating answer...

Answer:
To attach the legs to the MALM desk, follow these steps shown on pages 5-6:

1. First, insert the wooden dowels into the pre-drilled holes on the leg 
   pieces (you'll see "4x" indicating four dowels are needed)

2. Apply the cam locks to the desk top using the Phillips screwdriver

3. Align each leg with the corresponding holes on the underside of the desk

4. Tighten the cam locks by rotating them clockwise until the legs are 
   firmly attached

The instructions show this should be done with the desk top face-down on 
a soft surface to avoid scratching.

Sources: malm_desk.pdf, Page 5, malm_desk.pdf, Page 6, malm_desk.pdf, Page 7

अच्छा लग रहा है! सिस्टम विवरणों के आधार पर पेज रिट्रीव करता है, असली इमेज लोड करता है, और GPT-4o डायग्राम्स को सीधे देखकर जवाब जनरेट करता है।

मल्टीमोडल RAG का मूल्यांकन

मल्टीमोडल RAG सिस्टम बनाना एक बात है। यह जानना कि वह कितना अच्छा काम करता है, दूसरी। और उसे प्रोडक्शन में भरोसेमंद ढंग से चलाना एक और परत जोड़ देता है। 

मानक RAG मैट्रिक्स अब भी लागू होते हैं, पर आपको उन्हें विज़ुअल कंटेंट के लिए बढ़ाना होगा।

रिट्रीवल क्वालिटी

Precision@k मापता है कि जो पेज आपने रिट्रीव किए, उनमें से कितने सच में प्रासंगिक थे। यह टॉप-k में अप्रासंगिक पेजों को दंडित करता है, लेकिन जेनरेशन के लिए, मुख्य इमेज मिलना परफेक्ट टॉप-k हासिल करने से अधिक मायने रखता है।

Recall@k मापता है कि सही पेज कितनी बार आपके टॉप k नतीजों में आते हैं। हमारे IKEA RAG उपयोग मामले में यह ज़्यादा प्रासंगिक है क्योंकि हम चाहते हैं कि सिस्टम टॉप-k (जैसे, k=3) में कम-से-कम एक प्रासंगिक पेज ढूंढ ले। इस तरह GPT-4o को सही जवाब देने के लिए विज़ुअल कॉन्टेक्स्ट मिल जाता है, भले ही बाकी नतीजे नॉइज़ हों। 

हमारे IKEA सिस्टम के लिए, आप लगभग 50 सवालों का एक टेस्ट सेट बनाएँगे और हर एक के लिए "सही" पेजों को लेबल करेंगे। फिर आप उन्हें इस फ़ंक्शन से इवैल्युएट कर सकते हैं:

def evaluate_retrieval(test_cases, top_k=3):
    """
    test_cases: [{"query": str, "relevant_pages": ["image_path1", ...]}]
    """
    hits = 0
    for test in test_cases:
        results = retrieve(test["query"], top_k=top_k)
        retrieved = [m["image_path"] for m in results["metadatas"][0]]
        if any(p in retrieved for p in test["relevant_pages"]):
            hits += 1
    return hits / len(test_cases)

जेनरेशन क्वालिटी

जिस चीज़ को ऑटोमैटिकली मापना कठिन है, वह है जेनरेशन क्वालिटी। 

आप लोग-आधारित रेटिंग ले सकते हैं—एक्यूरेसी और हेल्पफुलनेस के लिए—या किसी LLM को जज की तरह उपयोग कर सकते हैं कि सोर्स इमेज के आधार पर जवाब सही है या नहीं। 

नंबर वाले स्टेप्स वाले डायग्राम्स के लिए, वेरिफाई करें कि मॉडल सही स्टेप नंबर असाइन कर रहा है।

मल्टीमोडल-विशेष जाँच

आगे की जाँचें इन सवालों के जवाब दे सकती हैं:

  • जब जवाब विज़ुअल हो, क्या सिस्टम इमेज-आधारित जवाब ढूँढता है? 

  • जब मॉडल इमेज में किसी चीज़ का संदर्भ देता है, क्या वह संदर्भ सटीक है? 

इनके लिए कम-से-कम कुछ क्वेरीज़ पर मैन्युअल समीक्षा ज़रूरी है।

मल्टीमोडल RAG के लिए सर्वश्रेष्ठ अभ्यास

अगर आप प्रोडक्शन-रेडी मल्टीमोडल RAG पाइपलाइन बनाना चाहते हैं, तो कुछ बातों का ध्यान रखें:

  • प्रीप्रोसेसिंग लागतें बढ़ती हैं: इमेज विवरण के लिए विज़न कॉल्स महंगे हिस्से हैं। बड़े डॉक्युमेंट कॉर्पस के लिए, ऑफ-पीक घंटों में बैच प्रोसेस करें और विवरणों को आक्रामक रूप से कैश करें। अगर आपकी ज़रूरत के लिए गुणवत्ता ठीक है, तो विवरण जनरेट करने के लिए सस्ता मॉडल (हमारे केस में, जैसे GPT-4o-mini) विचार करें।

  • क्वेरी लेटेंसी मायने रखती है: इमेजेज़ के साथ GPT-4o को प्रति क्वेरी 5 से 10 सेकंड लगते हैं। स्ट्रीमिंग से यूज़र्स को आंशिक नतीजे जल्दी दिखते हैं, और कॉमन क्वेरीज़ कैश करने से दोहराए गए सवालों में मदद मिलती है। 5 की जगह 2 पेज रिट्रीव करना फर्क डाल सकता है, अगर उतना ही कॉन्टेक्स्ट काफी हो।

  • सही चीज़ें मॉनिटर करें: रिट्रीवल लेटेंसी (छोटी कलेक्शंस के लिए 100ms से कम), जेनरेशन लेटेंसी (इमेज के साथ 5–10 सेकंड अपेक्षित), प्रति क्वेरी API लागत, और अगर आप यूज़र फीडबैक इकट्ठा कर रहे हैं तो उसे ट्रैक करें।

  • एरर हैंडलिंग बचाव करती है: चीज़ें टूटती हैं। फॉलबैक्स बनाइए, जैसे नीचे दिए स्निपेट में।

def answer_question_safe(query):
    """Wrapper with error handling."""
    try:
        return answer_question(query)
    except Exception as e:
        print(f"Error: {e}")
        # Fall back to text-only using descriptions
        results = retrieve(query)
        if results["documents"][0]:
            context = results["documents"][0][0][:500]
            return f"Based on the instructions: {context}...", []
        return "Sorry, couldn't process that question.", []

जब आपकी पाइपलाइन को कई स्टेप लेने हों (खोज → सत्यापित → पुनः-खोज → जवाब), agentic RAG पैटर्न इसे साफ़-सुथरे ढंग से स्ट्रक्चर करने में मदद करते हैं।

निष्कर्ष

मल्टीमोडल RAG रिट्रीवल-ऑग्मेंटेड जेनरेशन को वास्तविक दुनिया के डॉक्युमेंट्स में मौजूद पूरी रेंज के कॉन्टेंट—इमेज, डायग्राम, टेबल और चार्ट—को संभालने तक बढ़ाता है। 

हमने इसे IKEA निर्देशों को क्वेरी करने में सफलतापूर्वक उपयोग किया, जो कुख्यात रूप से विज़ुअल होते हैं, जहाँ केवल-टेक्स्ट दृष्टिकोण काम नहीं करता। पूरी पाइपलाइन API कॉल्स को छोड़कर बाकी सब लोकली चलती है।

मुख्य इनसाइट यह है कि इसे काम कराने के लिए आपको जटिल मल्टीमोडल एम्बेडिंग्स की ज़रूरत नहीं है। विज़ुअल कंटेंट के अच्छे टेक्स्ट विवरण जनरेट करके, आप स्टैंडर्ड टेक्स्ट रिट्रीवल से प्रासंगिक इमेज ढूँढ सकते हैं, और फिर जेनरेशन समय पर विज़न-लैंग्वेज मॉडल को भारी काम करने दें।

यदि आप यहाँ कवर की गई बातों पर आगे बनाना चाहते हैं, तो आज ही AI Engineer करियर ट्रैक से अपनी यात्रा शुरू करें!

मल्टीमोडल RAG FAQs

मल्टीमोडल RAG किन प्रकार के डेटा संभाल सकता है?

इमेज, चार्ट/टेबल/डायग्राम वाले PDFs, ऑडियो (ट्रांसक्रिप्ट्स के ज़रिए), और वीडियो कीफ़्रेम्स। मूलतः, कोई भी कंटेंट जिसे आप चाहते हैं कि AI सवालों के जवाब देते समय रेफ़र कर सके।

क्या मुझे अलग-अलग मोडैलिटी के लिए अलग वेक्टर डेटाबेस चाहिए?

ज़रूरी नहीं। अधिकतर प्रोडक्शन सिस्टम्स एक ही वेक्टर स्टोर में यूनिफ़ाइड एम्बेडिंग स्पेस का उपयोग करते हैं। आप सब कुछ उसी डायमेंशनल स्पेस में एम्बेड कर सकते हैं, ताकि रिट्रीवल सभी कंटेंट टाइप्स में काम करे।

क्या मैं पूरी पाइपलाइन के लिए OpenAI का उपयोग कर सकता/सकती हूँ?

हाँ। GPT-4o इमेज समझने और जवाब जनरेट करने—दोनों को संभालता है, जबकि text-embedding-3-small वेक्टर साइड को संभालता है। आप OpenAI इकोसिस्टम से बाहर जाए बिना पूरा मल्टीमोडल RAG सिस्टम बना सकते हैं।

मल्टीमोडल RAG में सबसे बड़ी चुनौती क्या है?

एलाइनमेंट। टेक्स्ट, इमेज और स्ट्रक्चर्ड डेटा को एक ही एम्बेडिंग स्पेस में लाना ताकि रिट्रीवल वाकई मोडैलिटीज़ के पार काम करे। इसके बाद नज़दीकी चुनौती है चंकिंग स्ट्रैटेजी।

इसे चलाने की लागत कितनी है?

यहाँ दिए IKEA ट्यूटोरियल के लिए, कुल मिलाकर लगभग $2 अपेक्षित हैं। सबसे महंगा हिस्सा प्रीप्रोसेसिंग के दौरान इमेज विवरण के लिए GPT-4o कॉल्स हैं। एक बार इंडेक्स बन जाने पर, क्वेरीज़ सस्ती होती हैं।

Josep Ferrer

लेखक

Josep Ferrer

जोसेप यूरोपीय परियोजनाओं में विशेषज्ञता रखने वाले फ्रीलांस डेटा साइंटिस्ट हैं, जिन्हें डेटा स्टोरेज, प्रोसेसिंग, उन्नत एनालिटिक्स, और प्रभावशाली डेटा स्टोरीटेलिंग में महारत है। 

एक शिक्षक के रूप में, वे यूनिवर्सिटी ऑफ नवार्रा के मास्टर’s प्रोग्राम में बिग डेटा पढ़ाते हैं और Medium, KDNuggets, तथा DataCamp जैसे प्लेटफ़ॉर्म पर लेखों के माध्यम से अपने विचार साझा करते हैं। जोसेप अपनी न्यूज़लेटर Databites (databites.tech) में भी डेटा और टेक के बारे में लिखते हैं। 

उन्होंने पॉलिटेक्निक यूनिवर्सिटी ऑफ कैटालोनिया से इंजीनियरिंग फिजिक्स में बीएस और पॉम्पेऊ फाब्रा यूनिवर्सिटी से इंटेलिजेंट इंटरएक्टिव सिस्टम्स में एमएस किया है।

विषय