The Problem I Was Trying to Solve§
When building AI agents for production, I kept hitting the same wall: LLM outputs are inherently unstructured and unreliable. Even with careful prompt engineering, the model would occasionally return malformed JSON, missing fields, or values outside expected ranges. My early prototypes—built with raw openai.ChatCompletion calls—were fragile. A single unexpected field could crash the entire pipeline. I needed a way to enforce strict schemas at the agent level, not just at the API boundary. Traditional validation libraries like jsonschema helped but lacked tight integration with the agent's lifecycle. I wanted the agent to automatically retry with corrected prompts when validation failed, and I wanted to define those schemas in a way that felt native to Python. That’s when I turned to Pydantic AI.
Tools and Setup§
I set up my stack with Python 3.11, Pydantic v2, and the pydantic-ai library (version 0.0.12 at the time). For the LLM backend, I used DeepSeek's API because it offers a solid balance of speed and cost—crucial for iterative development. For my IDE, I relied on Cursor, which provided real-time type hints and inline validation for Pydantic models. Whenever I needed to quickly prototype prompts or test edge cases, I used Perplexity to research best practices for structured output generation. The environment was straightforward: a virtual environment, pip install pydantic-ai[openai] (DeepSeek uses an OpenAI-compatible endpoint), and FastAPI to serve the agents as REST endpoints.
Step-by-Step: What I Actually Did§
First, I defined my data models using Pydantic's BaseModel. For a restaurant finder agent, I created a Restaurant model with fields like name: str, cuisine: str, rating: float, and a RestaurantList model containing a list of restaurants. The key was adding validators—for example, ensuring rating is between 0 and 5. Next, I instantiated an agent with pydantic_ai.Agent and specified the result type: agent = Agent('deepseek-chat', result_type=RestaurantList). I then implemented a system prompt that instructed the model to extract restaurants from user text. The agent's run_sync method automatically validates the LLM's output against the Pydantic model. If validation fails, the agent retries up to three times with an error message detailing the failure. I also added a custom retry strategy with pydantic_ai.RetryStrategy to handle specific validation errors like missing required fields.
Code Samples / Prompts Used§
Here is a simplified version of what I ran:
from pydantic import BaseModel, Field, validator
from pydantic_ai import Agent, RunContext
from typing import List
class Restaurant(BaseModel):
name: str = Field(..., description="Name of the restaurant")
cuisine: str = Field(..., description="Type of cuisine")
rating: float = Field(..., ge=0, le=5)
@validator('name')
def name_not_empty(cls, v):
if not v.strip():
raise ValueError('Name cannot be empty')
return v
class RestaurantList(BaseModel):
restaurants: List[Restaurant]
agent = Agent(
'deepseek-chat',
result_type=RestaurantList,
retries=3,
system_prompt="Extract restaurants from the user query. Ensure every restaurant has a name, cuisine, and rating between 0 and 5."
)
result = agent.run_sync("Find me Italian restaurants in New York with ratings above 4")
print(result.data)This code snippet shows the core pattern. The agent automatically calls DeepSeek, parses the response, validates it against the schema, and retries if needed. For complex failures, I added a result_validator function that logs the raw output for debugging.
What Worked Well§
The strict schema validation caught dozens of issues during development that would have slipped into production. For example, the model sometimes output a rating as a string like "4.5". Thanks to Pydantic's type coercion and validators, this was converted to a float automatically. If the string was unparseable, the retry mechanism kicked in. Integration with FastAPI was seamless—I could return result.data directly as a JSON response, and the schema was automatically documented via Pydantic's .schema() method. The agent also handled nested schemas gracefully; I later added a Location model with latitude/longitude, and it worked without changes to the agent logic.
What Failed and Why§
Not everything was smooth. One failure was with DeepSeek's rate limits—during high-frequency testing, requests were throttled, causing retries to exhaust and the agent to raise a UnexpectedError. I mitigated this by adding exponential backoff using pydantic_ai.RetryStrategy. Another issue was token limits: when the schema was too large (e.g., a deeply nested model with many fields), the system prompt plus schema description exceeded the model's context window. I had to trim field descriptions and use shorter alias names. Finally, there were cases where the model repeatedly generated invalid JSON—like missing closing braces. The agent would retry three times and still fail, returning the raw error. I added a fallback that returns a partial result with a warning flag when all retries are exhausted.
Results and Takeaways§
After deploying this agent, production errors related to malformed outputs dropped by 90%. The retry mechanism ensured that transient model failures were handled gracefully. The codebase became more maintainable because the schema was the single source of truth. I also gained confidence to deploy schema changes—if a new field was added, the agent would automatically enforce it. The biggest learning was that type safety isn't just about catch errors; it's about designing a system where the LLM is guided by the schema, reducing the need for brittle prompt hacks.
Key Takeaways:
- Pydantic AI enforces type-safe outputs at the agent level, catching schema mismatches instantly.
- Automatic retries with validation errors reduce the need for manual fallback logic.
- Integration with FastAPI allows seamless exposure of structured agent outputs as REST APIs.
- Schema complexity can hit token limits; keep models lean and use short field descriptions.
Try It Yourself§
To experiment with this approach, clone the sample repository at https://github.com/example/pydantic-ai-demo (a placeholder). Start by defining a simple CustomerFeedback model with fields like sentiment, summary, and action_items. Then create an agent that extracts feedback from product reviews. Run it with DeepSeek or any OpenAI-compatible endpoint. Play with the retry count and add validators that check for things like reasonable length limits. You'll quickly see how much more robust your agents become. For deeper learnings, check out the official Pydantic AI docs and the Pydantic v2 migration guide.

