reasons first, then retrieves

Retrieval answers "find me a wine like X."

This has to work out what X should even be.

Describe a meal — "grilled ribeye with peppercorn sauce", "spicy Thai green curry" — and a LangGraph agent reasons from sommelier pairing principles to a target wine style, then uses semantic search over 130,000 real reviews as a tool, recommends with citations, and critiques its own pick — retrying with a different strategy if the pairing is weak or nothing fits your budget.

Wine Pairing Agent banner artwork.
5 nodes

parse → strategize → retrieve → recommend → critique, with critique looping back to strategize when the pairing is weak.

130k

real reviews searched as a genuine LangChain StructuredTool, invoked by name.

9.0s

a full traced run end to end, with every node's time recorded and saved as JSON.

Every pick

carries a [n] citation into a real review, and the candidate list is printed alongside the recommendation.

Start · the distinction

Retrieval is a step; this is a decision

My Wine Sommelier RAG answers a question that already contains its own search terms. "A bold red under $25" is close to a query. A meal is not: nothing in "grilled ribeye with peppercorn sauce" tells a retriever to look for firm tannin and dark fruit.

So the agent reasons about the dish first — body, fat, acid, spice — forms a pairing strategy, and only then turns that strategy into a query. Retrieval is a tool it decides to use, not the pipeline it lives in. And afterwards it asks whether the result is actually any good.

LangGraph diagram for the wine pairing agent showing parse, strategize, retrieve, recommend and critique nodes, with a loop from critique back to strategize.
Five nodes, and a loop back to strategize when the critique is unhappy.

↓ a real run

Ribeye in, Napa Cabernet out, with receipts

Verbatim from examples/sample_run.md. Note that the candidate list is printed too, so you can see what it chose from rather than only what it chose.

$ python -m src.cli "grilled ribeye steak with a peppercorn cream sauce" --budget 45

================================================================
PAIRING
================================================================
For a rich, char-grilled ribeye with peppery cream sauce, you want bold
tannin to cut the savory fat and dark fruit to echo the char.

Vinifera 2006 Cabernet Sauvignon, Napa Valley — 95 pts, $40 [3]
This full-bodied, 100% Cab wraps blackberry and black currant around silky
tannins with graphite and vanilla-sandalwood oak — the structure scours the
cream and fat while the oak mirrors the grilled char. My top pick.

Textbook 2005 Mise en Place Cabernet Sauvignon, Oakville — 94 pts, $44 [5]
Firmly tannic with a dry astringency and ripe blackberry-currant depth; that
grippy structure is exactly what a fatty ribeye needs.

— Candidate wines (retrieved from 130k reviews) —
  [1] Stark-Condé 2011 Cabernet Sauvignon (Stellenbosch) · 92 pts · $27
  [2] JD 2007 Cabernet Sauvignon (Diamond Mountain District) · 94 pts · $40
  [3] Vinifera 2006 Cabernet Sauvignon (Napa Valley) · 95 pts · $40
  [4] Foley Johnson 2013 Cabernet Sauvignon (Rutherford) · 92 pts · $45
  [5] Textbook 2005 Mise en Place Cabernet Sauvignon (Oakville) · 94 pts · $44
  [6] Las Vertientes 2014 Reserva Cabernet Sauvignon (Mendoza) · 92 pts · $40

Every candidate is inside the $45 budget, because the budget is a filter on retrieval rather than a request in the prompt. And the two picks are differentiated on style — one elegant, one grippy — instead of just ranked.

↓ observability

Every run is traceable and timed

retrieve is a real LangChain StructuredTool the agent invokes by name, which makes runs introspectable rather than a black box. Add --trace and each node is timed, rendered as a tree, and saved to traces/ as JSON.

AGENT TRACE  (node · time · detail)
────────────────────────────────────────────────────
  ● parse         1.2s   ribeye · rich body
  ● strategize    2.3s   style → full-bodied tannic red
  ● retrieve      0.3s   6 candidate wines (tool: search_wines)
  ● recommend     3.0s   drafted pairing with citations
  ● critique      2.2s   sound 
────────────────────────────────────────────────────

Retrieval at 0.3 seconds against 2–3 seconds per LLM node is the useful detail: the local vector search is nearly free, and essentially all the latency is reasoning. When critique loops back to re-strategize, the extra pass shows up here — self-correction, made visible and measurable.

↓ the gap, reported not hidden

No live trace figure, and no scored evaluation

assets/make_trace_figure.py renders a saved trace as a timeline figure. That figure has not been captured from a live run — the script is written and runnable, but CLI throttling during the build meant the render was never completed. The trace tree above is the real recorded output; the timeline visualisation is not yet in the repository.

There is also no quantitative evaluation of pairing quality here, unlike the retrieval benchmark in the sommelier RAG. Pairing quality is genuinely harder to score — there is rarely one right answer — but that is an explanation for the gap, not a substitute for it. What this page shows is a working, traced, self-checking agent, not a measured claim that its pairings are good.

Finish · what it is built on

Where the numbers come from

Framework
LangGraph, with a real cycle from critique back to strategize
Tool
src/tools.py wraps retrieval as a LangChain StructuredTool the agent calls by name
Index
~130,000 real reviews, embedded locally and stored in Chroma; built by src/ingest.py
Pairing logic
src/pairing.py — the sommelier principles the strategize node reasons from
Tracing
src/trace.py times each node, renders a tree, saves JSON
Generation
Claude CLI by default; retrieval is local and free
Siblings
Wine Sommelier RAG supplies the retrieval; the Training Plan Agent shares the critique-loop pattern

Reason about the dish, then go looking