Table of Contents
1. The Puzzle
Say you ask Claude Code: "How do I make a chocolate cake?" You get back a clear, confident answer — something like: preheat the oven to 350°F, mix flour, sugar, cocoa powder, baking soda, and salt, add eggs, milk, oil, and vanilla, stir in boiling water, then bake 30–35 minutes.
A week later, you ask the exact same question, word for word. This time you get: preheat to 175°C (350°F), cream butter and sugar, beat in eggs one at a time, combine flour/cocoa/baking powder/salt separately and fold in, alternate dry mix and buttermilk, then bake 25–30 minutes.
Both answers are correct. Both would produce a perfectly good chocolate cake. But they're not the same answer — different order of steps, different ingredients (oil vs. butter, milk vs. buttermilk), different bake time. If you'd asked a professional baker the same question twice, you'd expect close to word-for-word repetition, because they're reciting a memorized recipe. The model isn't doing that. So what is it doing?
Understanding the answer is the foundation for everything else in this series — because it explains a limitation of AI systems that most people don't discover until it costs them something: you cannot assume an LLM will do the same thing twice, and any system you build on top of one has to account for that.
2. What's Actually Happening Under the Hood
An LLM is not a lookup table and it is not retrieving a stored recipe. At its core, an LLM is a next-word (technically, next-token) predictor. Given everything written so far — your question, and whatever the model has already written in its answer — it computes a probability for every possible next word it could produce, and then picks one.
For the prompt "How do I make a chocolate cake? Step 1: Pre—", the model isn't recalling a fact. It's computing something closer to:
| Candidate next token | Probability |
|---|---|
| "heat" | 61% |
| "pare" | 9% |
| "heated" | 3% |
| ... (thousands of other tokens) | remainder |
It picks a token, adds it to the text, and repeats the process for the next token — over and over, one small piece at a time, until the response is complete. This happens because of how the model was trained: on an enormous amount of text, it learned the statistical patterns of how words follow other words in this kind of context. It didn't memorize one chocolate cake recipe — it absorbed the patterns across thousands of them, and can produce any recipe consistent with those patterns.
This is the first thing to internalize: an LLM response is a chain of probabilistic choices, not a retrieved fact. Consistency was never guaranteed by how the system works — it has to be deliberately engineered in.
3. Where the Randomness Actually Comes From
Knowing the model predicts probabilities explains why variation is possible. But why does it actually happen in practice? There are four distinct sources, and it matters which one you're dealing with because each has a different fix.
1. Sampling — the model doesn't always pick the most likely word
Once the model computes the probability table, it has to choose one token. Always taking the single highest-probability token (greedy decoding) produces flat, repetitive, oddly robotic text. So most LLMs instead sample from the probability distribution: a high-probability token is likely to be picked, but a lower-probability one still has a real chance.
This is controlled by temperature:
- Temperature = 0 pushes toward always picking the top token (closest to deterministic).
- Higher temperature (e.g., 0.7–1.0) flattens the distribution, making less-likely tokens more likely to be picked — more variety, less repeatability.
This is why "cream butter and sugar" can beat out "mix flour, sugar, cocoa" on one run and lose on the next — both were plausible, reasonably high-probability openings.
2. Every choice compounds into every later choice
This is what turns a small amount of per-token randomness into two entirely different-looking recipes. Once the model samples "Cream" instead of "Mix" for step 2, everything downstream is now predicted conditioned on "Cream" — the model is far more likely to continue with "butter and sugar together" than "flour, cocoa powder." One early, small, random fork sends the rest of the response down a completely different (but equally valid) path.
3. The model itself changes over time
AI providers regularly ship updated model versions, adjust system prompts or default behavior, and update or swap the infrastructure serving the model. If the underlying model changed between your two questions, you're not even asking the same model the second time. This is a second, independent source of inconsistency layered on top of sampling — one that's easy to miss because nothing about the product visibly changed.
4. Infrastructure-level non-determinism
Even at temperature = 0, with the exact same model version, you can still occasionally see different output. Large models run across many GPUs with batched, parallelized floating-point math, and the order operations get combined in can vary slightly run to run — producing tiny numerical differences that can, rarely, tip a near-tied probability from one token to another. "Fully deterministic" is a spectrum you approach, not an absolute guarantee you get for free.
4. Walking Through the Cake Example, Token by Token
Put the four causes together and the two different cake recipes stop being mysterious:
"How do I make a chocolate cake? Step 1: "
│
┌───────────┴───────────┐
"Preheat" "Cream"
(~55% likely) (~20% likely — but sampled anyway)
│ │
"...the oven..." "...butter and sugar..."
│ │
→ Recipe A: quick, → Recipe B: creaming-method,
oil-based batter butter-based batter
(30–35 min bake) (25–30 min bake)
Neither branch was "wrong." Both were reasonable, learned patterns for how a chocolate cake recipe can start. Sampling picked a different branch on the two occasions you asked — and possibly, if a week passed, you weren't even walking the same tree, because the model itself had been updated in the meantime.
5. From Recipes to Code: Analyzing the Same File Twice
The exact same mechanism applies the moment you move from asking Claude Code for a recipe to asking it to analyze code. Say you hand it a real, 100-line JavaScript file and ask: "Analyze this code and tell me what issues you see."
You get back something like:
- Line 23 — Possible null reference:
user.profile.emailis accessed without checking thatuser.profileexists. - Line 41 — The
forloop iterates to<= items.length, which will read one element past the end of the array. - Line 58 —
fetch()call has no.catch()— an unhandled promise rejection will fail silently. - Line 74 — Nested loop over the same array is O(n²) where a
Maplookup would be O(n). - Line 90 — Inconsistent naming:
getUserDataalongsidefetch_order_details.
Ask again — same file, same prompt, nothing changed in the code — and you might get back:
- Line 23 — Possible null reference on
user.profile.email. - Line 58 — Missing error handling on the
fetch()call. - Line 74 — Nested loop could be optimized.
- Line 88 — Variable
tempis declared but never used.
Notice what happened: the off-by-one loop bound on line 41 — a real, fairly serious bug — was caught in Run 1 and silently missed in Run 2. Nothing about the code changed between the two runs — only which findings the model happened to sample into existence.
Why this is a bigger problem for code than for cake
Nobody is harmed if two chocolate cake recipes list their steps in a different order. But a code review, a security scan, or a regression check that silently drops a real bug on one run and catches it on the next has a real cost — the model isn't incapable of catching the bug, it's that whether it catches it on any given run is partly a matter of chance. This is exactly the gap that makes "I asked Claude Code to review it and it looked fine" an unsafe basis for confidence — and exactly the gap a proper eval process exists to close.
So — what do you actually do to get a consistent answer?
You can't make an LLM's output perfectly deterministic and still keep it useful — but you can push consistency a long way, and you can measure whatever variability is left. Roughly in order of increasing effort:
- Lower the temperature (ideally to 0). Removes most of the "which of several close-probability options do we pick" randomness — cheapest lever, but doesn't remove model drift or infra noise.
- Pin the model version. Call a specific, versioned model and keep using it for a given eval, so you can tell a real change from the model shifting underneath you.
- Force a structured output format. A fixed checklist of categories (null safety, error handling, performance, security, style) forces a verdict on every dimension, every time.
- Break one big analysis into several small, narrow ones. Narrower tasks have a smaller space of reasonable answers, so there's less room for sampling to wander.
- Run it more than once and look for agreement (self-consistency). Run the same analysis 3–5 times and keep only the findings that show up in a majority of runs.
- Pair the LLM with deterministic tools for parts that don't need judgment. A linter catches a bad loop bound the same way every time — reserve the LLM's judgment for what static tools genuinely can't do.
- Log everything the answer depended on. Model version, temperature, prompt version, and timestamp attached to every run — without this you can't tell a real regression from ordinary run-to-run noise.
6. How Does an LLM Analyze Code It Has Never Seen?
Here's a question worth sitting with: you write a piece of JavaScript — code that exists nowhere else, that the model could not possibly have seen during training — and hand it to an LLM with "analyze this." It comes back with a sensible explanation. How is that different from asking "What is the capital of the USA?", where the model can only be right because it memorized that fact somewhere in its training data?
The short answer: these are two different kinds of work. One is recall. The other is applying a learned pattern to something new — and an LLM is built to do both, using the exact same next-token machinery from Section 2.
"Capital of the USA" is a retrieval task
During training, the model encountered the pairing USA → Washington, D.C. an enormous number of times, in an enormous number of phrasings. Answering the question is mostly a matter of retrieving an association that already exists inside its learned parameters. There's very little construction involved.
Your JavaScript file is not a retrieval task
The model almost certainly never saw your exact function. But during training it saw millions of examples of code containing the same building blocks:
functionintroduces a reusable block of behaviorifintroduces a conditionage > 65is a comparisonbaseRate * 1.25is arithmeticreturnproduces an output
It learned the generalized pattern, not your specific instance of it. So when your code arrives, the model isn't looking up "this file" — it's recognizing familiar shapes inside a file it's never seen and reasoning about how those shapes relate to each other.
Your exact code (never seen before)
│
▼
Break into tokens
│
▼
Recognize familiar programming patterns
(learned from millions of OTHER examples)
│
▼
Understand relationships between
variables, conditions, and operations
│
▼
Construct an interpretation
│
▼
Answer your question
A human analogy: grammar, not memorization
Imagine you've been taught English grammar and shown 10,000 sentences. Then someone hands you a brand-new one you've never encountered: "The purple elephant flew over the moon."
You can immediately say: "purple" describes "elephant," "elephant" is the subject, "flew" is the verb, "over the moon" describes direction. You're not remembering this exact sentence — you're applying rules you generalized from thousands of other sentences to one you've never seen.
An LLM does something similar with code, although the mechanism underneath is a neural network's learned representations and next-token prediction, not an explicit symbolic grammar a human could write down.
The deeper distinction: knowledge vs. context
Say the model was only ever trained on a trivial add(a, b) function, and you hand it something entirely new:
function calculateRisk(policy) {
if (policy.driverAge < 25 && policy.vehicleValue > 50000) {
return "HIGH";
}
return "NORMAL";
}
The model has no idea what your policy object actually contains, whether vehicleValue is in dollars, or whether this business rule is even correct. That's all missing context. But it can still read the structure: IF condition A AND condition B THEN return HIGH, ELSE return NORMAL.
Two very different resources are at work here: learned knowledge (baked into the model's parameters during training — general programming concepts) and temporary context (your specific code, placed in the prompt, reasoned over only while it's in the context window and then forgotten). A useful shorthand: training gives the model its general education; the prompt gives it the exam question.
Where it stops being recall and starts being reasoning
Suppose the requirement is "apply the surcharge if driver age is below 25 OR vehicle value exceeds $50,000," and the code says:
if (driverAge < 25 && vehicleValue > 50000) {
return "HIGH";
}
To flag this as wrong, the model has to hold two new pieces of information — the plain-English requirement and your code — in its context at once, translate both into the same logical shape (A OR B vs. A AND B), and notice they disagree. That is not "look up the fact." That is comparing new information against new information — the same category of task as spotting that "over the moon" describes "flew," just applied to logic instead of grammar.
| Question | Main activity |
|---|---|
| What is the capital of the USA? | Recall learned knowledge |
| Explain this JavaScript function | Apply learned programming concepts to new input |
| Does this code match the requirement? | Reason over two pieces of provided information |
| Is this implementation correct? | Semantic comparison and judgment |
| Does this handle every edge case? | Analyze behavior under hypothetical inputs |
The last three rows in that table are the interesting — and the risky — ones, because the further you move from "recall" toward "judgment," the more convincing a wrong answer can sound. The LLM can understand your code without ever having seen your code before. But understanding is not proof of correctness. That gap is exactly why an eval framework can't just grade "did the explanation sound plausible" — it has to check whether the conclusion actually follows from the requirement and the code's real behavior.
7. Was the LLM Trained to "Switch On" Reasoning for Code?
Given Section 6's distinction between recall and applied reasoning, a natural next question is: does the model have some internal switch — if question looks like code, use reasoning mode; else use lookup mode? It doesn't, at least not as a hand-built rule. There is no FACT_MODE / CODE_REASONING_MODE flag anywhere in the architecture. What actually happens is subtler, and it follows directly from the next-token predictor described back in Section 2.
Different inputs activate different learned patterns
During training the model absorbed huge numbers of examples like:
Question: What is 2 + 2?
Answer: 4
Question: Why does this code fail?
Answer: Because the loop condition...
Requirement: The system should reject values below 18.
Code: if (age <= 18) reject();
Analysis: This incorrectly rejects 18.
The model isn't told "these are different modes" — it just learns, statistically, that text shaped like a code-and-requirement comparison tends to continue with step-by-step logical language, while text shaped like a trivia question tends to continue with a short, direct fact. Your prompt's shape is what steers it toward one learned pattern or the other — the same sampling-over-a-probability-distribution mechanism from Section 2, just conditioned on a different kind of input.
The subtle part: it isn't necessarily "thinking in steps" at all
A human reviewer explicitly runs through: understand requirement → parse code → compare → test the boundary. A base LLM, absent a specialized reasoning system, is still just predicting the next token, one at a time, as described in Section 2 — it's not guaranteed to execute those steps explicitly. But the learned representations can be sophisticated enough that the output looks like that reasoning happened:
Requirement: "age >= 65" Code: "age > 65"
│ │
▼ ▼
comparison, threshold, comparison, threshold,
INCLUSIVE boundary EXCLUSIVE boundary
└───────────────┬────────────────┘
▼
Difference detected at age = 65
The model has learned that "inclusive" and "exclusive" boundary phrasing are semantically different — not because it executed a formal comparison algorithm, but because that distinction showed up often enough in training data that it became part of the learned representation.
Traditional generation vs. reasoning-oriented models
This is also where "reasoning models" genuinely differ from a standard model doing next-token prediction straight through:
Prompt
↓
Predict next token
↓
Predict next token
↓
Answer
Prompt
↓
Explore interpretations
↓
Perform logical steps
↓
Check result
↓
Answer
Pretraining gives a model the ability to recognize these patterns at all. A reasoning-oriented system adds more computation on top — more space to explore, check, and revise — before committing to an answer. Neither approach changes the fact from Section 2: this is still sampling from a learned distribution, not executing a guaranteed algorithm.
The practical upshot for evals: whatever the model produces when it "analyzes code" is a candidate conclusion, generated the same probabilistic way as everything else in this chapter — never treat it as a formal proof by default. The pairing that actually gives you confidence is LLM for interpretation, deterministic tools for verification: let the model propose "this looks correct," then let a test generator, a linter, or an actual execution of the code try to disprove it.
8. Reverse Engineering: Code → Requirements, Where It Goes Wrong
Sections 6 and 7 covered checking code against a known requirement. Reverse engineering flips the direction: you hand the LLM a piece of code with no requirement attached and ask "what business requirement does this implement?" — CODE → INFERRED REQUIREMENTS. This is a meaningfully harder problem, because code proves behavior, but a requirement is a claim about intent — and intent is exactly the part the code doesn't contain.
| Reason | Why it happens | Example |
|---|---|---|
| Behavior isn't intent | The code proves what happens, never why. Any of several business stories can produce identical code. | if (age >= 65) premium *= 1.25; could be a senior surcharge, a risk-tier rule, a regulatory adjustment, or a promo — the LLM has to guess which. |
| Logic hidden elsewhere (scope problem) | The LLM analyzes the function in isolation and misses a condition imposed by its caller. | calculatePremium() looks like a general senior surcharge, but it's only ever invoked inside if (state === "CT") — the real rule is Connecticut-only. |
| Misleading variable names | A name is a hint, not proof. The model may guess a plausible business meaning that isn't the real one. | if (customer.flag === "Y") — could mean senior, preferred, fraud-risk, or military. Worse with names like CUST_TYP_04. |
| Dead / unreachable code | The LLM describes a branch as a real rule without checking whether control flow can ever reach it. | if (age > 65) return "SENIOR"; if (age > 75) return "VERY_SENIOR"; — the second branch is unreachable; the true behavior is just age > 65 → SENIOR. |
| Missing default / fallback behavior | Models tend to describe the named, positive cases and skip the else, the null path, or the fallback — often the most important business rule. |
AUTO and HOME get real logic; every other policy type silently returns 0 — easy to omit from the summary. |
| Missed rule interaction | Two independent-looking if blocks are described as two separate rules, but the combined-state behavior when both fire is never stated. |
Senior surcharge and good-credit discount each look fine alone, but a senior customer with good credit gets both multiplied together — an implicit third rule. |
| Order-of-operations sensitivity | "Apply X, then Y" isn't the same as "Apply Y, then X." The model can state the two operations without preserving which came first. | premium * 1.25 - 100 (=1150 on 1000) vs. (premium - 100) * 1.25 (=1125) — different requirements, same-sounding summary if order is dropped. |
| Logic buried behind helper calls | The real condition lives inside a function, config file, or DB lookup the model wasn't given — it can only see the call site. | if (isEligible(customer)) applyAdjustment(policy); — the actual eligibility rule cannot be reverse-engineered without isEligible()'s body. |
| Implementation artifact mistaken for a business rule | A literal number or check in the code might be a real policy — or just a technical default, a workaround, or an infra limit. | if (retryCount < 3) retry(); — is 3 a business SLA, or just an arbitrary technical retry cap? The code alone can't say. |
| Hallucinated justification | The model adds a plausible-sounding "why" that the code never actually proves. | if (age >= 65) premium *= 1.25; becomes "seniors are classified as higher risk" — a reasonable-sounding story, but not something the code demonstrates. |
The most important conceptual problem: reverse engineering is underdetermined
Take the same one-line rule: if (customer.age >= 65) premium *= 1.25; — every one of these could produce that exact implementation:
- Drivers aged 65 and older receive a 25% surcharge.
- Customers aged 65 and older are classified as high risk.
- A regulatory adjustment of 25% applies to insureds 65 or older.
- Apply a 25% adjustment to customers meeting the senior-age threshold.
All four are consistent with the same code. One implementation can correspond to multiple, equally plausible original requirements — there may be no way, from the code alone, to know which one is "true." That's not a bug in a particular model; it's a structural limit of the task.
What to evaluate instead
Since "did the LLM guess the original human requirement" is often unknowable, the evaluable question has to be narrower: "did the LLM accurately and completely describe the code's observable behavior?"
JavaScript Code
│
▼
AST / Control Flow / Data Flow
│
▼
Observable Behavior ──────► LLM ──────► Natural-Language Requirement
│
▼
Evaluate against Observable Behavior
For example, if the code is if (age >= 65 && state === "CT") premium *= 1.25; and the LLM outputs "Drivers aged 65 and older receive a 25% surcharge" — that's incomplete: it dropped the state === "CT" condition entirely, and an eval built to check completeness against the actual control flow will catch it, the same way the off-by-one bug in Section 5 was only caught on some runs and not others.
A stronger version of this, matching the "force a structured output format" lever from Section 5, is to make the LLM emit a structured rule (conditions, logical_operator, action, confidence, evidence) instead of free-form prose — something a test generator can check line by line, rather than a paragraph a human has to eyeball.
9. Why This Matters When You're Building (or Evaluating) an AI System
- A single test run tells you almost nothing. If you ask a coding agent to fix a bug once and it works, that is one sample from a probability distribution of possible answers — not proof the agent reliably fixes that class of bug.
- "It worked when I tried it" is not evidence of quality. This is the exact instinct evals exist to correct.
- Consistency has to be designed in, not assumed. Lower temperature, constrain output format, and run self-consistency checks to reduce — not eliminate — variability.
- Reproducibility requires pinning the model. Without recording the exact model version, temperature, and settings, you can't tell whether a change in results came from the agent's real behavior or ordinary sampling noise.
- Some tasks need low variance more than others. A creative brainstorming assistant benefits from sampling variety; a coding agent producing regulatory or financial output needs the variance suppressed and measured, not left to chance.
10. Key Takeaways
- LLMs generate text one token at a time by sampling from a learned probability distribution — they do not retrieve a single memorized "correct" answer.
- Small, valid differences early in a response compound into very different-looking (but often equally correct) final answers.
- Inconsistency has at least four distinct causes: sampling/temperature, compounding choices, model drift over time, and infrastructure-level floating-point non-determinism.
- One successful test run is not evidence that an AI system works reliably — the core justification for running structured, repeated evals instead of ad hoc spot-checks.
- The same effect applies to code analysis: reviewing the same, unchanged file twice can surface a different set of findings — a real bug caught on one run can be silently missed on the next.
- Consistency in code analysis is improved (not guaranteed) by lowering temperature, pinning the model version, forcing structured output, narrowing the task, running self-consistency checks, and offloading rule-based checks to deterministic tools.
- Reproducible evaluation requires recording exactly which model version and settings were used, every time.
- An LLM can analyze code it has never seen because training taught it generalized patterns (functions, conditions, operators), not memorized files — your code becomes temporary context, applied against permanent learned knowledge.
- There is no hard-coded "fact mode" vs. "code-reasoning mode" switch — the shape of the prompt activates different learned response patterns, and the underlying mechanism is still next-token prediction either way.
- Reverse engineering (code → requirements) is a harder, structurally different problem than forward checking (requirement → code): code proves behavior but not business intent, so one implementation is often consistent with several different plausible original requirements.
- The evaluable question for reverse engineering isn't "did the LLM guess the original intent" — it's "did the LLM's stated requirement completely and accurately match the code's observable behavior."
11. Glossary
| Term | Meaning |
|---|---|
| Token | A chunk of text (often a word or part of a word) — the basic unit an LLM predicts one at a time. |
| Sampling | Randomly selecting the next token according to its predicted probability, rather than always taking the top choice. |
| Temperature | A setting that controls how much randomness is applied during sampling; lower = more predictable, higher = more varied. |
| Greedy decoding | Always picking the single highest-probability next token; the most deterministic (but often lowest-quality) decoding strategy. |
| Model version drift | Output changing over time because the provider updated the underlying model, not because of anything in your prompt. |
| Determinism | The property of always producing the same output given the same input; LLMs approach but rarely fully guarantee this. |
| Self-consistency | Running the same task multiple times and treating findings that agree across runs as higher-confidence than ones that only appear once. |
| Static analysis | Rule-based code checking (linters, type checkers) that applies fixed logic rather than sampling from a probability distribution — deterministic by construction. |
| Learned knowledge vs. context | Learned knowledge is what's baked into the model's parameters during training (general programming concepts); context is the specific input — like your code — placed in the prompt and reasoned over only for that request. |
| Boundary condition | The exact edge value where a comparison's outcome changes (e.g., age >= 65 vs. age > 65 disagree only at age = 65) — a common place for both code bugs and LLM misreadings. |
| Unreachable / dead code | A branch that can never execute given the paths that lead to it — describing it as a "working rule" without checking reachability is a common reverse-engineering mistake. |
| Reverse engineering (code → requirements) | Asking an LLM to infer the business requirement a piece of code implements, with no requirement given as input — the reverse direction of checking code against a known requirement. |
| Underdetermined problem | A problem where the available evidence (the code) is consistent with more than one correct answer (the original requirement), so no amount of analysis alone can identify a single "true" answer. |
| Structured requirement extraction | Having the LLM output a fixed, checkable format (conditions, operator, action, confidence, evidence) instead of free-form prose, so a test generator can verify it against the code's actual behavior. |
This is Chapter 1 in the Eval Basics series. Understanding why LLM output is inherently non-deterministic is the prerequisite for the next core question the series tackles: if there's no single "correct" answer to compare against, how do you actually evaluate whether an AI agent's output is good? — continue to Chapter 2: What Is an Agent, and How Do You Judge a Good One? →