This is a post-mortem on building trip-graph, an agentic travel planner. It's about the kind of bugs you can't find by reading code.


I built trip-graph with a safeguard I was proud of: a critic node that checks the itinerary against budget and pace, loops back to the planner if something's wrong, capped at 3 iterations so a runaway loop can't spin forever.

I assumed that loop, if anything went wrong, would be the slow part of the system. Multiple LLM calls bouncing back and forth felt like the obvious place latency would pile up. I built the cap specifically because I was worried about it.

I was wrong about almost everything I assumed.


Adding Tracing: Two Variables, Full Visibility

I added LangSmith tracing to check. Two environment variables, no code changes:

LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your_key

Every run now shows a full waterfall of every node and every LLM call, with exact timing. It's one of those tools that makes you wonder why you waited so long to add it.


What the Trace Actually Showed

Total time on a real run: 61.56 seconds.

First thing I checked: did the research fan-out actually run in parallel? Yes. attractions_research, food_research, and logistics_research all started within the same second. That part worked exactly as designed.

Jaipur Failure

Then I looked at where the time actually went.

The critic loop — the thing I was worried about — ran once. Planner drafted an itinerary, critic caught a problem, planner revised, critic passed it. That whole exchange, the part I built the safety cap for, was not what was slow.

Planner was slow. 26.75 seconds on the first pass. 19.01 seconds on the second. Two planner calls accounted for roughly 46 of the 61 total seconds — about three quarters of the entire run.

The bottleneck wasn't where the architecture suggested it would be. It never is.


Two Fast Fixes

1. Swap the critic model.

Critic was running on Sonnet. But critic's job is narrow and well-scoped: check budget math, verify pace, flag dealbreaker violations. The deterministic checks run before the LLM even gets involved. It doesn't need Sonnet for that.

Switched it to Haiku. Roughly 3x faster, a fraction of the cost, no measurable drop in quality.

2. Trim Tavily snippets.

Tavily was handing the planner more than it needed. Search results were coming back with 1000+ character snippets — all of it flowing into planner's context. I trimmed to 300 characters at a word boundary. Same useful place names and context. Far fewer tokens for planner to process.

These two fixes took the baseline from 61 seconds to about 25 seconds. Good, but not the real find.


The Real Bug: A Cap That Didn't Cap

I tested a Jaipur trip with "hate museums" as a dealbreaker.

Research came back with Amer Fort, Hawa Mahal, City Palace. Heritage sites — museums by another name. Planner had nothing else to work with, kept proposing them, critic kept rejecting them. A MAX_LOOPS cap should have stopped it at 3 tries and surfaced an error to the user.

It didn't.

Planner was resetting loop_count to 0 on every single call — including the ones triggered by critic rejections. Critic would increment it to 1. Planner would silently wipe it back to 0. Forever.

The Jaipur run hit 9+ cycles and 276 seconds before I force closed it.

That's a different category of bug than "slow." Slow is annoying. A cap that doesn't actually cap anything is the kind of thing that would quietly eat API budget on a public demo — with no upper bound, no error, and no signal that anything was wrong.


The Fix: Three Parts

1. Fix the loop counter. loop_count now only resets when a human sends new feedback — a genuinely fresh cycle — not on every planner call. The state lives in the right place.

2. Fix the source. The real problem was research returning museums when the user explicitly ruled them out. Dealbreakers now get excluded directly in the Tavily query. Planner sees markets and food spots instead of the same rejected options on repeat.

3. Pass critic's complaints forward. Critic's specific objections now get injected into planner's next prompt. Revisions target the actual problem instead of regenerating something close to the original guess.


Results

Stage Time
Baseline ~61s
Haiku critic + snippet trim ~25s
Loop fix + dealbreaker filter ~21s
Jaipur, before fix 276s (force closed)
Jaipur, after fix ~23s

The model swap and token trim were the bigger single-run speed win. The loop fix was the more important one — not because of the seconds it saved, but because it turned an unbounded failure mode into a bounded one.


The Lesson

The architecture looked correct on paper: a capped loop, parallel research, a critic checking constraints. Everything was in the right place conceptually.

The trace is what showed the cap wasn't actually capping. A real edge case — a genuine dealbreaker with no easy alternative — is what exposed it. Reading the code and reasoning about it would not have found either of these things.

If you're building agentic workflows and you're not tracing them, you're flying blind. The thing you think is the bottleneck almost certainly isn't. And the bug you think is impossible probably isn't either.