Everyone knows AI can’t be trusted, gotta be slow if you haven’t caught on. To solve this for LlamaBrain, I created the Double-Lock Determinism. This approach uses two complementary systems to ensure that identical game states can produce byte-for-byte identical outputs, every single time.
LLM outputs are stochastic and non-deterministic by default. Using the same input can produce different outputs. In a stateful environment this can:
- Break save/load functionality.
- Make bugs impossible to reproduce.
- Force you to chase your tail…
While this randomness mimics creativity, it is a nightmare for software engineering. It breaks save/load functionality, makes automated testing unreliable, and renders bugs nearly impossible to reproduce.
The Concept
In the Double-Lock approach, determinism in Generative AI requires controlling two variables:
- The Input: The prompt must be constructed identically down to the last character.
- The Stochasticity: The random sampling of the model must be constrained.
I call this the Double-Lock: Context Locking and Entropy Locking.
Double-Lock Part 1: Context Locking
The first step is to deterministic prompt assembly is to ensure that when your system retrieves context items to build a prompt, it does so in a rigid, stable, and reproducible order.
If two context items have the same relevance score, standard sorting algorithms can flip their order randomly. To an LLM, [Item A, Item B] is a different prompt than [Item B, Item A]. This will result in a different output.
The Implementation: Enforce a strict sorting hierarchy that eliminates all ambiguity.
C#
// Implementation in ContextRetrievalLayer.cs
var memories = memoryList
.OrderByDescending(s => s.Score) // Primary: Relevance
.ThenByDescending(s => s.Item.CreatedAtTicks) // Secondary: Recency
.ThenBy(s => s.Item.Id, StringComparer.Ordinal) // Tertiary: Culture-ID
.ThenBy(s => s.Item.SequenceNumber) // Quaternary: Tie-breaker
.ToList();
Why this works:
StringComparer.Ordinal: This is crucial. If you use default string comparison, your game might sort differently on a PC in the US vs. a PC in Germany, breaking save compatibility across regions.SequenceNumber: This serves as the “Nuclear Option” for tie-breaking. Every item gets a unique incrementing integer upon creation. Even if relevance, time, and IDs match, the sequence number will differ.
Double-Lock Part 2: Entropy Locking ()
Even with two perfectly identical prompts, an LLM (with a temperature > 0) will output different tokens. This technique locks the random number generator.
Most modern AI APIs (like Anthropic, OpenAI, or Llama.CPP) allow you to pass a seed parameter. So for a seed I use a Counter. It is simply a persistent integer tracking how many times the system has interacted with the AI.
The Implementation:
C#
// Implementation in ApiClient.cs
var request = new CompletionRequest
{
Prompt = assembledPrompt,
Seed = interactionContext.InteractionCount
};
Why this works:
- It ensures that “Turn 50” in a conversation always rolls the same dice, provided the prompt is the same.
- It eliminates the need to save massive RNG state vectors; you simply need to persist a single integer (
InteractionCount) in your save file.
The Result
By combining these two locks, I can achieve a mathematical guarantee of reproducibility:
f(Prompt,Context,Counter)=Output
This enables:
- Reliable Save/Load: Players return to the exact narrative state they left.
- Regression Testing: I have written automated tests that expect specific text responses.
- Sanity-Saving Debugging: If a user reports a crash or a hallucination, I can reproduce it exactly by loading their save file.
You can see this in action on my GitHub.
Common Pitfalls
Implementing this requires discipline. Here are the most common mistakes that will silently break your determinism:
| The Mistake | The Solution |
| Culture-Sensitive Sorting | Always use StringComparer.Ordinal. Never let the user’s OS language settings dictate prompt construction. |
| Reassigning IDs on Load | When loading a save game, preserve the original SequenceNumber. Do not generate new IDs. |
| Transient Seeds | Forgetting to save InteractionCount means the “dice roll” resets on load. It must be part of the persisted save data. |
| Wall-Clock Time | Never use DateTime.Now in your logic. Use a deterministic “Snapshot Time” derived from the game ticks. |