← Make My GateHow I built this

How I built this

A five-day build to answer one question well, and to show why the retrieval pattern was chosen and not just wired.

The problem—A confident answer with nothing behind it

You have 50 minutes at Zurich. The airport shows a walking time to the gate. It does not tell you that you will cross passport control, that your Bangkok arrival means a second security check, or that the lounge you're entitled to is on the wrong side of the border. You find out by walking.

Ask a chatbot with the airport's pages pasted in and you get a fluent paragraph. Ask it 25 different ways and the paragraph changes its mind about whether there is a passport control at all. That is the failure this project is about: the answer needs to be computed from a structure (which zone am I in, which zone am I going to, which rules fire on that crossing), and text retrieval does not compute.

Why AI—Which part needs a model, and which part doesn't

Only one step needs a language model: reading the question. "I land at D33 from Bangkok, fly out from A63, 50 minutes, I'm Star Gold" has to become {arrival: D33, departure: A63, minutes: 50, origin: TH, status: star_gold}. That is fuzzy input and a small model (Claude Haiku 4.5) does it for a few hundred tokens.

Everything after that is deterministic. Gates map to gate areas. Gate areas have a Schengen zone. A crossing between zones is a passport control. An origin country is on the EU one-stop-security list or it isn't. The shortest route is a graph search. The verdict is arithmetic against a minimum connection time. None of that should be left to a model, and in this build none of it is: the answer text itself is a template over what the workers returned, so the model cannot introduce a fact that was not retrieved.

The trade-offs—Decisions, and what they cost

DecisionAlternativeWhy, and the cost
Zurich onlyFrankfurt, the bigger hubI live in Zurich and can check routes on my own flights. Frankfurt I can't ground-truth. Cost: one airport is a demo, not a product.
Hand-built graph (22 nodes, 27 edges) from public pages and OpenStreetMapIndoor-map vendor API, or OSM routingNo public floor-plan API exists for ZRH. OSM has every gate number and level but no corridors. Cost: walking times are my estimates from gate geometry, marked as such.
Explicit orchestrator with four narrow workers, run onceAn agent loop with toolsThe question is fully specified up front, so one pass is enough and the trace stays readable. Cost: nothing re-plans; a gate change is out of scope by design.
Deterministic composerLLM writes the answer from the traceThe validator can prove every route edge exists; it can't prove a paragraph. Cost: the prose is plain.
Same documents for all three systemsGive the baseline "fairer" documentsThe comparison is the project. Cost: the baseline gets tables rendered as text, which is exactly the point.
Public data only, every number with a URLUse what I know from workI work in this industry. If I can't cite a public page it doesn't go in. Cost: the minimum connection time is the weakest source in the corpus (below).

The solution—One pass, four workers, a validator, a visible trace

question → ORCHESTRATOR (Haiku 4.5 extracts slots; code decides which workers run)
             ├─ checkpoints : which passport / re-screening rules fire (zones + origin country)
             ├─ route       : shortest path over nodes.csv / edges.csv (networkx)
             ├─ mct         : minimum connection time lookup
             └─ lounge      : access rule + is a lounge on the route, on this side of the border
          → VALIDATOR: every edge exists; minutes = Σ walks + Σ queue waits;
                       passport control on route iff a border is crossed;
                       re-screen on route iff the origin requires it; verdict vs MCT and slack
          → COMPOSER: template over the trace, cites only what a worker retrieved
          → TRACE: rendered under every answer on the demo page

Ambiguous and out-of-scope questions ("gate 34 to gate 55", "will my flight be on time?") never reach a worker. The trace shows the orchestrator's reason and an empty worker list, which is the honest answer.

It is not a loop. There is no re-planning, no repeated tool call, no memory. Each worker runs at most once, in a fixed order, and the result is validated once. I organised the code this way so that "agentic orchestration" means something concrete an interviewer can point at, and so that debugging on day three meant reading a trace instead of a transcript. Where a loop would live is in the next section.

The two comparison systems share the prompt and the documents. Naive vector RAG: chunk the four rule documents and the two csv tables, embed with bge-small, cosine top-6, one model call. Hybrid + reranker: BM25 and dense retrieval fused by reciprocal rank, a cross-encoder reranks twelve candidates to six, same prompt. Neither has the graph.

The test set—25 questions, each with a named trap

Seven categories: same zone (the model may invent a passport control), passport control required in each direction, security re-screening (origin-dependent: Toronto is exempt, Dubai isn't), minimum connection time (walk fits, time doesn't), lounge (on route or a detour, before or after the border), ambiguous (must ask), out of scope (must decline), and lookalikes: "land at D34, depart from B34" is the same physical gate at Zurich, and still needs passport control.

Expected answers were derived from the graph and the rules on day one, before either system ran. Two caveats I want on the record. First, the draft questions used gate numbers from memory; most of them don't exist at Zurich (there is no A60, for instance). The planner maps any number to a gate area, so the eval still works, but the questions will be renumbered against the real gate list once I've walked it. Second, because the expected routes come from the same graph the planner searches, a perfect score proves that the question-reader extracts the right facts and that the pipeline is internally consistent. It does not prove the graph matches the building. That check happens on foot. Third, the question-reader's instructions were fixed three times against this set, which is why there is a second, held-out set below.

Results—Three designs, one table

systemanswered / asked / declined right (25)verdict right (19)route exactly right (19)rules cited right (19)all four right
naive vector RAG56%21%5%11%28%
hybrid search + reranker68%16%5%21%24%
graph + orchestrator100%100%100%100%100%

Held-out rewordings

The question-reader is the only learned part, and its instructions were tuned on the 25 above. So the 25 were reworded 50 times (typos, lowercase gates, "A10 -> A85", "half an hour", "LHR-ZRH-LIS") after the tuning stopped, and never shown to it before scoring.

systemanswered / asked / declined right (50)verdict rightroute exactly rightrules cited rightall four right
naive vector RAG50%16%8%8%24%
hybrid search + reranker54%18%8%16%24%
graph + orchestrator100%100%100%100%100%

Two caveats stay on the record. The expected routes come from the same graph the planner searches, so both 100% rows prove internal consistency and a robust question-reader, not that the graph matches the building. And the rewordings were written by me, so they cover the phrasings I could think of, not the ones travellers will type.

How the text-retrieval systems fail

failure modenaivehybrid + reranker
Hedged: asked for the dock letter when the question already gave it ("which dock is D50 in?")118
Route wrong or partial47
Missed a passport control that the route crosses33
Verdict flipped (mostly yes → no)48
Invented node ids that don't exist00

Two things surprised me. The naive system almost never hallucinated: it never invented a node, and its favourite failure was refusing to commit, because the retrieved chunk about the shared B/D pier made a plain "D50" look ambiguous. And better retrieval made the hedging better and the reasoning worse: the reranked system answered more often, cited the right rules more often, and flipped more verdicts, because it now had enough numbers in context to do the arithmetic wrong. Retrieval quality was not the bottleneck. Computation was.

The graph system missed three questions on its first run, all in the one place a model is used. "Both non-Schengen" was read as an unknown non-EU origin (so a re-screening appeared that shouldn't); "a one-stop-security origin" was read the same way; and "Land A40, depart A45" lost its letters. Three sentences added to the extraction instructions fixed all three. The baseline was not touched.

Cost: one full run of all three systems on 25 questions is about 15 cents. The graph system uses a quarter of the input tokens of the baseline, because the model only reads the question.

What I took away—Four things I'd carry into any retrieval build

Next—What a second week would add, in order

  1. Walk it. Replace estimated walking and queue times with observed ones from my own connections, then re-run the eval.
  2. Renumber the golden set against the real gate list from OpenStreetMap and re-verify five expected answers against a second source.
  3. The 2025 layer: live status and re-planning. This is where the loop lives. United shipped it in June 2025: gate-to-gate directions with boarding countdowns, alerts when a connection is held, automatic rebooking, backed by ConnectionSaver. It needs live flight and gate data I don't have on public terms, and it turns the one-pass orchestrator into an agent that re-runs workers when the world changes.
  4. Frankfurt. Same code, a second `nodes.csv`. Doubles the data-entry and I can't ground-truth it from Zurich, so it waits.

Vision—The 2026 layer

Live position, turn-by-turn, reading the sign in front of you, audio. That is a mobile app on top of indoor positioning, and vendors already sell the positioning (MappedIn, MapsPeople, Navigine sell wayfinding SDKs to airports; Frankfurt has run WiFi-based indoor routing since 2015). If one of those APIs were available it would replace `edges.csv` and nothing else. The rules layer on top, will I make it and what will I hit, is the part nobody exposes to passengers, and it is the part this build is about.

Production—Cost, limits, and what breaks

One Railway service (FastAPI, Docker, embedding and reranker models baked into the image) serves the API and these pages. Haiku 4.5 at $1 per million input tokens; a graph answer costs well under a tenth of a cent, a naive answer a few tenths. Eval runs are cached per system, model and corpus hash, so re-running is free until the data changes. Limits: static data, one airport, English only, no accounts, no live anything. What breaks first: the airport renumbers a gate area, and nobody tells the csv.

Sources

Not affiliated with Zurich Airport or any airline. Nothing here is travel advice; check your boarding pass and the signs.