Skip to content

第 5 章 · 结构化输出 output_schema

本章目标:让 Agent 的输出从"自由文本"变成"经过校验的 Pydantic 对象",掌握 output_schema 的用法、原理与设计技巧。

5.1 自由文本的痛点

假设你要做一个"电影推荐 Agent",让下游代码读取推荐结果。如果模型返回一段散文,你就得写正则或 json.loads 去解析——模型偶尔少个引号、多个解释性文字,程序直接崩。结构化输出把这个问题彻底消灭:你定义数据结构,模型负责填充,框架负责校验。

5.2 基础用法:output_schema + Pydantic

python
# 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 做了四件事:

  1. 把 Pydantic 模型转成 JSON Schema
  2. 若模型支持原生结构化输出协议(如 OpenAI 的 JSON Schema 模式),直接下发约束;
  3. 不支持时,把 schema 写进提示词并要求模型输出 JSON,再解析;
  4. 用 Pydantic 校验解析结果,失败则记录错误——此时 content 可能退化为原始字符串。

必须做的防御

DeepSeek 等三方模型走的多是"提示词 + 解析"路径,偶发解析失败。官方文档明确建议:访问 schema 字段前先检查 content 的类型

python
# safe_access.py —— 类型防御
result = agent.run("再写一个太空歌剧创意")
if isinstance(result.content, MovieScript):
    print("结构化成功:", result.content.genre)
else:
    print("解析失败,原始内容:", result.content)   # 降级处理

5.4 进阶:嵌套模型、列表字段与按次覆盖

真实业务的输出结构往往嵌套。Pydantic 的组合能力在这里完全可用:

python
# 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 与手写解析的对比 + 与工具结合

对比一下旧式手写解析,你就明白结构化输出的价值:

python
# 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 设计技巧

结合官方建议,四条实战经验:

  1. 每个字段都写 Field(description=...)——描述就是给模型的需求说明,越具体越稳定;
  2. 用约束表达枚举Field(description="必须为 buy/hold/sell 之一")Literal["buy", "hold", "sell"]
  3. 数值范围用 ge/le:如 confidence: float = Field(ge=0, le=1),校验层自动拦截越界值;
  4. 避免过深嵌套(建议 ≤3 层):层数越深,三方模型的格式错误率越高。

5.7 本章小结

  • output_schemacontent 从字符串变成校验过的 Pydantic 对象;
  • 原理是"JSON Schema 下发 → 生成 → Pydantic 校验",三方模型可能回退为提示词解析;
  • 访问字段前用 isinstance 防御解析失败的情况;
  • schema 可以构造时不给、运行时按次传入,一个 Agent 适配多种输出;
  • 字段描述写得越清楚,输出越稳定。

🧪 随堂测验

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

1. 设置 output_schema 后,response.content 的类型是?

2. 使用不支持原生结构化输出的三方模型时,Agno 的处理方式是?

3. 同一个 Agent 想在不同请求里返回不同结构,推荐做法是?

4. 下列哪个 Pydantic 写法最能保证 sentiment 字段只出现三个合法值?

🛠️ 动手实践

  1. 为"客服工单分类"定义 Pydantic 模型(字段:category、urgency 1-5、summary),让 Agent 对 5 条模拟工单输出结构化结果。
  2. 在 5.3 的防御代码基础上加"解析失败自动重试一次"的逻辑,重试时在提示词里强调"只输出 JSON"。
  3. 设计一个两层嵌套模型(如"旅行计划"包含多个"日程",日程包含多个"活动"),测试你的三方模型输出嵌套结构的成功率。

输出可控之后,进入第 6 章:Prompt 工程——instructions 与描述体系