Conquering LLM Chaos: Guaranteed Structured Outputs with Pydantic and Ollama Grammar Sampling
In Part 1, we built a lightning-fast, concurrent highway for Sentinel AI—our local license compliance auditor. By leveraging LangGraph’s Map-Reduce pattern, parallel subgraphs, and database caching, we dropped the execution time for 60+ dependencies down to the speed of a single item.
But building a fast highway is only half the battle. When you populate that highway with reasoning models like deepseek-r1, you quickly realize they are incredibly talkative. They emit long, unpredictable streams of internal monologues, chain-of-thought steps, and conversational nuance.
If your graph logic relies on regex or string matching to parse those responses, your system will inevitably break under production edge cases.
This article tackles the data integrity layer. We will explore why prompt engineering fails to guarantee output formats, how local engines like Ollama enforce structure directly inside the GPU token loop, and how to write type-safe multi-agent systems using Pydantic.
1. The Nightmare of Raw Text: “Word Contamination” and “Agent Schizophrenia”
When engineers try to extract structured data from an LLM, the first instinct is usually to modify the prompt:
“Return only a valid JSON object. Do not include markdown code blocks or introductory text.”
In production, this is a ticking time bomb. With advanced reasoning models, this approach fails in two catastrophic ways:
Word Contamination
Reasoning models use a <thought> tag to analyze problems before giving an answer. Suppose DeepSeek-R1 analyzes a perfectly safe package, but inside its reasoning block, it writes: “The corporate policy states that GPLv3 is strictly forbidden, however, this package uses MIT…” If your backend uses a simple regex pattern or substring search for the word "forbidden", your parser will trigger a false positive. The unstructured text contaminated your control flow.
Agent Schizophrenia
Sometimes a model writes a flawless logical defense explaining why a library is safe to use. But when it formats the final JSON key, a sudden token fluctuation causes it to emit verdict: "FORBIDDEN". The prose states one thing; the structural data states another.
To build an enterprise-grade application, you cannot rely on the model choosing to follow formatting rules. You must make it physically impossible for the model to break them.
2. The GPU Guardrail: Context-Free Grammars (GBNF)
To eliminate parsing anxiety entirely, we completely removed regex and text parsers from Sentinel AI. Instead, we use Grammar-Based Sampling.
When you pass a Pydantic schema to a local engine like Ollama (which runs on llama.cpp), the JSON schema is automatically compiled into a Context-Free Grammar—specifically a GBNF (GGML BNF) grammar.
Instead of letting the model freely guess the next most probable word, the grammar intercepts the generation loop at the GPU token selection level (Logit Masking).
What does GBNF look like under the hood?
When our DependencyAudit Pydantic model is sent to Ollama, the engine compiles it into a strict set of text-generation rules that look like this:
# Simplified GBNF representing our schema constraints
root ::= "{" ws "\"package_name\":" ws string "," ws "\"verdict\":" ws verdict_enum "," ws "\"justification\":" ws string "}"
verdict_enum ::= "\"SAFE\"" | "\"FORBIDDEN\"" | "\"REVIEW_REQUIRED\""
string ::= "\"" [^"\\]* "\""
ws ::= [ \t\n\r]*
Before a token is written to the screen:
- The model calculates the mathematical probability (logits) for thousands of possible next tokens.
- The GBNF engine checks the current position against the state machine (e.g., if the model just typed
"verdict":, the next valid token state shifts exclusively toverdict_enum). - The engine dynamically sets the probability of all non-compliant tokens (like lowercase
"safe", any conversational fluff, or missing brackets) to absolute zero.
Here is how this validation guardrail intercepts token selection in real-time:
GBNF Grammar?"} Check -->|Yes| Keep["Keep Token Logit State"] Check -->|No| ForceZero["Force Probability to 0"] end Keep --> Output["Emit Valid Token"] ForceZero --> Output
Because the GPU sampler literally refuses to select forbidden tokens, your model cannot fail the schema validation. It is mathematically impossible for it to drift into conversational fluff when it’s supposed to emit your structured variables.
3. Implementing Type-Safe Nodes in LangGraph
Let’s look at how to implement this architecture. First, we define our strict output requirements using standard Pydantic models. We want to capture the legal verdict, a structured list of issues, and keep the chaotic reasoning safely isolated inside a dedicated field.
from typing import List, Literal
from pydantic import BaseModel, Field
class DependencyAudit(BaseModel):
package_name: str = Field(description="The unique name of the software library")
version: str = Field(description="The evaluated version string")
license_type: str = Field(description="SPDX license identifier found (e.g., MIT, GPL-3.0-or-later)")
discovered_issues: List[str] = Field(
default=[], description="List of specific policy violations or compliance warnings found"
)
verdict: Literal["SAFE", "FORBIDDEN", "REVIEW_REQUIRED"] = Field(
description="The formal compliance standing based strictly on organizational policy guidelines"
)
justification: str = Field(
description="A mandatory 1-2 sentence technical summary explaining the final verdict chosen"
)
Now, we integrate this schema directly into our LangGraph node. By utilizing LangChain’s .with_structured_output(), the pipeline seamlessly binds the Pydantic class to Ollama’s grammar sampling engine.
# app/agents/nodes.py
from langchain_ollama import ChatOllama
from app.agents.state import PackageState
from app.agents.schemas import DependencyAudit
async def lawyer_node(state: PackageState):
"""
Analyzes a library's license terms and returns a strictly formatted
Pydantic object, enforced at the hardware sampling level.
"""
llm = ChatOllama(model="deepseek-r1:8b", temperature=0.0)
structured_llm = llm.with_structured_output(DependencyAudit)
system_prompt = (
"You are an elite open-source compliance attorney. Analyze the provided "
"metadata and verify if it complies with the standard corporate policy "
"allowing only non-copyleft commercial use."
)
audit_object: DependencyAudit = await structured_llm.ainvoke([
("system", system_prompt),
("human", f"Audit target: {state['package_name']} version {state['version']}")
])
return {"audit_report": audit_object}
Because audit_object arrives as an actual instance of your Python class, you can instantly reference attributes like audit_object.verdict or save it straight to an SQL database. No json.loads(), no try-except blocks, and no parsing wrappers required.
4. Hardening the System: The Evaluation Loop
Transitioning to structured outputs instantly solves the format reliability issue, but it introduces a new question: How do we ensure the model is actually making the correct decisions?
In standard software engineering, we write unit tests. In LLM engineering, we build Evaluation Suites (Evals).
Because our outputs are structured and predictable, setting up an evaluation loop for Sentinel AI became trivial. We curated a static dataset containing 100 “ground truth” package configurations with known license violations. During our continuous integration (CI) pipeline, we run an automated eval script:
# tests/test_evals.py
import pytest
from app.agents.nodes import lawyer_node
@pytest.mark.asyncio
async def test_compliance_accuracy():
mock_state = {"package_name": "some-copyleft-tool", "version": "1.0.0"}
response = await lawyer_node(mock_state)
report = response["audit_report"]
assert report.verdict == "FORBIDDEN"
assert "copyleft" in report.justification.lower()
If a prompt change or model update causes an agent to misclassify a license, our assertions catch it instantly. By moving away from unstructured text strings, we turned fuzzy AI behavior into measurable, trackable, and refactorable software components.
5. Expanding the Toolkit: Pydantic Alternatives for Structured Outputs
While combining LangChain, Pydantic, and Ollama is a robust approach, the AI engineering ecosystem offers other specialized libraries designed to solve the structural integrity problem. Depending on your architecture, you might consider these prominent alternatives:
Instructor
Created by Jason Liu, Instructor is one of the most popular libraries for handling structured outputs. It is built entirely on top of Pydantic but abstracts away much of the boilerplate orchestration.
- Why use it: It provides a unified client wrapper across almost every LLM provider (Ollama, OpenAI, Anthropic, Cohere, Groq). If your system needs to dynamically switch between local open-source models and commercial APIs, Instructor allows you to swap backends seamlessly while keeping your exact same Pydantic schema intact.
import instructor
from ollama import Client
client = instructor.from_ollama(Client())
response = client.chat.completions.create(
model="deepseek-r1:8b",
messages=[{"role": "user", "content": "Analyze target package..."}],
response_model=DependencyAudit,
)
Outlines
Developed by the team at .txt, Outlines is a powerful library built specifically for open-source models. While Instructor relies on the inference provider’s native JSON mode or tools API, Outlines compiles schemas directly into regex and context-free grammar constraints at the lowest engine level.
- Why use it: Outlines does not require Pydantic (though it supports it). You can define your constraints using raw Python type hints, regular expressions, or even a basic JSON schema string. It is heavily optimized for local serving setups (vLLM, llama.cpp) and offers incredible generation speed because its token-masking algorithms are written with extreme performance in mind.
import outlines
model = outlines.models.transformers("deepseek-ai/DeepSeek-R1-Distill-Qwen-8B")
generator = outlines.generate.json(model, DependencyAudit)
result = generator("Analyze target package...")
Conclusion: Production-Grade AI is Boring Architecture
When you look at the entire Sentinel AI architecture across this two-part build, the secret to its reliability isn’t a complex, hyper-tuned prompt or a massive 400B parameter cloud model.
The secret is boring software engineering principles applied to generative AI:
- Decouple workloads using Map-Reduce to eliminate runtime bottlenecks.
- Isolate state using Nested Subgraphs to stop memory mutations.
- Lock down boundaries using Grammar-Based Sampling to enforce data schemas.
By treating LLMs as probabilistic processing engines and wrapping them in deterministic structural frameworks, we achieved a reliable 100% data formatting compliance rate while cutting computing bills down to zero.