Skip to content

Chapter 5 · Structured Output with output_schema ​

Learning objectives: Make the Agent's output a validated Pydantic object instead of free-form text; master the usage, mechanics, and design patterns of output_schema.

5.1 The Pain of Free-Form Text ​

Imagine building a "movie recommendation Agent" where downstream code consumes the result. If the model returns a prose paragraph, you have to write regex or json.loads to parse it — and one missing quote or extra explanatory sentence will crash your program. Structured output solves this entirely: you define the data shape, the model fills it in, and the framework validates it.

5.2 Basic Usage: output_schema + Pydantic ​

python
# structured_basic.py —— Let the Agent return a Pydantic object
import os
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from agno.agent import Agent
from agno.models.openai import OpenAIChat

load_dotenv()

class MovieScript(BaseModel):
    setting: str = Field(description="Where the story takes place")
    genre: str = Field(description="Movie genre")
    storyline: str = Field(description="A plot summary in three sentences or fewer")

agent = Agent(
    model=OpenAIChat(
        id="deepseek-chat",
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        base_url="https://api.deepseek.com/v1",
    ),
    output_schema=MovieScript,     # Key: declare the output structure
)

response = agent.run("Write a movie idea set in Tokyo about a heist")

print(type(response.content))          # <class 'MovieScript'>, not a string!
print(response.content.setting)        # Direct attribute access
print(response.content.genre)
print(response.content.storyline)

response.content is directly a MovieScript instance — field access has type hints, IDE autocomplete works, and misspelled field names error immediately.

5.3 Under the Hood: Schema Injection and Validation Fallback ​

Understanding the mechanics helps you debug issues. When you set output_schema, Agno does four things:

  1. Converts the Pydantic model to a JSON Schema;
  2. If the model supports native structured output (e.g., OpenAI's JSON mode), sends the constraint directly;
  3. If not, embeds the schema in the prompt and asks for JSON, then parses it;
  4. Validates the parsed result with Pydantic — on failure, content may fall back to the raw string.

Mandatory defense

Third-party models like DeepSeek mostly go through the "prompt + parse" path, with occasional parse failures. The official docs explicitly recommend: always check the type of content before accessing schema fields.

python
# safe_access.py —— Type-safe access
result = agent.run("Write another space opera idea")
if isinstance(result.content, MovieScript):
    print("Structured OK:", result.content.genre)
else:
    print("Parse failed, raw content:", result.content)   # Fallback handling

5.4 Advanced: Nested Models, List Fields, and Per-Call Overrides ​

Real-world output structures are often nested. Pydantic's composition capabilities work fully here:

python
# structured_nested.py —— Nested structure and runtime override
from pydantic import BaseModel, Field
from agno.agent import Agent
from agno.models.openai import OpenAIChat
import os
from dotenv import load_dotenv

load_dotenv()
model = OpenAIChat(
    id="deepseek-chat",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1",
)

class Ingredient(BaseModel):
    name: str = Field(description="Ingredient name")
    amount: str = Field(description="Amount, e.g. 200g")

class Recipe(BaseModel):
    title: str
    difficulty: str = Field(description="Choose one: easy / medium / hard")
    ingredients: list[Ingredient] = Field(description="Ingredient list")   # Nested list
    steps: list[str] = Field(description="Step list")

agent = Agent(model=model)   # No schema at construction time

# Different schema per call — one Agent serves multiple output shapes
recipe = agent.run("Design a 20-minute tomato beef stew recipe", output_schema=Recipe).content
print(recipe.title, "has", len(recipe.ingredients), "ingredients")
for ing in recipe.ingredients:
    print(f"- {ing.name}: {ing.amount}")

Per-call overrides (run(..., output_schema=...)) are ideal for "one Agent, many tasks" scenarios — no need to clone an Agent for each output format.

5.5 Comparison with Hand-Parsed JSON + Tool Integration ​

Compare with the old manual approach to see the value of structured output:

python
# old_way.py —— Anti-pattern: hand-rolled JSON parsing
import json

resp = agent.run("Return a recipe in JSON with fields title, ingredients, steps")
try:
    # The model often wraps JSON in "Sure, here is..." — direct parse explodes
    data = json.loads(resp.content)
except json.JSONDecodeError as e:
    print("Parse failed, need manual cleanup:", e)
    # So you start stripping backticks, regex-capturing {...}, retrying...

Structured output also coexists with tools: the Agent calls tools to gather data, then formats the final answer according to the schema. The official example is stock analysis — the tool fetches real-time prices, and the StockAnalysis model constrains the final conclusion's fields. This "free process, constrained conclusion" pattern is common in production Agents.

5.6 Schema Design Tips ​

Combining official recommendations with practical experience, four rules of thumb:

  1. Write Field(description=...) for every field — the description is the requirement spec for the model; more specific means more stable;
  2. Use constraints for enums: Field(description="must be one of: buy/hold/sell") or Literal["buy", "hold", "sell"];
  3. Use ge/le for numeric ranges: e.g., confidence: float = Field(ge=0, le=1) — the validation layer intercepts out-of-range values automatically;
  4. Avoid deep nesting (recommend ≤3 levels): deeper nesting increases the error rate of third-party models.

5.7 Chapter Summary ​

  • output_schema turns content from a string into a validated Pydantic object;
  • The mechanism is "JSON Schema sent → generated → Pydantic validated"; third-party models may fall back to prompt-based parsing;
  • Use isinstance() to guard against parse failures before accessing fields;
  • Schema can be omitted at construction and passed per-call — one Agent adapts to multiple output shapes;
  • Clearer field descriptions yield more stable output.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. After setting output_schema, what type is response.content?

2. When using a third-party model that does not support native structured output, how does Agno handle it?

3. To have the same Agent return different structures on different requests, the recommended approach is?

4. Which Pydantic写法 best ensures the sentiment field only takes three valid values?

🛠️ Hands-On Exercises ​

  1. Define a Pydantic model for "customer service ticket classification" (fields: category, urgency 1–5, summary); have the Agent output structured results for 5 simulated tickets.
  2. On top of the defensive code from 5.3, add "auto-retry once on parse failure" logic; on retry, reinforce the prompt with "output only JSON".
  3. Design a two-level nested model (e.g., "travel plan" containing multiple "day schedules", each day containing multiple "activities"); test your third-party model's success rate on nested output.

With controlled output, move on to Chapter 6: Prompt Engineering — instructions and the description system.