Skip to content

16 min read

Instruction placement and compliance controls

On this page

Everything measured so far in this topic has been about evidence — a passage moved across positions, near-miss chunks added in steps, a prompt padded with filler. The other kind of material in a long prompt is instruction: the standing block sent ahead of every request that describes tone, forbids certain claims, lists supported currencies, and carries whatever legal or product requirements have accumulated. Instructions degrade with position and length the same way evidence does, and they cannot be scored the same way, because there is no single correct output to match against.

What can be scored is a compliance rate: the fraction of responses in which the instruction was obeyed. That rate is measured with a probe rather than read off a rule, because the slot that produces the highest rate differs between models. And whatever the probe reports, a rate is what it reports. A requirement that is categorical — a fixed disclaimer that must appear in every response discussing expected returns, in full and verbatim — is not satisfied by a high proportion, so it needs a control in your own code that does not sample. The running example is that disclaimer, because it is the case where the requirement is a fixed string and a deterministic fix is therefore available at all; the harder requirements are the ones where it is not.

An instruction probe is not an evidence probe

An evidence probe moves a passage that answers a question and scores whether the answer is right. An instruction has no gold answer, because the outputs it governs are open-ended — a paragraph about a fund's performance can be written a thousand ways and all of them are acceptable as long as the disclaimer is there. What you score instead is a compliance rate: over a set of realistic requests, the fraction of responses that satisfy a checkable predicate.

python
DISCLAIMER = ("Past performance does not guarantee future results.")

def complies(response: str) -> bool:
    return DISCLAIMER.lower() in " ".join(response.lower().split())

def probe(requests, slots, template_sections):
    """slots: indices into the production system prompt's section list."""
    results = {}
    for slot in slots:
        sections = list(template_sections)
        sections.insert(slot, DISCLAIMER_SECTION)
        system = "\n\n".join(sections)
        hits = 0
        for req in requests:                       # real request shapes
            r = client.messages.create(
                model=MODEL, max_tokens=800, temperature=1.0,
                system=system, messages=req.messages,
            )
            hits += complies(r.content[0].text)
        results[slot] = hits / len(requests)
    return results

Three things separate this from an evidence probe. The template is the production one, section for section, including the tool schemas and the currency table, because the whole finding of this topic is that these effects are template-dependent and a probe run against a clean 500-token prompt measures a system you do not ship. The temperature is production temperature rather than zero, since compliance is a rate over a stochastic process and measuring it at temperature 0 answers a different question than the one an auditor asked. And the predicate is exact and mechanical, with whitespace normalisation and nothing else, so that the measurement does not inherit a judge's own error rate.

Sample size follows from what you are trying to detect. Distinguishing 94% from 97% with any confidence needs roughly 1,500 requests per position, since the standard error on a proportion near 0.95 at n=1,500 is about 0.6 points. Running four positions is 6,000 calls, which on a 30,000-token system prompt is 180 million input tokens — a real cost, in the region of $500 at typical mid-tier pricing, and one that prompt caching on a fixed system prefix reduces by most of that.

Each step there is worth doing out loud, because sample size is the part of a compliance probe most often chosen by feel and it decides whether the result means anything. A compliance rate is a proportion measured on a sample, and how far a proportion wanders from the truth by chance alone falls with the square root of the sample size rather than with the sample size itself. At 1,500 requests and a rate near 0.95 that wandering is the 0.6 points quoted above. Comparing two slots is comparing two such measurements, each wandering independently, and the difference of two independent measurements wanders further than either one alone — by a factor of the square root of two. Three points of separation between 94% and 97% clears that combined wobble with room to spare, and the same arithmetic run at a quarter of the sample doubles the wobble and no longer clears it.

The cost then follows by multiplication, and each multiplication is licensed by something the design fixes rather than by an assumption. Four positions at 1,500 requests each is 6,000 calls, because the whole request set is replayed at every position and nothing is shared between them. Every one of those calls carries the entire 30,000-token system prompt, since the template under test is the production one, so the call count multiplied by the prompt size gives 180 million input tokens. Caching can remove most of that precisely because of a property of this experiment: within one position the system prefix is byte-identical across every call, and only the user turn varies.

Two models, two different best slots

Instruction position in the 30k system promptModel AModel B
First section99.1%97.6%
After the tone guidelines (~6k in)97.8%98.2%
Middle (current, ~15k in)94.1%96.9%
Last section, before the user turn98.6%94.3%
First section and last section99.6%99.1%

That table only means anything if an instruction is the kind of thing that can have a position at all, and the intuitive picture says it is not. Instructions feel like configuration: settings attached to a request and in force for the whole of it. Nothing in the model works that way. A system prompt is text, concatenated in front of the user's turn, and by the time a response is being produced there is no record of which stretch of that text arrived as instruction and which as evidence.

What happens instead is the operation the first chapter of this topic described. At every token it produces, the model computes a weighting over every position already in the prompt, and that weighting sets how much each stretch contributes to what comes next. The weights are non-negative and they sum to one, so the disclaimer sentence is competing for a share of a fixed total against the tone guidelines above it, the currency table below it and every tool schema in between — on exactly the terms a retrieved passage competes against the other nineteen passages in a document block.

Two things follow, and both are measurable in this chapter. The ends of a prompt carry the two advantages the first chapter set out, since material at the front is upstream of everything computed after it and material at the end is separated from the user's turn by almost nothing, so an instruction should show the same U that evidence does. And the length of the system prompt becomes a variable in its own right: hold the disclaimer in one slot and add several thousand tokens of new instruction above it, and the sentence now competes with several thousand more claimants for a total that did not grow, so compliance falls without anyone having touched the line that matters. What the mechanism does not settle is which end wins, and the table shows that it does not.

The first column reproduces the U-shaped position curve from earlier in this topic in a different currency, with the middle worst and both ends strong. The second column does not: model B is best in the second slot and worst at the end, which is the opposite of the ordering that "put it last, closest to the question" would predict. Neither model is misbehaving, and there is no version of this table that lets you write down a rule and stop measuring. The honest general statement is that the best position is the one you verified, on your model, inside your template, and it is a fact with an expiry date, since a provider updating the model behind an unchanged model string can reorder that table without telling you.

There is an intervention this table does not contain, and it is often the largest one available. A 30,000-token system prompt is itself a finding: length costs quality on the surrounding task, and a prompt that has grown to this size by accretion usually contains several thousand tokens of instruction that no longer applies to any live feature. Auditing it down to 8,000 tokens moves every row of the table upward at once, because there is less material for the instruction to compete with, and it improves the answers as well as the compliance rate. Running the position probe first and the prompt audit second is a common order of operations and the wrong one, since the probe's numbers are only valid for the template they were measured on and the audit invalidates them.

The last row is the one most teams end up shipping, because duplicating a short instruction at both ends costs a few dozen tokens and picks up most of the available compliance in both columns. It is also the row that shows the limits of the whole approach most clearly, because at 99.6% the failure rate is 0.4% and the sample of 1,500 contains six failures, which is too few to tell 99.6% from 99.9%.

A rate is not a guarantee

A disclaimer requirement of this kind is categorical. It does not say "usually", and an auditor examining a response that lacks it is not interested in the aggregate. That makes the difference between 94.1% and 99.6% a difference in the number of violations rather than a change in kind: on 40,000 qualifying responses a day, it is the difference between about 2,360 violations and about 160. The second number is much better and it is not zero, and it is worse than it looks, because improving the rate also removes your ability to see the failures. At 94% a weekly audit of 200 responses finds a dozen problems and keeps the issue visible; at 99.6% the same audit usually finds none, and the requirement quietly becomes something everyone believes is solved.

Both halves of that are multiplications worth doing rather than accepting. A compliance rate and a violation rate are complements, since every qualifying response either carries the sentence or does not, so 94.1% compliance leaves the remainder in violation and applying that remainder to 40,000 qualifying responses a day is where about 2,360 comes from. The same multiplication at 99.6% uses a remainder of 0.4% and gives about 160. The audit runs the same arithmetic in the other direction: a sample of fixed size turns up violations in proportion to how common they are, so what a weekly audit of 200 responses can expect to find is the violation rate times 200, which at 94% is a handful and at 99.6% is a fraction of one. The audit's sensitivity was borrowed entirely from the failure rate it was measuring, which is why raising the rate improves the outcome and blinds the instrument in the same edit.

The position table above is where the most common wrong conclusion in this chapter forms, and it separates into two steps of which the first is correct. Moving the instruction from the middle to the first section raised compliance from 94.1% to 99.1% on model A: a real improvement, measured on the production template, bought with an edit that costs no tokens and no latency. Anyone would take it. The step that does not hold is the one from "the rate improved" to "the requirement is met", and it fails on both halves. A higher rate changes how many responses violate a categorical requirement rather than whether any do. And the move traded a known failure rate for an unknown one, since 5.9% was a measured quantity established on a sample large enough to contain failures, while what replaced it is a smaller number the same sampling audit will now report as nothing at all, week after week.

The fix is to put the control somewhere that does not sample. If the requirement is that a fixed string appears in the output, that is a property of the output, and outputs pass through code you own before they reach a user.

python
async def answer(req):
    resp = await model.generate(req)              # prompt still asks for it
    if requires_disclaimer(req) and not complies(resp.text):
        metrics.increment("disclaimer.missing")   # the rate is now observable
        resp = await repair(resp, req)            # one bounded retry
        if not complies(resp.text):
            resp.text = resp.text.rstrip() + "\n\n" + DISCLAIMER
            metrics.increment("disclaimer.appended")
    return resp
Flowchart: Request → Model call instruction in prompt; Model call instruction in prompt → Predicate holds?; Predicate holds? → Return (yes); Predicate holds? → One repair attempt (no); One repair attempt → Predicate holds?; Predicate holds? → Return (yes); Predicate holds? → Append deterministically + increment counter (no); Append deterministically + increment counter → Return.

The step that turns a rate into a guarantee is short enough to walk through, and walking it is what shows how narrow the conditions are. The requirement is that one fixed string appears in the response. Whether a string appears in a text is a property of that text and of nothing else — it does not depend on how the text was produced, on which slot the instruction sat in, or on what the model was attending to while writing. The response passes through code you own before any user sees it, so the property can be tested at that point. A test that comes back false can be made true by appending the string, and appending is deterministic: no sampling, no model call, and no way for a second run on the same input to come out differently.

The natural reading of the diagram is that the prompt and the validator each contribute a share of that guarantee. They do not. All of it comes from the final branch and none from the model, and the same diagram driven by a model that complied half the time would still put the disclaimer on every qualifying response — more clumsily, and on more of them. That is the test for whether a proposed control is one at all: ask which branch produces the required outcome when the model does the worst thing it is capable of, and if the answer is a branch that calls the model, what is on offer is a rate in different clothes.

The repair step in the middle is optional and worth including for a specific reason. A response that is missing the disclaimer is often missing it because the model went somewhere unexpected, and asking it to revise with the requirement restated in a short prompt — the original question, its own answer, and the one instruction — succeeds most of the time and produces a better-integrated result than concatenation. It costs one extra call on the small fraction of requests that need it, which at a 2% failure rate is 2% of your generation spend rather than 100% of it. If your latency budget cannot absorb an occasional second call, drop the branch and go straight to the append; the guarantee is unchanged either way, because the guarantee never depended on the model.

The instruction stays in the prompt, because a model that produces the disclaimer in its own flow writes a better paragraph than one that has a sentence stapled to the end, and because a high in-prompt rate keeps the deterministic path rare. The guarantee comes from the last branch, which cannot fail. The counters are the part people leave out and the part an auditor will ask about, since they turn an unmeasured 0.4% into a dashboard line that shows exactly how often the model needed rescuing.

ControlGuaranteeCostFails when
Prompt placement, verifiedstatistical, ~99%tokens onlymodel or template changes
Prompt placement + audit samplingstatistical, with visibilityaudit labourbetween audits
Validator with retrystatistical, better rate+1 model call on failuresretry also fails
Deterministic append in codecategoricalnonerequirement is not a fixed string
Fine-tuning for the behaviourstatisticaltraining and eval cycledistribution shifts

Requirements with no string to append

The last column of the control table is where the argument for the deterministic path stops being universal. Appending a fixed string works because the requirement is a fixed string. A requirement that the response must not give personalised investment advice, or must state the fee structure whenever a product is recommended, has no such implementation — there is no string to append, the predicate needs a judge or a classifier, and the judge has its own error rate, which puts you back to a statistical control with an extra component to maintain. Those requirements are genuinely harder, and the correct answer for them is usually narrower system scope rather than a better prompt.

That substitution is worse than a wash rather than equivalent, because the two rates compose in both directions. A judged predicate has two ways to be wrong: it can pass a response that violates the requirement, which leaves the original failure exactly where it was, and it can fail a response that satisfies it, which sends a correct answer into the repair path. The predicate in this chapter has neither, because substring matching after whitespace normalisation is a decision with no error rate — and that is what buys the guarantee, bought by the requirement being a fixed string rather than by anything clever in the design. Replace the string with a judgement and the control inherits a second error rate on top of the model's own, so the counter on the dashboard becomes a report about two systems instead of one.

There is one more thing an exact-match predicate does not capture. "Honoured" in a legal requirement may mean more than "present": a disclaimer appended after a paragraph that has just described guaranteed returns is technically compliant and substantively worse than no answer at all. The predicate is a floor and not the requirement, and knowing the difference is what stops a green dashboard from being mistaken for a solved problem.

Instruction placement is worth measuring and worth acting on, and it is worth about five points on a long prompt — real, cheap and specific to the model and template you measured it on. What it cannot become is the mechanism that satisfies a categorical requirement, because every number in this chapter's first table is a rate over a stochastic process, and no amount of moving text around turns a rate into a guarantee. The two live together: the instruction in the prompt, in the best slot you verified, so that the model writes a coherent answer that includes it; and a predicate in your own code, with a counter, for the fraction of the time it does not.

Everything in this topic has been a measurement — a passage moved across positions, distractors added in steps, a cutoff swept on held-out data, a prompt padded with filler, two evaluations run side by side, an instruction relocated inside a real template. None of them is expensive, and all of them replace a plausible argument with a number. The work now is deciding which of them your system needs first, which follows from where its answers are going wrong, and the diagnostic tree that accompanies the filler control is the cheapest place to start: it costs a day and it tells you which of these chapters you are actually going to use.

Context degradation — read the current version at https://daviddominguez.dev/learn/en/context-degradation/instruction-placement-and-compliance-controls

Back to top