The central promise of agent-based AI systems is seductive: give an LLM the tools, give it a goal, and let it figure out the path. In demos, this works because demos use curated, unambiguous inputs. In production, ambiguous inputs are the norm.
When Gitloom v1 launched, our code analysis pipeline was built exactly this way: a single system prompt with fifteen tool definitions (file retrieval, AST parsing, type checking, documentation lookup) and an autonomous `while (!done)` execution loop. Engineers building the demo loved it. It felt magical watching the agent self-direct through a repository.
The first time a user submitted a TypeScript monorepo with circular type references, the agent entered a loop. The LLM called the type-checker tool, received ambiguous output, called it again with different parameters, received more ambiguity, and continued for 11 iterations before the 30-second timeout killed the request. The user got a 504. We got a $0.43 API bill for a failed query.
The failure was not the LLM's fault. LLMs are extraordinarily good at reasoning about language. They are not designed to make deterministic control flow decisions under computational uncertainty. We had given the LLM a job it was not architected to perform.
The correct insight: routing decisions belong to the application, not the model. An LLM should answer 'what does this code do?' — not 'should I call this tool again or is my current context sufficient?'
We rebuilt the pipeline as a typed LangGraph state machine with five nodes. The Planner Node classifies the incoming request into one of three task types (`code_analysis`, `summary`, `unknown`) using a single structured LLM call with a strict JSON schema output. The LLM never again makes a routing decision after this point.
The `code_analysis` path runs a pgvector similarity search against the indexed repository, retrieves the top-8 semantically relevant chunks, and passes them to a Synthesis Node that generates the answer. The LLM at the Synthesis Node has exactly the context it needs — no more retrieval decisions to make.
The `unknown` path routes immediately to a human review gate that returns a 202 Accepted with a callback URL. The user is told their request required human triage. We stopped trying to force the model to answer questions it could not reliably answer. This is a better product decision, not a compromise.
After the rebuild: p99 latency dropped from 14.2 seconds to 3.1 seconds. Token cost per successful query dropped 68%. The incoherence rate on ambiguous inputs dropped to zero — because ambiguous inputs no longer reach the synthesis node.
The opinion I'll defend: any AI system where an LLM is making control flow decisions is not well-designed. Control flow belongs in typed application code. Reasoning belongs in the model. The moment you blur that boundary, you have traded observability, debuggability, and cost predictability for the appearance of autonomy.