with one click
llm-output-validation
// Post-schema semantic validation checklist for structured LLM output
// Post-schema semantic validation checklist for structured LLM output
| name | llm-output-validation |
| description | Post-schema semantic validation checklist for structured LLM output |
When an LLM produces structured data, schema validation (Pydantic, JSON Schema, etc.) only guarantees format correctness. It cannot catch semantically wrong but schema-valid output: hallucinated identifiers, out-of-scope selections, impossible values. Treat structured LLM output the same way you treat untrusted user input — validate meaning, not just shape.
Apply these checks in code, immediately after model_validate() / schema
parse. Raise on failure (loud, not a warning).
Every identifier in the output must exist in the input. No invented IDs.
bundle_id="001", the output must reference "001", not "002".When the output selects from the input (e.g., "pick the top 3"), verify
selection ⊆ input.
[A, B, C] and the output picks [A, D], D is
hallucinated.assert set(output.selected) <= set(input.candidates).Numeric fields must be within the declared range.
0 <= v <= max.Field(ge=0, le=10).String references to entities (filenames, function names, URLs, paths) must resolve in the current context.
Path(ref).exists() or equivalent lookup.Catch generic garbage the LLM emits when confused.
# Immediately after schema parse — before ANY downstream use.
result = ResponseModel.model_validate(raw)
validate_response_integrity(result, original_input) # ← HERE
# Now safe to use `result`.
Raise ValueError (or a domain-specific error) on failure. Do not log a
warning and continue — the whole point is to prevent downstream consumers from
acting on garbage.
An LLM in the (then-current) retrospective pipeline was asked to evaluate
bundle_id="001". It returned a schema-valid result object with
bundle_id="002" — a hallucinated identifier. Pydantic didn't notice because
bundle_id was typed as str, not constrained to the input. The fix: a
three-line integrity check after parse that verified result.bundle_id == expected_bundle_id. Trivial code, but it caught a class of bug that no amount
of prompt engineering could reliably prevent.