Module 1: What Changes When a Component Is Non-Deterministic
The probabilistic contract
Overview
When you write a normal function, you have a contract with it: an agreement about what it will give you. normalize_sku(" wh 1000 xm5 ") gives you "WH-1000-XM5", today and in a year, on your machine and on your colleague's. That contract is so firm that you write it as a test: assert normalize_sku(x) == "WH-1000-XM5". If one day it fails, you know that something broke, and that's exactly what you want from a test. Your entire engineering instinct —test, verify, trust— is built on that exact-equality contract.
An LLM doesn't offer you that contract. With the same input it can give you different outputs, all valid, none identical to the previous one. It's not a bug: it's how a probabilistic model works. And that means assert ai_component(x) == "expected output" is a test broken from birth: it will fail not when something breaks, but when the model does exactly its job. This lesson dismantles that clash and repairs it. You're going to see the classic assert break on screen, understand why an LLM's contract can't be exact equality, and build the contract that does work: verifying properties and invariants of the output, not its literal value.
Connection with the module. Lesson 1 installed the thesis —an LLM is not a normal function—. This lesson makes it concrete in the most basic and most treacherous property: non-determinism, and what it does to how you test and trust the component. It's the basis of almost everything that follows: the eval in module 3 is this property contract scaled up to a set of cases with a threshold; the guardrail in module 4 is this contract applied at the boundary to reject invalid outputs; and the deterministic shell in module 6 is what executes these checks before letting the output touch the system. Here we only install it in its minimal form. The boundary with AI Engineering stays firm: we're not going to teach how to make the model more consistent (that's prompting, temperature, decoding techniques —AI Eng territory—); we're going to design the system that lives with the non-determinism the model has.
An analogy: ordering "the usual" at the coffee shop
You go to two places for breakfast.
The first is a vending machine. You press button B4 and a granola bar drops, exactly the same one, every time. You press B4 a thousand times and it's a thousand identical bars. If one day something else drops, the machine is broken, and you rightly demand it be fixed. Your contract with the machine is exact equality: B4 → that bar, always.
The second is a café with a barista. You say "the usual, a latte." They make it. You come back tomorrow and ask for "the usual": another latte, good, but the drawing in the foam is different, the temperature varies by a degree, maybe it's served in a different cup. Is the barista broken because today's latte isn't identical to yesterday's? Of course not. It would be absurd to hand back the coffee shouting "this isn't byte-for-byte the same as yesterday's!". Your contract with the barista was never exact equality. Your contract is a different one, and you have it perfectly clear even though you never wrote it down: that it's a latte (not a tea), that it's hot (not cold), that it fits in the cup (not overflowing), that it doesn't have salt instead of sugar. You verify properties, not an exact value. If the latte meets those properties, the barista did their job, however slightly different each cup is.
Here's the point: a deterministic function is the vending machine; an LLM is the barista. The mistake isn't that the barista varies —that's their nature—; the mistake is demanding the vending-machine contract from them. When you write assert ai_component(x) == "exact value", you're shouting at the barista that today's latte isn't identical to yesterday's. The fix isn't "make the barista a machine" (you'd lose exactly what makes them valuable); it's writing the right contract —verify that it's a latte, hot, in the cup, no salt— which is the contract you actually care about. This lesson writes that contract in code.
Worked example: the exact assert breaks, the property contract holds
We're going to put a deterministic function and a simulated ai_component side by side, and see which contract holds with each. The ai_component is a stub: it calls no real model, doesn't touch the network, uses no keys. It simulates the only thing that matters here —that with the same input it returns different texts—, delivering equivalent paraphrases in an order shuffled once with a fixed seed, so the result is reproducible and at the same time different on each call, just as a real LLM would with temperature > 0.
# Lesson 2: the probabilistic contract.
# Contrasts a deterministic function (exact assert) against an
# ai_component SIMULATED by a stub (same input -> different outputs).
import random
# --- Fixed seed: reproducible across runs. ---
_RNG = random.Random(42)
# --- Part A: a normal, deterministic function. ---
def normalize_sku(raw):
# Same input -> same output, ALWAYS. Testable with an exact assert.
return raw.strip().upper().replace(" ", "-")
# --- Part B: an AI component, SIMULATED (no network or real API). ---
# A real LLM with temperature > 0 returns different texts for the same
# input: here we mimic it by returning equivalent paraphrases in an order
# shuffled once with the fixed seed (reproducible, and different per call).
_PARAPHRASES = [
"Wireless noise-canceling headphones",
"Bluetooth headphones with active noise cancellation",
"BT headphones with noise cancelling and charging case",
"Wireless over-ear headphones with noise cancellation and mic",
]
_RNG.shuffle(_PARAPHRASES)
_call_index = 0
def ai_component(product_review):
# SIMULATES an LLM that summarizes a review into a short product title.
# Ignores the content and varies the output on each call: that's the point.
global _call_index
text = _PARAPHRASES[_call_index % len(_PARAPHRASES)]
_call_index += 1
return text
REVIEW = "loved them, they block out the subway noise so much"
print("=== Deterministic function: same input -> same output ===")
out1 = normalize_sku(" wh 1000 xm5 ")
out2 = normalize_sku(" wh 1000 xm5 ")
print(f"call 1: {out1!r}")
print(f"call 2: {out2!r}")
assert out1 == out2 == "WH-1000-XM5" # passes: the contract is exact equality
print("assert out1 == out2 == 'WH-1000-XM5' -> PASSES")
print()
print("=== ai_component: same input -> DIFFERENT outputs ===")
outs = [ai_component(REVIEW) for _ in range(3)]
for i, o in enumerate(outs, 1):
print(f"call {i}: {o!r}")
print()
print("=== The classic assert breaks ===")
try:
assert outs[0] == outs[1]
print("assert outs[0] == outs[1] -> PASSES")
except AssertionError:
print("assert outs[0] == outs[1] -> AssertionError (as we expected)")
print()
print("=== The contract that replaces it: properties, not equality ===")
def satisfies_contract(text):
# We don't assert WHAT it says; we assert it meets verifiable invariants.
return (
isinstance(text, str)
and 0 < len(text) <= 80 # non-empty and bounded
and "\n" not in text # a single line
and not any(w in text.lower() for w in ("http", "<script")) # no noise
)
for i, o in enumerate(outs, 1):
ok = satisfies_contract(o)
print(f"call {i}: contract={'OK' if ok else 'FAIL'}")
assert all(satisfies_contract(o) for o in outs)
print("assert all(satisfies_contract(o) for o in outs) -> PASSES")
What to expect. When you run the file, the output is exactly this:
=== Deterministic function: same input -> same output ===
call 1: 'WH-1000-XM5'
call 2: 'WH-1000-XM5'
assert out1 == out2 == 'WH-1000-XM5' -> PASSES
=== ai_component: same input -> DIFFERENT outputs ===
call 1: 'BT headphones with noise cancelling and charging case'
call 2: 'Bluetooth headphones with active noise cancellation'
call 3: 'Wireless over-ear headphones with noise cancellation and mic'
=== The classic assert breaks ===
assert outs[0] == outs[1] -> AssertionError (as we expected)
=== The contract that replaces it: properties, not equality ===
call 1: contract=OK
call 2: contract=OK
call 3: contract=OK
assert all(satisfies_contract(o) for o in outs) -> PASSES
Read the output in parts, because each block is a piece of the argument.
The first block is the vending machine. normalize_sku receives the same input twice and returns 'WH-1000-XM5' both times. The exact-equality assert passes, and it passes because it must: for a deterministic function, that's the right contract. If one day it failed, you'd know there's a real bug. Keep this sense of solidity, because it's exactly the one an LLM takes away from you.
The second block is the barista. The same REVIEW enters three times and three different titles come out —all three describe the same noise-canceling headphones, none identical to another—. Nothing broke; the ai_component did exactly what a probabilistic model does.
The third block is the clash. assert outs[0] == outs[1] —the reflex you carry from testing normal code— throws AssertionError. And notice the trap: it didn't fail because the component is wrong, it failed because the component is non-deterministic and your contract was the wrong one. A team that doesn't understand this will "fix" the test by copying the latest output into the expected value, and it will fail again on the next run, forever.
The fourth block is the repair. satisfies_contract doesn't ask what the output says; it asks whether it meets invariants: that it's a string, non-empty and at most 80 characters, a single line, and without dangerous noise (no http or <script). The three different outputs all three pass, and the assert all(...) passes. That's the probabilistic contract: you don't assert the value, you assert the properties. It's exactly the barista's contract —that it's a latte, hot, in the cup, no salt— written in code.
Going deeper: from exact equality to the property contract
It's worth making the mental shift explicit, because it's the one that holds up the whole guide.
A contract is an assertion about the output. With a deterministic function, the strongest possible assertion is exact equality: "the output is this value." It's the strongest because it leaves nothing unspecified. With an LLM you can't make that assertion —the output isn't a fixed value—, so you fall back to weaker but true assertions: "the output has these properties." It's not a defeat; it's the right contract for a probabilistic component, just as "that it's a hot latte" is the right contract for a barista.
The typical properties an LLM contract verifies —and that you're going to see again and again in the guide— fall into a few families:
Property family Example invariant Module that uses it
─────────────────────── ─────────────────────────────────────── ───────────────────
Structure / format valid JSON; has keys X, Y M4 (guardrail)
Length / shape non-empty; <= N characters; one line M4 (guardrail)
Allowed content contains no forbidden claims/words M4 (guardrail)
Set membership the category is in the valid catalog M6 (shell)
Aggregate quality >= 80% of an eval-set passes threshold M3 (eval gate)
Notice the last row, because it marks an important boundary. The first four families are verified per individual output: each output, on its own, meets or doesn't meet them. The fifth is different: an LLM's quality isn't judged well output by output (an individual "good" or "bad" output is subjective and varies), but in aggregate over a set of cases. "87% of the eval-set's 50 cases pass the threshold" is an assertion you can actually use as a gate, and it's exactly what module 3 builds. Here keep the intuition: the individual contract (properties per output) and the aggregate contract (eval over a set) are two layers of the same shift in perspective —from "I assert the value" to "I assert measurable properties"—.
Why this isn't "testing less." It might seem that verifying properties instead of the exact value is a weaker test, and therefore worse. It's the opposite. Exact equality is a fragile contract for an LLM: it passes by chance (when the model repeats an output) and fails by chance (when it varies), with no correlation to whether the component is fine. The property contract is robust: it passes when the output is useful and fails when it isn't, which is exactly what a test should do. Switching from equality to properties isn't lowering the bar; it's putting the bar where it really matters.
Where this contract lives in the system. A detail lesson 3 develops: the satisfies_contract function isn't part of the model —it's part of the deterministic shell that surrounds the model—. The LLM proposes the output; a deterministic function (this one) verifies its properties before the system uses it. It's deterministic, it's testable with an exact assert (its own inputs and outputs are fixed), and it's your control point. The probabilistic core produces; the deterministic shell verifies. You're already seeing it in miniature.
Common mistakes
Pinning the expected output to the value the model returned the first time. What happens: you write assert summarize(review) == "Noise-canceling headphones" copying what the model said today, the test passes, you commit it. Tomorrow the model says something else equally valid and the test fails; you "fix" it by copying the new output; the day after it fails again. Why it happens: the reflex of testing deterministic code is to pin the expected value, and that reflex is correct for normal code and poisonous for an LLM. How to spot it: you have AI tests that you "fix" by pasting the model's latest output, and they break by themselves again. How to fix it: never assert the exact value of an LLM output. Assert properties —format, length, allowed content, set membership—. If you really need to compare against references, that's an eval with an aggregate threshold (module 3), not an assert == per case.
Confusing "non-deterministic" with "broken" (or with "you have to force it to be deterministic"). What happens: an engineer sees the same input give different outputs and concludes the component is wrong, or gets obsessed with forcing temperature=0 and seeds so it "always gives the same thing," fighting against the model's nature. Why it happens: non-determinism feels like a failure because it contradicts everything we know about deterministic software. How to spot it: your plan to "stabilize" the AI is to make it repeat the same output, instead of containing its variation. How to fix it: accept variation as a fact of the component and design so it doesn't matter. Even with temperature=0 an LLM doesn't guarantee byte-for-byte determinism across model versions or infrastructures, so your robustness can't depend on that. The goal isn't for the barista to serve identical cups; it's for each cup to meet the contract. (How to tune the model itself is AI Engineering; here we design the system that lives with what the model is.)
Verifying only that it's "not empty" and calling it a contract. What happens: the team adds a single check —assert len(output) > 0— and feels it has already "validated" the LLM's output. Why it happens: it's the easiest property to verify, so it's taken as sufficient. How to spot it: your output contract has a single trivial condition, and clearly bad outputs (malformed JSON, a 5000-character text, a response with a <script>) would pass anyway. How to fix it: a useful contract verifies several families of properties relevant to that output: structure/format if you expect JSON, a length limit if it goes to a UI, allowed content if the user will see it, set membership if it must be a value from a catalog. Lesson 4 takes this to detail as a guardrail at the boundary; here it's enough that "not empty" is the floor, not the contract.
Exercises
Exercise 1 — Write the contract. Mercado uses an ai_component to classify each new product into one of these categories: {"audio", "computing", "home", "fashion", "books"}. The output must be exactly one of those five strings. Write the satisfies_contract(output) function that verifies the right contract, and explain why an assert classify(product) == "audio" would be the wrong contract.
See solution
The right contract is set membership: the output must be one of the five valid values.
VALID_CATEGORIES = {"audio", "computing", "home", "fashion", "books"}
def satisfies_contract(output):
return isinstance(output, str) and output in VALID_CATEGORIES
Why assert classify(product) == "audio" is the wrong contract: it pins the output to a specific value, but the point of a classifier is that different products fall into different categories, and even for the same product the model might hesitate between two valid categories on different calls. Asserting == "audio" proves a single thing (that this particular product gave "audio" this time), is fragile, and doesn't capture what really matters: that the output is always a valid category from the catalog, whatever it is. If the model returned "electronics" or "" or "audio, computing", the membership contract catches it; the assert == to a fixed value doesn't. This is exactly the contract that module 6's deterministic shell uses to contain the classifier.
Exercise 2 — Which property family? For each LLM output in Mercado, say which property family (structure/format, length/shape, allowed content, set membership, aggregate quality) is the most important to verify, and give an example of the concrete invariant: (a) the JSON the support agent proposes with {"order_id", "amount"}; (b) the short product title shown on a UI card; (c) the generated description the seller will publish; (d) knowing whether a new version of the search prompt is better or worse than the previous one.
See solution
- (a) The agent's JSON → structure/format. Invariant: "it's a valid JSON object, has exactly the keys
order_id(str) andamount(number ≥ 0)". Without this check, the shell couldn't even read the proposal. It's the first filter before validating the refund policy. - (b) Title on a card → length/shape. Invariant: "non-empty, ≤ 60 characters, a single line". A 400-character title would break the card's layout even if the text is correct. The shape matters because it goes into a bounded visual space.
- (c) Description to publish → allowed content. Invariant: "contains no forbidden claims (
cure,100% guaranteed,the best in the world) nor external contact info". Since the end user will see it published, the risk is the content, not the shape. - (d) Is the new prompt better? → aggregate quality. Invariant: "the eval-set's score (for example, recall@10 over 50 labeled queries) doesn't drop below the threshold". This one isn't verified per individual output, but over a set of cases —it's module 3's eval gate—. The other three are verified output by output; this one, in aggregate.
Exercise 3 — The test that fixes itself (and why it's wrong). A colleague has this test for the description generator and complains that "it fails about one in three times for no reason":
def test_description():
out = ai_component({"type": "headphones", "battery_h": 30})
assert out == "Headphones with 30 h of battery"
Explain exactly why it fails intermittently, why "copying the latest output into the expected value" doesn't fix it, and rewrite the test with the right contract.
See solution
Why it fails intermittently: the ai_component is non-deterministic, so with the same input it returns different texts —"Headphones with 30 h of battery", "Headphones with 30 hours of runtime", "Lightweight headphones, 30 h battery"—, all valid. The assert out == "Headphones with 30 h of battery" only passes the times the model, by chance, produces that exact string; the rest of the time it fails. There's no "reason" tied to a bug: the test is measuring the wrong property.
Why copying the latest output doesn't fix it: changing the expected value to the latest output the model gave only moves which is the "lucky" output that passes. The next run again produces another variant and fails again. It's a hamster wheel: the problem isn't what value you expect, it's that you expect a value.
The test with the right contract —verify properties, not equality—:
def test_description():
out = ai_component({"type": "headphones", "battery_h": 30})
assert isinstance(out, str)
assert 0 < len(out) <= 200 # non-empty and bounded
assert "\n" not in out # a single line
assert "30" in out # mentions the key data point from the input
banned = ("cure", "100% guaranteed", "the best in the world")
assert not any(b in out.lower() for b in banned) # no forbidden claims
This test passes for any valid description —however it varies— and fails only when the output really isn't useful (empty, extremely long, with a forbidden claim, or not even mentioning the 30 h). That's the probabilistic contract: assert properties that capture what matters, not a literal value the model has no reason to repeat.
Summary and next step
In this lesson you saw, executed, the heart of why an LLM is not a normal function: with the same input it gives different outputs, and the exact-equality assert breaks —not when something fails, but when the model does its job—. You understood that the right contract isn't asserting the value of the output but verifying its properties and invariants: structure, length, allowed content, set membership, and —in aggregate— quality over an eval-set. And you saw that this shift isn't testing less, but testing where it really matters: the property contract passes when the output is useful and fails when it isn't, which is exactly a test's job. Like the barista: you don't demand identical cups, you demand each cup be a hot latte in the cup with no salt.
Before moving on you should be able to: explain why assert ai_component(x) == value is a contract broken from birth; name at least three families of properties an LLM contract verifies; distinguish the individual contract (per output) from the aggregate one (eval over a set); and rewrite an exact-equality test as a property test.
Lesson 3 takes the satisfies_contract function you just wrote and shows where it lives in the system. Because verifying the output is only half the picture: the other half is that the LLM proposes and a deterministic layer disposes. You're going to see, executed, the antipattern where the model executes its own output and "refunds" $9999 hallucinated, against the pattern where the deterministic shell validates the proposal against Mercado's policy and blocks it. The LLM as a component behind a boundary, not as the whole system.
Resources
- Martin Fowler and Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. Treats evals and output verification as first-class patterns. The section on evaluating non-deterministic outputs is the direct complement to this lesson. In English.
- Chip Huyen, AI Engineering (O'Reilly, 2024), evaluation chapters. The distinction between verifying an individual output and evaluating quality in aggregate —this lesson's jump to module 3— is developed there. We keep the design layer; the detail of how to build the eval-set is the boundary with AI Eng. In English.
- Claude documentation — docs.anthropic.com. See the section on why outputs vary and what parameters like
temperaturecontrol, without fixating on a model version. Useful to understand where the non-determinism this lesson designs to contain comes from. In English. - Anthropic, "Building Effective Agents" (2024) — anthropic.com/engineering/building-effective-agents. Reinforces the idea of verifying and bounding what the AI component produces before acting on it, which is this lesson's bridge to the next. In English.