第 5 章 · 结构化输出 output_schema
本章目标:让 Agent 的输出从"自由文本"变成"经过校验的 Pydantic 对象",掌握 output_schema 的用法、原理与设计技巧。
5.1 自由文本的痛点
假设你要做一个"电影推荐 Agent",让下游代码读取推荐结果。如果模型返回一段散文,你就得写正则或 json.loads 去解析——模型偶尔少个引号、多个解释性文字,程序直接崩。结构化输出把这个问题彻底消灭:你定义数据结构,模型负责填充,框架负责校验。
5.2 基础用法:output_schema + Pydantic
# structured_basic.py —— 让 Agent 返回 Pydantic 对象
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="故事发生的地点")
genre: str = Field(description="电影类型")
storyline: str = Field(description="三句话以内的剧情梗概")
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
output_schema=MovieScript, # 关键:声明输出结构
)
response = agent.run("写一个发生在东京的抢劫电影创意")
print(type(response.content)) # <class 'MovieScript'>,不是字符串!
print(response.content.setting) # 直接用属性访问
print(response.content.genre)
print(response.content.storyline)response.content 直接就是 MovieScript 实例——字段访问有类型提示、IDE 自动补全、拼错字段名立刻报错。
5.3 底层原理:schema 注入与校验回退
理解原理才能排查问题。设置 output_schema 后 Agno 做了四件事:
- 把 Pydantic 模型转成 JSON Schema;
- 若模型支持原生结构化输出协议(如 OpenAI 的 JSON Schema 模式),直接下发约束;
- 不支持时,把 schema 写进提示词并要求模型输出 JSON,再解析;
- 用 Pydantic 校验解析结果,失败则记录错误——此时
content可能退化为原始字符串。
必须做的防御
DeepSeek 等三方模型走的多是"提示词 + 解析"路径,偶发解析失败。官方文档明确建议:访问 schema 字段前先检查 content 的类型。
# safe_access.py —— 类型防御
result = agent.run("再写一个太空歌剧创意")
if isinstance(result.content, MovieScript):
print("结构化成功:", result.content.genre)
else:
print("解析失败,原始内容:", result.content) # 降级处理5.4 进阶:嵌套模型、列表字段与按次覆盖
真实业务的输出结构往往嵌套。Pydantic 的组合能力在这里完全可用:
# structured_nested.py —— 嵌套结构与运行时覆盖
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="食材名")
amount: str = Field(description="用量,如 200g")
class Recipe(BaseModel):
title: str
difficulty: str = Field(description="简单/中等/困难 三选一")
ingredients: list[Ingredient] = Field(description="食材清单") # 嵌套列表
steps: list[str] = Field(description="步骤列表")
agent = Agent(model=model) # 构造时不指定 schema
# 同一个 Agent 按次指定不同 schema —— 一个 Agent 服务多种输出格式
recipe = agent.run("设计一道 20 分钟完成的番茄牛腩", output_schema=Recipe).content
print(recipe.title, "共", len(recipe.ingredients), "种食材")
for ing in recipe.ingredients:
print(f"- {ing.name}: {ing.amount}")按次覆盖(run(..., output_schema=...))非常适合"一个 Agent 多种任务"的场景,避免为每种输出格式克隆一个 Agent。
5.5 与手写解析的对比 + 与工具结合
对比一下旧式手写解析,你就明白结构化输出的价值:
# old_way.py —— 反面教材:手写 JSON 解析
import json
resp = agent.run("以 JSON 格式返回菜谱,字段包括 title、ingredients、steps")
try:
# 模型经常在 JSON 外面套一句"好的,以下是...",直接炸
data = json.loads(resp.content)
except json.JSONDecodeError as e:
print("解析失败,需要手写清洗逻辑:", e)
# 于是你开始 strip 反引号、正则截取 {...}、重试...结构化输出还支持与工具共存:Agent 先调工具取数据,最后按 schema 组织答案。官方示例是股票分析——工具查实时股价,StockAnalysis 模型约束最终结论的字段。这个"过程自由、结论受约束"的模式是生产级 Agent 的常用架构。
5.6 Schema 设计技巧
结合官方建议,四条实战经验:
- 每个字段都写
Field(description=...)——描述就是给模型的需求说明,越具体越稳定; - 用约束表达枚举:
Field(description="必须为 buy/hold/sell 之一")或Literal["buy", "hold", "sell"]; - 数值范围用
ge/le:如confidence: float = Field(ge=0, le=1),校验层自动拦截越界值; - 避免过深嵌套(建议 ≤3 层):层数越深,三方模型的格式错误率越高。
5.7 本章小结
output_schema让content从字符串变成校验过的 Pydantic 对象;- 原理是"JSON Schema 下发 → 生成 → Pydantic 校验",三方模型可能回退为提示词解析;
- 访问字段前用
isinstance防御解析失败的情况; - schema 可以构造时不给、运行时按次传入,一个 Agent 适配多种输出;
- 字段描述写得越清楚,输出越稳定。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 设置 output_schema 后,response.content 的类型是?
2. 使用不支持原生结构化输出的三方模型时,Agno 的处理方式是?
3. 同一个 Agent 想在不同请求里返回不同结构,推荐做法是?
4. 下列哪个 Pydantic 写法最能保证 sentiment 字段只出现三个合法值?
🛠️ 动手实践
- 为"客服工单分类"定义 Pydantic 模型(字段:category、urgency 1-5、summary),让 Agent 对 5 条模拟工单输出结构化结果。
- 在 5.3 的防御代码基础上加"解析失败自动重试一次"的逻辑,重试时在提示词里强调"只输出 JSON"。
- 设计一个两层嵌套模型(如"旅行计划"包含多个"日程",日程包含多个"活动"),测试你的三方模型输出嵌套结构的成功率。
输出可控之后,进入第 6 章:Prompt 工程——instructions 与描述体系。