Skip to content

15 min read

Step two cannot fetch what step one's condensation threw away

On this page

A stage that hands forward a condensation of its input has made a choice on behalf of every stage after it. Summarisation is a lossy encoding — one that discards material it has no way of restoring later — and its loss function is "what a reader would want in general". A downstream consumer with a particular need is relying on a compression optimised for someone else, and no amount of prompt work on the consumer brings back what the compressor dropped.

That is why a two-stage pipeline can score below the single prompt it replaced while every component looks fine on its own terms. The summaries read well, the answering step answers the question it was asked correctly, and the information left the system at the boundary. A handoff contract is the set of decisions that stops this happening, and it is a design activity rather than something to discover in an incident review.

The compression was tuned for a question it never saw

Pull failures and read the intermediate next to the question, and the pattern arrives immediately.

text
Q: "How many days do I have to appeal a denied claim?"

Source (§7.3): "A member may request reconsideration of an adverse benefit
determination within 180 calendar days of receiving written notice of denial.
Expedited appeals for urgent care must be filed within 72 hours."

Step 1 summary (excerpt): "The plan provides an appeals process for denied
claims, with expedited handling available for urgent cases. Members should
file promptly after receiving notice."

Step 2 answer: "You should file your appeal promptly after receiving the
denial notice. For urgent care, an expedited appeal is available."

Step two did nothing wrong. Given a summary that says "promptly", the answer it produced is the correct answer to the question it was asked, and it has no mechanism for knowing that 180 and 72 ever existed. Step one was asked to summarise a document without being told what the summary was for, so it had to compress for the average of every possible question, and a good general-purpose summary of a policy document keeps the structure and drops the specific numbers. Taking 8,300 tokens down to 400 is a 95% reduction with no knowledge of which 5% matters.

Why the numbers in particular are the first thing to go is worth one more step, because it is not an accident of this summariser. A summary is chosen to be useful to a reader whose question is unknown, so it keeps what most questions would want. What most questions want is the shape: that there is an appeals process, that urgent cases are handled differently, that a deadline exists. A specific figure is the opposite kind of content — it answers exactly one question and is dead weight for every other, and it cannot be inferred from anything else in the document if it is dropped. So a compressor optimising for the average question systematically discards precisely the material that a single specific question needs, and it does this most reliably when it is working well. The failing intermediate above is not a bad summary. It is a good summary of the wrong thing.

The reduction figure follows from the two token counts: 400 out of 8,300 is under one part in twenty, so nineteen parts in twenty were discarded. The choice of which nineteen was made by a step that had never seen the question.

A one-line change tests the diagnosis, and it is the cheapest thing to try before redesigning anything:

text
Summarise the sections of this policy document that bear on the member's
question. Preserve every number, deadline, dollar amount and named condition
verbatim. Question: {question}

What the boundary has to specify

Question-conditioning treats the symptom. The design decision underneath is what the intermediate is for, and four questions have to be answered explicitly.

Contract dimensionThe failing designA design that holds
What the intermediate carriesFree prose, unspecified lengthTyped fields plus provenance: section ids and verbatim quotes
Whether it is validatedNo; whatever comes back is passed onJSON Schema, validated before step two runs, retried on failure
Whether step two sees the sourceNoYes — the intermediate points into a document step two also holds
What step two does with a bad intermediateUndefined; it answers anywayDefined: abstain — return no answer and say why — escalate, or fall back to the single-call path

The third row is the one that changes the accuracy number, so it is worth being concrete about what it means. Step two does not need the intermediate to contain everything; it needs the intermediate to be a pointer into something it can still read. Restructured that way, step one's job stops being compression and becomes selection with evidence:

json
{
  "relevant_sections": [
    {
      "section_id": "7.3",
      "why": "states appeal deadlines",
      "quote": "within 180 calendar days of receiving written notice of denial"
    }
  ],
  "insufficient_evidence": false
}

Written as a schema the intermediate is enforced rather than requested, and the constraints that matter are not the obvious ones:

json
{
  "name": "policy_evidence",
  "schema": {
    "type": "object",
    "properties": {
      "relevant_sections": {
        "type": "array",
        "maxItems": 4,
        "items": {
          "type": "object",
          "properties": {
            "section_id": {"type": "string", "enum": ["1.1", "1.2", "7.3"]},
            "why":        {"type": "string", "maxLength": 120},
            "quote":      {"type": "string", "minLength": 20, "maxLength": 400}
          },
          "required": ["section_id", "why", "quote"],
          "additionalProperties": false
        }
      },
      "insufficient_evidence": {"type": "boolean"}
    },
    "required": ["relevant_sections", "insufficient_evidence"],
    "additionalProperties": false
  }
}

The enum on section_id is generated per request from the document's own table of contents, abbreviated above, which moves the check from validation into decoding: the model cannot emit a section number the document does not have, because those tokens are not available to it. That change alone removed the 0.9% of requests where step one cited an invented section, at the cost of compiling a schema per document, which measured at about 8 ms and is cached on document hash. The maxItems cap matters for a different reason — an unbounded selection step drifts towards returning most of the document, and capping it at four keeps step two's context predictable in size and cost.

The insufficient_evidence flag is not decoration. It is the field that gives step two something to do other than answer, and without an explicit way for step one to say "I found nothing", a selection step will always return its best guess.

Any abstention field needs calibrating against the queue it feeds, because the cost of the flag is entirely downstream of the model. A flag that fires too readily converts answerable questions into human work at whatever a human minute costs; one that fires too rarely leaves the confident wrong answers the flag existed to prevent. Neither error is visible in an accuracy number, because an abstention is scored as neither right nor wrong, so the measurement to make is the false-abstention rate — sample the flagged questions, have a reviewer say which of them the pipeline could in fact have answered, and multiply through by traffic. That converts a threshold into a monthly figure in hours, which is the unit the organisation absorbing the escalations reasons in.

The conversion is four multiplications long and every one of them is the kind a review can check on a whiteboard, which is what makes it usable in a meeting with people who do not read code. Start with the flag's fire rate. Sample the flagged questions and have a reviewer say which of them genuinely had no answer; the remainder is the false-abstention share of the flagged population. Multiply that share by the fire rate and you have the false-abstention share of all traffic, which is a much smaller number and the one that matters. Multiply that by monthly volume for a count of unnecessary escalations, and by the handling time for a figure in hours. Each step changes the unit, and only the last unit is one anybody outside engineering can act on.

Handling the intermediate that arrives wrong

Every handoff needs a defined behaviour for a malformed intermediate, because "step two answers anyway" is the default and it is the worst option available. Validation gives you a branch point:

Flowchart: Step 1: select sections + quotes → Valid against schema?; Valid against schema? → no; no → Retry once with the validator error; Retry once with the validator error → Valid now?; Valid now? → no; no → Fall back: single call over full document; Valid against schema? → yes; yes → insufficient_evidence?; Valid now? → yes; yes → insufficient_evidence?; insufficient_evidence? → yes; yes → Escalate to a human agent; insufficient_evidence? → no; no → Step 2: answer from quotes + source; Step 2: answer from quotes + source → Quotes present in source?; Quotes present in source? → no; no → Fall back: single call over full document; Quotes present in source? → yes; yes → Return answer.

Two things in that diagram are load-bearing beyond the validation itself. The fallback path keeps the original single-call prompt alive as a degraded mode, which costs you the maintenance of a prompt you hoped to delete and buys you a pipeline that cannot fail closed on a schema error; it fired on 1.1% of requests in the first month. And the check that the quotes actually appear in the source document is a cheap string containment test that catches step one inventing a quote, which happened on 0.6% of requests and is the failure that would otherwise produce a confidently wrong answer with a fabricated citation attached to it.

Prompt decomposition — read the current version at https://daviddominguez.dev/learn/en/prompt-decomposition/step-two-cannot-fetch-what-step-one-s-condensation-threw-away

Back to top