AI agents

When to Repair a Failed Response and When to Start Over

Antonio Evans11 min read

A split scene: on the left, a response document with one field circled in red and a small wrench tightening it; on the right, the same document dropped into a shredder and a blank page feeding into a machine.
Key points

Key points

  1. 1Repair the response when the validator can name a specific defect and the fix touches a small part of the output; start over when the failure is global, such as wrong reasoning, a refusal, or missing context.
  2. 2Feeding the validation error and the model's own bad output back into the next prompt turns a blind retry into a targeted edit, and it usually fixes the one field that was wrong.
  3. 3Escalate after a second repair pass fails, or when the same failure class appears twice on the same input, because that pattern points at the prompt or the context rather than the response.
  4. 4Each strategy has a different cost shape: a repair pass re-sends the bad output plus the error, a fresh call re-sends the original prompt, and escalation spends human or engineering time.

Classifying the failure before you spend a token

The decision after a validation failure is a classification problem. Before you choose between repair, a fresh call and escalation, answer one question: can the validator point at a specific, bounded defect?

A local failure is one where the defect has an address. A required key is missing, an enum value is not in the list, a number arrived as a string, a date is in the wrong format, a list was truncated before its closing bracket, or the object nested one level too deep. These are the failures schema validation and application-level checks are built to catch, and they are also the failures a model is good at patching when you tell it exactly what went wrong. Valid JSON is not a contract: JSON mode can produce parseable output that still omits required fields or invents enum values, which is why shape validation belongs after generation as well as during it (TrueFoundry on structured outputs).

A global failure is one where the defect has no address because the whole answer is the problem. The model answered a different question, refused the request, lacked the context it needed, or produced reasoning that lands on the wrong conclusion. There is no field to point at, so there is nothing for a repair prompt to correct. You can send the broken output back with "this is wrong" attached, but the model has no bounded target and will usually regenerate from scratch anyway, which is a fresh call wearing a repair pass's costume.

The practical test: if you cannot write the repair instruction as a single sentence naming a field, a value or a span, do not attempt a repair.

A third category is often lumped into "global" but deserves its own branch: the refusal. Modern structured output APIs make safety-based refusals programmatically detectable, so a refusal is not a validation failure at all but a distinct signal you can branch on (OpenAI structured outputs guide). Repeating a refusal with the same prompt tends to produce another refusal, so refusals route to escalation or a fallback path, not to the repair loop.

The decision rule, in one table

Read down the failure column, then across to the action.

Failure signalWhat it looks likeActionWhy
Schema violation, single fieldEnum miss, wrong type, missing required keyTargeted repairValidator names the defect; fix is local
Truncated outputUnterminated JSON, list cut short, no closing braceFresh call with a higher output budget or a split schemaNothing parseable to hand back for editing
Wrong answer, correct shapeSchema-valid but factually or logically wrongFresh call, then escalate if repeatedNo bounded defect to point at
RefusalModel declines the taskEscalate or fall backA retry rarely changes the outcome
Missing contextModel says it lacks information, or invents itEscalate for context, then fresh callRepair cannot invent source material
Second repair failsSame defect survives one repairEscalateThe prompt or context is the problem
Same defect twice on same inputIdentical failure class, same requestEscalateDeterministic failure, not model flakiness

The last two rows are the ones teams skip. A single repair attempt is cheap and worth trying. The second attempt is where the economics flip: if you sent a precise instruction naming the exact field and the model still got it wrong, the instruction is probably wrong, the schema is unsuited to the task, or the input genuinely does not contain the information. A third repair pays for the same failure.

Why a plain retry is the weakest of the three options

A retry that resends the identical prompt is the reflex when a validation error is raised, and it is the weakest move available. Same prompt, same temperature, roughly the same odds: you pay for another round and hope the dice land differently (n hirschfeld on feeding errors back into the retry).

Compare three options with rough numbers. Suppose each call costs a cent or two and takes a couple of seconds end to end. The exact figure does not change the shape of the argument:

  • Plain retry: you re-send the same input and instructions. If the failure was randomness, this works. If the failure was a structural mismatch between schema and data, it fails identically.
  • Targeted repair: you re-send the original prompt plus the bad output plus the error message. It costs more per attempt than a retry because the payload is larger, and it usually succeeds on the first try because the model is editing rather than generating.
  • Fresh call with a changed prompt: you spend one call on a better prompt, which is the only option that fixes a deterministic failure.

Repair is justified because it is more expensive per attempt than a retry and cheaper than a redesigned prompt plus the engineering time to replan it. The moment repair stops converging, you pay the higher per-attempt price for nothing.

Pushing structural constraints out of prose prompts and into a native schema also cuts the constant input cost of every call, since you stop resending formatting instructions. The schema itself is still billed as input tokens, but a compact schema plus a short semantic prompt usually costs less than a paragraph of rules and few-shot examples (agenta on structured outputs and function calling). A repair pass then adds only the delta of the bad output and the error line on top of that leaner base.

Building the repair prompt so the model can actually fix it

A repair pass built the same way as a plain retry defeats the purpose. The message has to be written for the model, not for a log line. There is a widely used pattern for this: capture the validation error, format it as plain feedback, add it to the conversation, and ask the model to try again with that information (Instructor retry mechanisms).

Here is a small example. The validator catches an enum miss on sentiment and an out-of-range confidence, and the repair message names both.

repair_message = f"""
The last response failed validation:
<errors>
- sentiment: value was "somewhat positive"; allowed values are positive, negative, neutral
- confidence: value was 85; must be a number between 0 and 1
</errors>

Return the corrected object. Change only the fields named in the errors.
Preserve every other field exactly as it was.

<previous>
{previous_output}
</previous>
"""

Two details make this work. The error is described in the model's language, not as a stack trace or exception type. And the model receives its own prior output as the artifact to edit, which keeps the parts that were already correct instead of gambling on full regeneration. Instructor's error formatting follows the same shape: a short bulleted list of failing fields with the plain-English correction, then the request to try again.

The example also hints at a design constraint. For the model to edit rather than regenerate, the output must be parseable enough to serialize back into the prompt. Truly empty or truncated output has nothing to hand back, so you need a normal fresh call under the repair path as the fallback (n hirschfeld).

When the schema itself is the problem

Sometimes the failure is not model flakiness but an over-constrained contract. The simpler the schema, the more reliable the output; the more complex and restrictive the schema, the more likely the model fails the restrictions (nuric on structuring LLM responses with JSON schemas). If repair passes keep failing on the same field, check whether the constraint is one the model can satisfy at all.

The most common trap is portability. A schema that works with one provider can fail with another, because strict modes support only a subset of JSON Schema. Optional fields expressed as a union of a type and null, numeric constraints expressed as minimum and maximum keywords, recursive models using references, and union types can all be rejected or behave differently in strict mode, which is why teams write sanitizers that reshape a rich schema into a provider-compatible subset for the call (LangChain forum on JSON Schema sanitization). If you move between providers, this mismatch shows up as a validation failure no repair prompt can fix, because the failure sits at the schema boundary, not inside the response.

The other trap is depth. Deeply nested schemas, such as rich text documents with indefinite list nesting, are hard for a model to emit in one pass even with schema enforcement. The fix is dividing the task: break the schema into smaller, simpler pieces and prompt for each part separately rather than generating the whole tree in one response (Stack Exchange discussion on complex JSON generation). If repair keeps failing on a deeply nested object, the repair is a symptom and the schema needs splitting.

A useful check before blaming the model: generate the JSON Schema from your application type and read it. Pydantic emits schemas that follow the JSON Schema 2020-12 dialect and the OpenAPI 3.1 specification, with constructs like $defs, anyOf and exclusiveMinimum appearing where you might not expect them (Pydantic JSON Schema docs). Each of those is a potential provider compatibility issue.

Escalation: what to do when repair and fresh calls both fail

Escalation needs an owner, a payload and a threshold, because "log the error and move on" is not escalation, it is data loss with extra steps.

A workable threshold has two triggers. The first is a failed second repair attempt on the same input. The second is the same failure class appearing on the same input twice, even across separate requests. The second trigger matters because deterministic failures quietly burn retry budget in every request: if a prompt consistently produces an enum miss on category, every request that hits it fails once and repairs once, and your dashboard shows a steady drip that looks like flakiness but is a schema-design problem.

The payload should carry enough to reproduce and fix the problem without asking the reporter anything: the original input, the exact prompt sent, the bad output, the validator error, the repair instruction, the repair output, the attempt count and the token usage per attempt. The Instructor exception object is a good model, since it exposes every failed attempt with its exception, raw completion, attempt count and token usage (Instructor retry mechanisms).

For tool calls, escalation has a second meaning: authorization. A structurally valid tool call is not the same as a permitted action. The runtime has to decide whether a schema-valid object may cause an external side effect, and that decision belongs on a separate branch from the repair loop (TrueFoundry). A validation failure on a refund request should never route to a repair pass that quietly produces a different, still-valid refund.

The instrumentation that tells you which branch to take

You cannot apply the rule without data, and the data has to be recorded at the moment of failure rather than reconstructed later. The guidance is blunt: log raw and parsed outputs, set validation failure metrics, and build in retries or fallbacks so schema violations surface early instead of silently regressing (MLflow on enforcing, validating and observing structured outputs).

The fields worth logging, per attempt:

FieldPurposeExample value
Request ID and attempt numberCorrelate attempts on one inputreq_8812, attempt 2
Failure classGroup failures for the escalation triggerenum_miss
Defect pathNames the field when the failure is localitems[3].category
Output characters changed by repairMeasures whether the fix was small14 of 890
Input, output and repair tokensAttributes cost to each branch210 / 890 / 1120
Latency per attemptShows the cost of repair versus fresh calls1.9 s
Branch takenRepair, fresh or escalaterepair
OutcomeSuccess, repeat failure, escalatedsuccess

Two fields do more work than they look like. Defect path is the classifier: a populated path means the failure was local and repair was a legitimate option, while an empty path means the validator could not localize it and repair was a gamble. Output characters changed tells you whether repairs are genuinely targeted. If repair outputs routinely differ from the original in most of their characters, your repair is a full regeneration, and you should charge it to the fresh-call budget and ask why fresh calls were not the first choice.

Aggregate defect path by value and you get a leaderboard of your most frequent local failures. The top entry is almost always cheaper to fix at the schema or prompt level than to keep repairing. Aggregate outcome and you get a convergence rate: successful first repairs divided by repairs attempted. When that rate drops, something upstream changed.

If you run this across more than one model provider, the same metrics tell you whether failures are provider-specific. Routing through one gateway with an OpenAI-compatible interface means the same schema and the same repair loop go to every model, which is the only way a side-by-side failure comparison means anything. Quanta Inference (https://quantaintelligence.ai/inference) offers that shape: text, image, video, audio and embedding models through one API key, prepaid and priced in US dollars.

A worked example with numbers

Suppose a document extraction pipeline processes 10,000 requests a day, and the initial call fails validation on 8 percent, so 800 failures. The logs split them: 70 percent local and nameable, 20 percent global, 10 percent truncated.

Applying the rule: 560 requests go to targeted repair, 160 return to the caller for more context, and 80 go to a fresh call with a larger output budget. The repair pass succeeds on 80 percent of its 560, so 448 are fixed in one extra call and 112 escalate. That is 560 repair calls, 240 fresh calls and 112 escalations a day.

Compare that to plain-retry-on-failure for all 800. If a blind retry resolves a local failure half the time, you spend 800 retry calls and still escalate 400. That is 240 extra calls and 288 extra escalations compared with the rule-based policy, and escalation is the expensive part because each one is a human or engineering minute.

The leverage sits in the middle column. Cutting the repair failure rate from 20 percent to 10 percent by writing better repair prompts and trimming the schema saves 56 escalations a day for the same number of calls. Cutting the local failure rate itself by fixing the top defect path saves more. Both are cheaper than adding a third repair attempt.

If many of your escalations are really "an expert needs to look at this input and decide," that is not a retry problem. Quanta Experts (https://quantaintelligence.ai/agents) is where that handoff lands: an expert reads the context, does the work and returns a finished deliverable, which is roughly what an escalation queue becomes once it has a stable input format.

FAQ

FAQ

When should I use a plain retry instead of a repair pass?

Almost never as the primary strategy, but there is one honest case: when the bad output is not parseable enough to hand back to the model, such as an empty or truncated response. There is nothing to edit, so repair becomes a fresh call. If you want the retry to be a repair, the prompt must carry the validator's error and the previous output.

How many repair passes should I allow?

One. Allow a single targeted repair, then escalate. A second attempt is almost always paying for a deterministic failure, because a precise instruction naming a field that the model still gets wrong points at the prompt, the schema or the input, not at the response.

What counts as a local failure?

A local failure is one where the validator can name a field, a value or a span: an enum miss, a wrong type, a missing required key, a malformed date, a list that ended early. The test is whether you can write the repair instruction as one sentence naming the defect. If you cannot, treat it as global.

Should I repair a refusal?

No. A refusal is a distinct signal, not a validation failure, and structured output APIs expose it programmatically so your runtime can branch on it. Repeating a refusal with the same prompt tends to produce another refusal, so route it to a fallback or an escalation path.

What should the escalation payload contain?

The original input, the exact prompt sent, the bad output, the validator error, the repair instruction, the repair output, the attempt count and the token usage per attempt. That is enough for someone to reproduce the failure and decide whether the fix belongs in the schema, the prompt or the pipeline.

Sources

Sources

  1. LLM Structured Outputs with JSON Schema
  2. Structured model outputs | OpenAI API
  3. When an LLM response fails validation, feed the error back ...
  4. The guide to structured outputs and function calling with ...
  5. Retry Mechanisms - Instructor
  6. Structuring LLM Responses with JSON Schemas | nuric Blog
  7. Add JSON Schema Sanitizer for OpenAI Structured Outputs ...
  8. How to generate structured data like JSON with LLM models?
  9. JSON Schema | Pydantic Docs
  10. Enforce, Validate, Observe: 3 LLM Structured Output ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles