Module 1: What Changes When a Component Is Non-Deterministic
Mini-project: place an AI feature into Mercado
Overview
This is the module's capstone. Over seven lessons you installed the method for seeing an AI component: its contract is probabilistic (lesson 2), it lives behind a boundary and it proposes (lesson 3), it drags in five properties at once (lesson 4), it's a core inside a shell (lesson 5), that shell is sized by its tolerance (lesson 6), and everything is summarized in a property sheet (lesson 7). Now you apply it end to end to a real Mercado feature —the "describe your product" generator, the one that, from the attributes a seller loads, proposes a ready-to-publish description— and you execute it.
The project's work is the real work of an architect before AI Engineering builds anything: taking an AI feature the business wants, and placing it —deciding where it lives, how much non-determinism it tolerates, how it splits into core and shell, what contract its output has, what contains its proposals, and emitting its sheet with a brief that justifies the design—. Notice the boundary, which is deliberate and is the one of the whole guide: this project does not build the generator. It doesn't write the prompt, doesn't design the RAG, doesn't choose the model internally —that's AI Engineering—. It produces what goes before and around that: the containment architecture that makes it safe to put that generator into Mercado.
Connection with the module. It's the integration of the seven lessons into a single executed deliverable. Step 1 uses the tolerance from lessons 1 and 6; step 2 uses the core/shell from lesson 5; step 3 uses the probabilistic contract from lesson 2; step 4 uses the "propose/dispose" boundary from lesson 3; and step 5 emits the sheet from lesson 7. When you finish it you'll have the artifact that opens the work of the rest of the guide: a placed AI feature, with its property sheet, ready for modules 2 through 7 to fill in each field well. The next step —building the budget, the eval, the guardrail, the fallback— is literally the rest of the modules, and the final capstone (M8) will do this same exercise building each mechanism.
The reference solution, executed
We're going to build the solution in a single program that does the five steps: classifies the tolerance, separates core and shell, demonstrates the probabilistic contract, shows the shell containing a bad proposal, and emits the sheet with the brief. All with fixed data and the LLM simulated by a stub —no network, keys, or real APIs—, so the output is reproducible.
The five steps, in one program
# Mini-project M1: architect the PLACEMENT of an AI feature into Mercado.
# Feature chosen: "describe_your_product" (the generator for sellers).
# Boundary: we do NOT build the generator/prompt/RAG (that's AI Engineering).
# Here only its ARCHITECTURAL PROPERTIES: tolerance, core/shell,
# the probabilistic contract, and the shell containing a bad proposal.
import random
from dataclasses import dataclass, asdict
_RNG = random.Random(2025)
# ------------------------------------------------------------------
# Step 1 — Classify its tolerance to non-determinism.
# ------------------------------------------------------------------
touches_money_or_state, error_cost, blast_if_wrong = 1, 3, 2
nd_tolerance = 18 - (touches_money_or_state + error_cost + blast_if_wrong)
print("=== Step 1: tolerance to non-determinism ===")
print(f" touches_money_or_state={touches_money_or_state} "
f"error_cost={error_cost} blast_if_wrong={blast_if_wrong}")
print(f" nd_tolerance = {nd_tolerance}/15 -> TOLERANT feature, MEDIUM shell")
print(" (the seller reviews the text before publishing: there's a human in the loop)")
# ------------------------------------------------------------------
# Step 2 — Probabilistic core vs deterministic shell.
# ------------------------------------------------------------------
print()
print("=== Step 2: core (LLM) vs shell (deterministic) ===")
print(" core : propose a description from the attributes")
print(" shell : validate length, forbid false claims, require human review")
# ------------------------------------------------------------------
# Step 3 — The probabilistic contract (the exact assert breaks).
# ------------------------------------------------------------------
_DRAFTS = [
"Lightweight wireless headphones with up to 30 h of battery.",
"Comfortable BT headphones with 30 h of runtime and charging case.",
"Cordless, lightweight headphones with great battery life.",
]
_RNG.shuffle(_DRAFTS)
_i = 0
def ai_component(attributes):
# SIMULATES the generator LLM: same input -> different drafts.
global _i
text = _DRAFTS[_i % len(_DRAFTS)]
_i += 1
return text
attrs = {"type": "headphones", "battery_h": 30, "wireless": True}
outs = [ai_component(attrs) for _ in range(3)]
print()
print("=== Step 3: probabilistic contract ===")
for k, o in enumerate(outs, 1):
print(f" draft {k}: {o!r}")
try:
assert outs[0] == outs[1]
print(" assert outs[0] == outs[1] -> PASSES")
except AssertionError:
print(" exact assert outs[0] == outs[1] -> BREAKS (non-determinism)")
def satisfies_contract(text):
return isinstance(text, str) and 0 < len(text) <= 200 and "\n" not in text
assert all(satisfies_contract(o) for o in outs)
print(" property contract (len<=200, one line, str) -> PASSES for all 3")
# ------------------------------------------------------------------
# Step 4 — The shell contains a bad proposal (forbidden claim).
# ------------------------------------------------------------------
BANNED_CLAIMS = ["cure", "100% guaranteed", "the best in the world", "miracle"]
def deterministic_shell(draft):
low = draft.lower()
for claim in BANNED_CLAIMS:
if claim in low:
return (False, f"forbidden claim: {claim!r}")
if len(draft) > 200:
return (False, "exceeds 200 characters")
return (True, "fit for seller review")
print()
print("=== Step 4: the shell contains an out-of-policy proposal ===")
proposals = [
"Comfortable headphones with 30 h of battery.",
"Miracle headphones that cure insomnia, 100% guaranteed.",
]
for p in proposals:
ok, reason = deterministic_shell(p)
print(f" {'FIT' if ok else 'BLOCKED':<9}: {reason:<28} <- {p[:40]!r}")
# ------------------------------------------------------------------
# Step 5 — The property sheet + the decision brief.
# ------------------------------------------------------------------
@dataclass
class AIComponentSheet:
name: str
location: str
nd_tolerance: int
latency_budget_ms: int
cost_budget_usd: float
eval_gate: str
guardrail: str
fallback: str
deterministic_shell: str
feedback_loop: str
sheet = AIComponentSheet(
name="describe_your_product",
location="behind catalog-service, in the seller's product-listing flow",
nd_tolerance=nd_tolerance,
latency_budget_ms=3000,
cost_budget_usd=0.004,
eval_gate="rubric over 20 samples: clarity + no false claims; drops -> blocks deploy",
guardrail="list of forbidden claims + length limit on the output",
fallback="if the model goes down -> template by attributes (no AI)",
deterministic_shell="medium: validates and requires the seller to approve before publishing",
feedback_loop="the seller's edits to the draft feed the eval-set",
)
print()
print("=== Step 5: the component's property sheet ===")
for field, value in asdict(sheet).items():
if field == "name":
continue
print(f" {field:<20} {value}")
print()
print("=== Decision brief ===")
print(" What: place describe_your_product as an AI component in catalog-service.")
print(" Why contained: it touches only an editable draft; the seller approves;")
print(" the shell blocks forbidden claims; the fallback is a no-AI template.")
print(" Boundary: how the generator is built (prompt/RAG) -> AI Engineering.")
What to expect. When you run the file, the output is exactly this:
=== Step 1: tolerance to non-determinism ===
touches_money_or_state=1 error_cost=3 blast_if_wrong=2
nd_tolerance = 12/15 -> TOLERANT feature, MEDIUM shell
(the seller reviews the text before publishing: there's a human in the loop)
=== Step 2: core (LLM) vs shell (deterministic) ===
core : propose a description from the attributes
shell : validate length, forbid false claims, require human review
=== Step 3: probabilistic contract ===
draft 1: 'Comfortable BT headphones with 30 h of runtime and charging case.'
draft 2: 'Lightweight wireless headphones with up to 30 h of battery.'
draft 3: 'Cordless, lightweight headphones with great battery life.'
exact assert outs[0] == outs[1] -> BREAKS (non-determinism)
property contract (len<=200, one line, str) -> PASSES for all 3
=== Step 4: the shell contains an out-of-policy proposal ===
FIT : fit for seller review <- 'Comfortable headphones with 30 h of batt'
BLOCKED : forbidden claim: 'cure' <- 'Miracle headphones that cure insomnia, 1'
=== Step 5: the component's property sheet ===
location behind catalog-service, in the seller's product-listing flow
nd_tolerance 12
latency_budget_ms 3000
cost_budget_usd 0.004
eval_gate rubric over 20 samples: clarity + no false claims; drops -> blocks deploy
guardrail list of forbidden claims + length limit on the output
fallback if the model goes down -> template by attributes (no AI)
deterministic_shell medium: validates and requires the seller to approve before publishing
feedback_loop the seller's edits to the draft feed the eval-set
=== Decision brief ===
What: place describe_your_product as an AI component in catalog-service.
Why contained: it touches only an editable draft; the seller approves;
the shell blocks forbidden claims; the fallback is a no-AI template.
Boundary: how the generator is built (prompt/RAG) -> AI Engineering.
Let's walk through the output step by step, because each block is one of the module's lessons, applied.
Step 1 — the tolerance (lessons 1 and 6). "Describe your product" scores 1 on touches_money_or_state (it touches neither money nor state: it produces an editable draft), 3 on error_cost (it's going to be published with Mercado's brand, so a bad output hurts), and 2 on blast_if_wrong (if it fails, it affects one seller, not half the platform). Tolerance = 12: a tolerant feature, with a medium shell. And a design observation that greatly lowers the risk: there's a human in the loop —the seller reviews the draft before publishing—, which makes even a model error not reach the end customer on its own.
Step 2 — core and shell (lesson 5). The partition is explicit: the core has a single responsibility —propose a description from the attributes, the only thing an LLM contributes here—; the shell carries everything else —validate length, forbid false claims, require human review—. Small core, robust shell, as the reactor rule mandates.
Step 3 — the probabilistic contract (lesson 2). The same input (attrs) enters the ai_component three times and three different drafts come out, all three valid. The assert outs[0] == outs[1] —the normal-code reflex— breaks. The right contract, by properties (str, non-empty, ≤ 200 characters, a single line), passes for all three. It's lesson 2 made real in this feature: you don't assert the text, you assert its properties.
Step 4 — the shell contains (lesson 3). Two proposals hit the deterministic_shell. The first ("Comfortable headphones with 30 h of battery") is fit. The second ("Miracle headphones that cure insomnia, 100% guaranteed") is blocked for containing a forbidden claim —the shell detected "cure" inside the text—. The model proposed; the shell disposed; the false claim never got published. Exactly the "propose/dispose" pattern of lesson 3, applied to content instead of money.
Step 5 — the sheet and the brief (lesson 7). The sheet summarizes the whole design in ten fields: where it lives (in catalog-service, in the listing flow), its tolerance (12), its budget (3 s, $0.004), its eval (a rubric that blocks the deploy if it drops), its guardrail (claims + length), its fallback (a template by attributes, no AI, for when the model goes down), its shell (medium, with seller review), and its data loop (the seller's edits improve the eval-set). And the brief closes with the what, the why it's contained, and —crucially— the boundary: how the generator is built internally is AI Engineering, not this project.
What to deliver
Your mini-project deliverable has two parts, and both are the ones the program produced:
- The executed program, with the five steps, running with the LLM simulated by a stub (no real API) and producing the literal output above —or your variant, if you chose another feature—.
- The property sheet and the decision brief of the feature: the ten fields filled in (none blank) and a paragraph that justifies why the component's non-determinism is contained.
You can do the project with "describe your product" (the reference solution) or, for a bigger challenge, with the semantic search or the support agent —the two features the guide's capstone (M8) recommends—. If you choose the support agent, your step 4 will be the richest: the shell validates refund proposals against policy (as in lesson 3), and your sheet will have all fields "thick." Whichever you choose, respect the boundary: you place and contain the component; you don't build it.
Common mistakes
Crossing the boundary and starting to build the generator. What happens: in step 2, instead of declaring the core's responsibility ("propose a description"), the student starts designing the prompt, choosing the model, thinking about the RAG. The project becomes an AI Engineering exercise and loses its goal. Why it happens: building the core is more concrete and tempting than placing it. How to spot it: your solution talks about tokens, about temperature, about how to write the prompt. How to fix it: treat the core as a black box that proposes (the ai_component stub). Your work is everything around it —the tolerance, the contract, the shell, the sheet—. If you catch yourself designing the prompt, you're in the wrong module of the ecosystem.
Leaving sheet fields blank "because the feature is simple." What happens: since "describe your product" is tolerant, the student fills in three fields and leaves fallback and eval_gate empty. Why it happens: tolerant is confused with careless. How to spot it: your sheet has empty fields. How to fix it: all ten fields carry something, even if it's light. The feature is tolerant, yes, but that means light fields (a "medium" shell, a guardrail of just two rules), not empty fields. The fallback to a no-AI template is short but indispensable: without it, the product-listing feature goes down when the model goes down.
Testing the generator's output with an exact assert. What happens: when validating step 3, the student writes assert ai_component(attrs) == "a specific text" and gets frustrated because it fails. Why it happens: it's the normal-code reflex, and the whole module warned against it. How to spot it: your generator test pins an expected value. How to fix it: verify properties, not the value. satisfies_contract (str, ≤ 200, one line, no forbidden claims) is the right contract —it passes for any valid draft, fails only when the output really isn't useful—. It's lesson 2, and the project puts it to the test again on purpose.
Exercises
Exercise 1 — Change the feature, redo the sheet. Take the support agent that executes refunds (tolerance 3) and adapt the program: change step 1 (its three axes), step 4 (the shell validates a refund proposal against policy, not a text claim) and step 5 (its sheet will have all fields "thick"). Describe what changes in each step relative to "describe your product."
See solution
The changes, step by step:
- Step 1 (tolerance): the axes rise to
touches_money_or_state=5(it executes refunds),error_cost=5,blast_if_wrong=5. Tolerance = 18 − 15 = 3: intolerant, thick shell. And there's no longer "the seller reviews before publishing" as a cheap safety net; here the human-in-the-loop, if it exists, is a refund approver, more costly. - Step 4 (the shell): instead of looking for forbidden claims in a text, the shell validates a structured proposal
{order_id, amount}against the refund policy —does the order exist?, is it in the window?, does the amount not exceed the total nor the maximum?— exactly like thedeterministic_shellfrom lesson 3. A hallucinated proposal ($9999 amount) is blocked. This step is much richer because what's at stake is money, not an editable text. - Step 5 (the sheet): all fields "thick."
guardrailvalidates input and output (the input because the customer's ticket is a trust boundary —prompt injection—).fallbackdegrades to a human (queue the ticket, never auto-approve).deterministic_shellis thick (validates each proposal against policy).nd_tolerance= 3.
What doesn't change: steps 2 and 3 keep their shape —the core is still a box that proposes, and its output's contract is still verified by properties (here, that the proposal is a valid JSON with the right keys)—. The difference between the two features isn't in the shape of the method, but in the thickness of the shell. That's exactly the point of lesson 6: same pattern, sized to the risk.
Exercise 2 — The fallback that degrades safely. In the "describe your product" sheet, the fallback is "a template by attributes (no AI)." Design that deterministic template: what it produces from {"type": "headphones", "battery_h": 30, "wireless": True}, and explain why it's a good fallback (honest, safe, useful) even though it's worse than the model's output.
See solution
A reasonable deterministic template:
def template_fallback(attrs):
parts = []
if attrs.get("type") == "headphones":
parts.append("Headphones")
if attrs.get("wireless"):
parts.append("wireless")
if attrs.get("battery_h"):
parts.append(f"with {attrs['battery_h']} h of battery")
return " ".join(parts) + "." if parts else "Product with no description."
With {"type": "headphones", "battery_h": 30, "wireless": True} it produces: "Headphones wireless with 30 h of battery."
Why it's a good fallback even though it's worse than the model's output:
- Honest: it invents nothing. It only states the attributes the seller already loaded. There's no risk of a false claim nor a hallucinated fact, because it generates nothing new —it just formats what's known—.
- Safe: it's 100% deterministic, so its output always meets the contract (bounded length, no forbidden claims) by construction. The shell doesn't even have to block it.
- Useful: a basic but correct description is infinitely better than no description (or than an error screen) when the model is down. The seller can edit it and publish; the feature doesn't go down with the model.
- Worse but sufficient: yes, it lacks the LLM's fluency. But the fallback doesn't compete with the model on its best day; it competes with the model on its worst day (down), and against that an honest template always wins. Lesson 5's principle: a fallback degrades safely and honestly, it doesn't fabricate certainty it doesn't have. This template is exactly that.
Exercise 3 — The brief that justifies the containment. Write, in your own words, the decision brief for "describe your product" in three sentences: (1) what is being placed, (2) why the component's non-determinism is contained —cite at least two concrete mechanisms—, and (3) what is explicitly out of scope because of the boundary with AI Engineering. Then explain why a brief like this is more valuable than just the feature's diagram.
See solution
A reasonable brief:
- What is placed: the "describe your product" generator is placed as an AI component in
catalog-service, within the seller's product-listing flow, where it proposes a draft description from the loaded attributes. - Why it's contained: its non-determinism can't do harm because (a) the model only proposes an editable draft —it publishes nothing directly—, (b) a deterministic shell validates that draft against a list of forbidden claims and a length limit before it can be published, (c) the seller reviews and approves it before it goes out (human in the loop), and (d) if the model goes down, a deterministic fallback —a template by attributes— keeps the feature standing. The feature is tolerant (nd_tolerance 12), and these mechanisms contain it to the measure of that risk.
- Out of scope (boundary): how the generator is built internally —the prompt, the model, whether it uses RAG or not, how it's fine-tuned— is the responsibility of the AI Engineering ecosystem, not of this architecture. This design treats the generator as a box that proposes and only concerns itself with placing and containing it.
Why the brief is worth more than the diagram: a feature diagram would show "seller → generator → catalog," but it would not show why it's safe to put a non-deterministic component there, what contains it, what happens if it fails, nor where this team's responsibility ends and AI Engineering's begins. All of that —what makes architecture architecture— lives in the brief, not in the boxes and arrows. The diagram encodes what connects to what; the brief encodes the containment decisions and their reasons, which is exactly what an architect of AI-native systems contributes. It's the same lesson the decisions guide teaches for architecture in general, applied to the AI component: the decision record, not the drawing, is where the design lives.
Summary and next step
In this mini-project you integrated the module's seven lessons into a single executed deliverable: you placed Mercado's "describe your product" generator end to end —you classified its tolerance (12, tolerant, medium shell), separated its core (propose) from its shell (validate, forbid claims, require review), demonstrated the probabilistic contract (the exact assert broke, the property one passed for all three drafts), watched the shell block a forbidden claim before it got published, and emitted its property sheet and its decision brief—. And you did it respecting the boundary that defines the whole guide: you placed and contained the component; you didn't build it. How the generator is made internally is AI Engineering; how it's put into Mercado without its non-determinism touching the system's trust is what this module taught you.
With this you close module 1. You know how to see an AI component: its probabilistic contract, its place behind a boundary, the five properties it drags in, the core/shell shape, its tolerance, and its property sheet. You have the vocabulary and the method to place any AI feature a system wants to add.
What follows is filling in each field of the sheet well, and that's the rest of the guide. Module 2 takes the latency_budget_ms and cost_budget_usd fields you left stated here and makes them first-class architecture: how the slow and expensive LLM is domesticated with budgets, a model cascade (cheap first, escalate only if needed), and cache —measured, with the real savings on screen—. The component is already placed; now you learn to pay for it and make it fast.
Resources
- Anthropic, "Building Effective Agents" (2024) — anthropic.com/engineering/building-effective-agents. The reference for placing an AI feature well —start simple, contain the component, human in the loop where the risk demands it—. The whole project is an application of its containment principles. In English.
- Martin Fowler and Bharani Subramaniam, "Emerging Patterns in Building GenAI Apps" — martinfowler.com/articles/gen-ai-patterns. The eval, guardrail, and fallback patterns the project's sheet states are there as developed patterns —the map of what modules 2 through 7 build—. In English.
- Michael Nygard, "Documenting Architecture Decisions" (2011) — cognitect.com/blog/2011/11/15/documenting-architecture-decisions. The project's decision brief is a light relative of the ADR; the guide's capstone (M8) writes a complete ADR for an AI feature. In English.
- Chip Huyen, AI Engineering (O'Reilly, 2024). For when you cross the boundary and want to build the core you only placed here —the prompt, the RAG, the generator's evaluation—: that's the book on the other side of the boundary. In English.
- Claude documentation — docs.anthropic.com. To ground the sheet's latency and cost budgets in real numbers when you actually implement, without fixating on a model version. It's the bridge to module 2. In English.