You run the same prompt on a new record and the output comes back shaped differently each time: a field renamed or a section missing, and the numbers themselves sitting in a table one run and buried in a sentence the next.
A model generates text one token at a time based on what is likely to come next, not from a fixed template it fills in. Longer, more detailed instructions lower the odds of drift but never remove them, because there is no hard boundary enforcing shape the way a form field does. The fix is a declared output schema, not firmer wording: validate what comes back and retry on mismatch.
Say you are turning adjuster notes into a short claim summary: claim number, loss date, status, next action. Described as a paragraph, that is four facts loose in prose, with no fixed slot for any of them.
Described as a schema, with claim_number as a string, loss_date as a date, status as one of a fixed set of values, and next_action as a string, each fact gets exactly one place to live. A response missing one is a validation error your code catches before a person ever sees it.
Validate the shape, retry on failure, never pass through a near-miss.
const ClaimSummary = z.object({
claim_number: z.string(),
loss_date: z.string(),
status: z.enum(["open", "pending_docs", "closed"]),
next_action: z.string(),
});
function summarize(notes) {
for (let attempt = 0; attempt < 3; attempt++) {
const raw = callModel(buildPrompt(notes));
const parsed = ClaimSummary.safeParse(raw);
if (parsed.success) return parsed.data;
}
throw new Error("model did not return a valid claim summary after 3 attempts");
}Telling a model 'please use this exact format' in plain English competes with everything else in the prompt for attention. Handing it a schema, through the structured-output or function-calling support most model APIs now have, changes what is actually being asked, from 'try to look like this' to 'return data matching this definition'. That alone fixes most drift, and the retry step catches what's left.
Once output has a schema, you can diff two runs or store results in a normal database column. Either way, it feeds straight into the next step of a pipeline without a person checking the shape first. None of that is available when the output is 'mostly the same paragraph as last time'.
If the summary also needs to cite where in a long document each fact came from, the playbook on reading a long document covers that per section, and the two combine cleanly: chunk and cite first, validate the combined result against the schema second.
See also: Missed a clause in a long document