6.2.10 · D2AI Agents & Tool Use

Visual walkthrough — Guardrails and constrained generation

2,021 words9 min readBack to topic

We assume nothing. If a symbol appears, we drew it first.


Step 1 — What a language model actually hands you at each step

WHAT. A language model does not "write words". At every single step it produces one number for every possible token (a token ≈ a word-piece). Call these numbers the logits. If the vocabulary has, say, 6 tokens, you get 6 numbers. That's it — a bare list of scores.

WHY. Before we can bend generation, we must know exactly what raw material we are bending. The raw material is a vector of scores, one per token — nothing is a "probability" yet. A logit can be any real number: big positive means "I like this token", big negative means "I dislike it", zero means "meh".

PICTURE. Look at the blue bars in the figure: six tokens, six heights. Some bars point up (positive logit), one points down (negative logit — token "z" at ). Height = the model's raw enthusiasm. Notice nothing here sums to 1; these are not yet probabilities.

Figure — Guardrails and constrained generation

Step 2 — Softmax: turning scores into a probability pie

WHAT. To choose a token we need probabilities (numbers in that add to ). The recipe that does this is softmax. For token :

Term by term: takes each score and makes it positive (so no negative probabilities); the numerator is this token's positive weight; the denominator adds up everyone's weight so the fractions sum to exactly .

WHY softmax and not just "divide the logits"? Because logits can be negative, and you cannot have a negative probability. The exponential is the natural fixer: it maps every real number to a positive one, it is monotonic (bigger logit → bigger probability), and the division makes them a valid pie that sums to .

PICTURE. The blue logit bars from Step 1 become a yellow pie. The down-pointing token "z" (logit ) did not vanish — is small but not zero. It still gets a thin slice. Hold onto that fact: a low logit is still a possible token.

Figure — Guardrails and constrained generation
Recall

Why can't a logit of ever give probability ? ::: Because and . The exponential of any finite number is strictly positive.


Step 3 — The constraint: a machine that says "yes" or "no" to each next token

WHAT. A constraint (JSON schema, a regex, a grammar) can always be turned into a finite-state machine: a little diagram of states with arrows. You are always sitting in one state; each token either follows a legal arrow (allowed) or has no arrow (forbidden).

WHY a machine and not just "check at the end"? Because we want to forbid the bad token before it is generated. To do that we need, at every step, a crisp yes/no verdict for each token: "does this keep me on a valid path?" A finite-state machine gives exactly that verdict instantly from the current state. (This is the same machine idea behind function calling schemas.)

PICTURE. The tiny machine for the regex \d\d ("two digits"): state →(any digit)→ →(any digit)→ (accept). Standing in , the only legal tokens are digits; letters and punctuation have no arrow — they are forbidden. Green arrows = allowed, red dashed = forbidden.

Figure — Guardrails and constrained generation

Step 4 — The naive fix that looks clean and is wrong: multiply by 0

WHAT. The tempting idea: build a mask that is for valid tokens and for invalid ones, then multiply the logits by it: (here means "multiply element by element").

WHY it feels right. Multiplying a forbidden token by seems to "erase" it, and the notation is pretty.

WHY it is actually broken — two separate disasters, both visible in the figure:

  1. A forbidden token whose logit was becomes . But , so after softmax it still gets a slice. It was not erased at all.
  2. Worse: a forbidden token whose logit was negative, say , becomes — which is larger than . Multiplying boosted it. The forbidden token got more likely.

PICTURE. Left panel: original logits. Right panel after multiplying-by-0: the two forbidden bars (red) both flattened to height — and the previously-negative one visibly rose. The red slice in the resulting pie is not zero; it grew.

Figure — Guardrails and constrained generation

Step 5 — The real fix: ADD before the softmax

WHAT. Instead of multiplying, we add a bias. Build :

Then compute the constrained probabilities:

Term by term: valid tokens get untouched, the model keeps its opinion. Invalid tokens get — sent to negative infinity, no matter what they started at.

WHY and why add? Because softmax's first move is , and:

Addition drags every forbidden logit to regardless of its starting value — this cures both disasters of Step 4 at once: the token and the token both land at , both become . A token contributing to the numerator gets exactly probability, and it also adds to the denominator, so the remaining valid tokens automatically renormalize to sum to .

PICTURE. Same six tokens. Green (valid) bars keep their height; red (invalid) bars are shoved off the bottom of the chart to . The resulting pie has only green slices, and they sum to on their own.

Figure — Guardrails and constrained generation

Step 6 — The degenerate case: in practice we never use literal

WHAT. Computers cannot store cleanly; can produce NaN (Not-a-Number) in edge arithmetic. So real code uses a huge finite negative number, typically .

WHY it still works. We don't need exactly zero; we need unimaginably tiny. If a valid logit is around and a masked one is , then:

The masked token's slice is smaller than any number your machine can represent — effectively zero — but the arithmetic stays finite and safe.

PICTURE. A number line. Real logits cluster near (blue dots). The mask value sits so far left it's off-screen (red arrow pointing far away), with a magnified inset showing is a decimal with hundreds of millions of leading zeros.

Figure — Guardrails and constrained generation
Recall

Why instead of literal in code? ::: can generate NaN; a large finite gives (smaller than machine precision) while keeping arithmetic finite and stable.


Step 7 — Zooming out: mask, sample, advance, repeat

WHAT. Constrained generation is Steps 1–5 run in a loop. Generate one token, feed it back to the constraint machine to advance its state, recompute which tokens are now valid, rebuild the mask, and go again.

WHY a loop? Because "what is valid" changes with each token. After you emit {, the valid set is " or }. After a digit in a phone number, the valid set shifts to the next machine state. The mask is not fixed — it is recomputed at every step from the machine's current state.

PICTURE. A cycle: logits → add maskbias → softmax → sample token → machine advances → new valid set → back to logits. Two full turns are drawn, showing the valid set shrinking and shifting as ( then 5 are emitted for the phone-number regex \(\d.

Figure — Guardrails and constrained generation

The one-picture summary

Figure — Guardrails and constrained generation

The whole derivation on one canvas: raw logits (blue) → a gate that adds or (the mask) → softmax → a probability pie with only valid slices → sample → the constraint machine steps forward and hands you the next legal set. Multiplication-by- is drawn crossed out to the side, with its two failure modes labelled.

Recall Feynman retelling — say it to a 12-year-old

Imagine the model shouting an eagerness number for every possible next word — that's the logit; big means keen, negative means "ugh, not that". To pick a word we turn those numbers into a fair spinner (softmax), where bigger numbers get bigger wedges — but every word still gets some wedge, even the disliked ones. Now suppose a rulebook says only certain words are allowed right now. The clumsy idea is "multiply the banned words' numbers by zero" — but zero is a perfectly good eagerness score, so banned words still get wedges, and if a word had a negative score, multiplying by zero actually raised it — you accidentally made the banned word more likely! The clean fix is to add a giant negative number (, or in real code) to every banned word's score. Turning a spinner wedge's size uses , and — so banned words get exactly zero wedge no matter what they started at, and the allowed words share the whole spinner among themselves. Do this every single step, letting the rulebook-machine tell you which words are legal next, and out comes text that is guaranteed to obey the rules — never a rejected retry, valid from the first character.


Related: contrast this proactive masking with reactive filtering in the parent main note; the softmax and sampling here connect to sampling strategies; content-level policy comes from RLHF; masks that hide rules can be probed by adversarial attacks; multiple constrained agents combine in multi-agent systems.