BusyIncorporated
Research Note / Hallucination Reduction

Deterministic identifiers for LLMs

Some citation failures are not retrieval failures. They are identifier failures: long random strings fracture into messy tokens, drift across model turns, and come back one character wrong. A better ID format can make failures easier to diagnose, and often makes the whole system more accurate too.

Working paperDeterministic Identifiers for Large Language Models6 pages · 60 KB · 140 runs, 14 modelsDownload
Why citations disappearTokenizer view
UUID / fragmented
f1166652-812d-480d-8d11-5495e8b92e31
f1166652-812d-480d-8d11-5495e8b92e31
Irregular chunks. Irregular punctuation. Easy to transpose. Hard to notice when one symbol drifts.
DTID / atomic
263-037-515-764-525
263-037-515-764-525
Fixed-width triplets behave like clean atoms. The model sees a repeating pattern instead of noise.
01 / The failure mode

Some broken citations are copy errors, not search errors

In a retrieval system, it is natural to blame missing citations on bad ranking, bad embeddings, or missing source data. But in practice, some failures happen after retrieval succeeds. The model finds the right citation, carries its identifier through multiple tool calls and model turns, and then emits a string that is almost right.

Almost right is still broken. If the database expects one exact identifier and the model returns a nearby variant, the frontend cannot resolve it. The citation vanishes. The user sees a plain sentence instead of a linked, inspectable source. From the outside it looks like the model never found the source at all.

That distinction matters because it changes the solution. If the failure is in recall, you improve search. If the failure is in representation, you improve the identifier. And just as important, you make the failure legible enough that engineers can tell where the system went wrong instead of treating every broken citation like a retrieval mystery.

What the system sees

A UUID is globally useful, but locally hostile to language models

UUIDs are great for databases and distributed systems. They are terrible when the same string must be carried through natural-language generation, tool output, prompt stuffing, and final rendering.

404e-a104-7b9c-48d8-b9c2-1f8af2c1147e One missing character. One swapped chunk. One extra hyphen. The citation is gone.
What the model needs

The identifier should have shape, rhythm, and obvious boundaries

An LLM-friendly identifier should be regular enough to survive repetition. The model should not have to memorize a pile of meaningless subfragments. It should carry a short sequence of clear units.

263-037-515-764-525 Five stable groups. Zero-padded. Easy to compare by eye. Easy to preserve across turns.
When an identifier becomes legible to the tokenizer, it becomes more durable inside the model.
02 / Tokenizers

The tokenizer is where the problem starts

LLMs do not read strings the way humans do. They read tokens: irregular chunks chosen by a learned tokenizer. Common words often map cleanly. Unfamiliar strings do not. A UUID tends to shatter into inconsistent pieces: one token for part of a hex group, another for a hyphen plus a letter, another for a trailing fragment that means nothing on its own.

That fragmentation creates two layers of difficulty. First, the model has more pieces to preserve. Second, those pieces do not correspond to semantic units. There is no human-like sense that one chunk belongs with the next. The string is just statistical debris.

A fixed pattern of three-digit groups changes that. The tokenizer tends to preserve each triplet as an atom. The resulting sequence is not shorter only in characters; it is cleaner in model-space.

01

Fixed width

Every group should look identical from the model's perspective. Zero padding matters because it removes visual ambiguity.

02

Stable separators

Hyphens or colons are fine. The key is a repeating rhythm the model can predict and preserve.

03

Enough entropy

Five groups of three digits gives about 10^15 combinations: plenty for citation-scale identifiers without turning them back into noise.

04

Optional determinism

The same shape can be random or derived from a stable source value like a record id or SHA, depending on the use case.

03 / The format

A simple answer: five zero-padded triplets

The proposed format is intentionally plain. Instead of a 36-character UUID, use five groups of three digits: 263-037-515-764-525. This is long enough to avoid practical collisions at citation scale, but structured enough that a model can carry it through a conversation without constantly breaking it apart.

The deeper point is not the punctuation. Hyphens, colons, brackets, or wrapper syntax can change by surface. What matters is the underlying shape: a repeated sequence of compact, fixed-width atoms.

Once the shape is stable, the rest of the system gets easier to inspect. Human debugging gets easier. Prompt examples get easier. Render-time validation gets easier. If a model drifts, the drift is visible, which reduces a lot of the chaos in understanding whether the failure lived in retrieval, transport, prompting, or rendering.

class DtidUtility
  def self.generate_random
    5.times.map { format("%03d", rand(1000)) }.join("-")
  end

  def self.generate_from_sha(input)
    digest = Digest::SHA256.hexdigest(input.to_s)
    digits = digest.scan(/[0-9a-f]{3}/).first(5).map { |chunk| chunk.to_i(16) % 1000 }
    digits.map { |n| format("%03d", n) }.join("-")
  end
end
04 / Benchmarks

In one benchmark, the error rate dropped substantially

A useful stress test is simple: give the model a large set of identifiers and ask it to sort them. The task forces the model to read, preserve, and re-emit every identifier. That makes transposition errors measurable.

On UUIDs, errors show up quickly. On deterministic triplet identifiers, the exact same task became noticeably more stable in this test. That matters for accuracy, but also because cleaner identifiers make it easier to understand what happened when a system does fail, especially on smaller or faster models where representation debt shows up sooner.

ModelUUID benchmarkDTID benchmarkChange
Gemini 2.5 Pro1.1% error / 989 valid0.0% error / 1000 validBest observed case: zero drift in this run
Gemini 2.5 Flash8.5% error / 915 valid1.2% error / 988 validAbout 7x fewer errors
Gemini 2.5 Flash Lite98.9% error / 11 validImproved materially / still unstableSuggests representation still matters here
OpenAI 4.14.9% error / 951 validNot shown in final rerunSuggests headroom even on stronger models

This can matter more in agentic systems than in single-turn chats

Single-turn prompting already stresses identifiers. Multi-step agents stress them far more. A citation id might move from retrieval output to tool result, from tool result to planner, from planner to model response, from model response to renderer. Every hop is a chance to mutate the string.

That is one reason production systems can look worse than toy benchmarks. The identifier is not merely generated once. It circulates. And when it circulates in a cleaner format, it becomes easier to spot whether the break happened in retrieval, in a tool handoff, or at final render time.

1. Search finds the right record
Retrieval succeeds and the system now has a correct internal citation id.
2. The id crosses model boundaries
Tool calls, reasoning traces, chain-of-thought summaries, and final prompts all repeat the identifier.
3. One microscopic drift breaks rendering
The model returns a string that looks almost right, but no longer resolves against the database.

Why it gets misdiagnosed as a retrieval problem

The renderer reports one thing
A citation tag either resolves against the database or it does not. That is the only signal it emits.
Two different causes land in it
A retrieval miss and a transcription error arrive as the same event: a tag that did not resolve.
So the obvious fix misses
Teams tune retrieval, because that is the cause they can see. The identifiers that were found correctly and then mangled in transit stay broken.
05 / System design

Identifiers should be treated as part of the model interface

Traditional software treats identifiers as back-office implementation details. In LLM systems, that is no longer true. The identifier is now part of the model interface. It lives inside prompts. It gets copied by a generative model. It is parsed by a renderer. That means identifier design belongs next to prompt design, tool design, and output-schema design.

The right mental model is not only “make IDs unique.” It is also “make IDs survivable.” Uniqueness is table stakes. Survivability makes the system easier to reason about when something goes sideways.

That does not mean every system should abandon UUIDs globally. It means systems should be willing to introduce an LLM-facing identifier layer where exact string fidelity matters. You can keep UUIDs internally and project a deterministic, tokenizer-friendly alias outward.

Implementation pattern

Keep the alias close to the data model

Create a dedicated citation-identifier mapping table, generate the deterministic id once, validate uniqueness, and let prompts speak only in that alias. The model never needs to see the raw UUID.

Operational advantage

The format is debuggable by humans too

When an identifier is visually structured, engineers can spot transpositions faster, compare examples faster, and write sanity checks that match the mental model of the system.

06 / Takeaway

Context engineering is not only about prompts. It is also about shapes.

LLM systems fail on small things that classical software barely notices: a separator, a token boundary, a repeated shape that the model can or cannot hold in working memory. Those details feel cosmetic until they become the difference between a working citation and an invisible one.

Deterministic identifiers are a narrow idea, but they point at a broader lesson. If a string has to survive inside a language model, its form matters. Human readability matters. Tokenizer regularity matters. Repetition matters. In agentic systems, representation is part of the architecture.

Results / August 21, 2026

What the format is worth, measured

Two effects, at two ends of the range. On small models the format buys accuracy: fewer identifiers come back wrong. On frontier models at production scale it buys something blunter — whether the model does the work at all.

UUIDDTID100 identifiers · 5 trials each
0%5%10%15%20%identifiers returned wronggpt-4.1-nano: UUID 21% → DTID 8.7%gpt-4.1-nano−12.3ptgpt-4o-mini: UUID 12.8% → DTID 11%gpt-4o-mini−1.8ptgpt-3.5-turbo: UUID 12% → DTID 6.8%gpt-3.5-turbo−5.2ptgpt-5.4-nano: UUID 9% → DTID 11.2%gpt-5.4-nanoworsegpt-5.4-mini: UUID 8% → DTID 0.2%gpt-5.4-mini−7.8ptclaude-3-haiku: UUID 5.6% → DTID 3%claude-3-haiku−2.6ptgemini-2.5-flash-lite: UUID 2.4% → DTID 0.4%gemini-2.5-flash-lite−2.0ptgemini-2.5-flash: UUID 0.6% → DTID 0%gemini-2.5-flash−0.6ptgemini-3.5-flash-lite: UUID 0.6% → DTID 0.2%gemini-3.5-flash-lite−0.4ptclaude-haiku-4.5: UUID 0.4% → DTID 0%claude-haiku-4.5−0.4pt
Frontier models are not shown here. At 100 identifiers they sit at zero in both formats — the task is too small to separate them. That is a fact about the test size, not about the format, and the next chart is what happens when the task grows.

At production scale, the frontier model quits

Raise the task from 100 identifiers to 1,000 — the scale a real citation pipeline hits — and a second effect appears on exactly the models that showed nothing before.

gpt-5.5 recovered zero of 1,000 citations under UUIDs. Not corrupted: abandoned. It stopped partway or declined outright, three runs from three. Under DTIDs the same model on the same task returned a perfect 1,000, three times from three. One variable changed.

gemini-2.5-pro lost citations on every UUID run and lost none on any DTID run. gemini-3.1-pro averaged 904 of 1,000 under UUIDs against 998 under DTIDs. claude-sonnet-5 could not complete the task in either format, at any setting we could give it.

UUIDDTID1,000 identifiers · 3 trials each
02505007501000citations recovered intact, of 1,000gpt-5.5: UUID 0/0/0 · DTID 1000/1000/1000gpt-5.50 0 · 0 · 0 — quit on every UUID run1000 1000 · 1000 · 1000gemini-2.5-pro: UUID 999/994/996 · DTID 1000/1000/1000gemini-2.5-pro996 999 · 994 · 9961000 1000 · 1000 · 1000gemini-3.1-pro: UUID 997/995/721 · DTID 999/997/999gemini-3.1-pro904 997 · 995 · 721998 999 · 997 · 999claude-sonnet-5could not complete the task in either format
Each bar is the mean of three trials; individual trials follow in grey. DTID returned every one of 1,000 identifiers, in every trial, on both models that finished. gpt-5.5 recovered nothing under UUIDs — it abandoned or declined all three runs — and returned a perfect 1,000 under DTIDs, three times out of three.

When gpt-5.5 declined, it said why: “I can’t reliably sort and return all 1000 identifiers manually without risking omissions or ordering errors.” Handed the identical task in the shorter format, it did not hesitate and did not miss.

One caveat on the other side. On several older small models, DTIDs sometimes invited invention: the model stopped transcribing and emitted hundreds of well-formed identifiers that were never in the input. Nine of 69 DTID runs did this; zero of 66 UUID runs did. Five random triplets are indistinguishable in form from a real identifier, so exact-match validation against the database stays mandatory under either format.

Why it works

Nine tokens, one shape, every time

Across 1,000 identifiers per format on both OpenAI tokenizers: every DTID is exactly 9 tokens with one identical fragmentation shape. UUIDs average 22.7 tokens, and all 1,000 fragment differently — no two share a shape. DTIDs are also 54% fewer tokens, so identifier-heavy prompts get cheaper at the same time.

UUID mean 22.7 tokens · 1,000 distinct shapes DTID always 9 tokens · 1 shape
Hypothesis

Representational overhead

Carrying an irregular string may cost the model something that competes with the task itself. A UUID is 22.7 tokens of unrelated fragments, and nothing about the four-hundredth identifier helps produce the four-hundred-and-first. A DTID is nine tokens in a shape already seen a thousand times. That would explain both results: on a small model the overhead shows up as errors, and on a large model facing a thousand of them, as giving up. Testing it properly is future work.

Harness, raw responses, and scoring code: the busy-research repository.