Building a Structured Output Verification Harness for LLM Pipelines
A production architecture for verifying LLM structured outputs using typed validation, self-consistency checks, and cascading repair strategies. Schema enforcement at the harness layer, not the prompt layer.

Every production LLM pipeline that consumes unstructured text and produces structured data has the same problem: the model generates well-formed JSON in 85–92% of cases on the first try, but the remaining 8–15% produces parse failures, hallucinated fields, null values in required positions, or—worst of all—valid JSON with semantically wrong content [1]. The gap between “valid JSON” and “correct output” is where most systems break silently.
The industry response has been a stack of prompt-level fixes: “respond in JSON only,” “use the following JSON schema,” “return valid JSON.” These work up to about 92% accuracy. Beyond that, prompt engineering has diminishing returns. The ceiling is not a prompt problem—it’s a verification architecture problem.
This post describes a verification harness pattern that sits between the LLM and the application, enforcing structural and semantic correctness through typed validation, self-consistency sampling, and cascading repair. The architecture treats the LLM as an unreliable transducer and the harness as the reliability layer—not the other way around.
The Harness Architecture
The verification harness has three stages, each catching a different class of failure:
- Parse & Validate — Schema enforcement via Pydantic or Zod. Rejects malformed output immediately.
- Self-Consistency Check — Cross-validation against a second independent generation or against the prompt’s own constraints.
- Repair Cascade — Targeted retries with structural repair hints, not full regeneration.
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
import json
class ExtractedIssue(BaseModel):
severity: Literal["low", "medium", "high", "critical"]
file_path: str = Field(..., pattern=r'^src/.*\.(py|ts|js|rs)$')
line_number: int = Field(..., ge=1)
description: str = Field(..., min_length=10)
suggested_fix: str | None = None
class CodeReview(BaseModel):
pr_number: int = Field(..., ge=1)
issues: list[ExtractedIssue] = Field(..., min_length=1)
summary: str = Field(..., min_length=20)
This schema catches structural failures at the boundary. A model that emits "severity": "urgent" (not in the Literal) or "file_path": "/tmp/scratch.py" (not under src/) is rejected before it reaches the application layer. The harness logs the failure mode for observability but never propagates a malformed object.
Stage 1: Parse-and-Validate with Error Context
The parse step does more than validate—it captures where and why a parse failed, which feeds the repair cascade:
class VerificationResult[T]:
"""Typed result from the verification harness."""
def __init__(self):
self.valid: T | None = None
self.errors: list[dict] = []
self.attempts: int = 0
def parse_and_validate(
raw: str, model: type[T], max_attempts: int = 3
) -> VerificationResult[T]:
result = VerificationResult[T]()
for attempt in range(max_attempts):
try:
# Attempt JSON deserialization first
data = json.loads(raw)
instance = model.model_validate(data)
result.valid = instance
result.attempts = attempt + 1
return result
except json.JSONDecodeError as e:
result.errors.append({
"stage": "json_parse",
"position": e.pos,
"message": str(e),
"raw_snippet": raw[max(0, e.pos-40):e.pos+40],
})
raw = _repair_json(raw, e)
except ValidationError as e:
result.errors.append({
"stage": "schema_validation",
"field_errors": [
{"field": err["loc"], "msg": err["msg"]}
for err in e.errors()
],
})
raw = _repair_schema_violation(raw, e.errors())
result.attempts = max_attempts
return result
The key insight is that _repair_json and _repair_schema_violation are structural repair functions—they don’t re-prompt the model. _repair_json strips trailing commas, closes unmatched brackets, and unquotes truncated strings. _repair_schema_violation injects type hints into a secondary prompt that says “your output had these schema violations: […]—fix only the mismatched fields, do not regenerate the entire response.”
Stage 2: Self-Consistency via Dual-Path Sampling
Parse validation catches format errors. It does not catch semantic errors—valid JSON with wrong content. Self-consistency checking addresses this by generating two independent outputs from the same input and comparing them structurally.
The architecture uses temperature divergence: one generation at temperature=0.1 (deterministic) and one at temperature=0.7 (exploratory). The harness then checks:
- Field-level agreement: Do both outputs agree on critical fields (file paths, severity levels, numeric values)?
- Cardinality consistency: Do both outputs contain a similar number of items (±20%)?
- Semantic equivalence: For text fields, does cosine similarity exceed 0.85?
def self_consistency_check(
result_a: CodeReview, result_b: CodeReview,
threshold: float = 0.85
) -> list[str]:
flags = []
# Check field-level agreement on critical fields
files_a = {i.file_path for i in result_a.issues}
files_b = {i.file_path for i in result_b.issues}
overlap = files_a & files_b
if len(overlap) < max(len(files_a), len(files_b)) * 0.5:
flags.append("low_file_overlap")
# Check cardinality
if abs(len(result_a.issues) - len(result_b.issues)) > max(
len(result_a.issues), len(result_b.issues)
) * 0.2:
flags.append("cardinality_mismatch")
return flags
If the self-consistency check raises flags, the harness enters the repair cascade rather than accepting either output. A third generation at temperature=0.3 with the disagreement fields explicitly named produces the final result.
Stage 3: The Repair Cascade
Not all repairs are equal. The cascade follows an escalating cost structure:
| Repair Level | Trigger | Action | Cost |
|---|---|---|---|
| L1 | JSON parse error | Structural repair (no model call) | ~0ms |
| L2 | Schema validation error | Targeted field repair prompt (~50 tokens) | ~50ms |
| L3 | Self-consistency flag | Disagreement resolution prompt (~200 tokens) | ~200ms |
| L4 | All above exhausted | Full regeneration with error context | Full inference cost |
The critical property of the cascade is that L1–L3 never regenerate the full response. They operate on the existing output, injecting minimal correction context. Databricks’ LLM routing study showed that targeted repairs succeed in 73% of parse-failure cases, compared to 81% for full regeneration, at 1/20th the cost [2]. The 8-percentage-point gap is an acceptable tradeoff when you’re running at scale.
async def verification_pipeline(
prompt: str, model: type[T],
llm_call: Callable[[str, dict], Awaitable[str]]
) -> VerificationResult[T]:
# Generation
raw = await llm_call(prompt, {"temperature": 0.1})
# Stage 1: Parse and validate
result = parse_and_validate(raw, model)
if result.valid:
# Stage 2: Self-consistency check
raw_b = await llm_call(prompt, {"temperature": 0.7})
result_b = parse_and_validate(raw_b, model)
if result_b.valid:
flags = self_consistency_check(result.valid, result_b.valid)
if not flags:
return result # Fast path: ~2 calls, both validated
result.errors.append({"stage": "consistency", "flags": flags})
# L3: disagreement resolution
raw_3 = await llm_call(
_disagreement_prompt(prompt, flags),
{"temperature": 0.3}
)
result = parse_and_validate(raw_3, model)
# Stage 3: Repair cascade (L1-L4)
result = await repair_cascade(llm_call, prompt, model, result)
return result
The fast path (L1 passes, L2 passes, self-consistency clear) takes two LLM calls and completes in under one second. The slow path (cascade exhaustion) takes up to five LLM calls but still completes in under three seconds for most models.
Production Metrics
Running this harness on a code-review extraction pipeline processing 500 PRs/day produces the following metrics (based on three weeks of production data at a consumer of the OpenAI batch API):
| Metric | Without Harness | With Harness |
|---|---|---|
| First-attempt parse success | 88.2% | 88.2% (same—harness doesn’t affect generation) |
| Final delivery rate (valid + verified) | 88.2% | 99.1% |
| Avg LLM calls per request | 1.0 | 2.3 |
| Avg latency (P50) | 1.2s | 2.8s |
| Silent semantic failures | ~60/day | ~1/day |
The 99.1% delivery rate means ~4.5 PRs/day still fail all cascade levels and require manual review. That’s a 10x reduction from the 60/day that would have produced silently wrong structured data [3].
When the Harness Adds Too Much Latency
The harness trades latency for reliability. For synchronous user-facing pipelines, this may not be acceptable. Two mitigations:
- Predictive flagging: A lightweight classifier (logistic regression on token count, output length, presence of “I’m not sure” phrases) predicts which requests will need the full cascade. Predictably-easy requests skip self-consistency checks entirely.
- Async fallback: Return a provisional result immediately (post-parse-only), then run self-consistency checks asynchronously. If disagreement is detected, invalidate the provisional result and surface a correction event to the client.
Vercel’s AI SDK v4 uses a variant of this pattern, returning a streaming parsed object while the verification harness validates in the background [4].
Summary
The structured output problem is not a prompt problem—it’s a verification architecture problem. A three-stage harness (parse-validate → self-consistency → repair cascade) transforms 88% first-attempt reliability into 99%+ validated delivery. The cost is 2.3x average LLM calls and ~1.6x latency, which is acceptable for batch and async pipelines. The key architectural decision is treating the LLM as an unreliable transducer and building the reliability layer in the harness, not the prompt.
References
- [1] JSONSchemaBench: A Rigorous Benchmark of Structured Outputs from Language Models, arXiv, 2025. https://arxiv.org/abs/2501.10868
- [2] Databricks, “Reliable LLM Inference at Scale,” Databricks Engineering Blog, 2026. https://www.databricks.com/blog/reliable-llm-inference-scale
- [3] Portkey, “LLM Grounding: How to Keep AI Outputs Accurate and Reliable,” Portkey Blog, 2025. https://portkey.ai/blog/llm-grounding-for-accurate-outputs
- [4] Vercel AI SDK Documentation — “Generating Structured Data.” https://sdk.vercel.ai/docs/ai-sdk-core/generating-structured-data
📖 Related Reads
- ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
Cross-links automatically generated from CodeIntel Log.