Chuyển đến nội dung chính

Multimodal RAG: Hướng dẫn thực hành học từ tài liệu

Tìm hiểu cách xây dựng pipeline RAG đa phương thức cho hệ thống Hỏi–Đáp bằng hình ảnh dựa trên hướng dẫn lắp ráp IKEA, sử dụng OpenAI API và cơ sở dữ liệu vector ChromaDB.
· 11 phút đọc

Khám phá cùng AI

Hãy tưởng tượng bạn đang lắp đồ nội thất IKEA. Bạn nhìn chằm chằm vào bước 14 trong hướng dẫn nhưng không hiểu con vít nào đặt vào đâu. Sơ đồ có gì đó, nhưng vẫn chưa “bật” ra được. 

Nếu bạn hỏi: "Miếng kim loại nhỏ ở bước 14 dùng để làm gì?", một hệ thống RAG (retrieval-augmented generation) chỉ dựa trên văn bản sẽ bó tay. Câu trả lời nằm ngay trong sơ đồ, không có trong bất kỳ dòng chữ nào. Hướng dẫn IKEA nổi tiếng là “không lời”.

Đây là lúc RAG đa phương thức phát huy tác dụng.

Trong hướng dẫn này, tôi sẽ cùng bạn xây dựng một hệ thống RAG đa phương thức có thể trả lời câu hỏi về các tài liệu chứa hình ảnh, sơ đồ và nội dung hỗn hợp. Chúng ta sẽ dùng hướng dẫn lắp ráp IKEA làm ca kiểm thử vì đây là bài “stress test” tốt: chủ yếu là hình, rất ít chữ, và cũng là loại tài liệu mọi người thực sự cần trợ giúp.

Chúng ta sẽ dùng các mô hình của OpenAI xuyên suốt: GPT-4o để hiểu hình ảnh và tạo câu trả lời, cùng text-embedding-3-small để tạo vector có thể tìm kiếm. Nếu muốn học cách xây dựng pipeline RAG với framework khác, tôi gợi ý khóa học RAG với LangChain của chúng tôi.

Kết thúc, bạn sẽ có một hệ thống chạy được tại máy và có thể điều chỉnh cho tài liệu của riêng bạn.

Multimodal RAG là gì?

Multimodal RAG mở rộng mẫu RAG chuẩn để xử lý nhiều hơn văn bản. Ý tưởng cốt lõi vẫn vậy, nhưng “ngữ cảnh” giờ bao gồm cả hình ảnh, bảng, sơ đồ và nội dung phi văn bản khác.

Tại sao điều này quan trọng? Vì tài liệu thực tế không chỉ có chữ.

Hãy nghĩ về những gì bạn thực sự làm việc cùng: 

  • Sổ tay kỹ thuật có sơ đồ đi dây 
  • Bài báo nghiên cứu có đồ thị thực nghiệm 
  • Báo cáo tài chính có biểu đồ doanh thu
  • Thông số sản phẩm có hình linh kiện 

Một hệ thống chỉ xử lý văn bản là đang làm việc với thông tin thiếu hụt. Giống như cố hiểu một công thức nấu ăn chỉ bằng danh sách nguyên liệu mà bỏ qua ảnh minh họa động tác “gấp bột nhẹ nhàng” trông thế nào.

RAG truyền thống xem PDF như một dòng ký tự. Nó trích xuất văn bản, chia khúc, nhúng các khúc đó và coi như xong. Mọi hình ảnh bị bỏ qua hoàn toàn, hoặc cùng lắm chuyển thành token giữ chỗ [IMAGE]. Bảng biểu bị biến thành các dòng chữ méo mó, mất cấu trúc.

Multimodal RAG tiếp cận khác: Hình ảnh được mô tả hoặc nhúng trực tiếp, còn bảng được phân tích sang định dạng có cấu trúc. Hệ thống truy xuất có thể ghép một truy vấn như "sơ đồ kiến trúc mạng" với cả mô tả văn bản về kiến trúc lẫn sơ đồ trực quan thực sự. Mô hình sinh có thể quan sát hình và suy luận về những gì nó mô tả.

Khía cạnh

RAG chỉ văn bản

RAG đa phương thức

Loại đầu vào

Văn bản thuần, có thể kèm markdown

Văn bản, hình ảnh, bảng, sơ đồ, tài liệu hỗn hợp

Những gì được nhúng

Các khúc văn bản

Khúc văn bản cộng mô tả hình ảnh hoặc embedding hình ảnh

Mô hình sinh

Bất kỳ LLM nào

Mô hình thị giác–ngôn ngữ (GPT-4o, Claude, Gemini)

Phù hợp nhất cho

Tài liệu nặng về chữ

Sổ tay, báo cáo, slide, mọi thứ có nội dung trực quan

Nếu “đa phương thức” vẫn còn là một chiếc ô mơ hồ, bài blog về AI đa phương thức này giải thích ngắn gọn kèm ví dụ cụ thể.

Multimodal RAG hoạt động thế nào?

Pipeline có nhiều bộ phận hơn RAG chỉ văn bản, nhưng cấu trúc tương tự. Dữ liệu được xử lý và nhúng, đưa vào kho vector, rồi được truy xuất khi có câu hỏi.

Multimodal RAG Pipeline

Luồng như sau:

Nạp và phân tích tài liệu

Tài liệu thô (PDF, hình ảnh, slide) được tách thành các thành phần. Bạn trích xuất văn bản, nhận diện hình ảnh, phân tích bảng và xác định quan hệ không gian giữa mọi thứ. Một sơ đồ ở trang 5 có thể có chú thích bên dưới và được nhắc tới trong đoạn văn ở trang 4. Bạn cần lưu lại các mối liên hệ đó.

Tiền xử lý nội dung

Mỗi loại nội dung cần cách xử lý khác nhau. Với hình ảnh, có hai hướng chính: tạo mô tả văn bản bằng mô hình thị giác (như GPT-4o), hoặc nhúng ảnh trực tiếp bằng bộ mã hóa đa phương thức (như CLIP). Cách mô tả văn bản đơn giản hơn và hoạt động tốt với embedding văn bản chuẩn. Bảng được chuyển sang định dạng có cấu trúc hoặc tóm tắt bằng ngôn ngữ tự nhiên.

Sau đó, tất cả được chuyển thành vector. Nếu dùng cách mô tả, bạn chỉ việc nhúng toàn bộ văn bản (văn bản gốc cộng mô tả hình ảnh được tạo) bằng một mô hình embedding văn bản chuẩn. Mấu chốt là truy vấn về nội dung trực quan giờ có thể khớp với các mô tả đó.

Lập chỉ mục và lưu trữ 

Vector được lưu trong cơ sở dữ liệu kèm metadata, gồm tài liệu nguồn, số trang, loại nội dung và, với hình ảnh, đường dẫn tới tệp gốc. Bạn sẽ cần đường dẫn hình này sau khi muốn hiển thị nó cho mô hình sinh.

Truy xuất

Khi nhận truy vấn, hãy nhúng truy vấn bằng cùng mô hình, tìm kiếm láng giềng gần nhất và trả về các khớp hàng đầu. Các khúc truy xuất có thể tham chiếu văn bản, hình ảnh hoặc cả hai.

Sinh

Truyền truy vấn cùng ngữ cảnh đã truy xuất cho một mô hình thị giác–ngôn ngữ. Nếu các khúc truy xuất tham chiếu hình ảnh, hãy tải các ảnh đó và đưa vào prompt. Mô hình thấy cả ngữ cảnh văn bản và hình ảnh thực tế, rồi tạo câu trả lời.

Bước sinh là nơi GPT-4o thực sự tỏa sáng. Nó có thể nhìn sơ đồ IKEA chỉ cách đặt vít và giải thích bằng ngôn ngữ dễ hiểu. Nó có thể đọc biểu đồ và tóm tắt xu hướng. Nó có thể nhận ra những chi tiết trong bản vẽ kỹ thuật mà mô tả văn bản có thể bỏ lỡ.

Triển khai Multimodal RAG

Hãy bắt tay xây dựng. Chúng ta sẽ tạo hệ thống có thể trả lời câu hỏi về hướng dẫn lắp ráp IKEA, đi từng bước từ thiết lập đến giao diện truy vấn hoạt động.

Bước 0: Chọn framework và công cụ

Trong hướng dẫn này, chúng ta giữ mọi thứ đơn giản:

Thành phần

Lựa chọn

Lý do

Embeddings

text-embedding-3-small

Rẻ, chất lượng tốt, cùng API

Kho vector

ChromaDB

Chạy tại máy, không cần thiết lập, lưu bền

Mô hình thị giác

GPT-4o

Hàng đầu trong hiểu hình ảnh

Sinh

GPT-4o

Cùng mô hình, xử lý cả hai tác vụ

Bạn có thể thay thế. Pinecone hoặc Weaviate làm kho vector nếu cần mở rộng. Claude hoặc Gemini là lựa chọn thay thế tốt cho khâu sinh. Kiến trúc tổng thể vẫn giữ nguyên trong các trường hợp đó.

Vì sao không dùng CLIP hoặc ColPali cho embeddings? Bạn có thể dùng. Chúng cho phép nhúng ảnh trực tiếp mà không cần tạo mô tả văn bản trước. Nhưng chúng làm tăng độ phức tạp và thường cần GPU. Cách mô tả hoạt động tốt với nội dung dạng tài liệu, nơi bạn muốn nắm bắt chi tiết ngữ nghĩa, không chỉ là độ tương tự trực quan.

Bước 1: Thiết lập môi trường

Bắt đầu với các phụ thuộc và biến môi trường để thiết lập môi trường dự án.

Cài đặt phụ thuộc

Trước hết, bạn cần cài poppler để xử lý PDF.

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

  • macOS (dùng Homebrew): brew install poppler

  • Windows: Tải từ trang phát hành và thêm vào PATH

Tiếp theo, cài các gói Python. Ngoài ChromaDB, chúng ta cần pdf2image để chuyển PDF thành ảnh, pillow để xử lý ảnh và requests để tải tệp PDF.

pip install openai chromadb pdf2image pillow requests

Thiết lập khóa API và tệp

Để dùng các mô hình, hãy đặt khóa OpenAI API của bạn thành biến môi trường.

Unix/Linux/macOS:

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

Windows: 

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

Giờ hãy thiết lập cấu trúc dự án và kéo dữ liệu mẫu. Script đã có sẵn URL tải xuống, nhưng nếu bạn muốn xem trước PDF, đây là liên kết trực tiếp tới cả bàn MALM và khung giường MALM. 

Tạo tệp tên 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")

Để hoàn tất thiết lập ban đầu, chạy script:

python step1_setup.py

Bạn sẽ thấy các PDF của IKEA được tải về thư mục data/.

Bước 2: Tiền xử lý tài liệu

Hướng dẫn IKEA khá “khó nhằn”. Chúng chủ yếu là sơ đồ với rất ít chữ. Ta cần chuyển các trang PDF thành ảnh, rồi dùng GPT-4o để mô tả mỗi trang hiển thị gì.

Tạo 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()

Các hàm làm gì:

  • convert_pdf_to_images(pdf_path): kết xuất mỗi trang PDF thành ảnh PNG ở 150 DPI bằng pdf2image. Điều này trích xuất các sơ đồ không có chữ có thể chọn, lưu với tên thống nhất như malm_desk_page_001.png.

  • describe_page(image_path): mã hóa ảnh thành base64 và gửi tới GPT-4o với system prompt tùy chỉnh. Mô hình trả về mô tả chi tiết nêu bước, linh kiện và số lượng để phục vụ tìm kiếm ngữ nghĩa.

  • main(): điều phối mọi thứ: tải mô tả đã cache để tránh xử lý lại, chuyển PDF thành ảnh, tạo mô tả mới khi cần, lưu cache tăng dần và xuất all_pages.json kèm metadata cho lập chỉ mục.

Chạy script như sau:

python step2_preprocess.py

Bước này mất vài phút. GPT-4o phân tích mỗi trang và tạo mô tả chi tiết. Các mô tả được cache, nên nếu chạy lại, nó sẽ bỏ qua trang đã xử lý.

Điều gì đang diễn ra? Chúng ta đang chuyển nội dung trực quan thành văn bản có thể tìm kiếm. Khi ai đó hỏi, "Làm sao gắn chân bàn?", bộ truy xuất sẽ tìm trong các mô tả này và thấy những trang nhắc tới việc gắn chân, dù trang gốc không có chữ nào.

Khi bạn đã xây xong pipeline cơ bản, các cải thiện tiếp theo đến từ mẹo truy xuất như tìm kiếm lai, rerank hoặc viết lại truy vấn. Để làm quen, đọc blog về Kỹ thuật RAG nâng cao.

Bước 3: Lưu trữ và lập chỉ mục dữ liệu đa phương thức

Giờ chúng ta nhúng các mô tả và lưu vào ChromaDB.

Tạo 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()

Tại đây, hàm get_embedding(text) chuyển văn bản từ mô tả và thông tin nguồn thành vector dày đặc bằng mô hình text-embedding-3-small của OpenAI. Với 1536 chiều, các vector đó có thể nắm bắt ý nghĩa ngữ nghĩa để tìm kiếm tương đồng.

main() tải các trang đã xử lý từ all_pages.json ở bước trước, khởi tạo ChromaDB với lưu trữ bền, xóa/tạo lại collection, tạo embedding cho từng trang, lưu kèm metadata phong phú (nguồn, số trang, đường dẫn ảnh) và chạy thử truy xuất nhanh để xác minh chỉ mục hoạt động.

Chạy:

python step3_index.py

Metadata rất quan trọng ở đây. Ta lưu image_path để sau có thể lấy ảnh thực và hiển thị cho GPT-4o trong lúc sinh. Phần mô tả là cái được tìm kiếm, nhưng ảnh mới là cái được hiển thị.

Bước 4: Xây dựng pipeline RAG

Đây là pipeline RAG đa phương thức đầy đủ. Một truy vấn được nhúng, các trang liên quan được truy xuất, ảnh được tải và GPT-4o tạo câu trả lời dựa trên mô tả văn bản lẫn hình ảnh.

Tạo 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()

Giải thích hàm chính:

  • get_embedding(text): tạo vector embedding cho truy vấn bằng text-embedding-3-small, cho phép tìm kiếm tương đồng ngữ nghĩa trong ChromaDB.​

  • retrieve(query, top_k=TOP_K): nhúng truy vấn, truy vấn collection để lấy top-k láng giềng gần nhất và trả về tài liệu cùng metadata (ví dụ đường dẫn ảnh) qua include=["documents", "metadatas"].​

  • generate_answer(query, retrieved_results): tải các ảnh đứng đầu dạng base64, xây prompt GPT-4o đa phương thức đan xen nhãn trang và ảnh, rồi tạo phản hồi chi tiết với hướng dẫn đặc thù IKEA.​

  • answer_question(query): điều phối truy xuất và sinh, kèm log tiến độ.

  • main(): kiểm tra chỉ mục tồn tại, hỗ trợ truy vấn đơn qua CLI hoặc chế độ trò chuyện tương tác (“quit”, “exit” hoặc Ctrl+C để dừng).

Bước 5: Chạy pipeline RAG

Bạn có thể chạy và gõ câu hỏi, hoặc chạy trực tiếp với một câu hỏi cụ thể như sau:

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

Ổn rồi! Hệ thống truy xuất trang dựa trên mô tả, tải ảnh thực tế và GPT-4o tạo câu trả lời bằng cách nhìn trực tiếp vào sơ đồ.

Đánh giá Multimodal RAG

Xây dựng một hệ thống RAG đa phương thức là một chuyện. Biết nó hoạt động tốt hay không lại là chuyện khác. Và đưa nó chạy ổn định ở môi trường production còn thêm lớp thách thức nữa. 

Các chỉ số RAG tiêu chuẩn vẫn áp dụng, nhưng bạn cần mở rộng cho nội dung trực quan.

Chất lượng truy xuất

Precision@k đo xem bao nhiêu trang truy xuất được thực sự liên quan. Nó phạt các trang không liên quan trong top-k, nhưng với khâu sinh, có được bức ảnh then chốt còn quan trọng hơn là đạt top-k hoàn hảo.

Recall@k đo tần suất các trang đúng xuất hiện trong top k. Nó phù hợp hơn cho bài toán IKEA vì chúng ta muốn hệ thống tìm được ít nhất một trang liên quan trong kết quả top-k (ví dụ k=3). Như vậy, GPT-4o có ngữ cảnh trực quan để trả lời chính xác, dù các kết quả khác là nhiễu. 

Với hệ thống IKEA của chúng ta, bạn sẽ xây một bộ kiểm thử cỡ 50 câu hỏi và gán nhãn các trang “đúng” cho mỗi câu. Sau đó, bạn có thể đánh giá bằng hàm sau:

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)

Chất lượng sinh

Một điều khó đo tự động hơn là chất lượng sinh. 

Bạn có thể nhờ người chấm điểm độ chính xác và hữu ích, hoặc dùng LLM làm “giám khảo” để đánh giá câu trả lời có đúng với ảnh nguồn hay không. 

Với sơ đồ có các bước đánh số, hãy xác minh mô hình gán đúng số bước.

Các kiểm tra đặc thù đa phương thức

Các đánh giá bổ sung có thể trả lời những câu sau:

  • Hệ thống có tìm được câu trả lời dựa trên hình ảnh khi câu trả lời là trực quan không? 

  • Khi mô hình tham chiếu thứ gì đó trong ảnh, tham chiếu đó có chính xác không? 

Những điều này cần rà soát thủ công, ít nhất cho một mẫu truy vấn.

Thực hành tốt cho Multimodal RAG

Nếu bạn muốn xây pipeline RAG đa phương thức sẵn sàng production, lưu ý vài điểm sau:

  • Chi phí tiền xử lý cộng dồn: Các cuộc gọi thị giác để mô tả ảnh là phần tốn kém. Với kho tài liệu lớn, hãy xử lý theo lô vào giờ thấp điểm và cache mô tả thật mạnh tay. Cân nhắc mô hình rẻ hơn (trong trường hợp này, ví dụ GPT-4o-mini) cho việc tạo mô tả nếu chất lượng chấp nhận được với bài toán của bạn.

  • Độ trễ truy vấn quan trọng: GPT-4o với ảnh mất 5–10 giây mỗi truy vấn. Streaming giúp người dùng thấy kết quả từng phần nhanh hơn, và cache các truy vấn phổ biến giúp lặp lại nhanh. Truy xuất 2 trang thay vì 5 có thể tạo khác biệt về token nếu từng đó đã đủ ngữ cảnh.

  • Giám sát đúng thứ: Theo dõi độ trễ truy xuất (nên dưới 100ms với bộ sưu tập nhỏ), độ trễ sinh (kỳ vọng 5–10 giây với ảnh), chi phí API mỗi truy vấn và phản hồi người dùng nếu bạn thu thập.

  • Xử lý lỗi cứu bạn: Mọi thứ có thể hỏng. Xây fallback, như đoạn mã sau.

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.", []

Khi pipeline của bạn cần nhiều bước (tìm kiếm → xác minh → tìm lại → trả lời), các mẫu agentic RAG giúp bạn cấu trúc gọn gàng.

Kết luận

Multimodal RAG mở rộng retrieval-augmented generation để xử lý đầy đủ dải nội dung trong tài liệu thực tế, gồm hình ảnh, sơ đồ, bảng và biểu đồ. 

Chúng ta đã dùng thành công để truy vấn hướng dẫn IKEA, vốn cực kỳ trực quan, nơi cách tiếp cận chỉ văn bản sẽ không hiệu quả. Toàn bộ pipeline chạy tại máy ngoại trừ các cuộc gọi API.

Điểm mấu chốt là bạn không cần embedding đa phương thức phức tạp để làm việc này. Bằng cách tạo mô tả văn bản tốt cho nội dung trực quan, bạn có thể dùng truy xuất văn bản chuẩn để tìm hình ảnh liên quan, rồi để mô hình thị giác–ngôn ngữ gánh phần nặng ở bước sinh.

Nếu bạn muốn xây tiếp dựa trên những gì đã học, hãy bắt đầu lộ trình với AI Engineer ngay hôm nay!

Câu hỏi thường gặp về Multimodal RAG

Multimodal RAG có thể xử lý những loại dữ liệu nào?

Hình ảnh, PDF có biểu đồ, bảng, sơ đồ, âm thanh (qua bản chép lời) và keyframe của video. Nói chung là mọi nội dung bạn muốn AI tham chiếu khi trả lời câu hỏi.

Tôi có cần cơ sở dữ liệu vector riêng cho từng phương thức không?

Không nhất thiết. Hầu hết hệ thống production dùng một kho vector duy nhất với không gian embedding thống nhất. Bạn có thể nhúng mọi thứ vào cùng một không gian chiều, để truy xuất hoạt động xuyên suốt các loại nội dung.

Tôi có thể dùng OpenAI cho toàn bộ pipeline không?

Có. GPT-4o xử lý cả hiểu hình ảnh và tạo câu trả lời, còn text-embedding-3-small xử lý phần vector. Bạn có thể xây trọn bộ hệ thống RAG đa phương thức mà không rời hệ sinh thái OpenAI.

Thách thức lớn nhất trong RAG đa phương thức là gì?

Căn chỉnh. Đưa văn bản, hình ảnh và dữ liệu có cấu trúc vào cùng một không gian embedding để truy xuất thực sự hoạt động xuyên phương thức. Chiến lược chia khúc là thách thức sát sườn thứ hai.

Chạy tốn bao nhiêu chi phí?

Với hướng dẫn IKEA ở đây, ước chừng khoảng 2 USD tổng cộng. Phần tốn kém là các cuộc gọi GPT-4o để mô tả ảnh trong giai đoạn tiền xử lý. Khi đã lập chỉ mục, truy vấn khá rẻ.

Josep Ferrer

Tác giả

Josep Ferrer

Josep là một Nhà khoa học dữ liệu tự do chuyên về các dự án châu Âu, có chuyên môn về lưu trữ dữ liệu, xử lý, phân tích nâng cao và kể chuyện bằng dữ liệu tạo tác động. 

Với vai trò giảng dạy, anh phụ trách môn Dữ liệu lớn trong chương trình Thạc sĩ tại Đại học Navarra và chia sẻ góc nhìn qua các bài viết trên các nền tảng như Medium, KDNuggets và DataCamp. Josep cũng viết về Dữ liệu và Công nghệ trong bản tin Databites (databites.tech). 

Anh có bằng Cử nhân Vật lý Kỹ thuật từ Đại học Bách khoa Catalonia và bằng Thạc sĩ Hệ thống Tương tác Thông minh từ Đại học Pompeu Fabra.

Chủ đề

Liên quan