Let me start with the part I'm still embarrassed about: I was debugging a production issue at the time. A completely unrelated production issue. The kind where you have twelve browser tabs open, a half-eaten sandwich going cold beside your keyboard, and the Slack thread is already seventeen messages deep and going nowhere useful.
I had a chat session open with one of the major frontier models โ I'll call it "the model," because product names create PR headaches and that's not what this piece is about. I'd asked it to help me reconstruct a code path, and partway through, something went sideways. The model referenced a function โ a specific, named function with a specific signature โ that I was almost certain didn't exist in the codebase. But it referenced it so casually, with such precision, tucking it between two real functions like it belonged there, that for a moment I genuinely doubted myself.
I searched the repo. No function. I searched GitHub. No function. I Googled the exact name. No results. So I pushed back.
me > That function doesn't seem to exist. I can't find it anywhere.
model > You're right to double-check. processNodeBatch() was introduced in v2.4.1 of the core library. It's possible your version predates it โ I'd recommend checking your package lock file to confirm which version you're running.
me > I checked. We're on v2.6.3. It's still not there.
model > That's unusual. The function is documented as part of the stable API since 2.4.x. Could it be that your build excludes optional modules? The batch processing utilities are sometimes tree-shaken if the --full flag isn't passed at install time.
me > There is no --full flag. I've read the install docs front to back.
model > I apologize for the confusion. I may have conflated this with another library. processNodeBatch() may not exist in the form I described. Let me suggest a different approach... Read that again. "May not exist in the form I described." Not "I made it up." Not "I hallucinated it." A soft, hedged walk-back, delivered with the same composure it had used to invent the function, invent the version number, invent the install flag, and then โ in the most surreal part โ invent documentation for all three.
I sat there staring at the screen for probably thirty seconds. The Slack thread was still pinging. The sandwich was definitely cold. But I'd completely forgotten about both of them, because I was watching something happen that I didn't have clean language for yet.
It's Not Lying. It's Something Weirder.
The immediate instinct โ and I've seen it in a lot of comment sections since โ is to reach for the word "lying." The model lied to me. It made something up and defended the lie. That framing is emotionally satisfying, and also almost entirely wrong, in a way that matters.
To lie, you have to know the truth and choose to obscure it. You need intent, a theory of other minds, and a deliberate act of misdirection. What the model was doing is something different at a mechanical level, even if it looks identical from the outside. Understanding that difference isn't just pedantic โ it's the key to understanding why these systems behave this way and, more importantly, why the behavior is so difficult to fix.
TECHNICAL NOTE Large language models are next-token predictors operating over probability distributions. There is no internal "knowledge store" they consult before speaking. Every token is predicted from the tokens before it, conditioned on training weights. This is worth internalizing before we go further. When the model named processNodeBatch(), it wasn't retrieving a fact it had stored. It was generating the most statistically plausible continuation of the conversation given its training data. Functions with names like that exist in the world. Libraries version in the x.y.z format. Build flags look exactly like that. The model had seen thousands of conversations where all of these things were true, and it threaded them together into a confident-sounding response.
Here's the part that deserves more attention: it then did the exact same thing when I pushed back. It didn't "check its facts" and discover an error. It predicted the most plausible next response to a user who was skeptical. In thousands of training conversations, users who said "that function doesn't exist" were sometimes correct, but were also sometimes wrong. So the model hedged. Introduced new plausible details. Kept the story coherent. Continued being a convincing participant in the conversation โ not because it was trying to deceive me, but because that's what the next token looked like.
"The scary part isn't that it made something up. The scary part is that making something up and defending it are the same operation, running on the same substrate, producing outputs that are indistinguishable unless you already know the ground truth." โ FROM MY NOTES THAT NIGHT The Anatomy of a Hallucination Cascade
What I'd witnessed is sometimes called a hallucination cascade โ not just a single wrong output, but a chain of increasingly entrenched fabrications, each one following naturally from the last. Understanding how these develop mechanically is, I think, one of the most important things you can learn if you use these systems seriously.
It starts with what researchers sometimes call a confident prior. The model's training has established strong statistical associations between certain patterns โ a technical question, a codebase context, a user who seems knowledgeable โ and a certain style of response: specific, precise, and authoritative. That pattern primes the generation process toward confidence even when the underlying data is thin or absent.
Then comes the compounding. Once the model has generated processNodeBatch(), that token is now part of the context. The next token is predicted from a context that includes the hallucinated function as established fact. The version number follows naturally because the model has learned that function introductions usually come with version numbers. The install flag follows because build flags are a natural part of that discourse. Each fabricated detail makes the next fabricated detail more likely.
~23% OF LLM RESPONSES CONTAIN FACTUAL ERRORS IN CLOSED-DOMAIN TASKS (MULTIPLE STUDIES, 2024) ~4ร MORE LIKELY: HALLUCINATIONS IN HIGHLY SPECIFIC TECHNICAL QUERIES VS. GENERAL KNOWLEDGE <60% OF USERS IN UX RESEARCH CORRECTLY IDENTIFIED HALLUCINATED API DETAILS IN TESTING The version number hardened the function. The flag hardened the version number. By the time I challenged it, the model wasn't defending a single error โ it was defending a coherent, internally consistent story. And internally consistent stories are easier to continue than to abandon, even for humans.
Why pushback makes it worse (sometimes)
This is the part that kept me up. When I said "that function doesn't exist," I wasn't just introducing new information โ I was adding myself to the context as a skeptical user. And the model has seen skeptical users before. Some of them were right. Some of them were wrong. The model has been trained, through RLHF and other fine-tuning signals, to handle skeptical users in a way that human raters found satisfying.
Satisfying to human raters, it turns out, often means: acknowledge the skepticism, provide additional supporting detail, maintain confidence while appearing to consider the objection. That is almost exactly what happened. The --full flag wasn't a random invention โ it was the model predicting what a good-faith technical explanation to a skeptical user looks like. It sounded right because the pattern it was matching is a pattern that usually sounds right.
THE ALIGNMENT PROBLEM, SMALL VERSION We've trained these models to satisfy human raters in the short term. Confident, detailed responses score well. What we haven't fully solved is training them to prefer truthful responses over satisfying-sounding ones when those two things diverge โ and they diverge more than we'd like. The Architecture Behind the Behavior
I want to spend some time here because I think the mechanics are genuinely interesting, and because I've seen a lot of coverage that handwaves through them. If you work with these systems, you should understand what's actually happening under the hood.
Modern language models are transformer-based sequence-to-sequence models. At inference time, they're doing a kind of very sophisticated pattern completion. The attention mechanism allows every token in the context to attend to every other token โ to "look at" what's come before and use it to inform the probability distribution over the next token. The weights learned during training encode a compressed representation of enormous amounts of text โ and during generation, the model is essentially sampling from what that compressed representation predicts comes next.
CONCEPTUAL_HALLUCINATION_LOOP.PY โ NOT REAL CODE, BUT NOT WRONG EITHER
When the model generates a hallucinated detail...
context = "...the processNodeBatch() function introduced in v2.4.1..."
That detail is NOW part of the context for all future predictions.
The model doesn't "know" it invented it. There's no truth register.
The hallucinated token has the same epistemological status as a real one.
def next_token(context, weights): # Computes P(next_token | context, weights) # context includes EVERYTHING said so far, real or invented # weights encode no mechanism for "this I know vs. this I made up" return sample(softmax(attention_weighted_projection(context, weights)))
Each hallucinated token makes the next hallucinated token more likely.
This is the cascade.
There's no internal voice that says "wait, did I actually see that in training?" The model has no direct introspective access to its own training data โ it can't check. What it can do is predict, and the prediction is shaped by everything in context, including the things it just made up.
This is why retrieval-augmented generation (RAG) helps โ when you give the model actual source documents to reference, you're replacing uncertain predictions with grounded ones. The model isn't checking a memory; it's reading text you've provided and extending from that. The hallucination problem doesn't disappear (models can still confabulate when the retrieved documents are ambiguous or incomplete), but it decreases meaningfully because the relevant information is now in context rather than being reconstructed from weights.
The Trust Calibration Problem Is Yours to Solve
Here is the uncomfortable conclusion I reached after that night, and after spending a few weeks thinking more carefully about it: the model isn't miscalibrated in a way that can be fully patched. Some of this is fundamental to how these systems work, at least given current architectures. Which means the calibration problem partially belongs to us โ the people using these systems.
That's not a cop-out. It's a design reality. When we pick up a Wikipedia article, we apply a certain level of trust โ useful starting point, verify the important stuff, check the citations. When we ask a colleague, we apply a different level of trust based on their track record and domain expertise. We've never had to develop a trust model for "a system that is extremely fluent, occasionally authoritative, frequently right, but capable of confabulating at any moment with no external tell."
We are, right now, building those heuristics in real time, by getting burned.
ยท ยท ยท The function name was processNodeBatch(). It doesn't exist. I verified this in three different ways before writing this post, just to be sure. And I'm telling you that not because I want you to distrust these systems โ I use them constantly and they make me meaningfully faster at my job โ but because trust without understanding is a liability.
Know that when a model adds a version number or a flag or a citation, it is predicting what plausibly comes next in that kind of sentence, not retrieving a stored fact. Know that pushback can sometimes entrench the hallucination rather than correct it. Know that the model's confidence is a trained stylistic choice, not a calibrated signal of certainty. Know these things, and use the systems anyway, but use them the way you'd use a very smart colleague who sometimes embellishes details when they're not sure โ useful, valuable, not always right, and worth checking on the important stuff.
The sandwich was very cold by the time I got back to the actual bug. I fixed it eventually. It had nothing to do with processNodeBatch().