使用 OpenAI Agents SDK 构建智能体——构建 AI 代理与代理式系统

209 阅读14分钟

到目前为止,我们已经把 AI 代理系统的各个部分分别搭建了出来:我们构建了简单代理,用工具扩展了它们的能力,加入了记忆与知识库,编排了多个代理协同工作,并学习了如何管理模型与护栏。现在,在最后一章里,我们要把这些零件拼到一起。目标是让你能够设计端到端的完整代理方案,充分利用你在本书中学到的一切。

本章你将学到:

  • 构建“客户服务员工”型 AI 代理:我们先搭一个虚拟客服助手,它会整合安全的数据库查询、知识库检索与输入护栏;当需要时,它还能把对话转交给“挽留专员”代理。这个案例会展示如何在一个系统里把工具与多代理接力结合起来。
  • 编排自动化的多代理工作流:接着,我们会编排一个用于自动化个性化客户外联的多代理流程。一个代理负责收集信息(来自数据库、历史聊天记录以及网页搜索),然后把材料交给第二个代理,由它撰写定制邮件。这个示例演示代理如何按顺序协作,端到端完成复杂任务。

读完本章,你将知道如何用 OpenAI Agents SDK 从零构建真实可用的 AI 代理系统。我们会把此前章节中的工具、记忆、会话与多代理编排全部统一起来。这个最终项目把你学到的内容贯通起来,帮助你在现实世界中打造强大的 AI 代理。

技术要求

请按照第 3 章中的详细步骤完成环境搭建。
本书各章的实用示例与完整代码都已发布在本书的 GitHub 仓库:github.com/PacktPublis…
建议你克隆该仓库,复用并改造里面的示例代码,并在学习过程中按需参考。

构建一个“客户服务员工”AI 代理

第一个案例是为一家虚构公司 PaperCo(向企业客户提供纸制品)打造一个由 AI 驱动的客服聊天机器人。

我们要构建的 AI 代理会扮演虚拟客服员工,处理客户问题、投诉与订单查询。它会整合我们之前讨论过的多项高级特性:

  • 用于查询订单状态的函数工具(连接数据库)
  • 在公司政策文档中检索信息的向量检索工具
  • 忽略无关用户输入的输入护栏
  • 当用户表达取消服务意图时,移交给专门的“挽留代理”

这些组件协同工作,形成一个稳健、可交互的聊天机器人:既能检索事实数据,也能智能地管理对话流程。下图展示了其工作方式:

image.png

图 9.1:代理可视化

接下来逐一介绍各组件:

  • 订单数据库与查询工具:一个存放订单的 SQLite 数据库,以及一个供代理调用的函数工具(query_orders)用来查询订单状态。该工具强制要求客户提供授权键(authorization key)后才返回订单信息(以确保数据安全)。
  • 知识库搜索工具:一个基于文件的向量检索工具(file_search),让代理可以从客服政策文档中检索答案(应对常见 FAQ 或公司政策问题)。
  • 输入护栏(相关性检查器) :一个护栏代理用来审查用户问题,拦截与客服无关的请求(防止跑题)。护栏并未在前面的图中展示。
  • 挽留代理:专门处理“想要取消服务”的场景。如果客户表示想要取消服务,该代理接管对话,以同理心理解用户并尝试挽留(甚至提供激励)。
  • 主客服代理:用户直接交互的主代理(“客户服务代理”)。它使用上述工具与护栏,并在需要时把对话移交给挽留代理。这个代理通过会话维护多轮对话的状态。

把这些部分拼在一起,就得到了一位能力完整的“AI 客服员工”。下面我们将逐步实现各个组件。

搭建数据库

首先需要准备一些数据供代理使用。本例中我们创建一个简易的 SQLite 数据库存放订单信息,并预置一些示例订单。

下面的初始化脚本会创建一个 SQLite 数据库文件,并填充 orders 表,同时也定义了几个测试查询函数用于验证数据。

新建文件 setup.py,并运行以下程序:

import sqlite3

# Set up SQLite DB
conn = sqlite3.connect("paper_data.db")
cursor = conn.cursor()

# Delete orders table if it exists
cursor.execute("DROP TABLE IF EXISTS orders")

# Create orders table
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
    order_id INTEGER PRIMARY KEY,
    authorization_key TEXT,
    order_status TEXT
)
""")

# Insert fake order data
orders_data = [
    (1001, "154857", "shipped"),
    (1002, "154857", "processing"),
    (1003, "958542", "delivered"),
    (1004, "445720", "cancelled"),
]
cursor.executemany("INSERT OR IGNORE INTO orders (order_id, authorization_key, order_status) VALUES (?, ?, ?)", orders_data)

conn.commit()
conn.close()

上述代码用 Python 创建了名为 paper_data.db 的 SQLite 数据库与 orders 表,并插入了四条示例订单记录,每条记录都关联一个客户授权键。

准备向量存储

下一步是创建一个向量存储,放入客服相关资料,供代理检索。步骤如下:

  1. 访问 OpenAI 平台 platform.openai.com/ 并登录(确保与生成 API Key 的账号一致)。
  2. 右上角进入 Dashboard(控制台) ,选择 Storage(存储) ,切换到 Vector stores(向量存储)
    image.png

图 9.2:向量存储

小贴士:需要查看高清图?请在 Packt 的 next-gen 阅读器或 PDF/ePub 版本中查看。购买本书可免费解锁 next-gen 阅读器:访问 packtpub.com/unlock,搜索本书名称并确认版本。

  1. 点击 Create 新建一个向量存储,例如命名为 PaperCoCustomerServiceMaterials
  2. 向下滚动,点击 + Add files 添加文件。
  3. 上传本书 GitHub 仓库(第 4 章目录下)提供的 PaperCoCustomerServiceMaterials 文件。将其命名为 PaperCoCustomerServiceMaterials.docxPurpose 选择 user_data,然后 Attach

上传完成后,向量存储会自动完成用于 RAG 的相关流程(生成嵌入等)。

  1. 复制并保存该向量存储的 ID(在向量页面的右上角)。

image.png

图 9.3:Storage 页面

到这里,我们已经把客服资料放进了向量存储,供代理在回答问题时使用。

创建用于查询数据的函数工具

接下来创建一个函数工具,供代理查询订单数据库。像我们之前所学,OpenAI Agents SDK 可以把一个 Python 函数“包装”为工具。这里我们定义 query_orders 函数,在 orders 表上执行 SQL 并返回结果。该函数使用 @function_tool 装饰,让代理可以调用。更重要的是,query_orders 会强制只返回与给定授权键匹配的订单结果(防止越权访问)。

新建 agent.py,加入以下代码:

from agents import ( 
    Agent, Runner, SQLiteSession, trace, 
    function_tool, FileSearchTool
)
import sqlite3
from agents import (
    GuardrailFunctionOutput, InputGuardrailTripwireTriggered, 
    input_guardrail, RunContextWrapper, TResponseInputItem
)
from pydantic import BaseModel
from dotenv import load_dotenv
from agents.extensions.visualization import draw_graph
load_dotenv()

@function_tool
def query_orders(sql_query: str, authorization_key: str):
    """
    Executes the given SQL query on the orders table and returns the result.
    You must provide the authorization_key.
    Table: orders
        order_id INTEGER PRIMARY KEY,
        authorization_key TEXT,
        order_status TEXT
    Only rows matching the provided authorization_key will be accessible.
    """
    db_path = "paper_data.db"
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        # Wrap the user's query as a subquery filtered by authorization_key
        sub_query = f"(SELECT * FROM orders where authorization_key = {authorization_key}) a"
        filtered_query = sql_query.replace("orders", sub_query)
        cursor.execute(filtered_query)
        result = cursor.fetchall()
        conn.close()
        return result
    except Exception as e:
        return f"Error querying orders.db: {e}"

这里我们定义了两个入参:sql_query(代理想执行的 SQL 字符串)与 authorization_key(应与客户的授权键匹配)。函数连接 paper_data.db 并执行查询。

函数内部有一个“巧思”来落实授权检查:它把传入 SQL 中对 orders 表的引用替换为一个已按 authorization_key 过滤过的子查询。
比如代理准备执行 SELECT * FROM orders WHERE order_id = 1003,函数会把它改写为:

SELECT * FROM (SELECT * FROM orders WHERE authorization_key = 958542) a
WHERE order_id = 1003

这样只有当订单属于该授权键时才会返回结果,防止未授权数据访问。

注意
authorization_key 做查询过滤适合教学示例,但不应视为生产最佳实践。将授权键硬编码或直接传入查询会带来风险(如 SQL 注入或密钥泄露)。真实的代理系统应通过安全的认证与授权层(如 OAuth、API Token、基于角色的访问控制等)来管理与校验凭证。

创建向量存储搜索工具

除了具体的订单查询,我们的客服机器人还应能回答通用问题(如“退货政策是什么?”),或需要参考公司指南的事项。为此,我们使用 OpenAI 的 FileSearchTool 在向量存储中检索文档。前面我们已经创建了一个包含客服信息的向量存储文档。

agent.py 中加入以下代码,为代理配置向量检索工具:

file_search_tool = FileSearchTool(
    vector_store_ids=['<Enter your vector store ID here>']
)

这样代理就可以检索向量存储来回答问题了。

创建输入护栏

为防止系统被误用,我们再加一个输入护栏:当用户问与客服完全无关的问题(例如“讲个笑话”)时,机器人不该作答。我们将加入一个输入护栏,拦截用户问题并判断其是否与客服相关。

实现方式:构建一个简易的分类器代理(guardrail_agent),它只负责检查输入并输出一个布尔标记,表示该问题是否与客服相关。然后用 @input_guardrail 装饰器把这道检查“插”到主代理前面。如果护栏判断不相关,就触发 InputGuardrailTripwireTriggered 异常,我们可以优雅地处理这种跑题请求。

agent.py 中加入以下代码定义护栏代理与护栏函数:

class GuardrailTrueFalse(BaseModel):
    is_relevant_to_customer_service: bool

# Create a guardrail agent
guardrail_agent = Agent(
    name="Guardrail check",
    instructions="You are an AI agent that checks if the user's prompt is relevant to answering customer service and order related questions",
    output_type=GuardrailTrueFalse,
)

# Create a guardrail
@input_guardrail
async def relevant_detector_guardrail(
    ctx: RunContextWrapper[None],
    agent: Agent,
    prompt: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
   
    result = await Runner.run(guardrail_agent, input=prompt)
    tripwire_triggered = False
    if result.final_output.is_relevant_to_customer_service == False:
        tripwire_triggered = True

    return GuardrailFunctionOutput(
        output_info="",
        tripwire_triggered=tripwire_triggered
    )

relevant_detector_guardrail 会在主代理处理用户输入之前异步运行。通俗讲,就是当用户说了句话,先由护栏代理判断是否相关;若判定不相关,就触发“绊线”(tripwire)来阻止主代理按常规流程作答。

创建挽留代理(Retention Agent)

接下来需要设置一个挽留代理。这是一个专门处理表示想要取消订单或对服务不满意的客户的代理。

我们把挽留代理定义为一个单独的 Agent 实例,并赋予它专属的指令。将下面的代码加入到 agent.py 文件中:

retention_agent = Agent(
    name="Retention Agent",
    instructions=(
        "You are a retention agent. Your goal is to encourage the customer not to cancel their service, "
        "understand their pain points, and empathize with their situation. If the customer insists on cancelling, "
        "you may offer up to $100 credit on their account as a retention incentive."
    ),
    tools=[query_orders],
)

我们的挽留代理将保持礼貌、具备同理心,并在必要时提供信用额度(最多 100 美元)来劝阻客户取消服务。

创建客服代理(Customer Service Agent)

到目前为止,我们已经具备构建主客服代理的全部组件。这个代理将整合前面提到的一切(工具、护栏与接力/移交)。将以下代码加入到 agent.py 文件中:

customer_service_agent = Agent(
    name="Customer Service Agent",
    instructions=(
        "Introduce yourself as the complaints agent."
        "Handle any customer complaints with empathy and clear next steps."
        "Use the file_search_tool to get general answers to questions"
        "For specific order related queries, you the query_orders function_tool"
        "To use the query_order tool, you will need the user's authorization key"
    ),
    tools=[query_orders, file_search_tool],
    input_guardrails=[relevant_detector_guardrail],
    handoffs=[retention_agent]
)

逐项说明这些参数:

  • name 与 instructions:我们将其命名为 “Customer Service Agent”,并指示它以“投诉处理代理”的身份行事。提示词比较详细,指导它在何时使用哪些工具与何时进行移交;同时明确说明使用 query_orders 时需要授权键。
  • tools:传入 query_ordersfile_search_tool。这意味着代理的 LLM 在推理时可以选择把它们作为函数调用。
  • input_guardrails:挂载 relevant_detector_guardrail。因此每条用户输入都会先经过该护栏;若判定不相关,将阻止代理按正常流程继续。
  • handoffs:在列表中加入 retention_agent。这样我们实际上构成了一个多代理系统——当对话需要挽留专员时,OpenAI Agents SDK 可以执行“移交”。

至此,客服代理的定义就完整了:它具备获取信息的工具、有护栏保证跑在正确轨道上,并在特定场景下能调用后备代理。现在我们来构建运行器(runner)。

构建 Runner

在真实应用中,我们会把系统部署到一个聊天界面里。这里我们用终端模拟交互循环。我们使用 SQLiteSession 来维持会话状态(让代理在多轮对话中记住上下文),并用 Runner.run_sync() 在循环里处理用户输入。同时,我们要捕获由护栏触发的异常,以便在用户提问超出范围时给出提示。

把以下代码加入 agent.py

# Create a session
session = SQLiteSession("session")
last_agent = customer_service_agent
with trace("Customer service agent"):
    while True:
        try:
            question = input("You: ")
            result = Runner.run_sync(last_agent, question, session=session)
            print("Agent: ", result.final_output)
            last_agent = result.last_agent
        except InputGuardrailTripwireTriggered:
            print ("This comment is irrelevant to customer service.")

现在我们已经可以开始模拟这个代理系统了。

测试代理

先问一个与客服无关的问题,验证护栏是否能成功触发:

You: What's 5 + 15?
This comment is irrelevant to customer service.

然后,问一个关于订单的问题。注意代理会主动索要授权键:

You: What's the status of my order? It's 1002
Agent: To check the status of your order, I'll need your authorization key. Could you please provide that?
You: Sure, it's 154857
Agent: Your order with ID 1002 is currently in the "processing" status. If there's anything else you need or if you have any concerns, feel free to let me know!

接着,问一个通用的客服问题,验证它是否能进行向量检索:

You: How much do I need to order to qualify for the bulk discount?
Agent: To qualify for a bulk discount, you need to place an order over $500. This qualifies for a 5% discount which is applied automatically. If you need any further assistance, feel free to ask!

最后,要求取消账号,观察移交到挽留代理的过程,并查看是否给出挽留优惠:

You: I want to cancel my account
Agent: I'm sorry to hear you're considering canceling your account. Could you let me know what's prompting this decision? I'd love to help resolve any issues or concerns you might have. If there's something specific bothering you, maybe I can assist or find a suitable solution for you.
You: It's just too expensive
Agent: I understand how important it is to manage expenses. To help with that, I can offer you a $100 credit on your account, which can ease the cost a bit. Would that work for you?

所有测试都顺利通过。至此,我们已经构建了一个可运行的代理系统,它作为聊天机器人扮演“客服员工”的角色。这个例子展示了一个功能完整的代理如何把多种高级特性协同起来:主代理既能检索事实数据、搜索文档、维持上下文,又能在需要时把对话移交给专门代理,同时确保自己始终在职责范围内工作。

在结束本节之前,回顾一下我们完成的内容:你看到一个客服代理如何将多种组件(用于查询结构化数据的工具、用于检索政策文档的向量搜索、让系统不跑题的护栏、以及向专门代理移交)组合成一个连贯的解决方案。这个案例之所以重要,是因为它演示了如何编排 OpenAI Agents SDK 的不同能力,去打造一个贴近业务、可落地的代理系统——它不仅能回答简单问题,还能管理上下文、加强安全与合规,并根据客户需求灵活调整对话策略。

在下一节中,我们会继续在此基础上构建,使用 AI 代理为 PaperCo 自动化一个工作流。

编排一个自动化的多代理工作流

我们的第二个案例展示了如何在工作流自动化中使用 AI 代理。公司 PaperCo 想要定期向客户发送个性化的跟进邮件,既关心他们的兴趣点,又“轻轻地”推广一款新产品。与其手动为每位客户做调研、写邮件,我们可以构建一个代理系统自动完成这些工作。

这个工作流包含按顺序协作的两个代理

  • 客户调研代理:收集客户信息——包括来自数据库的基本资料、最近的对话记录(回忆他们的兴趣或曾提及的个人信息),甚至通过网页搜索获取与这些兴趣相关的最新新闻
  • 邮件生成代理:接收调研代理整合好的信息,为该客户生成一封简短、个性化的邮件,同时提及新的产品优惠

下图展示了这些组件如何组合在一起:

image.png

图 9.4:工作流组件示意图

让我们逐一说明各组件:

  • 用户数据库与查询工具:一个包含客户详细信息(姓名、邮箱、所在地等)的 SQLite 数据库,以及用于按 ID 获取客户信息的 query_users 工具函数。
  • 客户对话数据与检索工具:一个保存客户历史对话记录的 JSON 文件,以及 get_user_transcripts 工具,用于提取指定用户的对话记录。这些记录里包含客户在先前聊天中提到的个人兴趣(例如喜欢的运动或食物)。
  • 网页搜索工具web_search_tool 让代理执行网页搜索,找到与客户兴趣相关的最新信息或新闻(例如客户喜欢某支球队,代理可以找到最近比赛的结果或相关新闻)。
  • 客户调研代理:使用上述工具汇总“客户画像/简报” ,输出可用于个性化邮件的摘要或关键信息集合。
  • 邮件生成代理:基于调研结果生成实际邮件。它会输出结构化结果(如主题与正文字段),将个性化元素(兴趣/新闻)与营销信息(如 PaperCo 新品优惠)结合起来;并选用更适合个性化写作的 GPT 模型。

我们还会创建一个简单循环(编排工作流) ,遍历客户列表:对每位客户先运行调研代理,再把结果交给邮件代理,最后保存生成的邮件

上述组件协同后,就形成了一个把分散数据转化为精致个性化邮件的完整工作流。下一节我们先从搭建客户数据库开始,这是调研代理的基础。

搭建客户数据库

与第一个示例类似,我们先准备数据。这里我们创建一个名为 customer_details.db 的数据库,其中包含存储客户基本信息(姓名、邮箱、所在地、业务类型、电话等)的 users 表。本示例的初始化脚本会创建数据库并插入几条示例用户。

新建 setup.py,运行以下代码以完成数据库初始化:

import sqlite3

# Set up SQLite DB
conn = sqlite3.connect("customer_details.db")
cursor = conn.cursor()

# Create users table for customer details
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
    user_id INTEGER PRIMARY KEY,
    first_name TEXT,
    last_name TEXT,
    email TEXT,
    location TEXT,
    business_type TEXT,
    phone_number TEXT
)
""")

# Insert fake user data
users_data = [
    (1, "Emily", "Clark", "emily.clark@example.com", "New York", "Retail", "555-1234"),
    (2, "Michael", "Nguyen", "michael.nguyen@example.com", "San Francisco", "E-commerce", "555-5678"),
    (3, "Sophia", "Patel", "sophia.patel@example.com", "Chicago", "Wholesale", "555-8765"),
    (4, "David", "Martinez", "david.martinez@example.com", "Houston", "Manufacturing", "555-4321"),
]
cursor.executemany(
    "INSERT OR IGNORE INTO users (user_id, first_name, last_name, email, location, business_type, phone_number) VALUES (?, ?, ?, ?, ?, ?, ?)",
    users_data
)

conn.commit()
conn.close()

每条记录都包含邮箱与基本画像信息。这些信息既可用于个性化内容,也可用来填充收件人等字段。

准备对话记录 JSON

我们还需要一个名为 customer_transcripts.json 的 JSON 文件,里面包含每位客户的一些历史对话记录。这些记录是以往客服聊天的日志,往往夹带一些个人兴趣/偏好,能让邮件更有“人味儿”。

新建 customer_transcripts.json 文件,并从本书的 GitHub 仓库中拷贝内容(示例结构如下):

{
  "conversations": [
    {
      "user_id": 1,
      "date": "2024-06-01",
      "transcripts": "Hi, I have a question about my order... (conversation with support agent)... I'm a big fan of the New York Knicks... (more chat)..."
    },
    {
      "user_id": 2,
      "date": "2024-06-02",
      "transcripts": "Can I change my delivery address?... I'm a sushi fan... also I love the San Francisco Giants... (more chat)..."
    }
    ...
  ]
}

每条会话包含 user_iddate 与整段合并的 transcripts。例如:用户 1 提到自己是 New York Knicks(篮球队) 的球迷;用户 2 提到喜欢寿司且是 San Francisco Giants(棒球队) 的球迷。

创建数据检索与网页搜索的函数工具

数据就绪后,需要给代理提供可调用的工具。我们将创建两个函数工具:

  • query_users:对 users 表执行 SQL 查询,获取用户信息
  • get_user_transcripts:读取 JSON 并提取指定用户的所有对话记录,合并为一个长字符串

新建 agent.py,加入以下代码以创建这两个函数工具:

from agents import (
    Agent, Runner, SQLiteSession, trace, 
    function_tool, WebSearchTool
)
import sqlite3
from pydantic import BaseModel
from dotenv import load_dotenv
from agents.extensions.visualization import draw_graph
import json
load_dotenv()

@function_tool
def query_users(sql_query: str):
    """
    Executes the given SQL query on the users table and returns the result.
    Table: users
        user_id INTEGER PRIMARY KEY,
        first_name TEXT,
        last_name TEXT,
        email TEXT,
        location TEXT,
        business_type TEXT,
        phone_number TEXT
    """
    db_path = "customer_details.db"
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute(sql_query)
        result = cursor.fetchall()
        conn.close()
        return result
    except Exception as e:
        return f"Error querying users: {e}"

@function_tool
def get_user_transcripts(user_id: int) -> str:
    """
    Extracts and returns all transcripts for the given user_id from customer_transcripts.json as one long string.
    """
    json_path = "Chapter9/WorkflowAutomation/customer_transcripts.json"
    try:
        with open(json_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        transcripts = [
            conv["transcripts"]
            for conv in data.get("conversations", [])
            if conv.get("user_id") == user_id
        ]
        return "\n\n".join(transcripts) if transcripts else ""
    except Exception as e:
        return f"Error reading transcripts: {e}"

query_users 很直观:连接 customer_details.db,执行传入的 SQL(预期针对 users 表),并返回结果。这与我们先前做的 query_orders 工具有异曲同工之妙。
get_user_transcripts 打开包含对话的 JSON 文件,找出所有匹配 user_id 的会话,并把它们合并成一个大字符串——若用户有多段历史会话,就会聚合在一起。二者都用 @function_tool 装饰,暴露为可被代理调用的工具。

此外,我们还会使用一个 OpenAI 托管的网页搜索工具 让代理能够进行搜索。在 agent.py 中加入:

web_search_tool = WebSearchTool()

至此,构建代理所需的工具已全部就位。

创建客户调研代理

现在来创建本工作流中的第一个代理:客户调研代理。它的职责是为邮件生成完整上下文,并产出包含以下信息的报告:

  • 客户基本资料(姓名、所在地等)
  • 从对话记录中提炼的个人兴趣/备注(用于个性化邮件)
  • 与这些兴趣相关的一两条最新新闻/事实(通过网页搜索获得)

agent.py 中加入以下代码:

customer_research_agent = Agent(
    name="Customer Research Agent",
    instructions=(
        "You are an AI agent that performs research on customers to create a customer profile."
        "Given a customer ID, you should create a customer report that:"
        "- retrieves customer details"
        "- reads previous customer transcripts on the customer interests, to be used to personalize emails"
        "- summarized latest news (search the web) on things related to their interests they've noted in the transcript"
    ),
    tools=[web_search_tool, query_users, get_user_transcripts]
)

在这个代理中,指令(instructions)至关重要:我们明确了它的角色,并用条目列出所需任务;同时把先前创建的所有工具一并提供给它。这样,它就能围绕“客户画像”自动查库、读对话、搜新闻并汇总,为下一步的邮件生成打下坚实基础。

创建邮件生成代理

这个代理将把调研代理的输出作为输入,生成一封实际邮件。为确保邮件结构清晰(便于发送) ,我们让该代理产出一个包含特定字段的 JSON 对象:收件人邮箱、发件人邮箱、主题,以及邮件正文(正文可以用 HTML)。

我们用 Pydantic 模型来定义期望的邮件输出模式(如 To、Subject 等字段)。在 agent.py 中加入下面的模型定义:

class EmailOutput(BaseModel):
    to_email: str
    from_email: str
    subject: str
    html_email: str

现在可以创建邮件代理,并指定其输出类型:

email_creation_agent = Agent(
    name="Email Creation Agent",
    instructions=(
        "You are an AI agent that generates emails to keep in touch with customers of PaperCo."
        "Your goal is to create an email given the information that you have been provided from another agent"
        "Use the information in a subtle way, like you're trying to share with them a news story related to their interests or a personal feature"
        "The goal of the email is to be personable and catch up with them, and also to let them know about our newest offer on Paper Products"
        "The newest offer in Paper products includes a premium subscription plan where all their orders are 10 percent off"
        "The email should be very concise, just a few sentences, and to the point"
    ),
    output_type=EmailOutput,
    model="gpt-4.1-2025-04-14"
)

在这个代理中,我们把 output_type 设为 EmailOutput,这样代理会尝试输出符合该模式的 JSON。SDK 会用 Pydantic 解析模型输出,因此在 Python 里你会直接拿到一个 EmailOutput 对象(字段可直接访问)。

例如,假设调研代理发现某位名叫 Sarah 的客户最近谈到她热爱可持续办公用品。邮件生成代理可能会输出如下内容:

{
  "to_email": "sarah@example.com",
  "from_email": "support@paperco.com",
  "subject": "A quick note on eco-friendly supplies",
  "html_email": "<p>Hi Sarah,</p><p>We saw that sustainability is important to you, so we thought you'd enjoy this recent article on eco-friendly office trends. We're also excited to share that our new premium subscription plan gives you 10% off all orders, including our recycled paper line.</p><p>Best,<br>PaperCo Team</p>"
}

这说明代理不是只吐出纯文本,而是结构化成一个可直接发送的 JSON 对象。开发者可以把它直接接入邮件发送系统,无需额外解析或格式化。

我们还手动将模型调整为 GPT-4.1。要把个性化元素与促销信息自然融合,通常需要更强的模型(如 GPT-4)来完成,因此我们明确选择了更擅长个性化写作的 GPT-4 变体。

编排工作流

最后,需要把两个代理串起来,对每位客户运行一次完整流程。对每个用户 ID,我们将:

  1. 以该用户 ID 作为输入运行 customer_research_agent,得到包含画像/新闻摘要的结果;
  2. 将该结果作为输入传给 email_creation_agent
  3. 获取最终邮件输出(一个 EmailOutput 对象);
  4. 将其保存为文件。

agent.py 中加入以下代码:

for user_id in ["1", "2", "3", "4"]:
    with trace(f"Workflow automation agent for user: {user_id}"):
        result = Runner.run_sync(customer_research_agent, input=user_id)
        print(result.final_output)
        email = Runner.run_sync(email_creation_agent, result.final_output)
        print(email.final_output)
        # Write email to a new JSON file with title equal to the user_id
        with open(f"{user_id}.json", "w", encoding="utf-8") as f:
            json.dump(email.final_output.dict(), f, ensure_ascii=False, indent=2)

这里我们遍历 users 表中插入的 1~4 号用户。对每位用户,我们用一个 trace 包裹操作,便于在 Traces 模块中查看该用户运行的日志。现在,一切就绪,可以开始测试这套自动化工作流了。

测试工作流

运行程序,观察结果。以下以用户 1 为例。首先,customer_research_agent汇总客户信息并概括其历史对话生成报告。你可以在 Traces 模块中看到这一过程:

图 9.5:该工作流在 Traces 模块中的展示

该代理输出的报告示例如下:

Customer Profile: Emily Clark

Personal Information:
Name: Emily Clark
Email: emily.clark@example.com
Location: New York
Business Type: Retail
Phone Number: 555-1234

Customer Interests:
Based on previous interactions, Emily has expressed a strong interest in basketball, particularly as a fan of the New York Knicks. She enjoys playing basketball recreationally with friends and has recently purchased new sneakers for the court. Additionally, she has a preference for pepperoni pizza, especially after playing basketball.

Recent News Related to Interests:
Mikal Bridges' Contract Extension:
On August 1, 2025, Mikal Bridges agreed to a four-year, $150 million contract extension with the New York Knicks. The deal includes a player option for the 2029-30 season and a trade kicker, making Bridges ineligible for trade for six months. By accepting a slightly reduced salary compared to his maximum extension, Bridges has provided the Knicks with additional salary cap flexibility to strengthen their roster. Since joining the Knicks in 2024, Bridges has been instrumental in the team's success, helping them reach their first Eastern Conference Finals in 25 years. (reuters.com)

Appointment of Head Coach Mike Brown:
In July 2025, the New York Knicks appointed Mike Brown as their new head coach following their first conference finals appearance in 25 years. Brown, 55, brings a wealth of experience from previous coaching roles with the Cleveland Cavaliers, Los Angeles Lakers, and Sacramento Kings. He emphasized the importance of relationships, trust, and accountability within the team and is eager to lead the Knicks to their first NBA championship since 1973. (reuters.com)

Karl-Anthony Towns' Impact:
Since being traded to the New York Knicks on October 2, 2024, Karl-Anthony Towns has made significant contributions to the team. Notably, on October 29, 2024, he scored 44 points and grabbed 13 rebounds in a 116107 victory over the Miami Heat. This performance marked the highest point total by a Knicks center since Patrick Ewing in 1995. (en.wikipedia.org)

随后,这份报告被传入 email_creation_agent。邮件代理基于报告生成个性化邮件,并产出 EmailOutput 对象;该对象接着被保存为 1.json,如下所示:

{
  "to_email": "emily.clark@example.com",
  "from_email": "hello@paperco.com",
  "subject": "Big Knicks News & Exclusive PaperCo Offer!",
  "html_email": "<p>Hi Emily,</p><p>Exciting times for Knicks fans—Mikal Bridges just signed a new contract extension, and Coach Mike Brown is now at the helm! With games heating up and sunny weather ahead, it's the perfect season for basketball.<p><p>As you gear up for fall, we wanted to share our newest PaperCo premium subscription: enjoy 10% off every order, making your retail supply runs even easier. Let us know if you'd like to learn more!</p><p>Stay energized and Go Knicks!<br/>The PaperCo Team</p>"
}

通过这种方式,两个代理协作,为该客户生成一封个性化外联邮件,顺带完成优惠信息的上促。这套工作流有很多可扩展方向:例如,加入一个SMTP 工具由代理自动发送邮件;再比如新增一个受众选择代理来决定联系哪些客户。一旦你掌握了如何自由组合工具与代理,延展空间将非常广阔。

总结

在本章结尾,我们构建了两个完整的、由代理驱动的解决方案,把你学到的所有技能串联了起来。首先,我们为 PaperCo 开发了一个客户服务聊天机器人,整合了多项高级能力:它使用数据库工具进行订单查询(带授权校验)、用知识库搜索解答常见问题(FAQ)、通过输入相关性护栏避免跑题,并在用户提出取消请求时移交给专门的挽留代理。接着,我们创建了一套自动化工作流系统,用于个性化客户外联邮件:在这个案例中,调研代理通过数据库查询、对话记录提取与网页搜索收集每位客户的资料与兴趣点,然后将结果交给邮件代理,由其生成包含新品优惠的定制化邮件。

回顾你在本书中的学习旅程:你从了解 AI 代理是什么、为何重要开始,学习了 OpenAI Agents SDK 的基础知识,完成环境搭建,并从零实现了简单代理。随后,你用工具与协议扩展代理能力,为其配置记忆与检索机制,探索多代理的接力/移交,并实践了模型与上下文的管理。你还学习了如何监控、加固与治理系统,确保其在生产环境下可靠运行。上述每一步都为你在本章搭建复杂的端到端系统打下了基础。

关键结论是:你现在已具备一整套构建“能处理真实业务任务”的代理的工具箱。你能够设计不仅仅回应单条提示的代理,而是能对接数据源、记住上下文、与其他代理协作,并在既定策略下安全运行。这套能力让你不再停留在试验层面,而是走向实用落地——无论是自动化重复的业务流程、打造专用助手,还是创新全新的应用形态。

更重要的是,这只是起点。代理化系统领域正在快速演进,而你掌握的知识让你站在这场变革的前沿。以 OpenAI Agents SDK 为基石,你可以探索新架构、整合新兴工具,不断拓展 AI 代理的能力边界。可能性广阔,下一代智能系统将由像你这样的建设者共同塑造。