A companion to this session's notebook, multimodal_rag.ipynb — the one where you
extend text RAG to images + text in one pipeline: a VLM that parses charts into
searchable chunks, three cross-modal retrieval strategies on Qdrant, generation
that reads exact numbers off chart pixels, and a video extension that answers with
timestamp citations. It's a run log: the whole notebook was actually executed —
uv sync, the ~600 MB CLIP download, every VLM call, the recall@3 eval, and the
video answer — so you have a "known-good" reference for what a healthy run looks
like and where people trip. Use it to tell a real problem apart from expected
run-to-run noise.
This is the student version. The engineering findings and setup gotchas are all here, but the graded reasoning is yours to do. You demonstrate understanding through your executed work — the parsed captions, the three strategies' rankings, the answers the model read off the pixels, your recall@3 table, and the timestamped video answer — plus the four ❓ questions and two 🏗️ activities in the notebook. Prompts to check your own understanding are near the end, without answers. No API keys appear here.
A reference environment (yours may differ): WSL2/Linux, uv, Python 3.13, run inside the session's own
.venv. LangChain v1 (init_chat_model
- image content blocks),
langchain-qdrant,sentence-transformers(CLIP),openai. AnOPENAI_API_KEYavailable when you run the notebook. Default models:openai:gpt-4o(VLM) andtext-embedding-3-small(text embedder).
Legend: ✅ verified by actually running it ·
Yes — the whole notebook runs clean, top to bottom, once your kernel is right.
uv sync resolves without a fight, the VLM reads exact figures straight off chart
pixels, all three retrieval strategies behave as described (including the "modality
gap" you're asked about), and the video answer cites a real timestamp. There are
no broken imports and no wrong APIs. The one thing that turns a healthy notebook
into "nothing works" is which Python kernel is running it — that's #1 below, and
it's the single most common ticket. Everything else is the CLIP download and your
API key. None of it is a code edit.
When it's working you should see, in order:
-
Section 0 — setup:
Environment OK, thenVLM_MODEL=openai:gpt-4o EMBED_MODEL=text-embedding-3-small, thenVision-language model ready. If Cell 0.1 says packages are missing, don't re-runuv sync— jump to trip-wire #1, it's almost certainly your kernel. -
Section 3 — the corpus: 6 charts + 6 docs load, and the data dictionary prints each chart's hidden fact (
Q4 highest at $27M,APAC worst at 240ms, …). Those numbers live only in the chart pixels — that's the whole design. -
Section 4 — the VLM as parser: the model reads a chart and returns strict JSON. For the revenue chart you should see a
takeawaylike "Q4 had the highest revenue at $27M" anddata_points["Q1: $12M", …, "Q4: $27M"]. It's reading numbers off an image. (Your exact wording will differ — that's fine.) -
Section 5 — three strategies:
- A (caption→text): "Which quarter had the highest revenue?" puts
fig_revenue_quarterly.pngfirst. - B (unified CLIP) — the important one: the query "a bar chart comparing API
latency across regions" returns only text documents in the unified search
(the images get crowded out!), but the images-only slice puts
fig_latency_region.pngfirst, matched by pixels alone. Seeing text outrank the obviously-correct chart is the modality gap — it's the setup for Question #2, not a bug. - C (separate stores + RRF): "why did on-call get paged about APAC?" brings the latency chart back into the top-3 alongside the infra doc.
- A (caption→text): "Which quarter had the highest revenue?" puts
-
⚠️ Activity #1 ships withREPLACE ME:placeholder queries. Running it as-is prints meaningless, identical rankings — that's expected. The work is replacing them with two real ACME questions (try one visual phrasing and one factual phrasing) and explaining where A/B/C diverge. Different rankings across the strategies is the goal, not a mistake.
Breakout Room #2 — Generation, Evaluation & Video (Sections 6–10) ✅ runs clean · ⚠️ one score surprises people
A healthy run shows:
-
Section 7 — generation reads the pixels: the model answers with the exact numbers and cites the chunk ids — e.g.
$27M(revenue),March … 6.2%(churn),Compute 55% / Storage 20% / Network 15% / Other 10%(the pie),NPS 22 → 41, dip to 28 in Q3. Every one of those came from the model looking at the retrieved image, not from a text file. That "retrieve to find the chart, look at the chart to answer" split is the core pattern. -
Section 8 — recall@3: you should see something like A = 1.00, B = 0.25, C = 1.00 (see the
⚠️ below — B is supposed to be low). -
Section 9 — video: 5 transcript segments, ~9 keyframes sampled, and the video question about APAC latency comes back citing a timestamp like
[00:26-00:37]with the 240ms detail — a window that matches the transcript's APAC segment. -
⚠️ Strategy B's recall is low on purpose — do not "fix" it. On a real run, Strategy B scores far below A and C (we got 0.25). That's not a broken run: the recall harness runs B as one mixed ranking, so the modality gap buries the chart images and B only catches the questions whose answer is a text document. That low number is the exact weakness Question #2 is about, made numeric. Reason about why B is low (text crowds out images in one unified ranking); don't treat it as a score to rescue. -
⚠️ Activity #2 ships withmy_gold = {}empty, so it re-prints the samen=8numbers. The work is adding two real gold rows — one whose answer is a text doc, one whose answer is a chart image — then reporting whether any strategy's recall moved and why. "No movement" is a perfectly valid, well-reasoned answer on a corpus this small.
⚠️ Your kernel — the "I ranuv syncand it still says packages are missing" trap. This is the #1 issue by a mile. If Cell 0.1 fails withMissing ['sentence_transformers', 'imageio'] — run uv sync, the problem is almost neveruv sync— it's that the notebook is running on the wrong Python (a globalpython3kernel instead of this folder's.venv). Two reliable fixes:- Launch from the environment:
uv run jupyter lab(the kernel is then guaranteed to be the.venv), or - In Cursor/VS Code, select this folder's
.venvas the kernel (kernel picker, usually bottom-right). - Sanity check inside the notebook's kernel:
import sys; print(sys.executable)— it must point inside14_multimodal_rag/.venv. If it doesn't, that's the whole problem. Re-runninguv syncwon't help; switching the kernel will.
- Launch from the environment:
⚠️ The first Strategy-B cell downloads ~600 MB and looks like it hung — it didn't. The first timeClipEmbedder()runs (Section 5.2), it downloads the CLIP model (clip-ViT-B-32, ~600 MB) from Hugging Face. On a slow connection this takes a while and the cell just sits there. Let it finish — it's cached after the first run and instant thereafter.⚠️ Your API key. Unlike some earlier sessions, this notebook does load.envfor you (Cell 0.2 callsload_dotenv), so copying.env.example→.envand putting yourOPENAI_API_KEYin it works exactly as the README says. (Exporting it in your shell before launching also works.) If the key is missing you get a clear error telling you what to do — no silent failure.⚠️ Two shipped-doc mismatches, so you don't chase ghosts. The README mentionsmultimodal_rag_vlm.ipynb, but the real file ismultimodal_rag.ipynb— open that one. And.env.exampleshows a commented example modelopenai:gpt-5.6-luna; that's just an illustration of how to override — the real, working default isopenai:gpt-4o. Don't paste the example name in expecting it to resolve.- 💡 What's random and what isn't. The retrieval results — the strategy
rankings, the modality-gap demo, the recall@3 table — are stable run-to-run
(fixed embedders). What does change each run is the VLM's caption wording and
its answer prose. So if your
$27Manswer is phrased differently from a classmate's, or your B score is 0.12 vs 0.25, that's normal — the mechanism is what matters (right chunk found, number read off the image, timestamp cited).
This session is graded on your executed work plus your written answers to the four questions and two activities. If you can answer these from your own runs, you've got it. They're a method, not a key.
- LangChain's embedding interface only accepts text, but chat models can see images. Why does that one fact force the notebook to build three different retrieval strategies instead of one? For each of A, B, and C, say how it routes around the text-only embedder — not just its name.
- In Strategy B, a query that literally describes a chart comes back as all text documents, yet the images-only search finds the right chart instantly. What is that failure called, and why does a text query land closer to text than to the correct image even though CLIP shares one space?
- Strategy C merges two result lists with Reciprocal Rank Fusion instead of just keeping the highest similarity scores. Why would comparing the raw scores from the text store and the image store be a mistake — and what does RRF use instead of score magnitude?
- At answer time the notebook re-loads each retrieved image from disk and sends the actual pixels to the model, even though it already made a caption for that image during ingestion. Why not just reuse the caption? (Hint: what is a caption for versus what are pixels for — and where do the real numbers in this dataset live?)
- Look at your recall@3 table. Strategy B scored much lower than A and C. Which kind of question does B miss, which kind does it catch, and how does that connect to the weakness you described for Strategy B earlier? Is B's low score a bug or the point?
- In your Activity #1 output, did the three strategies rank your two queries differently? Tie each difference back to the tradeoffs table — why would a visual phrasing favor the CLIP-driven strategies and a factual one favor the caption/text ones?
- Your video answer cited a timestamp like
[00:26-00:37]. What two aligned streams make video RAG possible, and which Section-5 idea did the video pipeline reuse wholesale to get there?
The "known-good" sources when something disagrees with what you see.
- LangChain multimodal inputs & content blocks (
{"type":"image","source_type":"base64",…}) — https://docs.langchain.com/ init_chat_model(one line swaps providers) — https://docs.langchain.com/- CLIP via
sentence-transformers(clip-ViT-B-32) — https://www.sbert.net/ - Qdrant (
:memory:mode,langchain-qdrant) — https://qdrant.tech/documentation/ - Reciprocal Rank Fusion (the
1/(k+rank)fuse) — https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf - OpenAI vision (
gpt-4o) & embeddings (text-embedding-3-small) — https://platform.openai.com/docs/guides/
Tools move fast. If a signature or a model name disagrees with these docs, the docs win — and remember the models are overridable via the
VLM_MODEL/EMBED_MODELenv vars, so a model error is almost never a reason to edit the notebook.