第 7 章:结构化输出 -- 让 Agent 按格式说话

系列教程:OpenAI Agents SDK 从入门到实战

本章目标:掌握 output_type、动态指令、嵌套模型,让 Agent 返回程序能直接用的结构化数据。


为什么需要结构化输出?

第 3 章我们已经初步见识过结构化输出。但那只是开胃菜 -- 这一章我们要把这个能力用到极致。

先回顾一下痛点。如果 AI 返回的是自由文本:

这部电影非常精彩,我给它打8.5分,强烈推荐!导演的叙事手法很独特...

你想从里面提取评分?推荐还是不推荐?得写正则,得做字符串匹配,还得祈祷 AI 下次别换个说法。

但如果 AI 返回的是结构化数据:

MovieReview(title="盗梦空间", rating=8.5, summary="叙事手法独特", recommend=True)

程序直接用 .rating 取评分,用 .recommend 判断是否推荐。稳定、可靠、不用猜。

一句话:自由文本是给人看的,结构化数据是给程序用的。要让 Agent 融入你的系统,结构化输出是必修课。


output_type 基础用法

核心思路很简单:

  1. 用 Pydantic BaseModel 定义你要的数据结构
  2. 把它传给 Agent 的 output_type 参数
  3. result.final_output 自动变成 Pydantic 对象
import asyncio
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled

# 关闭追踪
set_tracing_disabled(True)

# 配置模型(换成你自己的 API 地址和模型)
client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",
    api_key="sk-12345678",
)
model = OpenAIChatCompletionsModel(model="gpt-5.2", openai_client=client)


# 定义输出结构
class MovieReview(BaseModel):
    title: str = Field(description="电影名称")
    rating: float = Field(description="评分,1-10")
    summary: str = Field(description="一句话点评")
    recommend: bool = Field(description="是否推荐")


# 创建 Agent,指定 output_type
agent = Agent(
    name="影评Agent",
    instructions="你是一个专业影评人,请对用户提到的电影给出结构化评价。请严格按照指定的 JSON 格式输出。",
    model=model,
    output_type=MovieReview,
)


async def main():
    result = await Runner.run(agent, input="聊聊《盗梦空间》")

    # result.final_output 是一个 MovieReview 对象,不是字符串
    review = result.final_output
    print(f"电影:{review.title}")
    print(f"评分:{review.rating}")
    print(f"点评:{review.summary}")
    print(f"推荐:{'是' if review.recommend else '否'}")


if __name__ == "__main__":
    asyncio.run(main())

运行后你会看到类似:

电影:盗梦空间
评分:9.2
点评:一部关于梦境与现实的烧脑巨作,叙事层层递进
推荐:是

关键点:result.final_output 的类型跟着 output_type 走。没设 output_type 时它是 str,设了就是你定义的 Pydantic 类的实例。


final_output_as:类型安全的转换

在多 Agent 协作场景中,result.final_output 的类型可能不那么确定(比如经过了 handoff,最终输出的 Agent 可能不是你预期的那个)。这时候 final_output_as() 就派上用场了。

import asyncio
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled

set_tracing_disabled(True)

client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",
    api_key="sk-12345678",
)
model = OpenAIChatCompletionsModel(model="gpt-5.2", openai_client=client)


class WeatherInfo(BaseModel):
    city: str = Field(description="城市名称")
    temperature: float = Field(description="温度,摄氏度")
    condition: str = Field(description="天气状况,如晴、多云、雨等")


agent = Agent(
    name="天气Agent",
    instructions="你是一个天气预报员。根据用户提到的城市,给出天气信息(可以虚构数据)。请严格按照 JSON 格式输出。",
    model=model,
    output_type=WeatherInfo,
)


async def main():
    result = await Runner.run(agent, input="北京今天天气怎么样?")

    # 方式一:直接用 final_output(类型提示可能不够精确)
    print(type(result.final_output))  # <class 'WeatherInfo'>

    # 方式二:用 final_output_as 做显式转换(对类型检查器更友好)
    weather = result.final_output_as(WeatherInfo)
    print(f"{weather.city}{weather.temperature}度,{weather.condition}")

    # 方式三:加上运行时类型检查,类型不对直接报错
    weather = result.final_output_as(WeatherInfo, raise_if_incorrect_type=True)
    print(f"{weather.city}{weather.temperature}度,{weather.condition}")


if __name__ == "__main__":
    asyncio.run(main())

final_output_as 做了两件事:

什么时候该用哪种?

场景 建议
单 Agent,output_type 明确 直接用 result.final_output
多 Agent 协作,结果可能来自不同 Agent final_output_as
生产环境,需要防御性编程 final_output_as(cls, raise_if_incorrect_type=True)

复杂输出类型:嵌套模型

现实中的数据结构往往不是扁平的。比如一篇文章的分析结果,里面可能包含多个段落评价、多个关键词标签等。Pydantic 天然支持嵌套,Agent 也完全能处理。

import asyncio
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled

set_tracing_disabled(True)

client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",
    api_key="sk-12345678",
)
model = OpenAIChatCompletionsModel(model="gpt-5.2", openai_client=client)


# 子模型:单个角色的评价
class CharacterReview(BaseModel):
    name: str = Field(description="角色名称")
    actor: str = Field(description="饰演演员")
    rating: float = Field(description="角色塑造评分,1-10")
    comment: str = Field(description="简短点评")


# 子模型:技术评价
class TechnicalReview(BaseModel):
    cinematography: float = Field(description="摄影评分,1-10")
    soundtrack: float = Field(description="配乐评分,1-10")
    editing: float = Field(description="剪辑评分,1-10")


# 主模型:完整影评,嵌套了上面两个子模型
class DetailedMovieReview(BaseModel):
    title: str = Field(description="电影名称")
    director: str = Field(description="导演")
    year: int = Field(description="上映年份")
    overall_rating: float = Field(description="总评分,1-10")
    summary: str = Field(description="整体评价,两三句话")
    characters: list[CharacterReview] = Field(description="主要角色评价,至少列出2个角色")
    technical: TechnicalReview = Field(description="技术方面的评价")
    tags: list[str] = Field(description="标签,如:科幻、动作、烧脑等")


agent = Agent(
    name="深度影评Agent",
    instructions=(
        "你是一个资深电影评论家,对用户提到的电影进行深度分析。"
        "请从角色、技术、整体等多个维度给出评价。"
        "请严格按照指定的 JSON 格式输出。"
    ),
    model=model,
    output_type=DetailedMovieReview,
)


async def main():
    result = await Runner.run(agent, input="请深度分析一下《星际穿越》")
    review = result.final_output

    # 基本信息
    print(f"《{review.title}》({review.year})")
    print(f"导演:{review.director}")
    print(f"总评分:{review.overall_rating}/10")
    print(f"评价:{review.summary}")
    print(f"标签:{', '.join(review.tags)}")

    # 角色评价(嵌套列表)
    print("\n--- 角色评价 ---")
    for char in review.characters:
        print(f"  {char.name}{char.actor}):{char.rating}/10 - {char.comment}")

    # 技术评价(嵌套对象)
    print("\n--- 技术评价 ---")
    print(f"  摄影:{review.technical.cinematography}/10")
    print(f"  配乐:{review.technical.soundtrack}/10")
    print(f"  剪辑:{review.technical.editing}/10")


if __name__ == "__main__":
    asyncio.run(main())

预期输出类似:

《星际穿越》(2014)
导演:克里斯托弗·诺兰
总评分:9.3/10
评价:一部将硬科幻与人类情感完美融合的史诗之作,在宏大的宇宙叙事中探讨了爱与时间的主题。

标签:科幻, 太空, 烧脑, 亲情

--- 角色评价 ---
  库珀(马修·麦康纳):9.5/10 - 将父爱与探索精神完美融合
  墨菲(杰西卡·查斯坦):9.0/10 - 从倔强少女到坚毅科学家的成长令人动容

--- 技术评价 ---
  摄影:9.5/10
  配乐:9.8/10
  剪辑:9.2/10

嵌套模型的要点:


动态指令:instructions 用函数代替字符串

到目前为止,我们的 instructions 都是写死的字符串。但实际场景中,指令往往需要动态生成 -- 比如根据当前用户身份、时间、偏好来调整 Agent 的行为。

SDK 支持把 instructions 设成一个函数。这个函数接收 RunContextWrapperAgent 两个参数,返回一个字符串。

import asyncio
from dataclasses import dataclass
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, RunContextWrapper, RunConfig, set_tracing_disabled

set_tracing_disabled(True)

client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",
    api_key="sk-12345678",
)
model = OpenAIChatCompletionsModel(model="gpt-5.2", openai_client=client)


# 自定义上下文:存放用户相关信息
@dataclass
class UserContext:
    user_name: str
    language: str
    style: str  # "formal" 或 "casual"


# 输出结构
class Greeting(BaseModel):
    message: str = Field(description="问候语")
    tip_of_the_day: str = Field(description="今日小贴士")


# 动态指令函数:根据上下文生成不同的系统提示
def dynamic_instructions(ctx: RunContextWrapper[UserContext], agent: Agent) -> str:
    user = ctx.context
    style_desc = "正式、礼貌" if user.style == "formal" else "轻松、活泼"
    return (
        f"你是一个问候助手。当前用户是 {user.user_name}。"
        f"请用{user.language}回复,风格要{style_desc}。"
        f"请严格按照指定的 JSON 格式输出。"
    )


agent = Agent[UserContext](
    name="动态问候Agent",
    instructions=dynamic_instructions,  # 传函数,不是字符串
    model=model,
    output_type=Greeting,
)


async def main():
    # 场景一:正式风格
    formal_ctx = UserContext(user_name="张总", language="中文", style="formal")
    result = await Runner.run(
        agent,
        input="你好",
        context=formal_ctx,
    )
    greeting = result.final_output
    print(f"[正式] {greeting.message}")
    print(f"[贴士] {greeting.tip_of_the_day}\n")

    # 场景二:轻松风格
    casual_ctx = UserContext(user_name="小明", language="中文", style="casual")
    result = await Runner.run(
        agent,
        input="嘿",
        context=casual_ctx,
    )
    greeting = result.final_output
    print(f"[轻松] {greeting.message}")
    print(f"[贴士] {greeting.tip_of_the_day}")


if __name__ == "__main__":
    asyncio.run(main())

预期输出类似:

[正式] 张总您好,很高兴为您服务。
[贴士] 每天保持30分钟的阅读习惯,有助于提升思维深度。

[轻松] 嘿小明!今天过得怎么样?
[贴士] 累了就站起来活动活动,对颈椎好!

动态指令的要点:


完整可运行示例:智能简历解析器

把前面学到的知识点串起来 -- 嵌套模型 + 动态指令 + 结构化输出:

import asyncio
from dataclasses import dataclass
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, RunContextWrapper, set_tracing_disabled

set_tracing_disabled(True)

client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",
    api_key="sk-12345678",
)
model = OpenAIChatCompletionsModel(model="gpt-5.2", openai_client=client)


# ===== 定义嵌套的输出结构 =====

class WorkExperience(BaseModel):
    company: str = Field(description="公司名称")
    position: str = Field(description="职位")
    duration: str = Field(description="在职时间段,如:2020.03-2023.06")
    highlights: list[str] = Field(description="工作亮点,1-3条")


class Education(BaseModel):
    school: str = Field(description="学校名称")
    major: str = Field(description="专业")
    degree: str = Field(description="学历,如:本科、硕士、博士")
    graduation_year: int = Field(description="毕业年份")


class ResumeAnalysis(BaseModel):
    name: str = Field(description="候选人姓名")
    years_of_experience: int = Field(description="工作年限")
    skills: list[str] = Field(description="核心技能列表")
    education: list[Education] = Field(description="教育经历")
    experience: list[WorkExperience] = Field(description="工作经历")
    overall_assessment: str = Field(description="综合评价,两三句话")
    match_score: float = Field(description="与目标岗位的匹配度,0-100")


# ===== 自定义上下文 =====

@dataclass
class RecruitContext:
    target_position: str  # 目标岗位
    required_skills: list[str]  # 必要技能
    min_experience: int  # 最低工作年限


# ===== 动态指令 =====

def recruiter_instructions(ctx: RunContextWrapper[RecruitContext], agent: Agent) -> str:
    req = ctx.context
    skills_str = "、".join(req.required_skills)
    return (
        f"你是一个专业的HR简历分析师。"
        f"当前招聘的岗位是:{req.target_position}。"
        f"必要技能包括:{skills_str}。"
        f"最低工作年限要求:{req.min_experience}年。"
        f"请根据以上要求分析候选人的简历,给出匹配度评分。"
        f"请严格按照指定的 JSON 格式输出。"
    )


# ===== 创建 Agent =====

resume_agent = Agent[RecruitContext](
    name="简历分析Agent",
    instructions=recruiter_instructions,
    model=model,
    output_type=ResumeAnalysis,
)


# ===== 运行 =====

async def main():
    # 模拟招聘需求
    context = RecruitContext(
        target_position="高级Python后端工程师",
        required_skills=["Python", "FastAPI", "PostgreSQL", "Redis", "Docker"],
        min_experience=5,
    )

    # 模拟简历文本
    resume_text = """
    张伟,8年Python开发经验。

    教育背景:
    - 浙江大学,计算机科学与技术,硕士,2016年毕业

    工作经历:
    1. 字节跳动(2020.03 - 至今):高级后端工程师
       - 负责推荐系统后端架构设计,日均处理请求1亿+
       - 主导微服务拆分,将单体应用拆分为20+个微服务
       - 引入 FastAPI 替换 Flask,接口响应时间降低40%

    2. 美团(2016.07 - 2020.02):后端工程师
       - 负责商家端订单系统开发和维护
       - 优化 PostgreSQL 慢查询,将核心接口P99降低60%
       - 搭建基于 Redis 的分布式缓存方案

    技能:Python, FastAPI, Django, PostgreSQL, Redis, Docker, Kubernetes, gRPC
    """

    result = await Runner.run(
        resume_agent,
        input=resume_text,
        context=context,
    )

    analysis = result.final_output

    # 打印分析结果
    print(f"候选人:{analysis.name}")
    print(f"工作年限:{analysis.years_of_experience}年")
    print(f"核心技能:{', '.join(analysis.skills)}")
    print(f"匹配度:{analysis.match_score}/100")
    print(f"\n综合评价:{analysis.overall_assessment}")

    print(f"\n--- 教育经历 ---")
    for edu in analysis.education:
        print(f"  {edu.school} | {edu.major} | {edu.degree} | {edu.graduation_year}年毕业")

    print(f"\n--- 工作经历 ---")
    for exp in analysis.experience:
        print(f"  {exp.company} | {exp.position} | {exp.duration}")
        for h in exp.highlights:
            print(f"    - {h}")


if __name__ == "__main__":
    asyncio.run(main())

这个例子同时用到了:


小结

本章你掌握了结构化输出的进阶用法:

一句话总结:output_type 让 Agent 从"聊天"变成"干活",动态指令让 Agent 从"固定脚本"变成"随机应变"。


下一步预告

Agent 能按格式输出了,能动态调整行为了。但 Agent 系统越来越复杂,出了问题怎么排查?

下一章我们学习追踪与调试(Tracing & Debug),给 Agent 做体检,搞清楚它到底干了啥。

← 第6章 上下文与记忆 第8章 追踪与调试 →