Chapter 2 of 7 · 10 min read
Ninety-seven percent six times over assumes six independent steps
On this page
The payables design doc has a reliability section three lines long. Each of the six stages — intake, extract, validate, price, write, assemble — is expected to succeed about 97% of the time; 0.97 to the sixth is 0.833; therefore roughly 83% of invoices should complete end to end without intervention, and the AP team should staff for a 17% exception queue. Six months later the dashboard reports 84.1% end to end. The doc was right to within a point, the exception queue was staffed correctly, and nobody has revisited the model since.
The model is wrong, and it being wrong is not visible from that one number. The measured per-stage rates are 0.99, 0.96, 0.98, 0.97, 0.99 and 0.98, whose product is 87.7% — three and a half points above what the pipeline actually delivers. The doc's 83% matched reality by starting from a per-stage estimate that was too pessimistic and then compounding it with an assumption that was too optimistic. Two errors in opposite directions, roughly the same size, one number that looks validated.
What the multiplication assumes
Multiplying per-stage rates along a chain is correct when the stages fail independently: knowing that the extractor failed on this invoice tells you nothing about whether the pricer will. That assumption holds for a chain of unrelated network services and does not hold for a chain of language-model agents working on the same document, for two reasons that push in opposite directions.
The first is shared input. Every stage after intake works from a restatement of what intake produced, so a defect there is inherited by all five downstream stages, each of which handles it faithfully and reports success. Topic 11.10 gave this its shape in a delegation tree — failures with a shared origin arrive clustered rather than spread evenly — and the same term is present in a linear pipeline. On this workload, 5.2% of runs carry an intake defect that no downstream stage can repair: the supplier is matched to the wrong contract, or the invoice is classified as a credit note, and everything after that is a correct answer to the wrong question.
The second is downstream recovery, which is positive rather than negative and is usually left out of the model entirely. The validator catches 61% of the extractor's line-item errors, and the write stage catches 12% of the pricer's rounding errors, because both stages recompute rather than trusting. A pure product of per-stage rates ignores this and understates the chain. Put the two effects together and they partly cancel, which is exactly why an unmodelled chain can produce a plausible number and stay unexamined.
What counts as a stage succeeding
Before any of the arithmetic can be trusted, the per-stage rates have to mean something specific, and on this pipeline they did not. Each stage was scored a success if its output validated against its schema and was judged defensible given its input — that was the instruction given to the reviewers who labelled the sample. Under that definition, a pricer that correctly applies the wrong contract's rates to a correctly extracted set of line items is a success. So is a writer that faithfully renders a total it was handed.
That definition measures conformance: did the stage do its job given what it was given. End-to-end success measures correctness: is the payable record right. The product of conformance rates is an upper bound on correctness, not an estimate of it, and the size of the gap is exactly the rate of defects that pass every stage's local check. On the labelled sample of 400 runs, 21 runs had every stage marked conformant and a wrong final record — 5.25% of runs, which is the 5.2% shared-origin rate arriving under a different name.
This is worth being pedantic about because the two definitions produce per-stage numbers that differ by several points and are reported identically on dashboards. If a stage is scored on correctness instead — is its output right in absolute terms — the extractor's 0.96 becomes 0.91, because the 5% of runs where it faithfully extracts from a misclassified document now count against it. Neither number is wrong; a model that multiplies one and compares the result against the other is.
Getting the per-stage numbers at all
Both definitions need labelled data, and the labelling protocol is where most of these numbers go wrong before any arithmetic happens. On the payables pipeline the sample was 400 runs drawn from production over four weeks, stratified by supplier so that no single high-volume supplier dominated, with every run labelled twice by different reviewers and disagreements resolved by a third. Reviewers saw the stage's rendered input and its output side by side and answered two questions per stage: is this output well-formed and consistent with its input, and is this output correct in absolute terms against the source document.
Two questions rather than one is the whole trick. Asking a single "did this stage succeed" question forces the reviewer to silently pick a definition, and different reviewers pick differently — inter-rater agreement, the share of runs on which two reviewers independently returned the same verdict, was 0.71 on the single-question version of this protocol, against 0.89 once the questions were separated. A conformance rate and a correctness rate computed from the same 400 runs are comparable to each other; two teams' single numbers are not comparable to anything.
Sample size matters in the ordinary way and is worth a sentence because 400 sounds like a lot. A stage measured at 0.96 on 400 runs has a 95% interval of roughly ±1.9 points — the band the true rate would fall inside in 95 of every 100 samples that size — so 0.96 and 0.98 are not reliably distinguishable at that size. The extractor's gap from the rest of the chain is large enough to survive; the ordering of the other five stages is not, and treating that ordering as real is how teams end up optimising the wrong stage.
The mean is the wrong summary
The doc quoted a single per-stage figure, "about 97%", which was an average across six stages whose real rates ranged from 0.96 to 0.99. Averaging destroys the information you need, because for a fixed mean the end-to-end product varies by several points depending on how the failure is distributed.
| Chain | Per-stage rates | Mean | Product | Stages to fix to reach 97% end to end |
|---|---|---|---|---|
| Evenly weak | 0.965 × 6 | 96.5% | 78.1% | all 6 |
| One weak stage | 0.995 × 5, 0.865 | 96.5% | 84.4% | 1 |
| Two weak stages | 0.995 × 4, 0.905 × 2 | 96.5% | 80.3% | 2 |
| Payables, measured | 0.99, 0.96, 0.98, 0.97, 0.99, 0.98 | 97.8% | 87.7% | 1 (the extractor) |
Three chains with an identical mean land 6.3 points apart, and the ordering is the opposite of most people's intuition: concentrated failure beats spread failure, because the log of the product is the sum of the logs and the log function punishes the low end. The last column is the practical consequence. A chain whose losses concentrate in one stage is a chain with a fix; a chain whose losses are spread evenly across six stages needs six pieces of work, each of which will be argued about separately.
The payables chain is the first kind. The extractor at 0.96 accounts for 4.0 of the 12.3 points lost to conformance failures, and the other five stages account for 8.3 between them. That single fact is worth more than the end-to-end number, and reporting a mean is what hides it.
A retry is not a fresh draw
The pipeline retries a stage whose output fails its schema check, up to twice, and the reliability model treats each retry as an independent attempt at the same per-stage rate. Under that assumption a stage at 0.97 with two retries has a residual failure rate of 0.03³, or 0.0027%, which rounds to never and is why retries were left out of the doc's arithmetic entirely.
The measured retry success rate on this pipeline is 41%. Not 97% — 41%. The reason is mechanical: a retry re-sends the same agent the same input, and most failures are deterministic given that input. An extractor that cannot find a line-item table in a scanned invoice will not find it on the second attempt either. The failures that do clear on retry are the genuinely stochastic ones — a malformed JSON field, a truncated response — and they are a minority of the total.
With the measured figure the residual failure rate for that stage is 0.03 × 0.59 × 0.59, or 1.04%. That is 385 times the modelled value, and it is the difference between a stage you can ignore and a stage that contributes a percentage point of end-to-end loss on its own. Retries also cost: the 23% retry rate from chapter 1 is what turned a 3x raw-token ratio into part of a 10.8x bill, and 59% of those retries bought nothing.
Modelling it end to end
The model that reproduces the measured number has four terms rather than one. It is short enough to write down and worth writing down, because the act of assigning a value to each term forces someone to go and measure it.
import random
STAGES = ["intake", "extract", "validate", "price", "write", "assemble"]
CONFORMANCE = {"intake": 0.99, "extract": 0.96, "validate": 0.98,
"price": 0.97, "write": 0.99, "assemble": 0.98}
Q_SHARED = 0.052 # intake defect every downstream stage inherits
RETRY_P = 0.41 # measured; NOT the stage's base conformance rate
MAX_RETRIES = 2
CATCHES = { # downstream stage -> {upstream stage: catch rate}
"validate": {"extract": 0.61},
"write": {"price": 0.12},
}
def run(rng: random.Random) -> bool:
"""True if the final payable record is correct."""
if rng.random() < Q_SHARED:
return False # inherited premise; nothing downstream repairs it
live_defects: set[str] = set()
for stage in STAGES:
for upstream, catch in CATCHES.get(stage, {}).items():
if upstream in live_defects and rng.random() < catch:
live_defects.discard(upstream)
ok = rng.random() < CONFORMANCE[stage]
attempts = 0
while not ok and attempts < MAX_RETRIES:
ok = rng.random() < RETRY_P # correlated: same input, same agent
attempts += 1
if not ok:
live_defects.add(stage)
return not live_defects
def estimate(n: int = 200_000, seed: int = 11) -> float:
rng = random.Random(seed)
return sum(run(rng) for _ in range(n)) / nThree figures for the same pipeline: the naive product of conformance rates gives 87.7%, this model gives 84.3%, and the dashboard reports 84.1% over the last 90 days. The model is not impressive — it is four measured constants and a loop — and that is the point. It agrees with reality because every one of its four terms was measured rather than assumed, and any of them can be moved to ask a question. Setting Q_SHARED to zero gives 88.9%, which is what fixing intake would buy. Setting RETRY_P to the base conformance rates, which is what the doc assumed, gives 90.2% — the doc's model would have predicted a system six points better than the one that exists.
Where the model says to spend
A model with four movable terms is worth having mainly because it ranks interventions, and the ranking on this pipeline is not the one the roadmap had.
Fixing the extractor from 0.96 to 0.98 — the largest single-stage loss, and the item that had been on the roadmap for two quarters — moves end-to-end correctness from 84.3% to 86.0%, a gain of 1.7 points. Fixing intake so that the shared-origin rate halves, from 5.2% to 2.6%, moves it to 86.6%, a gain of 2.3 points, and intake is a 900-token classification step that costs 5% of the pipeline. Making retries useful — which here means not re-sending identical input, but perturbing something, such as re-rendering the document with a different extraction tool before the second attempt — takes the measured 41% retry success toward 70% and moves end-to-end correctness to 85.9%, a gain of 1.6 points, for about a day of work.
The reason the ranking is surprising is that the naive model has no term for two of those three interventions. A chain modelled as a product of per-stage rates can only be improved by improving a per-stage rate, so the only visible lever is the stage with the lowest number, and the roadmap follows the model. Adding the shared-origin and retry-correlation terms does not just make the prediction more accurate; it makes two thirds of the available work visible.
The same arithmetic sets an upper bound worth knowing before anyone promises anything. Even with every stage at 0.99, this chain tops out at 88.9% end to end, because the 5.2% shared-origin rate does not compound and does not respond to per-stage quality at all. A target of 95% is unreachable by any amount of stage-level work, and someone was going to spend a quarter discovering that.
What the model does not give you
It does not tell you which stage caused a particular failure. It gives population-level rates, and a rate is not an attribution: knowing that 5.2% of runs carry an inherited intake defect does not let anyone look at yesterday's wrong invoice and say which agent to fix. live_defects is a set the simulation can inspect because the simulation created it; the production pipeline has no equivalent, and reconstructing one after the fact from six agents' prompts and outputs turns out to be much harder than it sounds.
It also does not license the reverse inference. A chain measured at 84% end to end with conformance rates above 96% could have that gap for reasons this model does not contain — a stage whose schema check is too permissive, an orchestrator that silently truncates a handoff, a tool that returns stale data under load. The model reproduces the aggregate; several different mechanisms produce the same aggregate.
Both limits point the same direction. Until a failed run can be traced to the stage that actually caused it, every reliability conversation stays statistical, every fix is a guess weighted by a prior, and a pipeline whose stages each produce defensible output has no way to name which defensible output was the wrong one. That is the problem the next chapter is about.
When multi-agent hurts — read the current version at https://daviddominguez.dev/learn/en/when-multi-agent-hurts/ninety-seven-percent-six-times-over-assumes-six-independent-steps