Switch to light mode

Structured Output Reliability: Why LLM Tool Calls Fail Silently

- 7 min read

JSON schema validation catching a malformed field in an LLM tool call response

Structured Output Reliability: Why LLM Tool Calls Fail Silently

When an LLM calls a tool or returns structured data, the failure mode people expect is an obvious one: malformed JSON, a missing field, something a parser chokes on immediately. The failure mode that actually causes problems in production is quieter, a response that’s syntactically valid and structurally almost right, but wrong in a way nothing downstream catches until much later.

Where This Actually Bites

  • A field that’s the wrong type but coerces without erroring. A price returned as the string "29.99" instead of the number 29.99 will often flow silently through several layers of code before something finally breaks on it, far from where the actual problem originated.
  • An enum value that isn’t one of the allowed options. If a status field is supposed to be one of pending, active, closed and the model returns completed, plenty of downstream code will just treat that as an unhandled default case rather than an error, silently misrouting the record.
  • A confident but fabricated value in an optional field. Models sometimes fill in a plausible-looking value for a field that should have been left null or omitted, and a plausible wrong value is far harder to catch in review than an obviously broken one.

Building Real Validation

  • Validate against a schema, not just check that JSON parses. Parsing success only confirms the syntax is valid, not that the structure or types match what the rest of your system expects. A schema validator (JSON Schema, Zod, Pydantic, whatever fits your stack) should reject anything that doesn’t match the contract, including wrong types and out-of-range enum values.
  • Treat validation failure as a retry trigger, not a crash. The point of catching these failures early is to give the model another attempt with the specific validation error included in the retry prompt, which is usually enough for it to self-correct. A hard crash on the first failure wastes that opportunity.
  • Set a retry ceiling. Retrying indefinitely on a task the model consistently can’t produce a valid response for just delays the failure instead of preventing it. Two or three attempts, then a clear error surfaced to whatever’s calling the tool, is a reasonable default.
  • Log the raw, invalid response before discarding it. When a validation failure happens, the raw output is the only evidence of what actually went wrong. Losing it makes debugging a recurring failure pattern much harder than it needs to be.

Why “It Usually Works” Isn’t Good Enough

A tool call that succeeds 95% of the time sounds reliable until it’s running at volume, or unsupervised, or feeding a system where a bad value has a real downstream cost (a wrong price, a wrong permission level, a wrong routing decision). The failures that matter aren’t the ones a person is watching for, they’re the ones that pass every check someone thought to write, and the fix isn’t a smarter model, it’s a validation layer that doesn’t assume the model’s output is trustworthy just because it parsed.

A Reasonable Default Setup

Define the expected output shape as an explicit schema before writing the prompt, not after seeing what the model returns. Validate every response against it, retry with the specific error on failure, cap retries, and log what actually came back when it doesn’t work. None of this is exotic engineering, it’s the same discipline you’d apply to any external API that might return something malformed, and an LLM’s output should be treated with exactly that level of trust: useful, usually correct, and never assumed correct without checking.

© 2024 Shawn Mayzes. All rights reserved.