← 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

One pass: the model reads the question once, code does everything else one model call everything else is code the question "D33 from Bangkok, A63, 50 min, Star Gold" Haiku 4.5 reads the question fills 9 slots does not answer it orchestrator not Zurich, live status, rebooking: decline gate or minutes missing: ask otherwise: dispatch checkpointsrules by zone + origin routeshortest path on graph mctminimum connection time loungeonly if status or asked validator 6 checks on the assembled answer composer a template over what the workers found trace: slots, each worker's reason and sources, every check, the verdict. Rendered under every answer.
Every arrow is code. The model touches the question and nothing after it. If a worker did not retrieve a fact, the composer has no way to say it.

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 box below.

Is this "agentic"?

Yes, in a bounded form, and I'd rather say exactly which form. Anthropic's guide to building agents separates workflows (a model and tools run through code paths you wrote) from agents (the model directs its own process). This is a workflow, organised in the orchestrator-workers shape. Claiming more would not survive a good follow-up question; claiming less would undersell a deliberate design.

What is here

  • A coordinator that decomposes the question and decides: decline, ask, or plan
  • Four narrow workers with structured inputs and outputs, each returning its sources
  • A validator with six checks, including "passport control on the route if and only if a border is crossed"
  • A trace that shows which worker ran, why, and what it retrieved

What is not, on purpose

  • No loop: nothing re-plans or retries, because the question is fully specified up front
  • The model does not choose tools; three facts in the question decide that by simple rules
  • No memory between questions
  • One model call to read, zero to compute, zero to write

Where a loop would earn its place: live gate data and a trigger when it changes. The orchestrator would re-run the route and checkpoint workers; the workers and the validator would not change.

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 graph—Places, walks, and checkpoints that cost time

The airport is 22 places joined by 27 walks. Gates are grouped into areas, because walking times only make sense at that scale: there is no node for A63, there is one for "Dock A gates A62 to A86". Each place has a zone, Schengen or not. Passport control is a place with a 10-minute queue, and it is the only way across the border. Security re-screening is a short loop you leave the pier for and come back from, taken only when the origin country requires it.

The Zurich graph, simplified, with the Bangkok D33 to A63 route highlighted SCHENGEN NON-SCHENGEN border 5 2 3 2 2 5 3 4 4 4 4 3 5 1 3 + 2 wait 4 2 2 + 10 wait + 2 Dock A, far gatesA62 to A86 Dock A, mid gatesA50 to A59 Dock A, bus gatesA1 to A6 Airside Centercentral hall Lounge ASchengen side B/D pier, B sideSchengen flights passport outleaving Schengen, 10 min passport inentering Schengen, 10 min Skymetromain station SkymetroDock E station Dock E gatesE numbers Lounge Enon-Schengen side B/D pier, D sidesame doors, D numbers D gates, far endD48 upward security re-screen10 min queue start end Highlighted: D33 from Bangkok to A63 security loop 14 · passport in 14 · walk to far gates 11 = 39 minutes. 50 available: yes, 11 to spare. Same gates from Toronto: no loop, 25 minutes.
Numbers on the walks are minutes. Bus gates on the D side and the two paid lounges, and the matching security loop at Dock E are left off for space. Walking times are estimates from gate positions on OpenStreetMap, marked as such in the data.

Two things in this picture do the work. Checkpoints are places, not labels. If a route crosses the border, the passport node is on the path and its queue is in the total; the system cannot forget a checkpoint the way a text system can, because the checkpoint is part of the geometry. B and D are one pier. Gate B34 and gate D34 are the same door, named B for a Schengen flight and D for a non-Schengen one, so the graph has two nodes for one pier with passport control between them. "Land at D34, depart from B34" is the same gate and still needs passport control. That case shaped the graph before a line of code was written.

What text retrieval asks

"Which sentence in the documents looks most like this question?" No sentence says D33 to A63 crosses the border. There are thousands of gate pairs and the airport's page describes zones, not pairs. The model has to infer it, and across 25 questions it infers it differently each time.

What the graph asks

"Which area is D33 in? Which zone? Which area is A63 in? Which zone? Do they differ?" Three lookups and a comparison. Then a shortest-path search whose answer is the same every time. The rules documents are read by section heading, so the passage cited is always the one that applies.

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

3same zonemay invent a passport control that isn't there
4passportborder crossed, each direction; queue must be counted
3securityorigin-dependent: Toronto exempt, Dubai not
3minimum timewalk fits, minutes given are below the minimum
4loungeon route or a detour; right side of the border?
3ambiguousa fact is missing; must ask, not guess
3out of scopeother airport, live status, rebooking: decline
2lookalikeD34 and B34 are one gate; passport control anyway

25 questions, 8 traps. 19 expect an answer; 6 expect the system to ask or decline.

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.

How a question is scored

behaviouranswered, asked, or declined as expected
verdictyes / tight / no matches
routeordered places match exactly
rulesthe set of rule documents cited matches
all fouronly then is the question "right"

"Route exact" is strict on purpose. A route with the right destination that skips the security loop is wrong, because the minutes are wrong and the traveller would not be warned. All three systems return the same answer shape, so one scorer compares all three to the same expected columns.

Results—Three designs, one table

Share of questions with all four checks right, per system All four checks right GOLDEN 25 naive vector RAG28% hybrid + reranker24% graph + orchestrator100% HELD-OUT 50 REWORDINGS naive vector RAG24% hybrid + reranker24% graph + orchestrator100% Read the 100% carefully The expected routes come from the same graph the planner searches. So the score proves the question- reader and the pipeline are consistent, not that the graph matches the building. That check is on foot. The prompt was tuned on the 25; that is why the 50 rewordings exist. They were written by me, so they cover what one person could think of. Seven verdicts depend on a 40-minute minimum connection time that nobody publishes. Tagged.
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

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.