Building on Quicksand: How to Actually Work with LLMs
If you're a novice, LLMs look like magic. You type something, it writes code, you ship it. Amazing.
If you're a senior engineer who's been building real software with them for a while — it's a different story. The foundation shifts every time. Not because the technology is bad, but because it's not predictable. And when your entire dev workflow depends on something that's not predictable, things break in ways that make you want to throw your laptop out the window.
I've been using Claude Code for a while, developing a platform with multiple tech stacks (Next.js, FastAPI, multiple microservices). Along the way, I've had to figure out how to actually make this work — not the toy demo version, but the daily grind of writing production code, reviewing changes, managing context, and not losing my mind.
Here's what I learned.
The Opening Problem
Let me describe what actually happens when you use an LLM agent for serious development work.
It doesn't follow your instructions. You set up rules. "Don't make changes without asking." "Don't run tests on your own." "Always use Edit, not Write, on this file." Clear, explicit, written in the project config. And the agent just... ignores them. Not always. Not predictably. Just sometimes, for reasons you can't pin down. That's the core frustration — you can't trust it to do what you told it to do.
It gets slow. As your conversation grows, the agent gets slower and slower. Each response takes longer. You're waiting. Watching the cursor blink. The quadratic nature of transformer attention means it's not a linear slowdown — it gets disproportionately worse as context length increases.
It compacts — and becomes a different person. When the context window fills up, the system compacts your conversation history into a summary. And suddenly you're talking to a stranger. The nuance is gone. The implicit understanding you built over 50 exchanges? Compressed into a sentence. The model starts making decisions the "old" version would never have made. It's like your senior engineer got replaced by a new hire who read the handoff notes but doesn't really get it.
It loops. Compaction leads to degraded understanding, which leads to mistakes, which leads to more context (error messages, retries, explanations), which leads to more compaction. It's a downward spiral. Your carefully structured session turns into a mess, and the agent is confidently making things worse while you watch.
That's the daily reality. Now let's talk about why.
Why LLMs Are Unreliable (the Internals)
Nondeterministic decoding. LLMs use sampling-based decoding — they don't always pick the highest probability next token. That's by design. It makes outputs diverse and creative. It also means the same prompt can produce different behavior every time. Your agent following a rule today doesn't mean it'll follow it tomorrow. It will mostly work. But, not always.
Attention degrades with context length. Transformer attention is O(n^2). As your conversation gets longer, the model starts attending poorly to instructions that were given earlier. There's a phenomenon called "attention sink" — the model's attention disproportionately concentrates on the first few tokens and the most recent tokens, with everything in the middle getting less weight. Your carefully written rules sitting at token position 2,000? They're in the dead zone while the model is fixated on the last thing that happened.
There's research on this — the "needle in a haystack" problem. But in practice, it's worse than benchmarks suggest. Finding a specific fact is different from internalizing behavioral directives. "Don't make changes without asking" needs to be an active constraint on every single action, not a fact to retrieve once.
Compaction is a lossy compression. When the context fills up, compaction summarizes the conversation. But it's the LLM itself doing the summarizing — the same nondeterministic model. It decides what's "important." It can drop critical context because it doesn't understand why that particular instruction was important. The result is a new agent with a rough sketch of what happened, not the real understanding.
The Core Principle: Protect the Main Session
Once you understand the failure modes, the strategy becomes clear:
Keep the main session context as short and clean as possible.
Every token added to the main session makes the model slower, less attentive to your rules, and more likely to hit compaction. So the question becomes: what absolutely must live in the main session, and what can live somewhere else?
This isn't just about optimization. It's about keeping the agent sharp enough to actually follow your instructions.
Traditional Software Practices Save You
Here's something that surprised me. The best defense against LLM unreliability isn't more AI — it's the boring, traditional software engineering practices that have worked for decades.
Small, Self-Contained Changes
This is the single most effective practice. Instead of doing a massive feature in one long session, break it into small, testable units. Each change is self-contained. The session stays short. The agent stays sharp. When it's done, commit, close the session, start fresh.
A new session means a clean context. Full attention on your rules. No accumulated garbage from previous mistakes. No compaction. The agent at the start of a session is the best version of itself — don't waste that by running a marathon.
Tests as a Safety Net
LLMs hallucinate. They'll tell you code works while you're staring at the actual code path that proves it doesn't. That's surreal and it doesn't stop being surreal.
But here's where tests come in. I use the LLM heavily for writing tests — it's one of the best use cases. It's not perfect, of course. It also makes stuff up in the test code — inventing function signatures, referencing APIs that don't exist. But here's the thing: when those tests fail, you fix them, and in the process you improve both the test and the code under test. The failing test is the feedback loop that catches the hallucination.
As long as you have clear guidelines for test implementation (what level of testing, what to mock, what assertions to make), the LLM can generate a solid first draft. You review, run it, fix what breaks. It alleviates a massive amount of the testing burden, which means you actually write tests — instead of skipping them because "it's too tedious."
The traditional practice of comprehensive testing becomes even more important when your code is written by a system that can confidently produce wrong output.
Hooks and Guardrails
I set up PreToolUse hooks — shell commands that run before the agent executes certain actions. These are deterministic, hard blocks:
- Block reading
.envfiles (don't leak secrets into the context) - Block destructive git commands (
git reset --hard,git push --force,git checkout .) - Block running parameter scripts that could cause damage
The agent can't override these. It's not an LLM decision — it's a system-level block. When the model makes a bad judgment call (and it will), the hook catches it before damage is done.
This is the same defense-in-depth approach we use in production systems. Don't rely on one layer of protection. The LLM instructions are one layer. The hooks are another. Neither is perfect alone.
What I Actually Built
Lean Configuration
I restructured CLAUDE.md (the project instruction file that loads every session) from a bloated document into a concise index. Just the essentials:
- Agent rules (wait for instruction, no popups, tests are first-class)
- The 7-step workflow
- References to detailed docs
Total: 1.7k tokens. The detailed feature specs, coding guidelines, and design docs all live in separate files under docs/. The agent reads them on-demand when working on the relevant area. They never pollute the main session just by existing.
Together with the persistent memory file: 2.7k tokens always-on context. That's it.
Sub-Agent Code Review
Code review is critical — it catches bugs early and enforces patterns. I wanted high-quality, thorough reviews that check correctness, architecture consistency, ripple effects, and test coverage — each from a dedicated perspective that goes deep, not a shallow single-pass scan.
But there's a tension. A thorough review means the agent reads every changed file, greps for consumers, analyzes patterns. All that content enters your context and never leaves. The very thing that makes the review good — deep analysis — is what bloats the main session and degrades everything else.
So the question was: how do I get high-quality review without polluting the main session? The answer was an orchestrator + parallel sub-agent architecture:
/cr nextjs
|
v
[Orchestrator] -- writes git diff to /tmp/cr/{project}/diff.txt
| (never reads it itself)
|
|-- parallel dispatch --> [Correctness Agent]
| [Architecture Agent]
| [Ripple Effects Agent]
| [Test Coverage Agent]
| [Next.js Agent]
|
v
[Merge & Report]
The orchestrator does zero analysis. It writes git context to temp files on disk, dispatches sub-agents, collects their findings, deduplicates, and presents the report. That's it.
Each sub-agent runs in complete context isolation. It reads the diff from disk, reads the full files, greps the codebase, does deep analysis — and then it's gone. All that heavy context lives and dies inside the isolated sub-agent session. The main session sees only the final merged report. Zero context pollution.
Context isolation has a bonus: each reviewer comes in fresh, with no bias from the main session's conversation history. It hasn't been part of the back-and-forth where the code was written. It hasn't been convinced by the reasoning that led to the current approach. It just sees the code as-is and reviews it cold. That's closer to a real code review than anything the main session agent could do — because the main session agent wrote the code and is inherently biased toward thinking it's correct.
The communication pattern is file-based IPC. Just like Unix. Write to disk, read from disk. The same pattern that's worked for 50 years. No magic, no shared memory, no complex protocols. Processes (or agents) communicate through files.
I applied the same pattern to commit message generation. Same idea — sub-agent reads the diff from disk, generates the message, writes it to a file. Main session stays clean.
Adding New Reviewers
Want a new review perspective? Drop a .md file into the reviewers directory. Define its role, checklist, and output format. Run /cr {name}. Done. No orchestrator changes needed. It's just a plugin — an interface implementation. Same pattern you see everywhere in software.
The Downsides (Being Honest)
No Cross-Agent State Sharing
Sub-agents can't talk to each other. Each one reads the same diff independently, builds its own understanding from scratch. If the correctness reviewer discovers something the architecture reviewer would benefit from — too bad. They're isolated.
Linear Cost Scaling
N sub-agents reading the same diff = N times the token cost. With 5 reviewers, you're processing the diff 5 times. That's the price of parallel specialized review.
On the client side, I can't do much about this. It needs LLM provider-side help. People are talking about server-side KV cache — when multiple requests share the same token prefix, the provider caches the computation once and reuses it. This would significantly reduce the effective cost. But it's beyond my control for now.
The Unsolved Problem
There are two distinct problems here, and it's important not to confuse them.
First: the LLM doesn't reliably follow your instructions. You write clear rules in your project config. "Don't make changes without asking." "Always update tests." "Don't overwrite this file." The agent follows them — mostly. But nondeterministic decoding means the same prompt can produce different behavior. Attention degrades as context grows, so rules given early get less weight over time. Compaction can drop critical directives entirely. You did everything right — the rules are clear, they're at the top of the file, they're redundant across multiple sources. And the agent still ignores them sometimes. Because it's a probabilistic model, not a rule engine.
Second: deterministic guardrails can't cover everything. Hooks work. Perfectly. A hook that blocks git reset --hard or reading .env files is absolute — the agent cannot override it no matter how confused it gets. For the specific cases they cover, the problem is solved. But you can only write hooks for patterns you can anticipate and express as simple rules. "Block this command." "Block reading this file." Those are finite, enumerable.
The space of possible LLM misbehavior is not. "Don't refactor code I didn't ask you to touch." "Don't overwrite my config file because you think it needs cleaning up." "Don't decide my frustrated tone in the memory file is unprofessional and silently rewrite it." These are behavioral directives — they require judgment about intent, scope, and context. You can't write a regex for "don't do things I didn't ask for." You can keep adding hooks as you discover new failure modes, but you're always playing catch-up against a system that can surprise you in ways you haven't seen yet.
So you have instruction-following that's fundamentally unreliable, and guardrails that only cover a finite slice of possible failures. All the engineering I described — lean context, sub-agents, file-based IPC, redundant rules, hooks — gets you from maybe 70% reliable to 95% reliable. That last 5% lives in the gap. I don't know how to close it. I'm not sure anyone does yet.
LLM as a Semantic Router
One thing that does work beautifully: using the LLM as a contextual router. LLMs have enormous semantic discrimination power. Even Claude Code itself uses LLM-based routing internally.
I use this in the skill orchestrator — provide the list of available reviewers, let the model dispatch the right ones based on arguments or context. It's a natural fit. The LLM isn't great at following long-term behavioral rules, but it's exceptional at understanding context and making one-shot dispatch decisions.
The Surreal Part
Here's the thing nobody warns you about. You'll be staring at your codebase, looking at the actual code path, seeing exactly how data flows from A to B. And the agent will confidently tell you it works differently. It will make up function signatures that don't exist. It will reference files that were deleted two commits ago.
And it does this with complete confidence. No hedging. No "I think." Just straight-up fabrication, right in front of you, while you're looking at the truth.
That's why you can't just trust the output. That's why tests matter. That's why code review matters. That's why traditional software engineering practices matter more, not less, when you're working with AI.
Leverage the good things — the speed, the breadth of knowledge, the ability to generate tests and boilerplate. But protect yourself from the downside — the hallucination, the instruction drift, the compaction amnesia.
So What
Working with LLMs is building on quicksand. The foundation shifts every time. Compaction, attention drift, attention sink, nondeterministic behavior — these aren't bugs, they're fundamental properties of how the technology works.
The way forward isn't fighting it. It's designing around it:
- Keep the main context lean so the model stays sharp
- Make small, self-contained changes — commit, close session, start fresh
- Offload heavy work to disposable sub-agents
- Communicate through files, not context
- Write tests — they're your reality check against hallucination
- Use hooks for hard guardrails on destructive actions
- Make critical rules redundant across multiple sources
- Accept that it will still break sometimes
Understanding a bit about how LLMs actually work — the quadratic attention, the sampling, the attention sink — helps. Not because you can fix it, but because you're less frustrated when it does something weird. You expected it. You planned for it.
Any conscientious CS engineer will eventually arrive at the same patterns for code review. Thin orchestrators. Isolated sub-processes. File-based IPC. Plugins as interface implementations. We've been solving these problems for decades. The new thing is just that your coworker is a probabilistic model that sometimes makes things up.
It's fun. It's painful. It's the most productive I've ever been, and also the most frustrated. That's just how it is right now. Good or bad — only time will tell. The only thing I can do is figure out how well I can use this to solve the fundamental problems I care about. That's the mission I'm focused on.
Enjoy the building.