目录

03 - 让 Agent 说”结构化的话”:输出控制

一句话总结:用 Pydantic 模型告诉 Agent “按这个格式回答我”,拿到的就是干干净净的 Python 对象,不用再跟字符串搏斗了。


自由文本的烦恼

默认情况下,LLM 返回的就是一段字符串。对人类来说,这读起来很自然;但对程序来说,这就是一坨没法直接用的东西。

想象一下,你让 Agent 帮你评价一部电影。它洋洋洒洒写了三段话,非常精彩。然后你想把评分提取出来存数据库,把优缺点渲染到前端页面上......你怎么办?正则表达式?字符串切割?那不是回到原始社会了吗。

Agno 的 output_schema 参数解决了这个问题。你给 Agent 一个 Pydantic 模型,它就会严格按照这个结构返回数据。拿到手的 response.content 直接就是一个 Pydantic 对象,字段该是什么类型就是什么类型,干净利落。


基本用法:output_schema

先来看一个最简单的例子 – 让 Agent 结构化地评价一部电影:

from typing import List
from pydantic import BaseModel, Field
from agno.agent import Agent
from agno.models.openai import OpenAIChat

class MovieReview(BaseModel):
    title: str = Field(..., description="电影名称")
    rating: float = Field(..., description="评分,1-10")
    pros: List[str] = Field(..., description="优点,2-3个")
    cons: List[str] = Field(..., description="缺点,1-2个")
    one_line: str = Field(..., description="一句话总结")

agent = Agent(
    model=OpenAIChat(id="gpt-4o-mini"),
    output_schema=MovieReview,
)

response = agent.run("评价一下电影《盗梦空间》")
review: MovieReview = response.content

print(f"电影: {review.title}")
print(f"评分: {review.rating}/10")
print(f"一句话: {review.one_line}")
print("优点:")
for p in review.pros:
    print(f"  - {p}")
print("缺点:")
for c in review.cons:
    print(f"  - {c}")

几个关键点:

  • output_schema 接受一个 Pydantic BaseModel 的子类(注意是类本身,不是实例)
  • response.content 返回的就是解析好的 Pydantic 对象,不再是字符串
  • Fielddescription 很重要,它相当于在告诉 LLM “这个字段是干嘛的”,描述越清晰,LLM 填得越准

你可以把 Field(description=...) 理解成写给 LLM 看的注释。普通注释是写给人看的,这个是写给 AI 看的。


双向类型安全:input_schema + output_schema

上面的例子只控制了输出格式。但如果你希望输入也有明确的结构呢?比如你在做一个分析服务,用户需要指定分析主题、分析深度、输出语言,这些参数不能靠自然语言随便传。

Agno 支持同时指定 input_schemaoutput_schema,实现两端的类型安全:

from typing import List, Literal
from pydantic import BaseModel, Field
from agno.agent import Agent
from agno.models.openai import OpenAIChat

class AnalysisRequest(BaseModel):
    topic: str = Field(..., description="分析主题")
    depth: Literal["简要", "详细"] = Field(default="简要", description="分析深度")
    language: str = Field(default="中文", description="输出语言")

class AnalysisResult(BaseModel):
    topic: str = Field(..., description="分析主题")
    summary: str = Field(..., description="核心观点,一句话")
    key_points: List[str] = Field(..., description="关键要点")
    conclusion: str = Field(..., description="结论")

agent = Agent(
    model=OpenAIChat(id="gpt-4o-mini"),
    input_schema=AnalysisRequest,
    output_schema=AnalysisResult,
)

# 方式一:传字典(自动转成 Pydantic 对象)
response = agent.run(input={"topic": "远程办公的利弊", "depth": "详细"})
result: AnalysisResult = response.content
print(f"主题: {result.topic}")
print(f"摘要: {result.summary}")
print(f"结论: {result.conclusion}")
for point in result.key_points:
    print(f"  - {point}")

# 方式二:直接传 Pydantic 对象
request = AnalysisRequest(topic="人工智能对教育的影响", depth="简要")
response = agent.run(input=request)
result = response.content
print(f"主题: {result.topic}")
print(f"结论: {result.conclusion}")

注意这里 agent.run() 用的是 input= 参数而不是直接传字符串。当你定义了 input_schema 后,Agent 会期望接收结构化的输入。

这种模式特别适合做 API 后端。想想看,一个 FastAPI 路由可以写成这样:

@app.post("/analyze")
def analyze(request: AnalysisRequest) -> AnalysisResult:
    return agent.run(input=request).content

输入验证、输出格式,全部由 Pydantic 搞定。


关于流式输出的注意事项

当你使用 output_schema 时,流式输出的行为会有所不同。因为 Agent 需要等到完整的 JSON 生成之后才能解析成 Pydantic 对象,所以结构化输出本质上是”先收集完整响应,再一次性解析”。

最简单的做法就是直接用 run(),不要纠结流式:

# 结构化输出用 run() 就好
response = agent.run("评价星际穿越")
review: MovieReview = response.content
print(f"电影: {review.title}")
print(f"评分: {review.rating}")

如果你既想要流式显示过程、又想要结构化结果,可以考虑不使用 output_schema,而是在拿到文本后自己解析 – 但一般来说,选一个就够了。


实战场景:数据管道

结构化输出最大的威力在于,它让 Agent 的输出可以直接接入你的数据管道。来看一个新闻分析的例子:

from typing import Literal
from pydantic import BaseModel, Field
from agno.agent import Agent
from agno.models.openai import OpenAIChat

class NewsItem(BaseModel):
    headline: str = Field(..., description="新闻标题")
    category: str = Field(..., description="新闻分类,如科技、财经、体育等")
    sentiment: Literal["positive", "negative", "neutral"] = Field(
        ..., description="情感倾向"
    )
    relevance_score: float = Field(
        ..., ge=0, le=1, description="与科技行业的相关度,0-1之间"
    )

agent = Agent(
    model=OpenAIChat(id="gpt-4o-mini"),
    output_schema=NewsItem,
)

response = agent.run("分析这条新闻: NVIDIA发布新一代GPU,性能提升50%")
news: NewsItem = response.content

# 现在你可以用程序逻辑处理了
if news.sentiment == "positive" and news.relevance_score > 0.7:
    print(f"[高相关正面新闻] {news.headline}")
    print(f"分类: {news.category}")
    print(f"情感: {news.sentiment}")
    print(f"相关度: {news.relevance_score}")

注意看 relevance_score 字段用了 ge=0, le=1 约束。Pydantic 的字段验证在这里也能发挥作用 – LLM 返回的值如果不在范围内,解析阶段就会报错,帮你兜底。

你可以把这个模式推广到很多场景:

  • 批量分析新闻,按 sentiment 分桶存储
  • 筛选 relevance_score 大于阈值的内容推送给用户
  • 把 NewsItem 直接 model_dump() 后写入数据库

这就是结构化输出的价值 – Agent 不再是一个只会聊天的 chatbot,而是数据管道中一个可靠的处理节点。


要点总结

  • output_schema 接受 Pydantic BaseModel 类,Agent 会严格按照这个结构返回数据
  • response.content 是 Pydantic 对象,不是字符串,可以直接 .field 访问
  • input_schema + output_schema 实现双向类型安全,适合构建 API 和管道
  • Field(description=...) 是写给 LLM 的说明,描述越精确,输出越靠谱
  • 适用场景:API 后端、数据管道、UI 渲染、批量处理 – 任何需要程序化处理 Agent 输出的地方

下一篇预告

结构化输出解决了”格式”问题,但 Agent 回答的”质量”怎么提升?下一篇我们聊聊 指令的艺术 – 如何通过 instructionsdescription 把一个普通 Agent 调教成领域专家。好的指令就像好的需求文档,写对了,Agent 的表现会有质的飞跃。