Validation retries: when the model gets it wrong
When a model's answer fails validation, Pydantic AI sends the error back and asks again. The model reads what was wrong and can fix it.
This model function gets the priority wrong on its first try, then corrects it once it is told:
from pydantic_ai import Agent, ModelResponse, ToolCallPart
from pydantic_ai.models.function import FunctionModel
def clumsy(messages, info):
last = messages[-1].parts[-1]
priority = 4 if last.part_kind == "retry-prompt" else 9
return ModelResponse(parts=[ToolCallPart("final_result", {"category": "billing", "priority": priority})])agent = Agent(FunctionModel(clumsy), output_type=Ticket)
result = agent.run_sync("I was charged twice")
print(result.output)
print(result.usage.requests)The run still ended with a valid Ticket, and it took two requests. The messages show why:
for message in result.all_messages():
print(message.kind, [part.part_kind for part in message.parts])
print(result.all_messages()[2].parts[0].model_response())The second request holds a retry-prompt part. model_response() is the text the model reads: Pydantic's validation error as JSON, with the field, the rule and the bad value, then Fix the errors and try again. A real model reads it the same way. The last request, tool-return, is the agent confirming the output tool call, so the message list stays valid if the conversation continues.
When it never gets it right
def stubborn(messages, info):
return ModelResponse(parts=[ToolCallPart("final_result", {"category": "billing", "priority": 9})])
agent = Agent(FunctionModel(stubborn), output_type=Ticket)
agent.run_sync("I was charged twice")By default the model gets one retry. After that the run raises UnexpectedModelBehavior, from pydantic_ai. The last validation error is attached as its cause, which is why Python prints it first. Catch it where your app can fall back, for example by sending the ticket to a person.
attempts = iter([9, 7, 4])
def slow_learner(messages, info):
return ModelResponse(parts=[ToolCallPart("final_result", {"category": "billing", "priority": next(attempts)})])
agent = Agent(FunctionModel(slow_learner), output_type=Ticket, retries=2)
result = agent.run_sync("I was charged twice")
print(result.output, result.usage.requests)retries=2 allows two corrections, and this model needs both. Each retry is another paid request, so a high number hides a bad prompt or schema instead of fixing it.
- Make
clumsysend"category": "refunds"first and read the retry prompt. - Set
retries=0on the first agent. - Send a string,
"high", as the priority and read the error type.
You understood something today that you didn't yesterday.