LLM Reliability, Structured Outputs & Evaluation
Deploying LLMs in production requires transforming non-deterministic text generation into reliable, predictable application components. Software engineers must enforce strict output formatting at the model layer and deploy robust, programmatic evaluation suites to detect model drift and regression.
๐ก๏ธ Structured Outputs & Schema Enforcement
Raw LLM text outputs frequently violate structure requirements (e.g., missing brackets in JSON, hallucinated keys, or conversational filler like โHere is your JSON:โ). AI engineers use two primary strategies to enforce structure:
1. Guided Logit Generation
Instead of parsing text post-generation, guided logit generation enforces structure during the decoding process. Tools like Outlines or PydanticAI construct a Finite State Machine (FSM) or Context-Free Grammar (CFG) from a Pydantic schema or regex.
At each token-generation step $t$, the FSM identifies which vocabulary tokens are syntactically valid next states. The logits of invalid tokens are set to $-\infty$, ensuring the model cannot select them.
- Pros: Guarantees 100% syntactically valid JSON/regex outputs.
- Cons: Requires hosting control over the inference engine (e.g., running vLLM) to modify logit-bias samplers.
2. Native Tool & Function Calling
For serverless APIs (OpenAI, Anthropic), logit-level access is hidden. Instead, developers pass a JSON Schema under the tools or response_format API parameters. The providerโs gateway uses internal logit-masking and specialized fine-tuning to force the model to output schema-compliant text.
๐ป Schema Validation & Self-Correction Loop
When working with APIs that do not support logit-level constraints, formatting failures can still happen. The Python script below shows how to enforce structured outputs using Pydantic validation, catch parsing exceptions, and feed compiler error traces back to the model in an automated retry loop.
import json
import logging
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field, ValidationError
import openai
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("SelfCorrectionLoop")
# 1. Define the target structured output schema
class APIEndpointSpec(BaseModel):
endpoint_path: str = Field(description="The REST endpoint path, starting with a slash.")
http_method: str = Field(description="Must be GET, POST, PUT, or DELETE.")
required_parameters: List[str] = Field(default=[], description="List of mandatory query parameter names.")
response_fields: List[str] = Field(description="Fields included in the returned JSON payload.")
class SystemSpecCollection(BaseModel):
endpoints: List[APIEndpointSpec]
# 2. Implement the validator and self-correction router
class ReliableLLMParser:
def __init__(self, openai_client: openai.OpenAI):
self.client = openai_client
def generate_specs(self, prompt: str, max_retries: int = 2) -> Optional[SystemSpecCollection]:
system_prompt = (
"You are a backend API architect. You must describe the endpoints "
"as a JSON object matching this schema:\n"
f"{json.dumps(SystemSpecCollection.model_json_schema(), indent=2)}\n"
"Output ONLY valid raw JSON. Do not include markdown code block backticks."
)
current_prompt = prompt
previous_output = ""
for attempt in range(max_retries + 1):
try:
logger.info(f"Generating schema - Attempt {attempt + 1}")
# Append error context to the user prompt if this is a retry
if attempt > 0:
user_payload = (
f"Your previous output failed validation. Schema parsing threw this error:\n"
f"{error_message}\n"
f"Previous Output:\n{previous_output}\n"
"Please correct the errors and output the complete JSON object."
)
else:
user_payload = current_prompt
response = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_payload}
],
temperature=0.0 # Force deterministic generation
)
raw_text = response.choices[0].message.content.strip()
previous_output = raw_text
# Attempt parsing and schema validation
parsed_json = json.loads(raw_text)
spec_collection = SystemSpecCollection.model_validate(parsed_json)
logger.info("Successfully generated and validated schema.")
return spec_collection
except (json.JSONDecodeError, ValidationError) as e:
error_message = str(e)
logger.warning(f"Attempt {attempt + 1} failed: {error_message}")
if attempt == max_retries:
logger.error("Max retries reached. Output validation failed.")
raise RuntimeError(f"Unable to generate valid schema: {error_message}")
# Example Usage:
# client = openai.OpenAI(api_key="your-api-key")
# parser = ReliableLLMParser(openai_client=client)
# specs = parser.generate_specs("Build endpoints for a book catalog GET books and POST book purchase.")๐ LLM Evaluation Methodologies
Evaluating non-deterministic text outputs cannot be done with simple unit tests. Instead, AI engineers use evaluation frameworks to run tests at scale.
1. LLM-as-a-Judge
This pattern uses a larger model (e.g., Claude 3.5 Sonnet or GPT-4o) to grade outputs generated by smaller production models.
- Single-Answer Grading: The judge reviews a single output against a rubric (e.g., grading helpfulness on a scale from 1 to 5) and provides a score.
- Pairwise Elo Rating: The judge is shown a prompt and two anonymized outputs (Model A and Model B). The judge decides which is better, updates the modelsโ relative Elo ratings, and logs the decision. This mimics human preference evaluation (RLHF).
2. G-Eval (Criteria-Based Evaluation)
G-Eval is a framework that uses Chain-of-Thought (CoT) prompts to guide the judge model. Instead of asking for a raw score, the evaluation process runs in three phases:
- Phase 1: Criteria Selection: Define the target metric (e.g., Coherence, Relevance).
- Phase 2: Step-by-Step Prompting: Instruct the judge to generate its own evaluation steps (e.g., โStep 1: Check if the answer addresses all parts of the questionโฆโ).
- Phase 3: Weighted Token Probabilities: The judge outputs a numerical score. To mitigate token sampling bias, you can extract the log probabilities of the score tokens to calculate a weighted average:
\[\text{G-Eval Score} = \sum_{s=1}^{S} s \cdot P(\text{score} = s)\]
Where $s$ represents the individual score value and $P(\text{score} = s)$ is the probability of the model outputting that score token.
3. Reference-Free vs. Reference-Based Metrics
[Evaluation Pipeline]
โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
โผ โผ
[Reference-Based] [Reference-Free]
- Evaluates against gold standard - Evaluates system outputs directly
- Metrics: ROUGE, BLEU, BERTScore - Metrics: Faithfulness, Relevance- Reference-Based Evaluation: Matches outputs against a curated โgold standardโ ground truth. Commonly uses overlap metrics like ROUGE-L (longest common subsequence), BLEU (n-gram precision), or BERTScore (embedding similarity). These are useful for extraction or translation tasks, but struggle to grade creative generation.
- Reference-Free Evaluation: Grades outputs without using ground-truth answers. It evaluates the output against the input context. For example, in RAG systems, it evaluates Faithfulness (checking if the output is supported only by the retrieved context) and Answer Relevance (checking if the output directly answers the userโs question).
โ๏ธ Mitigating Judge Bias
Using LLMs as judges introduces systematic biases that must be mitigated:
- Position Bias: Judges tend to favor the first option shown in pairwise evaluations.
- Mitigation: Run the evaluation twice, swapping the order of candidates (A/B and B/A), and average the scores.
- Verbosity Bias: Judges favor longer, more detailed responses, even if they contain irrelevant information.
- Mitigation: Enforce strict character/word count boundaries in prompt templates, or instruct the judge to penalize fluff.
- Self-Favoring Bias: Models tend to award higher grades to outputs generated by models from their own family (e.g., GPT-4 judging GPT-3.5 outputs).
- Mitigation: Use a different model family for evaluation than the model being evaluated (e.g., use Claude 3.5 Sonnet to judge OpenAI model outputs).