Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Agents often produce structured data, not prose: a JSON object, a function-call schema, a row of fields. Free-form text outputs are fragile to parse, hallucinate fields, and break downstream code. Structured outputs (Anthropic 'tool use as response', OpenAI JSON mode, Outlines/Instructor for local models) force the model to emit data matching a schema. The result is bulletproof parsing and dramatically lower failure rates.
Patterns: (1) Tool calling as response — define an output tool with your schema; force the model to call it. (2) OpenAI structured outputs with response_format: json_schema. (3) Pydantic + Instructor for local validation. The trick: a schema with explicit field types and descriptions prevents 95% of 'the model gave me a string when I wanted a number' bugs. Combine with retry-on-validation-fail for the remaining 5%.
Use these three in order. Each builds on the one before.
Why prefer structured outputs over text-parsing? Give one concrete bug each prevents.
Walk me through 'tool_choice: forced' in Anthropic: how does forcing a specific tool change the model's generation?
Design a structured output for an agent that produces a multi-step plan (5 steps, each with 'tool_to_call', 'reasoning', 'expected_output'). What's the schema and how do you validate?
# Pattern: structured outputs via tool calling
from pydantic import BaseModel, Field
class TicketClassification(BaseModel):
category: str = Field(description="one of: billing, technical, sales, other")
urgency: int = Field(description="1-5, where 5 is most urgent")
summary: str = Field(description="1-sentence summary")
requires_human: bool = Field(description="true if a human should handle this")
# Anthropic: use tool calling to force structure
OUTPUT_TOOL = {
"name": "submit_classification",
"description": "Submit the classification result.",
"input_schema": TicketClassification.model_json_schema(),
}
def classify_ticket(ticket_text):
resp = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=300,
tools=[OUTPUT_TOOL],
tool_choice={"type": "tool", "name": "submit_classification"}, # force this tool
messages=[{"role": "user", "content": f"Classify this ticket:\n{ticket_text}"}],
)
# The model MUST call submit_classification; the input is the structured result
for block in resp.content:
if block.type == "tool_use" and block.name == "submit_classification":
return TicketClassification(**block.input)
raise ValueError("no structured output returned")
# OpenAI: use response_format with json_schema (since gpt-4o-2024-08-06)
from openai import OpenAI
oai = OpenAI()
resp = oai.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[{"role": "user", "content": ticket}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "ticket_classification",
"strict": True,
"schema": TicketClassification.model_json_schema(),
}
}
)
result = TicketClassification.parse_raw(resp.choices[0].message.content)python3 main.py