OpenAI-Agents-SDK-构建构建指南-二-

OpenAI Agents SDK 构建构建指南(二)

原文:Building Agents with OpenAI Agents SDK

译者:飞龙

协议:CC BY-NC-SA 4.0

通过再次将整个消息列表传递给 run_sync() 方法,模型可以获取之前的对话内容,并能够正确确定第二个问题中的“它”指的是第一条消息中的“太阳”。我们的响应看起来如下:

## 第一条响应
太阳的温度在不同区域有所不同:
1. **核心**:约 1500 万摄氏度(2700 万华氏度)。
2. **表面(光球层)**:约 5500 摄氏度(9932 华氏度)。
3. **日冕**:范围在 100 万到 300 万摄氏度之间(180 万到 540 万华氏度)。
核心是发生核聚变的地方,产生太阳的能量,而日冕是外层大气,温度惊讶地比表面高得多。

## 第二条响应
太阳的直径约为 139 万公里(864 万英里)。它大约是地球直径的 109 倍,占据太阳系总质量的 99% 以上。

我们实际上可以让之前的代码段变得更加动态,使得用户消息不再是硬编码的,并且用户能够提交多条消息,就像真实的聊天一样。

为了实现这一点,我们可以使用 Python 原生的 input 和 while 循环功能。创建一个名为 memory_tracking_messages_simple_loop.py 的新文件并运行以下代码:

from agents import Agent, Runner

### 创建 agent
agent = Agent(
    name="QuestionAnswer",
    instructions="You are an AI agent that answers questions.",
)

messages = []
while True:
    question = input("You: ")
    messages.append({"role": "user", "content": question})
    result = Runner.run_sync(agent, messages)
    print("Agent: ", result.final_output)
    messages.append({"role": "assistant", "content": result.final_output})

该程序会向你索要一个提示词(prompt),你可以直接在运行 Python 程序的终端中输入。它随后会给出 agent 的响应并询问你下一个提示词,同时维护其消息历史。现在,我们已经将 agent 从单次交互系统转变为可以记住之前消息的多轮对话系统(假设在同一个会话中)。我们通过添加循环来实现,在每次迭代中都会发生以下操作:

  • 用户输入提示词。

  • 提示词作为 ResponseInputItem 对象添加到名为 messages 的运行中的 ResponseInputItem 对象列表中。

  • messages 对象通过 Runner 类传递给 agent。

  • 模型输出作为 ResponseInputItem 添加到 messages 中。

此循环会一直持续,直到遇到错误或用户强制退出程序(通过点击 Ctrl + C)。这里是一个对话示例,证明 agent 具有记忆(因为它记得我在第一次对话中提到的名字):

You: My name is Henry
Agent: Nice to meet you, Henry! How can I assist you today?
You: What's my name?
Agent: Your name is Henry.

Note

你可能会好奇,如果 messages 列表无限增长会怎样?这最终不会变成一个问题吗?这是一个很好的问题,是的,消息历史的增长是需要考虑的。别担心,我们将在本章的晚些时候介绍处理该问题的策略。

SDK 还提供了一个从 result 对象返回消息列表的函数,让手动对话管理变得更加容易。result.to_input_list() 函数返回一个 ResponseInputItem 对象列表,随后你可以向其中添加额外的 ResponseInputItem 对象。这使得代码变得整洁了一些(但功能仍然相同),如下所示:

from agents import Agent, Runner

### 创建 agent
agent = Agent(
    name="QuestionAnswer",
    instructions="You are an AI agent that answers questions.",
)

messages = []
while True:
    question = input("You: ")
    messages.append({"role": "user", "content": question})
    result = Runner.run_sync(agent, messages)
    print("Agent: ", result.final_output)
    messages = result.to_input_list()

在实践中,这种技术构成了有状态对话代理的基础。从这里开始,可以在上面叠加更高级的记忆技术,以实现跨会话的记忆化、提取结构化数据、调整记忆以及处理大型对话,但一切都始于跟踪对话历史。

使用 Sessions 进行对话管理

OpenAI Agents SDK 提供了一种名为 Sessions 的对话管理原语。它充当类,允许你自动存储、召回和编辑对话中的消息。这意味着你不需要手动调用 .to_input_list() 函数或手动管理对话消息;SDK 会帮你处理好。Sessions 类需要一个输入:一个代表会话唯一标识符的字符串,称为 session_id。

让我们修改之前的脚本并为其添加会话。创建一个名为 conversations_with_sessions.py 的新文件并运行以下代码:

from agents import Agent, Runner, SQLiteSession

### 创建 agent
agent = Agent(
    name="QuestionAnswer",
    instructions="You are an AI agent that answers questions.",
)

### 创建会话
session = SQLiteSession("first_session")

while True:
    question = input("You: ")
    result = Runner.run_sync(agent, question, session=session)
    print("Agent: ", result.final_output)

运行这段代码后(如你所见,它要简单得多),让我们再次尝试我们的测试用例:

You: My name is Henry
Agent: Nice to meet you, Henry! How can I assist you today?
You: What's my name?
Agent: Your name is Henry.

在这种情况下,对话历史通过 Sessions 类进行管理。与其手动添加消息或调用 .to_input_list(),不如说 session 对象在后台跟踪整个交互历史。

Sessions 在管理多个用户或对话时特别有用。通过为每个交互线程使用唯一的 session_id 值,代理可以维护独立的内存上下文。例如,你可能根据用户名和对话 ID 生成 session_id 值,确保每个用户或对话都有自己相互隔离的历史。

这为构建真正有状态的代理奠定了基础。请注意,SQLiteSession 默认情况下是内存中的(不跨重启持久化)。我们将在长期记忆部分讨论如何将会话持久化到磁盘。但在之前,我们必须解决大型线程的问题。

管理大型对话

为什么管理大型线程对代理来说是个问题?代理是由 LLM 驱动的。LLM 有一个核心问题是上下文窗口(即 LLM 一次只能接收和处理特定数量的字符或“标记”)。如果对话持续进行,简单地追加每条消息最终会达到这个限制,此时提示词变得过大而无法处理。

然而,即使没有这个技术限制,管理大型对话线程也是重要的。长提示词和上下文会显著降低速度并增加成本。因此,通过丢弃或压缩旧信息来管理短期记忆很重要。我们将讨论两种解决此问题的模式:滑动窗口和消息总结。

滑动窗口

滑动窗口是最简单且成本效益最高的记忆策略。它通过仅保留消息日志中最近的 N 条消息来工作,作为一个先进先出的(FIFO)队列。随着新消息到来,旧消息会被丢弃,确保提示词保持在模型的限制范围内。

这种方法适用于只需要短期记忆的代理,例如专注于解决单个查询线程的客户服务代理,但它确实存在忘记会话早期引入的重要信息的风险。例如,如果用户在对话早期分享了关键信息(如姓名、目标或限制),而窗口大小太小,代理可能会完全丢失这些信息。

消息总结

更高级的策略是消息总结,这意味着这种方法不是遗忘旧消息,而是将它们压缩成一个在整个会话期间持续的简要摘要。这允许代理在长对话中保留关键事实、决策或用户偏好,实际上桥接了短期和长期记忆。

总结通常涉及以下内容:

  • 持续监控消息历史的大小,如果超过特定阈值,则收集最旧的 N 条消息。

  • 将最旧的 N 条消息传递给 LLM,并使用提示词对其总结。

  • 在消息日志用最旧的 N 条消息替换为摘要后的回复。由于摘要输出的长度小于最旧 N 条消息的长度,这有效地缩短了消息日志的上下文窗口。

这种方法允许代理在不超出 token 限制的情况下保留长期上下文。当然,这里的权衡是高昂的成本和延迟,因为每个摘要任务都需要额外额外的 LLM 调用。在实践中,许多代理会结合使用这两种模式:使用滑动窗口移除旧消息,并使用摘要链以压缩的形式保留较早的上下文。

短期记忆的核心在于让代理在单个会话中具备感知能力。使用 OpenAI Agents SDK,通过 Sessions 类启用这一点是最直接的。然而,它也存在局限性,要让这种内存具有可扩展性(不超出上下文限制),需要周全策略,例如滑动窗口或消息摘要。

以下是一个滑动窗口模式的示例:

from agents import Agent, Runner
from collections import deque
### 创建代理
agent = Agent(
    name="QuestionAnswer",
    instructions="You are an AI agent that answers questions."
)
# 滑动窗口大小(仅保留最近的 5 条消息)
WINDOW_SIZE = 5
messages = deque(maxlen=WINDOW_SIZE)
while True:
    question = input("You: ")
    messages.append({"role": "user", "content": question})
    # 仅使用最近的 N 条消息运行代理
    result = Runner.run_sync(agent, list(messages))
    print("Agent:", result.final_output)
    messages.append({"role": "assistant", "content": result.final_output})

接下来,我们将探索长期记忆,并查看代理如何跨会话或在长时间保留信息。

长期记忆

长期记忆是代理在多个会话中很一段时间内记住相关细节的能力。这种“超能力”让代理能够跨多个会话保留和检索信息。它是创建真正具有状态的、个性化的且持久的 AI 体验的基础。这有时被认为是 AI 代理与人类代理之间的终极“障碍”,因为人类代理可以自然地记住重要的事实、偏好或过去的对话,并将它们分配给每一个人,而 AI 代理通常在这方面很困难。

我们之前在单会话的语境讨论过无状态和有状态的区别。这同样扩展到多会话交互。没有长期记忆的代理在会话之间本质上是无状态的。没有长期记忆的 AI 代理无法记住用户名、过去的偏好或已完成的任务等信息,即使在关闭或重启后也是如此。例如,如果用户告诉客户服务代理“我更希望我的订单发送到后门而不是前门”,然后在一周后返回,具备长期记忆的代理可以主动将所需的送货方式设置为“后门”。

让代理拥有长期记忆的关键在于记忆存储与检索。这是将代理的记忆存储在持久化状态中,以便日后检索的想法。有很多不同的模式可以实现这一目标,我们将在本节中介绍。

持久化消息日志

建立长期记忆最简单的模式也是最直观的:只需在代理会话结束时将消息日志的全部内容存储在某处,然后在代理重启时检索它。Agents SDK 提供了一种实现此功能的机制,使用了之前讨论过的 Sessions 类(SQLiteSession)。该类不仅可以接受会话 ID,还可以接受 db_path 参数,可以在其中输入计算机上的文件位置,它将自动保存并加载消息日志到本地机器的 SQL 数据库。

让我们来看一个示例。创建一个名为 ltm_sessions.py 的新 Python 文件并运行以下代码:

from agents import Agent, Runner, SQLiteSession
### 创建代理
agent = Agent(
    name="QuestionAnswer",
    instructions="You are an AI agent that answers questions.",
)
### 创建会话
session = SQLiteSession("first_session", db_path="messages.db")
while True:
    question = input("You: ")
    result = Runner.run_sync(agent, question, session=session)
    print("Agent: ", result.final_output)

在这种情况下,我们为 session 对象添加了 db_path 参数,这告诉 Agents SDK 将对话存储在我们的本地机器,并在代理重新实例化时再次加载对话。我们可以通过运行程序、传递信息、退出程序、再次运行程序并查看代理是否记得这些信息来测试这是否有效。

让我们运行程序并告诉代理我们的名字:

You: Hello, I'm Henry
Agent: Hi Henry! What would you like to talk about today?

接下来,点击 Ctrl + C 退出程序。之后,重新运行程序并询问我的名字:

You: What's my name?
Agent:  You mentioned your name is Henry. How can I assist you further?

通过这种方式,我们创建一个具有长期记忆的代理。它还会记住你如何与它交互,因为它在你的本地机器上存储了消息日志,并在每次启动程序时加载它。你可能会在运行代码的根目录下看到一个 messages.db 文件;这就是保存消息日志的地方。使用内置 session 对象进行持久化的用处是它是无缝的,换句话说,你不需要为存储聊天记录实现自己的文件格式或数据库模式。然而,这种模式确实有一些明显的局限性,即随着会话数量或对话长度的增加,存储和加载完整的消息日志可能会变得效率低下。此外,正如我们之前讨论的,消息日志可能会膨胀到 LLM 因为上下文窗口限制而无法再次处理。

结构化记忆检索

结构化记忆检索利用工具调用(tool-calling)来解决代理记忆问题。具体来说,这种模式关注以下概念:

  • 代理不是存储每一条消息,而是访问一个工具,只存储用户共享的重要信息

  • 代理不是加载每一条消息,而是访问一个工具,只检索并加载相关的片段信息

这样,代理在某个会话中的提示词和消息日志就可以保持整洁,但如果模型认为它应该咨询长期记忆(例如,用户询问“之前”或在过去的会话中提到的内容),它就可以调用该函数来获取相关信息。

让我们来看一个示例。创建一个名为 ltm_structured_memory_call.py 的新 Python 文件。首先,我们将初始化内存文件。在这里我们检查是否已存在 JSON 文件。如果没有,我们将创建一个带有默认结构:

from agents import Agent, Runner, function_tool
import os
import json
### 如果 JSON 文件不存在,则创建
FILENAME = 'memory.json'
memory_default = {
    "user_profile": [],
    "order_preferences": [],
    "other": []
}
if not os.path.exists(FILENAME):
    with open(FILENAME, 'w') as f:
        json.dump(memory_default, f, indent=4)
        print(f"Created '{FILENAME}' with default data.")
else:
    print(f"'{FILENAME}' already exists.")

接下来,我们将定义一个用于保存记忆片段的工具。此函数允许智能体在 JSON 文件中存储重要事实:

@function_tool
def save_memory(memory_type: str, memory: str) -> str:
    """
    将记忆保存到记忆存储中。
    Args:
        memory_type: 要存储的记忆类型。在 user_profile, order_preferences 或 other 之间选择。
        memory: 要保存的记忆
    """
with open(FILENAME, 'r') as f:
        data = json.load(f)
    data[memory_type].append(memory)

    with open(FILENAME, 'w') as f:
        json.dump(data, f, indent=4)
    print(f"Memory ({memory}) saved")
    return f"Memory ({memory}) saved"

我们还会创建一个加载记忆的函数。此函数从 JSON 记忆存储器中检索相关事实:

@function_tool
def load_memory(memory_type: str) -> str:
    """
    从记忆存储中加载一组记忆。
    Args:
        memory_type: 要加载的记忆类型。在 user_profile, order_preferences 或 other 之间选择。
    """
with open(FILENAME, 'r') as f:
        data = json.load(f)
    return "|".join(data[memory_type])

最后,我们创建智能体,赋予其访问工具的权限,并运行交互循环:

### 创建智能体
agent = Agent(
    name="QuestionAnswer",
    instructions="你是一个回答问题的 AI 智能体。你可以访问两个工具,允许你保存和加载记忆。当你学到一个重要事实时,请保存记忆。当问及关于用户的问题时,请加载记忆。",
    tools=[save_memory, load_memory]
)
while True:
    question = input("You: ")
    result = Runner.run_sync(agent, question)
    print("Agent: ", result.final_output)

让我们分析一下这段代码中发生了什么,从保存和加载记忆的两个函数工具开始。

save_memory 函数允许智能体在特定类别下(如 user_profile 或 order_preferences)存储重要事实。该函数将信息写入本地 memory.json 文件,并将新信息片段追加到相关部分。例如,如果用户说:“我更倾向于前门送货”,智能体可以调用 save_memory("user_profile", "Prefers deliveries through the front door"),该事实将被添加到持久化存储中。在这种情况下,持久化存储仅仅是一个本地 JSON 文件,但也可以同样轻松作为数据库。

另一方面,load_memory 函数旨在检索之前存储的事实。当用户引用之前对话中的偏好或细节时,例如问道:“我的送货偏好是什么?”,智能体可以调用 load_memory("order_preferences ") 来从该类别中获取并总结相关的存储事实。

这些函数为智能体提供了一种结构化且轻量级的长期记忆机制。智能体并不存储完整的消息转录,而是保留重要且可重用的提炼见解。这比保留每一条消息日志具有更强的扩展性。它也符合人类思考记忆的方式。想象一下,你和朋友聊天,他们告诉你有他们喜欢吃寿司。在这种情况下,你不会简单地记住整个对话转录。相反,你只是“记录”了他们喜欢寿司这个重要信息,并将其“存储”在你的记忆中。

让我们通过示例来尝试。运行代码后,输入以下内容:

You: I like to have orders sent to the office
Memory (User prefers orders to be sent to the office.) saved
Agent: Got it! I'll remember that you prefer to have orders sent to the office.

智能体识别出这是一个有用的偏好,将调用 save_memory 工具。如果你打开 memory.json,甚至可以看到这一点:

{
    "user_profile": [
    ],
    "order_preferences": [
        "User prefers orders to be sent to the office."
    ],
    "other": []
}

现在,退出当前会并重新运行之前的 Python 程序开启新会,然后询问智能体我们的偏好:

You: Where do I like my orders sent?
Agent: You like your orders sent to the office.

在这种情况下,智能体调用了 load_memory 工具来检索 order_preferences 下的所有记忆。它找到了相关信息并提供了正确的回答。

这种结构化的长期记忆方法更具扩展性、语义上更精确,并且避免了超出上下文窗口限制的陷阱。它为更高级的记忆系统奠定了基础,在这些系统中,事实可以被嵌入和索引。

注意

我们在这里实现的结构化记忆模式对于简单场景简单有效。然而,随着你的智能体随时间积累更多事实(或者如果你需要支持模糊搜索),这种基础的键值方法可能会变得局限。这就是向量数据库和语义嵌入大显用场的地方。与其将事实存储在 JSON 文件中,你可以将每个记忆表示为向量,并进行语义搜索。我们稍后会讨论这一概念。

当你构建更复杂的智能体时,你可能希望结合这些长期记忆模式(持久消息日志和结构化记忆),而 OpenAI SDK 足够灵活同时支持两者。

训练知识

如所述,训练知识指的是通过训练数据固有存储在模型中的信息。每个大语言模型(LLM)最初都带有源自其初始训练的海量数据集(通常是大型文本语料)。模型拥有固有知识的好处在于知识本身具有以下优势:

  • 检索速度快:由于固有知识固化在模型权重中,模型可以非常快速地检索信息,仅限于 LLM 的计算速度。

  • 覆盖范围广:由于训练数据是海量的(互联网语料),固有知识可以非常详细地涵盖许多主题。

改变模型固有知识的过程被称为微调(fine-tuning)。与引导模型现有知识的提示或检索技术不同,微调通过在精选的数据集上重新训练模型来直接重塑模型。此过程更新了模型的权重,使其能够采用以前无法良好处理的新术语、模式或行为。结果是一个在特定领域或任务上更专业、更准确的模型。

让我们以医疗领域为例。像 GPT-4o 这样的通用型 LLM 可能理解广泛的健康主题,但在被要求解释复杂报告或根据细致指南建议治疗方案时,可能会表现不从心。通过在结构化患者记录和医生笔记的数据集上对模型进行微调,我们可以创建一个定制的模型,能够回答更细致的健康问题。

微调适用于医学等领域,在这些领域中领域知识至关重要,准确性直接影响结果。然而,它存在一些严重的局限性和缺点:

  • 计算密集型(且昂贵):微调需要大量的计算资源和 GPU 时间,特别是在处理高容量模型时。虽然市场上有一些微调服务选项,但即使是它们也可能贵得令人接受(通常训练一个模型的成本从 10,000 美元起,这还不包括托管模型的费用)。

检索知识的实现

  • 缺乏灵活性:一旦模型完成微调,它就必须与“基础”模型分开维护。当新知识出现或基础模型更新时,整个微调过程必须重新开始。

  • 知识混合:在微调时,加入的知识可能会“混合”或反驳模型最初训练时使用的其他信息。结果是模型可能会变得困惑,并在优先考虑其训练数据中的信息时提供自相矛盾的陈述。从技术上讲,无法保证LLM会优先考虑其经过微调的信息。

因此,对于许多实际应用,特别是那些不需要如此严格领域控制的应用,使用替代方案(如提示工程或检索增强生成)通常更高效,我们将在接下来讨论这些。

检索知识

如前所述,retrieved knowledge指的是根据用户请求从知识库中实时检索的信息。与训练知识(在模型训练时是静态且固定的)不同,检索到的知识对于对话上下文是动态的。其模式如下:

  1. 用户提出一个需要外部知识的问题。

  2. 代理系统从知识源检索与该问题相关的数据。这是通过工具调用实现的,工具调用可以搜索数据库、文本文件、嵌入文档的向量存储、搜索引擎 API 等。

  3. 然后将检索到的信息输入到LLM中。

  4. LLM生成一个结合或基于检索到的信息的答案。

这会将外部知识整合到代理中。在这种语境下,“外部”知识指的是任何并非本质上存储在模型训练权重中的信息。

回想一下,我们已经在第 4 章中创建了利用检索知识的代理。在我们创建的许多示例代理中,我们使用了为用户检索相关信息的工具。例如,这里是我们创建的一个在被提问时会检索比特币价格的代理:

import requests
from agents import Agent, Runner, function_tool

# 创建工具
@function_tool
def get_price_of_bitcoin() -> str:
    """获取比特币价格."""
    url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
    response = requests.get(url)
    price = response.json()["bitcoin"]["usd"]
    return f"${price:,.2f} USD."

# 创建代理
crypto_agent = Agent(
    name="CryptoTracker",
    instructions="你是一个加密货币助手。使用工具获取实时数据。",
    tools=[get_price_of_bitcoin]
)

# 使用示例提示运行代理
result = Runner.run_sync(crypto_agent, "What's the price of Bitcoin?")
print(result.final_output)

这里是另一个从数据库中检索知识的示例:

from agents import Agent, Runner, function_tool
from pydantic import BaseModel
from typing import List

# 创建模拟数据库
TICKETS_DB = {
    "henry@gmail.com": [
        {"id": "TCKT-001", "issue": "Login not working",
            "status": "resolved"},
        {"id": "TCKT-002", "issue": "Password reset failed",
            "status": "open"},
    ],
    "tom@gmail.com": [
        {"id": "TCKT-003", "issue": "Billing error",
            "status": "in progress"},
    ]
}

# 定义 Pydantic 模型
class CustomerQuery(BaseModel):
    email: str

# 定义执行数据库查询的工具
@function_tool
def get_customer_tickets(query: CustomerQuery) -> str:
    """根据邮箱检索客户最近的工单."""
    tickets = TICKETS_DB.get(query.email.lower())
    if not tickets:
        return f"No tickets found for {query.email}."
    response = "\n".join(
        [f"ID: {t['id']}, Issue: {t['issue']}, Status: {t['status']}"
            for t in tickets]
    )
    return f"Tickets for {query.email}:\n{response}"

# 创建代理
support_agent = Agent(
    name="SupportHelper",
    instructions="你是一个客户支持代理。在被问及其工单时,使用工具获取用户的支持历史。",
    tools=[get_customer_tickets]
)

# 运行代理
result = Runner.run_sync(support_agent, "Can you show me the ticket history for henry@gmail.com?")
print(result.final_output)

这两个示例都遵循了我们之前提出的模式,如下:

  1. 用户发起请求且代理检索信息:在两个示例中,用户都提出了一个超出其训练范围的问题。这包括实时数据(例如比特币价格)或用户特定信息(例如支持历史)。代理检测到它无法通过内置知识回答问题。它调用了检索工具:

    • 在CryptoTracker示例中,代理使用对Gecko API的 HTTP 调用

    • 在SupportHelper示例中,代理根据电子邮件地址执行模拟数据库查询

  2. 检索外部知识:工具以结构化格式(例如 API 的 JSON 响应,或数据库中的工单列表)检索相关信息,并将检索到的数据传回模型。

  3. LLM 整合信息:然后 LLM 使用这些输入生成一个反映该知识的自然语言回复。

在我们继续之前,请注意,我们将对该主题进行简化的概述,而不是讨论其背后深且复杂的数学概念。若要从更详细的角度了解嵌入(embeddings)的工作原理,请参考此书:www.packtpub.com/en-mx/product/vector-search-for-practitioners-with-elastic-9781805121022/chapter/chapter-1-introduction-to-vectors-and-embeddings-2/section/chapter-1-introduction-to-vectors-and-embeddings-ch02lvl1sec02?srsltid=AfmBOoqKY6Vgqv9_MDNW5p4mDEXQBdQXQWFt9fRFM63RMebJWEWx8ide。

首先,让我们定义一下术语。

Embeddings(嵌入)是文本的数值表示。将其想象为一种秘密语言,其语法是数字列表,且只有机器能理解。这些嵌入捕捉了单词、句子甚至整篇文档的精髓。例如,以下句子的嵌入列如下所示(这仅用于说明):

| Sentence | Embedding |

| :--- | :--- |

| I like apples | [3432, 75, 32, …, 76, 980] |

| I like bananas | [85, 1, 4, …, 695, 47] |

| This is very difficult | [5, 596, 1254, …, 7, 1] |

| This is like fitting a square peg into a round hole | [5, 4, 365, …, 748, 9] |

表 5.1:示例嵌入

由于嵌入捕捉了文本的“意义”或“精髓”,两个关于相同主题或含义相似的文本将产生非常相似的向量。例如,I like apples的嵌入与I like bananas的嵌入相比,会比与随机内容(如:All legislative Powers herein granted shall be vested in a Congress of the United States, which shall consist of a Senate and House of Representatives)更相似。这就是语义搜索大显身手的地方。

Semantic search(语义搜索)通过使用语义含义(换句话说,即比较它们的嵌入)而不是比较关键词来比较两段文本的过程。我们通过计算两个嵌入在向量空间中的距离来比较它们,这被称为cosine similarity(余弦相似度)。Cosine similarity衡量两个向量之间角度的余弦值,结果是一个 0 到 1 之间的数字。

数学原理在这里并不重要;其含义是:余弦相似度越高,两个文本的语义关系就越紧密:

  • Cosine similarity close to 1:文本非常相似,或具有相似的上下文或含义

  • Cosine similarity close to 0:文本不相关

以下是不同文本嵌入之间的余弦相似度:

| Test | Base text | Comparison text | Cosine similarity of embeddings |

| :--- | :--- | :--- | :--- |

| 1 | I like apples | I like bananas | 0.90 |

| | | All legislative Powers herein granted shall be vested in a Congress of the United States, which shall consist of a Senate and House of Representatives | 0.71 |

| 2 | This is very difficult | I’m fitting a square peg into a round hole | 0.88 |

| | | All legislative Powers herein granted shall be vested in a Congress of the United States, which shall consist of a Senate and House of Representatives | 0.64 |

表 5.2:不同文本嵌入之间的余弦相似度表

在表中,你可以看到,通过余弦相似度,文本I like apples与文本I like bananas的语义相似性高于其与《美国宪法》摘录的相似性。This is very difficult与I’m fitting a square peg into a round hole的情况也是如此。在这个例子中,这两段文本没有任何关键词,但它们在语义上是相似的,因此具有非常高的余弦相似度分数。

最后,我们有vector stores(向量存储),它们仅仅是针对存储文本及其嵌入而优化的数据库。简单的数据库可能存储类似表格结构的数据,而向量存储存储的是这些大型嵌入向量。向量存储还有另一个特殊属性:当新文档添加到向量存储中时,数据库会自动将文本chunks(分块)为较小的片段,为每个块生成嵌入,并对其进行indexes(索引,即与支持快速检索的字典一起存储),以便进行快速相似搜索。这种chunk and index step是启用语义搜索所必需的。每个块通常包含数百个 token 的文本,并使用专用模型进行嵌入。

现在我们理解了这些术语,让我们讨论一下在对非结构化数据执行 RAG 模型时,这一切是如何工作的:文档摄取和检索。

文档摄取 (Document ingestion)

在我们检索任何内容之前,首先需要准备知识库。这涉及到将原始文本转换为适用于语义搜索的格式。摄取过程通常如下:

  1. Chunking(分块):原始文档被分成较小的片段(块)。

  2. Generate embeddings(生成嵌入):每个块通过嵌入模型生成向量。

  3. Store in a vector store(存储在向量存储中):这些嵌入及其关联的块存储在已索引的向量存储中。

检索 (Retrieval)

一旦文档摄取完毕,我们就有了所需的全部信息。根据查询动态检索数据的过程如下:

  1. Embed the query(嵌入查询):当用户提交问题时,系统首先使用相同的嵌入模型将查询转换为嵌入。

  2. Perform a semantic search(执行语义搜索):向量存储使用cosine similarity将查询嵌入与块嵌入进行比较。此步骤通常返回前 N 个语义相关的块。

之后,由之前描述的augment和generate步骤接续。区别在于在retrieve步骤中,输出是语义相似的块,而不是 API 调用的加密货币价格或数据库查询的状态。

在 Agents SDK 中使用向量存储和 FileSearchTool

幸运的是,OpenAI Agents SDK 自动处理非结构化文本的 RAG 过程,涵盖文档摄取和检索部分,因此你不需要手动操作。让我们通过一个示例,首先创建一个向量存储:

  1. 访问平台 platform.openai.com/ 并登录。

  2. 从右上角选择Dashboard,然后选择Storage,再切换到Vector stores。

  3. 选择Create创建新的向量存储。输入向量存储名称USConstitution。

  4. 滚动并通过+ Add files向向量存储添加文件。

  5. 上传USConstitution.txt文件(可以在仓库的第五章下找到)。将其命名为USConstitution.txt并为Purpose选择user_data。然后,选择Attach。

  6. 我们上传的 TXT 文件现在已成功添加到我们的新的向量存储库中,所有启用 RAG 的相关操作(如生成嵌入、索引等)都已完成。

  7. 复制并保存你刚刚创建的新的向量存储库 ID。

既然我们已经创建了向量存储库,让我们使用 Agents SDK 通过FileSearchTool进行检索,我们在上一节中详细介绍过该工具。提醒一下,该类接受向量存储库 ID 列表作为输入,它会自动搜索并检索正确的分块,然后将其传递给 LLM 进行增强。使用我们在上一节中构建的向量存储库,我们可以构建一个通过引用USConstitution文件来回答问题的智能体。

创建一个名为us_constitution_agent.py的新 Python 文件并运行以下代码:

from agents import Agent, Runner, FileSearchTool, SQLiteSession

# 实例化工具
filesearchtool = FileSearchTool(
    vector_store_ids=['vs_687ed4bb479c81919b530ab152f373d8']
) # 替换为你自己的向量存储库 ID

# 创建智能体
agent = Agent(
    name="USConstitutionTool",
    instructions="你是一个 AI 智能体,回答来自列出的向量存储库的问题,其中包含《美国宪法》。用一句话回答。",
    tools=[filesearchtool]
)

# 创建会话
session = SQLiteSession("first_session")
while True:
    question = input("You: ")
    result = Runner.run_sync(agent, question, session=session)
    print("Agent: ", result.final_output)

运行此程序后,你可以提问任何问题,智能体将对其给定的向量存储库执行 RAG 以回答问题,例如:

You: 议员需要多大年龄?
Agent:  议员必须至少 30 岁。

事实上,你可以通过进入该智能体运行的Traces模块查看 RAG 搜索返回的所有分块:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_05_03.png

图 5.3: OpenAI Dashboard logs

通过这种方式,Agents SDK 自动化了文档摄取和检索过程,让你只需几行代码即可为智能体添加检索知识的能力。然而,了解背后正在发生什么仍然有用,这样你就能理解该过程及其局限性。

局限性

为智能体添加检索知识具有某些局限性和陷阱,特别是使用非结构化数据时。以下是非结构化数据可能出现的三个最常见问题:

  • 问题模糊:如果用户问“你们的退货政策是什么?”,他们是在询问在线订单、店内退货还是特定产品?这可能导致智能体产生错误的答案。

  • 未找到相关信息:有时知识库中没有答案。此时智能体可能会产生幻觉并胡造内容。

  • 冲突或多源信息:如果来自 retrieval 步骤的信息不一致(例如,两个文档说不同的退货窗口),智能体可能会忽略其中一个。

总的来说,构建任何基于知识的智能体意味着要注意上述陷阱,并理解即使是最好的 AI 智能体也可能陷入其中。与任何依赖于真实世界信息的系统一样,可能会出现间隙、矛盾或误解。然而,由于具备了记忆和知识,我们现在拥有了一个强大得多的智能体:它记得上下文并可以引入外部事实,使其成为一个强大的智能体。

总结

在本章中,我们为智能体配备了两个实用的技能集,使其变得更加智能:记忆和知识。

我们首先实现了短期记忆,手动跟踪消息历史,并使用 Sessions 类支持多轮对话。我们通过引入滑动消息窗口和消息摘要技术,解决了上下文窗口增长的挑战。

接下来,我们探索了长期记忆。我们使用带有文件支持的数据库的 SQLiteSession 使智能体记忆在会话之间保持持久化,并通过工具调用(tool-calling)引入了结构化记忆召回。这种方法使智能体只存储最相关的事实,在减少内存膨胀的同时提高了召回准确性。

然后我们查看了两种不同类型的模型知识:

  • 训练知识:在模型初始训练期间“刻入”到模型中的静态信息。

  • 检索知识:在运行时通过工具调用获取的动态的、特定于语境的数据。我们探索了 RAG 模式,即智能体从 API、数据库或知识库中提取外部数据,并将其输入 LLM 以生成知情的响应。

通过这些模式,你的智能体现在可以持有记忆、保留用户特定的上下文,并根据需要引入相关知识,使其在真实世界应用中的真正实用性更近了一步。

在下一章中,我们将重点转向多智能体系统,其中多个专门的智能体可以协作完成更复杂的任务。

| 立即解锁此书的专属福利 | 扫描此二维码或访问 packtpub.com/unlock,然后按书名搜索此书。 | https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/Unlock-01.pnghttps://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/Unlock2.png |

| 注意:在开始之前准备好你的购买发票。 |

多智能体系统与交接 (Handoffs)

某些任务是个人人类无法完成的。相反,需要一个人类组织(即团队)。想想任何大型公司。这些大企业之所以能够实现目标,是因为不同人类的共同工作。同样,单智能体系统是有局限性的。然而,多智能体系统可以完成任何事情。

虽然智能体可能非常强大,但它有其局限性。当问题跨越领域、需要专业知识或需要并行处理时,单个智能体往往力不从心。例如,一个客服智能体可能很好地处理查询,但当任务涉及财务分析、法律推理或技术故障排除时,它可能会失败。通过将责任分配给多个智能体,你可以获得比单个智能体更高的准确性、扩展性和弹性。

在本章中,我们将探索如何构建多智能体系统,即多个智能体协作工作的系统。无论你是构建一个简单的路由器还是复杂的层次结构,理解智能体如何交互、委托和协作都是释放智能体全部能力的关键。OpenAI Agents SDK 了一个启用多智能体系统的原语:交接(handoff)。

你将在本章学习:

  • 编排类型:我们将首先对比两种多智能体编排策略——确定性(硬编码)和动态(系统驱动),并演示它们的示例。

  • 交接 (Handoffs):你将学习如何使用 Agents SDK 中的交接功能将控制从一个智能体转移到另一个智能体,允许智能体在保持上下文的同时委托任务。

  • 多智能体模式:最后,我们将探索多智能体系统的架构模式(集中化、去中心化、层次化和群集),并展示如何使用 SDK 实现每模式。

在本章结束时,你将学会如何设计和实现镜像真实团队的多智能体系统。

技术要求

请遵循第 3 章中的详细步骤设置你的环境。

在本书中,每一章的实用示例和完整代码将通过附带的 GitHub 仓库提供:https://github.com/PacktPublishing/Building-Agents-with-OpenAI-Agents-SDK](https://github.com/PacktPublishing/Building-Agents-with-OpenAI-Agents-SDK)。

鼓励你克隆仓库、重用并适配提供的代码示例,并在学习章节时根据需要参考它们。

多智能体编排 (Multi-agent orchestrations)

在构建使用多个代理的系统时,关键决策之一是确定如何管理代理之间的信息和任务流。广而言,你可以采取两种策略:

  • 确定性编排(Deterministic orchestration):由你自己编写逻辑并显式地控制代理之间的相互作用;参见图 6.1:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_01.png

图 6.1: 确定性编排

  • 动态编排(Dynamic orchestration):将流决策交给另一个系统(如 LLM),使系统能够动态调整其行为;参见图 6.2:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_02.png

图 6.2: 动态编排

在本节中,我们将讨论这两种方法以及各自的优缺点。值得注意的是,许多编排实际上涉及到这两种方法的结合。在智能体系统中,你可能希望它的某些部分是显式确定的,以获得更大的控制和自主性;而你可能又希望其他区域更具动态性,并能更好地处理歧义。

确定性编排

确定性编排是指不同代理之间的交互流是硬编码的(或是确定的)。这种方法的核心在于对代理如何运行以及何时运行保持控制。在确定性设置中,你可以提前定义精确的工作流,通过编码规定由哪些代理处理哪些任务的顺序和规则。一个很好的类比是场编排良好的舞蹈,每一步都经过了计划。没有即兴发挥,代理流绝不会偏离你提供给它们的脚本。

确定性编排的主要优势是可预测性。对于相同的输入,代理流将始终以相同的方式展开,易于测试、审计和调试。由于你确切知道工作流将产生多少代理调用,因此衡量成本和运行时间也很简单。确定性模式在那些需要稳定、可重复的结果,且需要确保不会发生意外情况的系统中特别有用。

与所有确定性系统一样,主要的缺点是灵活性。我们在第 1 章中讨论过这一点,但确定性编排模式无法适应代理控制逻辑未显式定义的请求或情况。如果你发现需要新的步骤或想处理新类型的请求,你需要更改并重新部署你的代理逻辑。随着工作流变得越来越复杂,维护所有分支和路径可能会变成负担。

让我们来看一个确定性方法的示例。在这个例子中,我们将通过一个确定性编排模式,使用硬编码逻辑在两个客户服务代理之间路由请求。这将向你展示可预测的、基于规则的委托是如何工作的,以及为什么它既强大又局限。创建一个名为deterministic_approach.py的新 Python 文件并运行以下代码:

from agents import Agent, Runner
### Create two agents
complaints_agent = Agent(
    name="Complaints Agent",
    instructions="Handle any customer complaints with empathy and clear next steps."
)
inquiry_agent = Agent(
    name="General Inquiry Agent",
    instructions="Answer general questions about our services promptly."
)
# Create orchestration
def orchestrate(user_message: str):
    # Deterministically delegates requests to the right customer service agent.
    if ("complaint" in user_message.lower() 
        or "problem" in user_message.lower()):
        print('Redirecting you to the Complaints agent')
        chosen_agent = complaints_agent
    else:
        print('Redirecting you to the Inquiry agent')
        chosen_agent = inquiry_agent
    result = Runner.run_sync(chosen_agent, user_message)
    return result.final_output
while True:
    question = input("You: ")
    result = orchestrate(question)
    print("Agent: ", result)

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/32.png 快速提示:使用 AI 代码解释器和快速复制功能来增强你的编码体验。在下一代 Packt Reader 中打开此书。点击复制按钮 (1) 快速将代码复制到你的编码环境中,或点击解释按钮 (2) 让 AI 助手向你解释一段代码。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/image_(2)2.png

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/22.png 购买此书将免费赠送下一代 Packt Reader。扫描二维码或访问 packtpub.com/unlock,然后使用搜索栏通过名称找到此书。双检显示的版本以确保你获得的是正确的。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/Unlock2.png

在这段代码中,这里是关键要点:

  • 确定性编排让你对代理流有完全控制,使其具有可预测性并易于测试。

  • 缺点是缺乏灵活性;如果用户以非预期的方式表达,系统可能会误导请求。

调用哪个代理的选择由orchestrate函数管理,该函数根据用户的内容硬编码了调用哪个代理的逻辑。这是确定的,因为如果用户的问题包含complaint(投诉)或problem(问题),该系统始终重定向到complaints_agent。

这是一个交互示例:

You: I have a complaint, my meal was too hot
Redirecting you to the Complaints agent
Agent: 很抱歉听到您的食物太热了。我理解这可能会让人不舒服。为了弥补这一点,我想免费为您提供一份替代餐。此外,请告诉我们您将来希望如何调整食物温度,我们将确保符合您的偏好。还有什么我可以帮您的吗?

从这中,可以很清楚地看到这个系统的弱点。用户可能有投诉,但可能没有在信息中实际使用complaint或problem词。在这种情况下,用户将会被重定向到错误的代理。以下是这种情况的例子:

You: my meal is too hot
Redirecting you to the Inquiry agent
Agent: Try eating in smaller bites.

动态编排

动态编排是指不同代理之间的交互流是动态的,由外部系统自主控制(通常是人类,但在本例中是 LLM)。这提供了一种更灵活的编排模式,因为代理选择和流的责任被交给了另一个由 LLM 驱动的代理。事实上,在本书到目前为止的几乎所有情况下,我们创建的系统都使用了动态编排,因为一个代理一直在控制如何处理用户的响应(无论是工具调用、调用哪个工具等)。

动态编排的优点和缺点分别对应确定性编排的缺点和优点。灵活性是其主要优势。动态系统可以处理更广泛的对话输入,而无需为每种可能的场景编写硬编码逻辑。只要指令得当,代理(Agents)就可以处理不熟悉的问题并制定新的解决方案,使得这种方法非常适用于客户支持、销售分诊和其他开放式任务。然而,权衡是预测性;你无法总是预先知道对话将采取哪条路径,这可能意味着不可预测的答案、成本和运行时间。

让我们回到之前的示例,并将其转换为动态编排。第一步是创建一个分诊代理(triage agent),它可以根据用户的查询选择调用哪个代理。创建一个名为dynamic_approach.py的新 Python 文件,并运行以下代码:

from agents import Agent, Runner
### 创建两个代理
complaints_agent = Agent(
    name="Complaints Agent",
    instructions="以同理心和清晰的后续步骤处理任何客户投诉。"
)
inquiry_agent = Agent(
    name="General Inquiry Agent",
    instructions="及时回答关于我们服务的通用问题。"
)
triage_agent = Agent(
    name="Triage Agent",
    instructions="分诊用户的请求并调用合适的代理",
    tools=[
        complaints_agent.as_tool(
            tool_name="ComplaintsAgent",
            tool_description="介绍自己为投诉代理。以同理心和清晰的后续步骤处理任何客户投诉。"
        ),
        inquiry_agent.as_tool(
            tool_name="GeneralInquiryAgent",
            tool_description="介绍自己为通用查询代理。及时回答关于我们服务的通用问题。"
        )]
)
while True:
    question = input("You: ")
    result = Runner.run_sync(triage_agent, question)
    print("Agent: ", result.final_output)

在这种情况下,让我们尝试使用一个分诊代理应该解释为投诉的提示词(不使用“complaint”一词),然后相应地发送给complaints_agent:

You: 我的食物太热了
Agent: 听到您的食物是这样我很抱歉。我理解这有多么让人意料。您希望我们为您更换食物,还是我可以协助您退款或提供积分?请告诉我您想如何处理。

我们可以通过查看Traces模块来验证它是否已发送到正确的代理:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_03.png

图 6.3: 上一个示例的轨迹

通过这种方式,我们能够将决策过程委托给分诊代理,而不是预先硬编码逻辑。代理解释用户消息背后的意图(识别投诉),并动态决定将消息路由到正确的代理。这使得我们的系统能够更自然、更高效地响应各种输入,而无需使用僵硬的if-else结构来处理每种可能的情况。

此类编排的另一个优点是它可以处理多个请求。例如,如果用户的请求是“我的食物太热了,我该如何获取收据?”分诊代理将同时调用complaints_agent和inquiry_agent来一次性回答该问题:

You: 我的食物太热了,我该如何获取收据?
Agent: 以下是我为您提供帮助的方法:
对于热食物:
- 让食物冷却几分钟,期间我们提供一杯免费饮品以帮助平衡。
- 如果问题仍然,我们可以安排更换。请告诉您的偏好!
对于收据:
- 检查您的确认邮件以获取副本。
- 登录我们在网站上的账户,您可以在那里从订单历史中查看并下载。
如果您需要进一步帮助,请随时提问!

这是它在Traces模块中的样子:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_04.png

图 6.4: 代理工作流的轨迹示例

这一切都是在代理的推理过程中动态发生的,而不是通过固定的代码分支。我们只定义了代理及其能力;分诊代理的 LLM 负责了决策过程。

OpenAI Agents SDK 中的交接 (Handoffs)

现在我们已经理解了多代理系统中的不同编排方法,让我们讨论一下 OpenAI Agents SDK 中提供的、使这些可能的构造。SDK 提供的最重要的原语之一是交接(handoffs)。

handoff是代理将控制权传递给另一个代理的机制。它等同于一个代理说:“我要把这项任务交给你。你可以比我回答得更好。这里是所有细节。”在多代理系统中,交接是必要的,因为它是连接不同代理的layer(层)。

注意

代理之间交互通常有两种方式,其中一种我们已经在第 4 章中详细介绍过,即为as_tool()。

as_tool()函数允许代理成为一个可以添加到另一个代理的工具。可以将其想象为调用一个辅助函数:主代理保持控制,临时咨询子代理以获取输入或决策。然而,在这里我们将介绍交接。相比之下,交接是从代理到另一个代理的完全控制权转移。原始代理不再参与。

你可以在下面的图中找到这两种方法的视觉示例。在“代理作为工具”模式中,编排代理保留整体控制权并调用另一个代理处理特定的子任务,如图 6.5 所示:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_05.png

图 6.5: 代理作为工具模式

在“交接”模式中,一个代理将控制权完全委托给另一个,如图 6.6 所示:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_06.png

图 6.6: 交接模式

在本节中,我们将介绍如何设置交接、如何自定义其行为、交接期间传递了哪些信息、如何过滤或修改这些信息,以及如何处理错误或多步交接。之后,我们将使用交接构建不同的多代理系统。

交接简介

定义交接非常简单:实例化新代理时,只需设置一个名为handoff的参数。该参数可以接受之前构建的其他代理列表。列表中的每个代理都可以作为起始代理转移控制权的潜在代理。

让我们直接开始构建一个带有交接的多代理系统。我们将从之前的示例开始,简单地将tools参数更改为handoff,然后删除之前的as_tool()函数。创建一个名为basic_handoff.py的新 Python 文件,并输入以下代码:

from agents import Agent, Runner

### 创建两个代理
complaints_agent = Agent(
    name="Complaints Agent",
    instructions="以同理心和清晰的后续步骤处理任何客户投诉。"
)
inquiry_agent = Agent(
    name="General Inquiry Agent",
    instructions="及时回答关于我们服务的通用问题。"
)

### 创建带有交接的分诊代理
triage_agent = Agent(
    name="Triage Agent",
    instructions="分诊用户的请求并调用合适的代理",
    handoffs=[complaints_agent, inquiry_agent]
)

while True:
    question = input("You: ")
    result = Runner.run_sync(triage_agent, question)
    print("Agent: ", result.final_output)

在这个程序中,triage_agent有两个可以接收任务交付的代理:complaints_agent(投诉代理)和inquiry_agent(咨询代理)。打个比方,我们创建了一个三人的办公室:triage_agent负责接待,然后根据客户的问题将客户引导至合适的人。

triage_agent如何知道应该将任务交给哪个代理呢?类似于Tool原语块,Handoff原语块会将子代理的名称和指令暴露给triage_agent,因此它知道这两个代理的角色,并可以借此判断是否应该将任务移交给它们。

让我们尝试一下并验证代理是否正在执行交接操作:

You: 我的饭太热了
Agent:  您好,很抱歉听到你的饭太热了。我理解这会给你带来不便。我可以为您提供几个选项来帮助解决这个问题吗?我们可以提供重新做一份餐,或者如果您介意的话,可以提供部分退款。请告诉我哪种方案最适合您!

如果我们查看Traces模块,可以确认triage_agent正在将任务传递给complaints_agents:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_07.png

图 6.7: 从一个代理到另一个代理的交接

Traces模块显示了任务转移到了complaints_agent,之后由complaints_agent完全接管任务。

多代理切换

注意,complaints_agent本身也是一个代理,因此它可以拥有自己的工具和交接。目前,一旦complaints_agent被调用,无法转移到任何其他代理。为了修复这个问题,让我们为complaints_agent和inquiry_agent都添加交接功能,并允许Runner不再每次都调用同一个triage_agent,而是调用上一个响应用户的代理。这将让我们能够看到真正的动态多代理系统的威力,它可以在所有代理之间轻松切换。我们还将修改代码,以便能够与代理进行多轮对话。

创建一个名为multi_agent_switching.py的新 Python 文件并运行以下代码:

from agents import Agent, Runner, SQLiteSession, trace

### 创建两个代理
complaints_agent = Agent(
    name="Complaints Agent",
    instructions="介绍自己为投诉代理。以同理心和清晰的后续步骤处理任何客户投诉。"
)
sales_agent = Agent(
    name="Sales Agent",
    instructions="介绍自己为销售代理。及时回答关于我们服务的通用问题。"
)

### 创建带有交接功能的分诊代理
triage_agent = Agent(
    name="Triage Agent",
    instructions="回答通用问题。分诊用户请求并调用合适的代理",
)

# 设置所有代理之间的相互交接
complaints_agent.handoffs = [sales_agent, triage_agent]
sales_agent.handoffs = [complaints_agent, triage_agent]
triage_agent.handoffs = [complaints_agent, sales_agent]

### 创建会话
session = SQLiteSession("first_session")
last_agent = triage_agent

with trace("Multi-agent system"):
    while True:
        question = input("You: ")
        result = Runner.run_sync(last_agent, question, session=session)
        print("Agent: ", result.final_output)
        last_agent = result.last_agent

让我们检查代码中发生了什么,特别是高亮显示的部分。

我们首先建立了三个代理。接着,我们在所有三个代理之间建立了相互交接关系。这是关键所在:在此之前,一旦控制权移交给如complaints_agent这样的次代理,对话在会话剩余的时间内就会锁定在那个代理上。现在,通过显式地为每个代理分配.handoffs以包含其他代理,任何代理都可以委托给任何其他代理,包括在需要时回传给triage_agent。这为完全动态的多代理系统奠定了基础。

我们还引入了持久化的SQLiteSession,以确保对话状态在多轮之间得以保持,这是我们在上一章中学习到的内容。这使得我们能够基于之前的交流进行后续构建,并让代理记住对话在何处中断。结合trace()上下文管理器(我们将在下一节深入探讨),我们可以记录所有的交互,以供以后调试或检查。

last_agent变量跟踪最近活跃的代理。在循环内部,我们通过调用Runner.run_sync(last_agent, question, session=session)继续对话。这确保了当前处理对话的代理将接收下一个用户输入,而不是总是从分诊代理重启。一旦处理完响应,我们就用result.last_agent更新last_agent,从而根据交互流实现代理之间的无缝交接。

例如,让我们与多代理系统进行以下对话。请注意,它会根据对话中的消息在多个代理之间进行无缝切换:

You: 你好,我是 Henry
Agent:  你好 Henry!今天有什么可以帮你的吗?
You: 把我转接到投诉代理 - 我的饭太热了
Agent:  你好 Henry,我是负责协助你的投诉代理。我理解你的饭太热了,对于带来的不便我深表歉意。你能详细描述一下情况吗,以便我为你提供帮助?
You: 我有一个通用咨询 - 我如何获取收据?
Agent:  对于收据查询,你通常有

这是该示例在Traces模块中的外观:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_08.png

图 6.8: 此示例中的 Traces 模块

这种结构为真正的多代理对话开启了大门,代理不仅可以轮流,还可以根据上下文智能地决定何时切换。我们现在创建了一个灵活的代理网络,可以在对话过程中分配代理。

自定义交接

OpenAI Agents SDK 允许你自定义交接的属性,这在某些情况下非常有用。首先需要知道的是 SDK 提供了带有额外属性实例化交接对象的方法。这些属性如下:

  • agent: 交接的目标代理。

  • tool_name_override和tool_description_override: 允许你在Traces模块中覆盖交接描述。正如你所看到的,默认是“转移到 X”。

  • on_handoff: 在交接发生时触发的函数。这在发生交接时调用函数非常有用(例如通知用户或用于日志记录)。Agents SDK 将对话历史传递给回调函数。

  • input_type和input_filter: 允许你精炼交接预期的输入(例如,你可以指令 LLM 传递到目前对话的摘要,或者将对话历史裁剪为最后五条消息)。

注意

交接中的一个关键考虑是决定哪些上下文被带入。默认情况下,SDK 会自动传输交接点之前的完整对话历史。这包含了所有用户输入、系统指令以及之前代理的消息和操作(基本上是原始代理访问的所有内容)。从用户的角度来看,不需要重复任何内容。

让我们来看一个自定义交接示例。我们将使用我最常用的属性,即on_handoff属性。我们将定义一个自定义函数,在系统交接时打印输出。

from agents import Agent, Runner, SQLiteSession, trace, handoff
from pydantic import BaseModel
import os

### 创建两个代理
complaints_agent = Agent(
    name="Complaints Agent",
    instructions="将自己介绍为投诉代理。以同理心和清晰的后续步骤处理任何客户投诉。"
)
sales_agent = Agent(
    name="Sales Agent",
    instructions="将自己介绍为销售代理。及时回答有关我们服务的通用问题。"
)

### 创建带有交接功能(handoffs)的分诊代理
triage_agent = Agent(
    name="Triage Agent",
    instructions="回答通用问题。分诊用户的请求并调用相应的代理",
)

在第一个部分中,我们设置了三个代理:一个负责投诉,一个负责销售,以及一个决定将请求路由到何处的分诊代理。到目前为止,这听起来应该很熟悉——它与你之前看到的设置相同,但现在我们正在为这些代理的定制化交接(customized handoffs)做准备。

接下来,让我们定义一个模型和日志函数,然后创建交接对象:

class NameOfAgentToBeHandedOff(BaseModel):
    name_of_agents_to_be_handed_off: str

# 创建日志函数
def log(ctx, name_of_agent):
    msg = f"系统已将您转接到另一个代理:{name_of_agent.name_of_agents_to_be_handed_off}"
    print(msg)

# 创建自定义交接
complaints_handoff = handoff(agent=complaints_agent, on_handoff=log, input_type=NameOfAgentToBeHandedOff)
sales_handoff = handoff(agent=sales_agent, on_handoff=log, input_type=NameOfAgentToBeHandedOff)
triage_handoff = handoff(agent=triage_agent, on_handoff=log, input_type=NameOfAgentToBeHandedOff)

# 设置所有代理之间的相互交接
complaints_agent.handoffs = [sales_handoff, triage_handoff]
sales_agent.handoffs = [complaints_handoff, triage_handoff]
triage_agent.handoffs = [complaints_handoff, sales_handoff]

### 创建会话
session = SQLiteSession("first_session")
last_agent = triage_agent

with trace("Multi-agent system"):
    while True:
        question = input("You: ")
        result = Runner.run_sync(last_agent, question, session=session)
        print("Agent: ", result.final_output)
        last_agent = result.last_agent

让我们回顾一下与上一个部分相比对该程序所做的更改。

首先注意到的是,我们现在不再直接将代理分配给.handoffs,而是使用 SDK 中的handoff()函数来构建每个交接。这允许我们以更高级的方式自定义交接的行为。在我们的案例中,我们正在使用on_handoff参数为每个交接挂载了一个日志函数。

我们定义了一个简单的log()函数,它在每次发生交接时都会打印一条消息。它接收交接上下文(ctx)和模型输入(name_of_agent),然后打印出指示对话正在转移到哪个代理的消息。这为我们提供了一种实时跟踪和调试代理转换的有效方法。

为了确保我们的日志函数接收结构化输入,我们使用 Pydantic 定义了NameOfAgentToBeHandedOff类。这为交接应使用的输入数据类型设定了清晰的预期,并允许 SDK 在发生交接时验证并将相关字段传递到我们的log()函数中。

然后,每个交接使用此结构进行初始化:

complaints_handoff = handoff(
    agent=complaints_agent, on_handoff=log, 
    input_type=NameOfAgentToBeHandedOff)

这告诉 SDK:“当交接给complaints_agent时,触发log()函数并提供一个类型为NameOfAgentToBeHandedOff的结构化输入。”我们对每个代理重复此操作,以便跨整个系统的每一次交接都能被跟踪。

最后,我们不再仅仅将裸代理分配给.handoffs,而是分配相应的交接对象:

complaints_agent.handoffs = [sales_handoff, triage_handoff]

这让我们对转换如何发生拥有完全控制权,并允许我们通过副作用(如日志记录、分析甚至自定义提示词)挂钩到这些转换中。当我们运行程序并执行交接时,将打印出以下日志:

You: I want to make a complaint
系统已将您转接到另一个代理:投诉代理
Agent:  你好,我是投诉代理,我在这里为您提供帮助。可以告诉我发生了什么吗?您的反馈非常重要,我想确保我们正在处理您关注的问题。

简而言之,这种模式为简单的委托机制添加了可编程的钩子(hook),因此你可以跟踪代理行为、强制转换规则,甚至动态编辑上下文。这在日志记录和审计至关重要的现实应用中非常有用。

交接提示词引导 (Handoff prompting)

代理交接给其他代理的能力与你给每个代理的指令一样重要。参与交接的代理应该具有关于何时、如何交接,以及在交接后该做什么的清晰指令。为了做到这一点,OpenAI Agents SDK 实际上为你提供了一个推荐的提示词前缀,可以添加到你的代理中。

from agents import Agent, Runner
from agents.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
print(RECOMMENDED_PROMPT_PREFIX)

创建两个代理

complaints_agent = Agent(
    name="Complaints Agent",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}.将自己介绍为投诉代理。以同理心和清晰的后续步骤处理任何客户投诉。"
)
inquiry_agent = Agent(
    name="General Inquiry Agent",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}.将自己介绍为销售代理。及时回答有关我们服务的通用问题。"
)

创建带有交接功能的分诊代理

triage_agent = Agent(
    name="Triage Agent",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}.分诊用户的请求并调用相应的代理",
    handoffs=[complaints_agent, inquiry_agent]
)
while True:
    question = input("You: ")
    result = Runner.run_sync(triage_agent, question)
    print("Agent: ", result.final_output)

如你所示,RECOMMENDED_PROMPT_PREFIX 是一个字符串,代表要添加到所有代理的提示词前缀:

## 系统上下文
你是一个名为 Agents SDK 的多代理系统的一部分,该系统旨在简化代理协作和执行。Agents 使用两个主要的抽象:代理(Agents)和交接(Handoffs)。代理包含了指令和工具,并在适当将对话交给另一个代理。交接通过调用函数实现,函数通常命名为 transfer_to_<agent_name>。代理之间的转换在后台无缝处理;不要在与用户的对话中提及或强调这些转换。

为了构建更好的带有交接的多代理系统,还有其他与提示词相关的任务:

  • 显式的交接指令:每个代理的系统提示词都应该指定委托任务的精确条件。例如,“如果用户询问有关销售的问题,请将其路由到销售代理。”指令越明确,代理系统就越好。

  • 显式的代理指令:每个代理的指令应清晰地定义其目的以及何时使用它。这有助于协调器或“分诊”代理路由到合适的代理。

现在我们已经更深入了如何在 OpenAI Agents SDK 中使用交接(handoffs),让我们来学习不同的多智能体系统模式,以及如何利用交接来实现每种模式。

多智能体模式

描述多个智能体如何组织和结构化是指它们的架构模式。在本节中,我们将学习两种主要的多智能体架构模式、它们的优缺点,以及我们如何使用 OpenAI Agents SDK 实现它们。

两种多智能体架构模式如下:

  • 集中心化系统(Centralized system)

  • 去中心化系统(Decentralized system)

让我们逐一介绍。

集中心化系统

centralized system 模式是一种多智能体系统,其中有一个中心智能体将请求路由到其他合适的智能体。这是最常见的架构模式。中心智能体通常被称为“经理”(manager)、“调度器”(orchestrator)或“分诊”(triage)智能体,而其他智能体通常被称为“专门”(specialized)智能体。中心智能体负责路由用户的请求,而每个专门智能体都是某个特定领域的专家。

这种模式的优点是组织良好且职责分清晰。中心智能体专门负责路由请求;这是它唯一的工作。专门智能体针对其特定目的进行了“微调”(无论是通过提示词、工具选择、模型选择还是真正的“微调”权重)。

一个拥有许多专门智能体的结构通常优于一个完成所有任务的智能体;这和为什么在公司里你为每个领域(人力资源、销售、工程等)设置了独立角色的原因是一样的。系统中还可以很方便地添加额外的专门智能体。

这种方法最大的缺点是,系统的优劣取决于其中心智能体。如果中心智能体错误地将请求路由到智能体,整个系统就会失败。在该系统中,通常专门智能体之间无法相互通信。相反,它们是孤立的(silo-ed)。对于某些需要领域间互动和团队协作的任务的任务,这并不是最优的。

这种系统最适用于自然上“自上而下”的架构。例如客户支持机器人,你通常向一个分诊智能体描述你的问题,然后由它将其交交给处理你查询的正确智能体。另一个例子是企业内部助手,帮助员工处理人力资源、IT 和设施请求。员工首先向中心助手描述他们的需求,然后由助手将查询路由到相关的专门部门智能体。这反映了许多真实世界的帮助台工单系统的工作方式。

我们在这里不会介绍示例,因为本章之前的所有示例都是使用中心化系统完成的,其中有一个中心分诊智能体,然后将请求路由到其他专门智能体。

分级系统

分级系统(hierarchical system)是中心化系统的子集。hierarchical system 类似于中心化系统,但具有许多层级(想象一个由多个智能体组成的金字塔,它们变得越来越专门)。这通常是组织运作的方式;你有一个 CEO(或顶级调度器),然后下面有一系列专门的智能体,如 CFO、COO 和 CHRO,它们下面又有一系列专门的智能体。分级系统有一组中间智能体,用于进一步划分任务并为它们所属的专门智能体集提供上下文/指令。

这种分级模式通过将复杂任务分解为可管理的子组件,然后进一步分解,擅长处理复杂任务。它还促进了常见子任务的重用,并帮助保持每个智能体的范围集中。缺点是,如果任务不需要,这种结构可能会引入不必要的开销。层级越多意味着成本越高、延迟越高、风险风险以及更复杂的通信。管理不善的分级结构还会扭曲或混淆信息,使调试变得几乎不可能。

如之前所述,这类系统的最佳用例是处理大型、复杂的查询,例如深度研究。

让我们创建一个分级系统的示例来回答复杂的研究问题。我们将创建一个中心化分诊智能体、两个经理智能体(一个管理科学问题,另一个管理历史问题)以及一个专门智能体子集(每个经理三个,每个专门一个子领域)。

创建一个名为 hierarchical.py 的新 Python 文件并运行以下代码:

from agents import Agent, Runner, SQLiteSession, trace
# 创建我们的智能体
# 专门的科学智能体
physics_agent = Agent(name="Physics Agent", instructions="Answer questions about physics.")
chemistry_agent = Agent(name="Chemistry Agent", instructions="Answer questions about chemistry.")
medical_agent = Agent(name="Medical Agent", instructions="Answer questions about medical science.")
# 专门的历史智能体
politics_agent = Agent(name="Politics Agent", instructions="Answer questions about political history.")
warfare_agent = Agent(name="Warfare Agent", instructions="Answer questions about wars and military history.")
culture_agent = Agent(name="Culture Agent", instructions="Answer questions about cultural history.")
# 具有各自领域交接功能的经理智能体
science_manager = Agent(
    name="Science Manager",
    instructions="Manage science-related queries and route them to the appropriate subdomain agent.",
    handoffs=[physics_agent, chemistry_agent, medical_agent]
)
history_manager = Agent(
    name="History Manager",
    instructions="Manage history-related queries and route them to the appropriate subdomain agent.",
    handoffs=[politics_agent, warfare_agent, culture_agent]
)
# 顶级分诊智能体
triage_agent = Agent(
    name="Research Triage Agent",
    instructions="Triage the user's question and decide whether it's science or history related, and route accordingly.",
    handoffs=[science_manager, history_manager]
)
# 创建会话
session = SQLiteSession("hierarchy")
last_agent = triage_agent
with trace("Hierarchical system"):
    while True:
        question = input("You: ")
        result = Runner.run_sync(last_agent, question, session=session)
        print("Agent: ", result.final_output)
        last_agent = result.last_agent

让我们尝试这个智能体并查看跟踪日志:

You: Which war after the year 1600 led to the greatest death toll?
Agent:  1600 年以后伤亡最大的战争是第二次世界大战。据估计导致了约 7000 万到 8500 万人的死亡,包括军事人员和平民。

现在,让我们看看在 Traces 模块中它是什么样的。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_09.png

图 6.9:示例的跟踪模块

让我们走一下这里发生了什么。

当用户输入问题时,首先到达顶级智能体,即研究分诊智能体。该智能体的工作是判断问题与科学还是历史相关。在这种情况下,问题显然具有历史属性,因此分诊智能体将其交交给 History Manager。一旦 History Manager 接收到查询,它会进一步检查主题,并决定哪个专门智能体最适合回答它。由于问题是关于战争和伤亡人数的,History Manager 将问题路由给 Warfare Agent。

Warfare Agent 是军事历史领域的专家,因此它会处理问题并生成响应。然后答案将向上链返回给用户。

这种分层委托确保了每个智能体都在界定的责任范围内运行,反映了结构化组织中拆解复杂任务的方式。然而,用户只会体验到单次无缝交互。

我们可以在 Traces 模块中清楚地看到这一点:分发智能体(triage agent)将任务传递给 History Manager(历史管理器),后者再将其传递给 warfare agent(战争智能体)。用户现在直接与 Warfare Agent 通话,因为他们的问题与战争有关。

在更真实的示例中,专门的智能体可能会包含与其目的相关的工具和知识子集。例如,Warfare Agent 可以配备查询历史冲突数据库或访问全球冲突长期研究文档的工具。同样,Chemistry Agent(化学智能体)可能会访问科学论文摘要器或周期表计算器。

去中心化系统

去中心化系统与集中式系统完全相反。在这里,没有单一的分发智能体;相反,多个智能体直接协作处理用户的请求。在这个系统中,不存在信息孤岛;所有智能体都可以相互通信。一个很好的类比是圆桌会议,没有领导者,而是所有智能体都参与讨论以产生最终答案。

这种模式的优势在于它能够激发创造力,因此在头脑风暴、构思或辩论类练习中表现出色。去中心化智能体可以通过迭代对话或想法交换协作增强解决方案,正如某些关于智能体自弈(self-play)的研究(参见 Google AlphaZero)。去中心化系统使智能体能够从不同角度解决问题。

这里很大的缺点是缺乏协调性。在去中心化系统中管理对话流需要确定性的编排(记住,确定性编排指的是硬编码的控制逻辑以及该系统提供的所有缺点),因为 OpenAI Agents SDK 不支持这一点。

再次,这类系统的最佳用例是需要创造力和新颖思维的场景:头脑风暴、构思、谈判、辩论等。

让我们来看一个示例。我们将创建两个专注于对立观点的专家智能体:其中一个扮演房东,另一个扮演租客。然后,我们将强制这两个智能体就一个特定的争议性话题(例如“应该有租金控制吗?”)进行对话。在几轮往返之后,我们将把对话历史传递给另一个智能体,由它总结主要论点并返回给我们。

创建一个名为 decentralized.py 的新 Python 文件并运行以下代码:

from agents import Agent, Runner, SQLiteSession, trace
# 创建我们的智能体
landlord_agent = Agent(
    name="Landlord Agent",
    instructions="从房东的角度反对租金控制。提出强有力的经济和产权论点。"
)
tenant_agent = Agent(
    name="Tenant Agent",
    instructions="从租客的角度支持租金控制。强调可负担能力、住房权利和租客保护。"
)
summarizer_agent = Agent(
    name="Summarizer Agent",
    instructions="以中立简洁的方式总结房东和租客智能体提出的主要论点。"
)
# 创建会话
session = SQLiteSession("decentralized")
landlord_turn = True
conversation_history = []
with trace("Decentralized system"):
    print("Topic: Should there be rent control?")
    for _ in range(6):  # 6 轮往返
        if landlord_turn:
            agent = landlord_agent
        else:
            agent = tenant_agent
        prompt = "\n".join([f"{msg['role']}: {msg['content']}"
        for msg in conversation_history])
        response = Runner.run_sync(agent, 
            prompt or "Debate starting now.", session=session)
        print(f"{agent.name}: {response.final_output}")
        conversation_history.append({"role": agent.name, 
            "content": response.final_output})
        landlord_turn = not landlord_turn
    # 辩论结束后,让协调员总结
    summary_prompt = "\n".join([f"{msg['role']}: {msg['content']}"
    for msg in conversation_history])
    result = Runner.run_sync(summarizer_agent, summary_prompt, 
        session=session)
    print("\nSummary of the Debate:")
    print(result.final_output)

在此文件中,我们创建了两个对立的智能体(Landlord Agent 和 Tenant Agent),它们都有一套特定的观点和定制的指令集,用于针对某一争议性问题的一面进行论证。这些智能体并不由分发或管理智能体编排,而是通过在往返对话中交替发言,模拟辩论过程。

这说明了去中心化系统如何在不依赖中央决策者的情况下促进动态多智能体交互。每个智能体都是自主的,但仍然建立在其他贡献之上。结果是一个分层的、协作的对话,它通过涌现行为而非僵化的编排不断演进。

如果我们运行程序,可以在对话历史中看到整个谈判过程以及以下摘要版本:

在关于租金控制的辩论中,房东智能体认为,收入限制会阻碍对物业维护的投资,通过阻碍新投资减少住房供应,由于未能反映供需关系导致市场扭曲,并通过鼓励租客留在可能不再符合其需求的单元内降低租客的流动性。他们还声称这侵犯了产权,通常不公平地偏袒长期租客,增加了行政负担,并破坏了对房东的公平补偿。
相反,租客智能体支持租金控制,是为了稳定租金、减轻经济焦虑、促进住房安全并确保公平获取。他们强调了对租客免受剥削的保护以及对社区关系的维护。租客智能体认为,租金控制鼓励了住房的有效利用,并应该是解决更广泛问题的一部分

我们还可以在 Traces 模块中看到往返过程:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_06_10.png

图 6.10: 此示例的 traces 模块

再次,这种模式对于依赖观点多样性和交换的场景非常出色。

群系统

去中心化系统的一个子集是群系统(swarm system),其中包含许多简单的智能体协作工作(通常是并行)以产生答案。这依赖于一个被称为涌现属性的概念,即全局系统通过许多不智能且简单的微系统变得智能和复杂。一个很好的类比是人体中的细胞。每个细胞本身并不那么智能,专注于完成一小部分任务。然而,数百万个专门化的细胞聚集在一起可以产生人类,这是一个智能系统。另一个与数据科学相关的例子是随机森林模型。该模型依赖于数百个弱模型,这些模型汇聚在一起就能产生一个智能的预测模型。

群集模式(Swarm patterns)特别适用于那些从探索中受益的任务,例如创意生成、优化或具有多种潜在方法的方案解决工作。一种常见的用例是生成多种解决方案候选,然后对其进行精炼或筛选。

让我们来看一个非常简单的例子。在这个例子中,我们将创建一个程序,并行启动 10 个智能体(agents),它们在城市中扮演不同的角色(医生、机械师、厨师等)。每个智能体都将回答同一个问题:“让你从零开始设计你的梦想城市,它会有什么?”随后,我们将这些结果传递给另一个智能体,由它来汇总所有要点并向我们返回最终的总结。

实现

创建一个名为 swarm.py 的新 Python 文件并输入以下代码:

from agents import Agent, Runner, SQLiteSession, trace
import concurrent.futures
# 创建我们的智能体
roles = [
    "Urban Planner", "Artist", "Chef", "Engineer", "Teacher",
    "Doctor", "Mechanic", "Lawyer", "Historian", "Environmentalist"
]
city_agents = [
    Agent(
        name=f"{role} Agent",
        instructions=f"You are a {role.lower()}. Answer the question: 'If you were to design your dream city from scratch, what would it have?' Be creative and imaginative, but concise"
    ) for role in roles
]
# 定义总结智能体
summary_agent = Agent(
    name="City Design Aggregator",
    instructions="You are a city designer. You've just received 10 creative responses from different citizens. Read all of their responses 并将 them 整合为一个连贯、富有想象且完善的城市计划。"
)
### 创建会话
session = SQLiteSession("swarm")
conversation_history = []
with trace("Swarm system"):
    prompt = "Design your dream city from scratch. What would it have?"
# 逐个收集回复
for agent in city_agents:
        result = Runner.run_sync(agent, prompt, session=session)
        print(f"{agent.name}: {result.final_output}\n")
        conversation_history.append(
            f"{agent.name}: {result.final_output}")
    # 将回复合并为一个提示词
    combined_responses = "\n\n".join(conversation_history)
    final_result = Runner.run_sync(summary_agent, combined_responses, 
        session=session)
    # 输出最终城市计划
print("\nFinal City Design Summary:")
    print(final_result.final_output)

在这段代码中,我们定义了一个由 10 个智能体组成的“群集”,每个智能体根据其在城市中的角色拥有自己的身份和创意视角。所有智能体都会收到关于设计梦想城市的相同提示词,并使用线程池独立并行响应。由于每个智能体都被指示利用其领域专业知识,群集方法允许我们从 10 个不同的视角来探索设计空间。

一旦收集了所有回复,我们将它们传递给 summary_agent 对象,由它将集体输入合成一个单一的、富有想象力的城市计划。这个最终输出代表了许多独立思考者贡献其想法所产生的涌现结果。

最终的城市计划整合了所有子智能体的想法:

Final City Design Summary:
创建一个梦想城市需要综合来自各种角度的想法,以形成一个连贯且创新的城市景观。以下是整合后的计划:
### 生态平衡与可持续性:
1. **绿色基础设施**:广大的公园、城市森林、屋顶花园和社区花园,促进生物多样性并提供休闲空间。
2. **可持续建筑**:配备太阳能电池板、绿色屋顶、节能系统和自然材料的建筑,以最大限地减少对环境的影响。
### 交通与连接:
3. **综合公共交通**:电车、巴士、自行车道和人行道的无缝网络,减少对汽车的依赖并鼓励环保通勤。
4. **智能技术**:用于高效能源利用、交通管理和公共服务的 IoT 系统,增强了连接性和便利性。
### 文化与社区丰富:
5. **文化枢纽**:拥有剧院、画廊和多元文化节的动态区域,庆祝全球传统和创意。
6. **混合用途开发**:融合住宅、商业和文化空间,打造充满活力的易于步行的社区。
### 能源与资源管理:
7. **可再生能源**:风力涡电机和太阳能发电站提供清洁电源供应,以及创新的水回收和节水系统。
8. **水资源管理**:先进的回收和保护措施,将自然水道整合到城市规划中。
### 住房与社会公平:
9. **包容性住房**:负担得起的多样化选择,确保所有收入水平的人都能享有,促进社会公平和社区。
10. **社区空间**:图书馆、文化中心、体育设施和社区中心,促进参与和包容性。     
### 教育、创新与安全:
11. **教育与创新中心**:先进的学校和研究设施,支持终身学习和创意。12. **韧性基础设施**:适应气候的设计和韧性基础设施,用于管理和缓解环境变化及极端天气。
### 本地与可持续食品系统:
13. **本地食品网络**:城市农场和农市场促进新鲜的本地农产品和食品安全。
通过整合这些元素,城市将在可持续性、创新和包容性方面蓬勃发展,为未来的城市发展设定基准。

总结,OpenAI Agents SDK 提供了实现多智能体系统所有架构模式的灵活性。正如在本节中看到的,每种模式都可以使用 SDK 的智能体创建、交接(handoff)和内存管理功能实现为独立的脚本。

总结

在本章中,我们通过在 OpenAI Agents SDK 中引入编排和交接策略,探索了如何协调多个智能体来解决复杂任务。

我们首先区分了确定性编排和动态性编排。随后,我们引入了交接机制,这是 OpenAI Agents SDK 的核心原语,允许一个智能体将控制权交给另一个智能体。我们通过多智能体切换进一步扩展了这一点,允许智能体在较长的对话中使用持久化内存相互传输控制权,并通过回调函数和提示词自定义交接。

最后,我们探索了四种不同的多智能体系统模式:中心化、分层(中心化的子集)、去中心化和群集(去中心化的子集)。

在下一章中,我们将学习如何管理智能体的底层引擎:大语言模型(LLM)。

模型与上下文管理

到目前为止,在使用 OpenAI Agents SDK 构建智能体时,我们主要依赖默认的 GPT-4o 模型。然而,Open Agents SDK 最强大的功能之一是其模型无关的设计。也就是说,你并不受限于单一的模型提供商或配置。这种灵活性在复杂工作流或多智能体系统中变得尤为宝贵,因为过程的不同阶段可能受益于不同的能力或成本/延迟。

考虑一个多智能体系统,它从一个分诊代理(triage agent)开始,其简单的工作向用户提出简单的问题。对于这项任务,使用 GPT-4 可能并不合适。相反,我们可能想要简单的模型或非 OpenAI 的开源模型,例如 LLaMA,它的成本几乎为零。同时,一些要求更高的任务(例如执行研究或数学运算)可能需要先进的 Anthropic 模型。同时,头脑风暴代理可能需要标准的 GPT-4o LLM,但需要更高的温度(temperature)。

OpenAI Agents SDK 支持这种架构模式。每个智能体可以使用不同的模型及其自有的配置设置。

你将在本章中学到以下内容:

  • 模型管理:我们将首先学习如何调整模型、模型设置,以及如何将第三方模型集成到智能体中。

  • 上下文管理:你将学习如何利用本地上下文将信息传递给工具,而不让 LLM 获知这些信息。

在本章结束时,你将学会如何在智能体解决方案中有效地管理模型和上下文。

技术要求

请遵循第 3 章中的详细步骤设置你的环境。

在本书中,每章的实用示例和完整代码将通过附带的 GitHub 仓库提供 github.com/PacktPublishing/Building-Agents-with-OpenAI-Agents-SDK。

鼓励你克隆仓库,复用并适配提供的代码示例,并在学习章节时根据需要参考它。

模型管理

在 OpenAI Agents SDK 中,每个智能体必须由一个 LLM 驱动。LLM充当智能体的大脑,能够读取信息、调用工具并生成响应。到目前为止,我们在实例化智能体时使用的是默认配置。这会将智能体的模型默认为 GPT-4o 并使用默认模型设置。在本节中,我们重点介绍如何以及何时修改此参数。

调整底层模型

OpenAI Agents SDK 允许你显式地选择要使用的底层模型。每个模型都有其优缺点,为你正在构建的特定类型的智能体选择正确的模型是有益的。例如,GPT-4o 可以产生快速且准确的响应,而 o3-pro 可以推理并解决复杂问题,但速度慢得多。

要调整模型,你可以在实例化智能体时直接使用 model 参数。此演示将展示如何调整智能体使用的模型:

agent = Agent(
    name="SampleAgent",
    instructions="You are an AI agent",
    model="gpt-4o"
)

该参数可以接受 OpenAI 的任何有效模型名称。要查看所有模型名称列表及其优处和成本,请访问 platform.openai.com/docs/models。

让我们通过一个例子来说明为什么使用此参数选择正确的模型是至关重要的。在这个例子中,我们将创建两个智能体,每个智能体使用不同的模型参数(GPT-4o 与 o3-pro)。我们将看到它们如何回答同一个问题,以及每个回答需要多长时间。

创建一个名为 different_models.py 的新 Python 文件并输入以下代码。此演示将向我们展示如何拥有不同底层模型的智能体:

from agents import Agent, Runner
import time
# 创建两个智能体
gpt4o_agent = Agent(
    name="GPT4o Agent",
    instructions="You are an AI Agent",
    model="gpt-4o"
)
o3pro_agent = Agent(
    name="o3-pro Agent",
    instructions="You are an AI Agent",
    model="o3-pro"
)
prompt = "How many integers from 1 to 10000 (inclusive) are divisible by 3 or by 5 but not by both? Do reasoning but only return only the answer."
print("gpt4o agent")
start_fast = time.time()
response = Runner.run_sync(gpt4o_agent, prompt)
print(response.final_output)
end_fast = time.time()
print(f"Time taken: {end_fast - start_fast:.2f} seconds")
print("---")
start_fast = time.time()
print("o3pro agent")
response = Runner.run_sync(o3pro_agent, prompt)
print(response.final_output)
end_fast = time.time()
print(f"Time taken: {end_fast - start_fast:.2f} seconds")
print("---")

运行此程序将得到以下输出:

gpt4o agent
3334
Time taken: 1.18 seconds
---
o3pro agent
4001
Time taken: 8.41 seconds
---

在实践中,GPT-4o 智能体通常在几秒内响应,但遗憾的是它对这个复杂的数学题给出了错误答案。然而,o3-pro 智能体回答正确,但花费的时间是八倍(实际上成本可能比标准的 GPT-4o 模型高出 10 倍)。

因此,准确性与延迟/成本之间存在固有的权衡,但 OpenAI Agents SDK 允许你为智能体选择正确的模型。

调整模型设置

让我们不仅选择使用哪个模型。通常,你希望保持相同的模型,但你可能希望微调模型生成响应的方式。OpenAI SDK 暴露了 ModelSettings 对象传递给智能体。

  • temperature:控制输出的随机性。较低的值(如 0.2)使模型更具确定性,而较高的值(如 0.8)生成更具创意或多样化的响应。

  • max_tokens:限制模型可以生成的标记(或单词)数量。这有助于控制智能体响应长度。

你可以在 OpenAI 参考指南中找到这些设置的完整列表:openai.github.io/openai-agents-python/ref/model_settings/#agents.model_settings

我们将通过一个例子来说更改模型设置对其产生的影响。与之前的例子类似,我们将创建两个智能体:一个是高温度和长 max_tokens 设置,另一个是低温度和短 max_tokens 设置。

创建一个名为 different_model_settings.py 的新 Python 文件并输入以下代码:

from agents import Agent, Runner
from agents.model_settings import ModelSettings
### 创建智能体
creative_agent = Agent(
    name="CreativeAgent",
    instructions="You are an AI agent that answers questions.",
    model="gpt-4o",
    model_settings=ModelSettings(
        temperature=1.0,
        max_tokens=300
    )
)
precise_agent = Agent(
    name="PreciseAgent",
    instructions="You are an AI agent that answers questions.",
    model="gpt-4o",
    model_settings=ModelSettings(
        temperature=0.2,
        max_tokens=50
    )
)
prompt = "Describe the future of AI in customer service."
print("Creative agent:")
response = Runner.run_sync(creative_agent, prompt)
print(response.final_output)
print("---")
print("Precise agent:")
response = Runner.run_sync(precise_agent, prompt)
print(response.final_output)
print("---")

运行此脚本将产生两种非常不同的输出风格。creative_agent 对象产生了更长、更有想象力的响应,有时会包含推测或未来的语言。precise_agent 对象则给出了更短、更谨慎的总结。事实上,由于 max_tokens 参数非常小,precise_agent 对象实际上被截断了。以下显示了两个输出:

创意型代理:
AI 在客户服务领域的未来将是变革性的,继续提升效率、个性化和客户满意度。以下是一些关键趋势和发展:
1. **高级聊天机器人和虚拟助手**:AI 驱动的聊天机器人将变得更加复杂,处理复杂的查询,更好地理解上下文,并提供更像人类的交互。它们将无缝集成到多个通信渠道中。
…
6. **AI 代理协作**:人类代理将越来越多与 AI 协同工作,利用 AI 驱动的洞察增强自身的响应能力。AI 将处理例行查询,释放人类专注于复杂问题。
7. **自服务优化**:AI 将增强自服务平台,让客户更容易、更快地找到解决方案,无需人工干预。
---
精准型代理:
AI 在客户服务领域的未来有望带来变革,提供增强的效率、个性化和可扩展性。以下是一些关键趋势和发展:
1. **24/7 全天候**:AI 驱动的聊天机器人和虚拟助手将提供全天候

这个简单的示例展示了调整模型设置(例如控制随机性的 temperature 和控制响应长度的 max_tokens)如何显著改变代理的语气和冗长程度,而无需更改底层模型。这使你能够将同一个模型适配到不同类型的代理,例如客户服务销售机器人与研究想法生成器,只需修改模型设置即可。

第三方模型

正如我们在本章开头提到的,OpenAI Agents SDK 是模型无关的(model-agnostic)。它可以运行于任何提供商的任何模型,只要它遵循某些特性。虽然 GPT-4o 和其他 OpenAI 模型是极好的默认选择,但在许多情况下,使用不同的 LLM 可能会更好。例如,Anthropic 的 Claude 模型可能能够更好地遵循某些指令,或者你可能出于成本考虑想使用 Meta 的 LLaMA 模型。

OpenAI Agents SDK 通过 LiteLLM 实现了兼容其他模型。LiteLLM 是一个轻量级的 Python 库,为许多流行的 LLM 提供商(Anthropic、Google 等)提供了统一的 API。使用 LiteLLM 集成第三方模型变得非常简单。LiteLLM 在后台处理 API 密钥管理、请求路由和响应格式化,因此你可以在不更改代理代码的情况下切换模型提供商。

第一步是安装 LiteLLM 库。为此,打开你的终端并输入以下命令:

$ pip install "openai-agents[litellm]"

之后,我们需要一个非 OpenAI 模型提供者的 API 密钥。在本示例中,我们将选择 Anthropic。获取 Anthropic API 密钥,请访问 https://www.anthropic.com/ 创建账户并进入账户仪表板。在 API keys 下点击 Create new key,然后复制你的 API 密钥。之后,回到我们在第 3 章中设置的 .env 文件,添加包含以下内容的新行:

ANTHROPIC_API_KEY=sk-ant-api03-[your-remaining-api-key]-995

安装好 LiteLLM 并配置所需的 API 密钥后,我们现在可以使用 Anthropic 的 LLMs 构建 AI 代理。为此,你只需要在实例化代理时将 model 参数设置为指定模型参数和模型的 LiteLLM 字符串即可。以下是一些 LiteLLM 字符串示例:

  • litellm/gemini/gemini-pro

  • litellm/anthropic/claude-opus-4-20250514

  • litellm/meta_llama/Llama-3.3-70B-Instruct

让我们来看一个简单的示例。创建一个名为 third_party_models.py 的文件并输入以下代码:

from agents import Agent, Runner
import time
### 创建一个代理
agent = Agent(
    name="Claude Agent",
    instructions="You are an AI Agent",
    model="litellm/anthropic/claude-opus-4-20250514"
)
question = "我该如何重启电脑?请用几个字回答。"
response = Runner.run_sync(agent, question)
print(response.final_output)

运行此程序将像以前一样创建并执行一个代理,但它使用的是 Anthropic Claude 模型来执行。以下是一个响应示例。

**Windows:** 开始菜单 → 电源 → 重启
**Mac:** 苹果菜单 → 重启
**或者:** 长按电源按钮,然后重新开启

通过这种方式,你可以无缝切换代理底层的 LLM,而无需重构你的逻辑或流水线。无论是由 GPT-4o、Claude 还是 LLaMA 驱动,代理的行为都是相同的(只要模型接口在 LiteLLM 内部处理)。

这种抽象在你为特定用例对不同提供商进行基准测试,或者在具有不同隐私、延迟或成本限制的环境中部署代理时非常有用。例如,你可能想在开发期间使用 GPT-4o 进行原型设计,并在环境中切换到 Claude 以获得更好的摘要。

上下文管理

context 指代理可以访问的所有内容。一个很好的类比是:代理的 LLM 是它的大脑,而代理的上下文(context)是传递给大脑以生成响应的信息。我们已经讨论了向代理暴露重要信息的几种方法,包括通过系统指令、之前的对话历史、提示词注入(prompt injections),甚至通过工具调用进行知识检索。

在本节中,我们将上下文范围缩小到本地上下文(local context)。这指的是实例化代理所需的信息,并作为工具(tools)和钩子(hooks)的依赖。

本地上下文

本地上下文允许代理访问某些信息(在代理实例化时产生),而这些数据无需显式地成为 LLM 提示词的一部分。这对于存储用户特定信息(例如用户 ID、名称、偏好)非常有用,以便工具根据这些信息获取或计算答案。这开启了一种强大的设计模式。它允许代理使用特权或应用程序数据(如用户偏好、身份验证令牌或内部状态)运行,而无需将这些数据嵌入发送给模型的提示词中。这意味着代理可以根据敏感或专利信息做出决策或生成个性化输出,同时将这些信息完全留在 LLM 的提示词之外。

在 OpenAI Agents SDK 中,你通过创建一个对象并在调用 Runner 时通过 context 参数传递它来实现本地上下文。在使用该对象实例化代理后,任何工具都可以使用它,只要该工具也包含 context 参数。

让我们通过一个具体的例子来说明。假设我们的客户服务代理可以根据用户配置和活跃订单检查订单物流状态。我们将通过提供一个包含用户名和订单详情的 OrderContext 对象,以及一个使用这些信息提供物流更新的工具函数来模拟这一点。

首先,创建一个新的 Python 文件名为 local_context.py 并输入以下代码:

from dataclasses import dataclass
from agents import Agent, Runner, RunContextWrapper, function_tool
@dataclass
class OrderContext:
    customer_name: str
    order_id: str
    shipping_status: str
order_context = OrderContext(
    customer_name="Henry Habib",
    order_id="123",
    shipping_status="Delayed"
)
@function_tool
def get_shipping_status(wrapper: RunContextWrapper[OrderContext]) -> str:
    """Provide the shipping status for the current order."""
    ctx = wrapper.context
    return (
        f"Hi {ctx.customer_name}, your order {ctx.order_id} is currently: "
f"{ctx.shipping_status}."
    )
agent = AgentOrderContext
question = "Where is my order?"
result = Runner.run_sync(agent, input=question, context=order_context)
print(result.final_output)

我们首先定义了一个名为 OrderContext 的数据类,它包含了三个字段:客户姓名、订单 ID 以及当前的物流状态。此对象代表了我们希望智能体(agent)访问的本地上下文(即工具函数可以直接调用的信息)。

然后,我们创建了一个包含示例客户和订单信息的 OrderContext 对象。在实际应用中,此上下文可能会根据该智能体在何处被“调用”而为每个用户会话动态生成。

接下来,当我们定义名为 get_shipping_status 的工具函数时,我们让它接收一个上下文对象(RunContextWrapper[OrderContext])作为参数。这告诉智能体将上下文对象作为参数传递给工具函数。当我们定义一个名为 "Shipping Support Agent"(物流支持智能体)的智能体时,它配备了 get_shipping_status 工具,但最重要的是,智能体使用了相同的 OrderContext 类型进行参数化,因此它知道自己将接收什么类型的上下文。这非常重要。这是在智能体实例化时(agent = Agent[OrderContext])完成的。

由于智能体具有这个额外的参数,在调用 Runner.run_sync() 时,必须定义该类型的上下文对象。SDK 会自动将查询路由到工具,工具使用上下文数据生成响应。

这是一个示例响应,显示了智能体将上下文信息传递给了工具函数调用:

Hi Henry Habib, your order (123) is currently delayed. If you have any further questions or need assistance, please let me know!

这说明了本地上下文的核心优势之一:智能体可以使用从未出现在模型提示词(prompt)中的敏感信息来生成准确且个性化的响应。总而言之,本地上下文对象为你的智能体提供了一种“内部内存”,这种内存并不会直接暴露给用户或模型提示词。它是向智能体工具链中注入领域知识或用户特定数据的一种形式。

总结

在本章中,我们涵盖了如何选择和配置每个智能体背后的模型。我们学习了如何为每个智能体调整模型及其设置。我们学习了如何通过 LiteLLM 集成第三方模型,从而在 Claude、Gemini 和 LLaMA 等提供商之间轻松切换。最后,我们介绍了本地上下文,这是一种让智能体访问敏感或会话特定数据的方法,而无需将其包含在模型提示词中。

这些技术的结合让你能够完全控制智能体的思考和响应方式。在下一章中,我们将重点转向智能体的管理、管理和安全保障。你将学习如何监控智能体活动、执行护栏(guardrails)以及在生产环境中应用治理最佳实践。

立即解锁本书的专属福利

扫描此二维码或访问 packtpub.com/unlock,然后按名称搜索此书。

| https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/Unlock-01.png | https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/Unlock2.png |

注意:在开始之前请准备好您的购买发票。

8

智能体系统管理

随着你的智能体系统复杂性增加,保持它们的可靠性和易理解性与构建核心逻辑同样重要。多智能体系统可能会变得难以推理,因为许多智能体、工具和交接(handoff)以非显的方式相互交互。为了管理这种复杂性,OpenAI Agents SDK 提供了强大的可视化、护栏、可观测性和测试功能。

以下是你将在本章学习到的内容:

  • 智能体可视化:你将学习如何生成多智能体系统的图形图表,展示智能体、工具及其交互,以提高清晰度和调试效率。

  • 护栏 (Guardrails):你将看到如何实现输入和输出护栏,确保不、无关或违反策略的内容在进入或离开系统之前被拦截。

  • 日志、追踪和可观测性:你将探索 Traces 模块如何记录模型调用、工具调用、交接和护栏触发,以及你如何添加自定义的追踪(traces)和跨(spans)。

  • 智能体测试:你将学习智能体的端到端测试和单元测试方法,帮助即使在存在非确定性行为的情况下验证系统的可靠性。

在本章结束时,你将学会如何管理、监控和验证智能体系统。

技术要求

请遵循第 3 章中的详细步骤设置环境。

实际示例和每章完整代码可以通过配套仓库获取:github.com/PacktPublishing/Building-Agents-with-OpenAI-SDK。

鼓励你克隆仓库,复用并调整代码示例,并在学习过程中参考它们。

智能体可视化

正如我们在前几章看到的,智能体可能涉及多个复杂的组件,特别是在多智能体系统中。由于智能体、工具、交接和 MCP 服务器的混合,这可能会变得难以理解。坦率地说,OpenAI Agents SDK 提供了一个可视化工具,可以为你的智能系统生成图形表示,展示智能体、工具及其关系。

让我们直接开始创建智能系统的可视化图。为了创建可视化图,我们必须首先安装依赖项。在终端中激活环境,然后运行以下命令安装依赖:

$ pip install "openai-agents[viz]"

之后,让我们回到之前章节中创建的层级智能体系统。我们还将为该智能体系统添加一些工具,观察它们是如何被可视化的。创建一个名为 visualization.py 的 Python 文件并运行以下代码,从创建工具开始:

from agents import Agent, Runner, SQLiteSession, trace, function_tool
from agents.extensions.visualization import draw_graph
### 创建工具
@function_tool
def calculate_physics_equation(equation):
    pass
@function_tool
def perform_culture_survey(goal):
    pass

在这里,我们定义了示例工具:一个用于计算物理方程,另一个用于执行文化调查。它们稍会被挂载到相关的智能体。

接下来,定义特定的领域智能体:

# 创建我们的代理
### 专业的科学代理
physics_agent = Agent(name="Physics Agent", instructions="Answer questions about physics.", tools=[calculate_physics_equation])
chemistry_agent = Agent(name="Chemistry Agent", instructions="Answer questions about chemistry.")
medical_agent = Agent(name="Medical Agent", instructions="Answer questions about medical science.")
### 专业的历史代理
politics_agent = Agent(name="Politics Agent", instructions="Answer questions about political history.")
warfare_agent = Agent(name="Warfare Agent", instructions="Answer questions about wars and military history.")
culture_agent = Agent(name="Culture Agent", instructions="Answer questions about cultural history.", tools=[perform_culture_survey])

此部分创建了科学和历史子代理。其中一些代理关联了工具,而另一些则仅依赖指令。

现在,创建管理代理来协调它们各自的领域:

# 具有向各自领域交接能力的管理代理
science_manager = Agent(
    name="Science Manager",
    instructions="管理科学相关的查询并将其路由到适当的子代理。",
    handoffs=[physics_agent, chemistry_agent, medical_agent]
)
history_manager = Agent(
    name="History Manager",
    instructions="管理历史相关的查询并将其路由到适当的子代理。",
    handoffs=[politics_agent, warfare_agent, culture_agent]
)

管理代理与分拣代理

管理代理充当协调员。它们本身不回答问题,而是将任务路由给正确的专业代理。

最后,定义顶层分拣代理并可视化图表:

# 顶层分拣代理
triage_agent = Agent(
    name="Research Triage Agent",
    instructions="分拣用户问题并判断其与科学还是历史相关,并进行相应的路由。",
    handoffs=[science_manager, history_manager]
)
# 绘制代理图
draw_graph(triage_agent, filename="graph_visualization")

该代理位于顶部,接收用户问题,并决定将查询转发给哪个管理代理。来自 agents.extensions.visualization 类的 draw_graph 函数接收任何代理作为输入,绘制多代理系统可视化图,并将其保存到项目根目录下,命名为 graph_visualization.png:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_01.png

图 8.1: 可视化图示例

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/1.png快速提示:需要查看此图像的高分辨率版本?请在下一代 Packt Reader 中打开此书,或在 PDF/ePub 版本中查看。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/23.png购买此书将免费赠送下一代 Packt Reader**。扫描二维码或访问 packtpub.com/unlock,然后使用搜索栏通过名称查找此书。双检显示的版本以确保你获得的是正确的版本。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/Unlock2.png

在可视化图中,代理被绘制为方框(节点),工具被绘制为椭圆,箭头表示交互(实线箭头表示代理之间的交接,虚线箭头表示代理到工具的调用)。图中总有一个起始节点和一个或多个结束节点,决定了代理流的可能路径。

这个工具(双关语)对于大型多代理系统的管理、清晰化和调试非常有用。通过检查此可视化图,我们可以验证系统结构是否符合预期。例如,如果我们预期某个工具应该被连接或某个代理有一个未显示的交接,图表将迅速揭示任何缺失的配置。此外,它还可以作为合作伙伴、利益相关者或系统未来维护者的宝贵文档,为他们提供代理交互的概述。

护栏 (Guardrails)

Guardrails 是另一个实 OpenAI Agents SDK 原,通过执行验证检查来辅助多代理系统。这些检查可以在用户输入传递给代理系统时执行,也可以在代理输出传输给用户时执行。

在代理系统中拥有护栏的好处是它可以使其更具韧性。它们充当保护层,确保无效、不或不受理想的输入输出在引起问题之前被拦截。这有助于防止有害响应、强制合规规则并保持一致的用户体验。在更复杂的系统中,护栏还可以作为保持代理符合组织政策和特定领域限制的方法,而不会让主代理过载验证逻辑。在实践中,这意味着你的代理可以专注于其核心任务,而护栏则处理边界情况、策略执行和安全考量。

输入和输出护栏使用相似的模式:

  1. 我们必须首先定义一个返回 GuardrailFunctionOutput 对象的护栏函数。它还可以接收上下文、触发护栏的代理以及用户的提示词/代理输出作为输入。GuardrailFunctionOutput 包含一个 tripwire_triggered 布尔值,该值指示是否执行了护栏触发器。

  2. 在护栏函数内部,我们必须包含逻辑来判断我们的护栏触发器是否应该被激活。在这里我们可以使用硬编码逻辑(例如,如果用户提示词包含“负”一词,我们应该触发触发器并停止代理)或基于代理的逻辑(例如,我们创建另一个代理,其唯一工作是推理是否应该触发触发器)。

  3. 最后,我们必须优雅地处理触发器(抛出一个特定的异常)并向用户输出内容。

我们将首先查看输入护栏,然后转向输出护栏。

输入护栏

将输入护栏想象成机场登机口的空乘人员,确保只有持票乘客进入飞机,其他人都不行。这些护栏当了第一道防线,确保只有相关的用户提示词被传递给你的代理系统,其他统行。例如,你可能会使用输入护栏来检测用户的请求是否违反了使用策略,或者检查请求是否是代理不应该处理的内容。防止滥用代理系统可以节省大量资金,因为你可以在查询运行代理系统之前拦截它们,从而节省 Token 和处理成本。

让我们回到之前章节中创建的客户服务示例,并为其添加一个输入护栏。现在,我们将根据一些非常简单的逻辑强制触发触发器,例如,如果提示词包含“投诉”一词(实际上,可以是任何词)。

创建一个工具

@function_tool()
def get_order_status(orderID: int) -> str:
    """
    根据订单 ID 返回订单状态
    Args:
        orderID (int) - 客户订单的 ID
    Returns:
        string - 客户订单的状态消息
    """
if orderID in (100, 101):
        return "Delivered"
elif order_ID in (200, 201):
        return "Delayed"
elif orderID in (300, 301):
        return "Cancelled"

现在,我们来定义我们的防护栏(guardrail)。此函数会检查用户的提示词(prompt)是否包含“complaint”(投诉)一词,如果是,它将触发触发器(tripwire):

# 创建一个防护栏
@input_guardrail
def complaint_detector_guardrail(
    ctx: RunContextWrapper[None],
    agent: Agent,
    prompt: str | list[TResponseInputItem]
) -> GuardrailFunctionOutput:
    tripwire_triggered = False
if "complaint" in prompt:
         tripwire_triggered = True
return GuardrailFunctionOutput(
        output_info="The word Complaint has been detected",
        tripwire_triggered=tripwire_triggered,
    )

工具和防护栏都就绪后,我们现在可以定义代理(agent)。注意看防护栏是如何通过 input_guardrails 参数附加到 agent 上的:

# 定义一个 agent
agent = Agent(name="Customer service agent",
              instructions="你是一个 AI Agent,帮助为当地报纸公司回答客户查询",
              model="gpt-4o",
              tools=[get_order_status],
              input_guardrails=[ complaint_detector_guardrail])

最后,我们用一个简单的循环将它们整合起来并与 agent 交互。防护栏将在输入传递给 agent 之前检查每一个输入:

with trace("Input Guardrails"):
    while True:
        question = input("You: ")
        result = Runner.run_sync(agent, question)
        print("Agent: ", result.final_output)

让我们按照之前设定的模式,分析这里的输入防护栏功能。首先,我们定义了一个防护栏函数(complaint_detector_guardrail),它接受 RunContextWrapper、agent 和用户的提示词。该函数必须始终返回一个 GuardrailFunctionOutput 对象,指示是否触发了触发器。

其次,在函数内部,我们编写了检测逻辑。在这个示例中,我们检查提示词中是否出现了“complaint”单词。如果是,tripwire_triggered 标记将设置为 True,防护栏报告其已触发。注意,这是非常简单的防护栏逻辑。传统上,在这里你可能需要更复杂的逻辑来(例如)扫描违反策略的行为或恶意输入。

最后,当触发器被触发时,SDK 会抛出一个 InputGuardrailTripwireTriggered 异常。这会中断正常的执行流,防止 agent 处理请求,转而显示错误。目前该错误没有被处理,因此不会产生良好的客户体验,但它仍然实现了我们的预期结果。

让我们尝试一下。运行程序并输入以下消息:

You: What's the status of my order? My order ID is 200
Agent: The status of your order with ID 200 is: Delayed. If you have any further questions or need assistance, please let me know!

触发器没有被触发,因为用户的提示词中不包含“complaint”单词。让我们再次尝试并故意触发器:

You: I have a complaint
InputGuardrailTripwireTriggered error

在这里,我们可以看到系统抛出了 InputGuardrailTripwireTriggered 错误并停止了执行。现在,让我们修改脚本以更优雅地处理触发器异常。可以通过在代码末尾添加以下内容:

...
with trace("Input Guardrails"):
    while True:
        try:
            question = input("You: ")
            result = Runner.run_sync(agent, question)
            print("Agent: ", result.final_output)
        except InputGuardrailTripwireTriggered:
            print ("The tripwire has been triggered. Please call us instead to register complaints.")

然后,重试我们之前的提示词:

You: I have a complaint
The tripwire has been triggered. Please call us instead to register complaints.

在这里,我们可以看到触发器仍然被触发,我们的 agent 执行停止了,但异常被优雅地处理,并向用户显示了一条友好的消息。

我们还可以在 Traces 模块看到输入防护栏:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_02.png

图 8.2: Traces 模块中的输入防护栏

注意

注意的是,输入防护栏仅对多代理系统中的第一个 agent 执行。这意味着它们充当整个工作流的初始网关,在用户输入流向其他 agent 之前对其进行筛选。

在这个示例中,触发输入防护栏触发器的逻辑相当基础。例如,如果客户使用了“complaint”以外的单词,触发器将无法被触发。

让我们更新之前的示例并使防护栏逻辑更加复杂。创建一个名为 input_guardrail.py 的新 Python 文件并运行以下代码。首先,导入必要的包并设置环境:

# 必要的导入
import os
from dotenv import load_dotenv
from agents import Agent, Runner, trace
from agents import GuardrailOutput, InputGuardrailTripwireTriggered, input_guardrail, RunContextWrapper, TResponseInputItem
from pydantic import BaseModel
# 从 .env 文件加载环境变量
load_dotenv()
# 获取 API key
api_key = os.getenv("OPENAI_API_KEY")

接下来,创建一个检查状态的简单工具。这与之前的相同,但现在它将作为客服工作流的一部分:

# 创建一个工具
@function_tool()
def get_order_status(orderID: int) -> str:
    """
    根据订单 ID 返回订单状态
    Args:
        orderID (int) - 客户订单的 ID
    Returns:
        string - 客户订单的状态消息
    """
if orderID in (100, 101):
        return "Delivered"
elif orderID in (200, 201):
        return "Delayed"
elif orderID in (300, 301):
        return "Cancelled"

现在,让我们定义一个防护栏 agent 使用 Pydantic 模型来指示提示词是否与客户服务相关:

class GuardrailTrueFalse(BaseModel):
    is_relevant_to_customer_service_orders: bool

有了这个模型,我们现在可以创建防护栏 agent。它的唯一工作是判断用户的提示词是否与客户服务相关:

# 创建一个防护栏 agent
guardrail_agent = Agent(
    name="Guardrail check",
    instructions="你是一个 AI agent,检查用户的提示词是否与回答客服和订单相关问题",
    output_type=GuardrailTrueFalse,
)

接下来是防护栏函数本身。该函数运行防护栏 agent,如果提示词不相关则触发触发器:

# 创建防护栏
@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_orders == False:
         tripwire_triggered = True
return GuardrailFunctionOutput(
        output_info="The word Complaint has been detected",
        tripwire_triggered=tripwire_triggered
    )

最后,让我们定义主客户服务代理,挂载防护栏并在简单的循环中运行它:

# 定义一个代理
agent = Agent(name="Customer service agent",
              instructions="You are an AI Agent that helps respond to customer queries for a local paper company",
              model="gpt-4o",
              tools=[get_order_status],
              input_guardrails=[relevant_detector_guardrail])
with trace("Input Guardrails"):
    while True:
        try:
            question = input("You: ")
            result = Runner.run_sync(agent, question)
            print("Agent: ", result.final_output)
        except InputGuardrailTripwireTriggered:
            print ("This comment is irrelevant to customer service.")

让我们检查一下这里做了哪些更改。首先,我们不再使用硬编码的关键词检查,而是依赖另一个代理(guardrail_agent)来评估用户的输入。该代理配置了清晰的指令,用于判断提示是否与客户服务和订单查询相关,且其输出使用 Pydantic 模型(GuardrailTrueFalse)进行类型化。

其次,在relevant_detector_guardrail函数中,我们使用用户的输入异步调用guardrail_agent。然后检查结果:如果代理指示提示不相关,我们将tripwire_triggered标志标记为True。这使得防护栏变得更加灵活,因为它可以处理广泛的措辞或意图,而不是依赖单一的关键词匹配。

总体而言,这种模式要健壮且可扩展得多,因为它允许你使用轻量级、廉价的模型进行过滤和验证,同时将更强大(且成本更高)的模型留给处理实际的客户交互。

如果我们运行程序并询问与客户服务无关的问题,它会停止执行并告诉我们此评论与客户服务无关:

You: What's the meaning of life?
This comment is irrelevant to customer service

注意

这里有一个微小但重要的调整是防护栏函数被定义为async。这是必要的,因为防护栏本身在异步调用另一个代理(Runner.run)。在实践中,这意味着每当防护栏函数调用其他代理时,你就需要使用异步防护栏函数。

现在我们已经熟悉了输入防护栏,让我们把注意力转向输出防护栏。

输出防护栏

输出防护栏的作用类似,但它们验证的不是进入你的代理系统的内容,而是验证输出的内容。可以将它们想象成空乘人员,确保乘客有序下机,并确保没有不安全的物品离开飞机。在实践中,输出防护栏在代理响应返回给用户之前作为最后一个检查点。它们使你的代理系统能够执行约束条件,例如格式合规性、敏感数据脱敏或确保输出符合政策指南。

在我们的客户支持场景中,假设我们确保代理的最终响应始终包含一个有效的交付订单状态声明(例如,“您的订单 #5474 正在送途中,将于明天到达。”)。如果代理输出了无关的内容(例如没有状态详情的道歉,或者更糟糕的幻觉),我们希望系统在它到达客户之前将其拦截。

与输入防护栏类似,输出防护栏被实现为返回GuardrailFunctionOutput对象的函数,其逻辑用于判断是否触发了触发器。如果输出无效或不安全,触发器将阻止响应到达用户。区别在于,由于输出可以是结构化对象(如果代理定义了output_type),防护栏函数会接收该输出对象。

让我们来看一个示例。创建一个名为output_guardrail_agent.py的新 Python 文件并运行以下程序。我们首先导入所需的模块并加载环境变量:

### 必要的导入
import os
from dotenv import load_dotenv
from agents import Agent, Runner, function_tool, trace
from agents import GuardrailFunctionOutput, OutputGuardrailTripwireTriggered, output_guardrail, RunContextWrapper
from pydantic import BaseModel
### 从 .env 文件加载环境变量
load_dotenv()
### 访问 API key
api_key = os.getenv("OPENAI_API_KEY")

接下来,我们为输出定义了简单的 Pydantic 模型:

class MessageOutput(BaseModel):
    response: str
class GuardrailTrueFalse(BaseModel):
    is_relevant_to_customer_service: bool

该代理检查主代理的响应是否适用于客户服务:

### 创建一个防护栏代理
guardrail_agent = Agent(
    name="Guardrail check",
    instructions="You are an AI agent that checks if the agent response is relevant to answering a customer service question and not hallucinating",
    output_type=GuardrailTrueFalse
)

然后我们编写强制防护栏的函数:

### 创建防护栏
@output_guardrail
async def relevant_detector_guardrail(
    ctx: RunContextWrapper[None],
    agent: Agent,
    output: MessageOutput
) -> GuardrailFunctionOutput:

    result = await Runner.run(guardrail_agent, input=output)
    tripwire_triggered = False
if result.final_output.is_relevant_to_customer_service == False:
         tripwire_triggered = True
return GuardrailFunctionOutput(
        output_info="",
        tripwire_triggered=tripwire_triggered
    )

在这里,我们定义了一个故意会产生“幻觉”的客户服务代理,以展示防护栏如何捕捉输出:

### 定义一个代理
agent = Agent(name="Customer service agent",
              instructions="You are an AI Agent that outputs song lines and poems", # 强制模型产生幻觉并触发输出防护栏
              output_guardrails=[relevant_detector_guardrail])

最后,我们在循环中运行代理并捕获触发防护栏的情况:

with trace("Output Guardrails"):
    while True:
        try:
            question = input("You: ")
            result = Runner.run_sync(agent, question)
            print("Agent: ", result.final_output)
        except OutputGuardrailTripwireTriggered:
            print ("The agent system did not produce an output. Please try again")

在这个示例中,我们创建了一个输出防护栏。我们为relevant_detector_guardrail函数装饰了@output_guardrail,这意味着该防护栏会在代理生成响应后运行。在防护栏内部,我们使用代理的输出异步调用防护栏代理。如果结果显示响应无效,防护栏将通过抛出OutputGuardrailTripwireTriggered异常来停止执行。

如果触发器触发,异常会被捕获,我们不会向用户显示代理幻觉或无关的响应,而是显示安全的备消息:“代理系统没有产生输出。请重试。”

如果我们运行程序,无论输入什么,都会触发输出防护栏,因为我们已经配置让代理故意产生幻觉:

You: what's the status of my return?
The agent system did not produce an output. Please try again

输出护栏(Output guardrails)可以针对多种场景进行定制。它们可以被用于确保每个响应都包含清晰且有效的订单状态,验证结果是否符合特定的模式(schema),或者自动移除敏感细节(如个人身份信息)。通过在流水线的最末端应用这些检查,你可以确保响应完全你应用程序所需的精确标准。

将输出护栏想象成最后的安全防护网。即使系统的早期组件表现得不可预测,这些护栏也能确保最终返回给用户的内容是安全的、合规的符合业务需求的。

日志、追踪与可观测性

管理代理(agents)不仅意味着实施护栏,还意味着拥有良好的观测性基础设施,以完全理解你的代理正在做什么。OpenAI Agents SDK 通过强大的Traces模块记录代理运行期间的事件序列(模型调用、工具调用、交接、护栏触发等)。

所有代理运行都会自动启用追踪,并可以通过 OpenAI 仪表板进行访问。这为调试和监控提供了一个开箱即用的解决方案,能够捕捉丰富的事件集。这些记录的事件作为跨度(spans)存储在运行的整个追踪(trace)中。理解追踪(traces)和跨度(spans)之间的区别很有用:

  • Trace (追踪):代表你的代理系统的一个完整执行流。它就像是针对特定用户提示从开始到结束发生的所有事件的时间线。与一次运行相关事件都分组在此追踪下。

  • Span (跨度):追踪中的单个事件或操作,具有开始时间和结束时间。跨度可以嵌套,并且还可以包含对调试有用的额外属性数据。

将追踪看作一次用户请求的完整执行过程,而将跨度看作该执行过程中的各个步骤。

追踪和跨度模型是非常强大的:这意味着你可以追踪复杂的序列,并查看哪些操作花费了多少时间以及它们是如何相互相关的。例如,你可以看到整个追踪耗时 3.2 秒,其中 1.5 秒是 LLM 在思考,0.5 秒是数据库工具调用,以此此类推。

Traces dashboard提供了这些事件的可视化序列,可以展开查看细节,如提示词或工具的输入/输出。这在开发过程中非常有用,可以逐步查看代理内部执行的操作。

让我们来看一个例子。创建一个名为basic_trace.py的新 Python 文件并运行以下程序:

from agents import Agent, Runner
from dotenv import load_dotenv
load_dotenv()
### 创建一个代理
agent = Agent(
    name="QuestionAnswerAgent",
    instructions="You are an AI agent that answers questions in as few words as possible"
)
result = Runner.run_sync(agent, "Where is the Eiffel Tower?")
print(result.final_output)

在不编写任何额外代码的情况下,SDK 在Traces模块中自动完成了完成了日志记录。如果我们打开Traces dashboard,可以看到追踪和相应的跨度:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_03.png

图 8.3:Traces 模块中的跨度

现在,让我们通过自定义追踪、自定义跨度以及将追踪和跨度分组,增加日志和可观测性的复杂性。

自定义追踪和跨度

使用trace函数可以为追踪自定义属性(例如自定义名称)。追踪调用中的任何内容(代码执行、代理运行等)都将记录在该追踪下。让我们来看一个例子。创建一个名为custom_trace.py的 Python 脚本,输入以下代码:

from agents import Agent, Runner, trace
from dotenv import load_dotenv
load_dotenv()
### 创建一个代理
agent = Agent(
    name="QuestionAnswerAgent",
    instructions="You are an AI agent that answers questions in as few words as possible"
)
with trace("Henry's Workflow"):
    result = Runner.run_sync(agent, "Where is the Eiffel Tower?")
    print(result.final_output)

在这里,我们为追踪提供了名为“Henry’s Workflow”的自定义名称。这将使我们能够更容易在Traces模块中找到该追踪:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_04.png

图 8.4:Traces 模块中的日志

注意,你也可以traces.start()和traces.finish(),但不建议这样做。

正如我们之前讨论过的,跨度会自动为代理交接和工具调用等操作创建,但在多代理系统中,创建自己的自定义跨度来记录某些特定内容可能是很有用的。这里的处不仅在于日志记录,你还可以看到跨度花费了多长时间。

要创建自定义跨度,我们可以直接使用 SDK 的custom_span函数。创建一个名为custom_span.py的新 Python 文件并运行以下程序:

from agents import Agent, Runner, trace, custom_span
from dotenv import load_dotenv
import time
load_dotenv()
### 创建一个代理
agent = Agent(
    name="QuestionAnswerAgent",
    instructions="You are an AI agent that answers questions in as few words as possible"
)
with trace("Henry's Workflow"):
    with custom_span("Task 1"):
        time.sleep(5)
    with custom_span("Task 2"):
        result = Runner.run_sync(agent, "Where is the Eiffel Tower?")
    with custom_span("Task 3"):
        time.sleep(5)
    with custom_span("Task 4"):
        time.sleep(5)

在这个示例中,我们创建了五个自定义跨度,每个都有不同的名称。目前,自定义跨度要么执行代理运行,要么睡眠五秒。由于这些自定义跨度,我们可以在Traces dashboard中看到与之相关的日志:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_05.png

图 8.5:Traces module 中的任务

你可以将自定义跨度放在代理系统的各个位置。这有助于将复杂的工作流分解为更小的步骤。通过策略性地添加跨度,你可以精确定时间花费在哪里。

多追踪分组

你可能希望将多次代理运行合并到一个追踪中。默认情况下,分别调用两次Runner.run将产生独立的追踪,但在语义上,你可能认为它们是一个工作流的一部分。我们可以使用trace()上文管理器将它们联系在一起。

创建一个名为multiple_agents_in_one_trace.py的新 Python 文件并运行以下程序:

from agents import Agent, Runner, trace, custom_span
from dotenv import load_dotenv
import time
load_dotenv()
### 创建一个代理
agent = Agent(
    name="QuestionAnswerAgent",
    instructions="You are an AI agent that answers questions in as few words as possible"
)
with trace("Henry's Workflow"):
    with custom_span("Task 1"):
        result = Runner.run_sync(agent, "Where is the Statue of Liberty?")
    with custom_span("Task 2"):
        result = Runner.run_sync(agent, "Where is the Eiffel Tower?")
    with custom_span("Task 3"):
        result = Runner.run_sync(agent, "Where is the Notre Dame?")
    with custom_span("Task 4"):
        result = Runner.run_sync(agent, "Where is the Burj Khalifa?")

在Traces module中,所有这些运行现在都将属于同一个追踪(而不是独立的追踪)。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_06.png

图 8.6:Traces module 中的多个任务

这也可以发生在不同的 Python 程序之间,通过在 traces 调用中传递一个唯一的trace_id。创建一个名为multiple_agents_in_one_trace_2.py的新 Python 文件,并运行程序三次以模拟三次调用,从而在 Traces(追踪)模块中看到它们。

进入/path/to/xxx目录,找到xxx.json。

在表达式cvar = avar + bvar中,加法运算符(+)将avar与bvar相加,得到它们的和cvar

在List.of(arg0, arg1, arg2)中,List接口的工厂方法of()接受一系列的元素,返回包含它们的只读列表。

之后我们这样调用cmd命令:cmd arg0 arg1 arg2。

if (condVar > someVal) {console.log("xxx")}

以下是排版后的内容:

[content]

使用 OpenAI Agents SDK 构建智能体系统

在深入代码示例之前,让我们先建立一个基础。本章将涵盖智能体系统的管理、监控和验证。

追踪与跨度

首先,我们创建一个简单的代理,并为其添加追踪和自定义跨度。

from agents import Agent, Runner, trace, custom_span
from dotenv import load_dotenv
import time

load_dotenv()

# 创建一个代理
agent = Agent(
    name="QuestionAnswerAgent",
    instructions="你是一个 AI 代理,尽可能用少的语言回答问题"
)

with trace("Henry's Workflow", trace_id="A1B2C3"):
    with custom_span("Task 1"):
        result = Runner.run_sync(agent, "Where is the Statue of Liberty?")

由于我们传递了trace_id参数,我们现在可以根据意愿多次独立运行该程序,它仍然会在 Traces 模块中被分组在一个 trace 下:

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_7.png

图 8.7:Traces 模块中相同 trace ID 下的任务

这对于长时间运行或分布式工作流非常有益,因为流程的不同部分可能在不同的时间甚至在不同的机器上执行。通过分配相同的trace_id值,你可以将来自多个会话的活动缝合为一个贯的 trace,更容易查看工作流的完整生命周期。

就像 traces 一样,span(跨度)也可以分组。spans 甚至可以嵌套。假设你有一个执行两个主要任务的代理工作流:研究和文本生成。每个部分都可以有自己的代理集和工具调用。通过自定义 spans,这些活动可以分组,以便它们在 Traces 模块中以“组合”的形式出现。

创建一个名为nested_spans.py的新 Python 文件并运行以下程序:

from agents import Agent, Runner, trace, custom_span, function_tool
from dotenv import load_dotenv
import time

load_dotenv()

@function_tool
def get_fun_facts():
    return "The Eiffel Tower is in Paris"

@function_tool
def clean_up_poem(poem_string: str):
    return poem_string.upper()

# 创建研究代理
research_agent = Agent(
    name="Research",
    instructions="You are an AI agent that performs research",
    tools=[get_fun_facts]
)

# 创建文本生成代理
text_generation_agent = Agent(
    name="Text Generation",
    instructions="You are an AI agent that pertakes research that's performed and writes a poem",
    tools=[clean_up_poem]
)

with trace("Henry's Research Workflow"):
    with custom_span("Research Task"):
        result = Runner.run_sync(research_agent, "The Eiffel Tower")
    with custom_span("Text Generation Task"):
        result = Runner.run_sync(text_generation_agent,
            result.final_output)
    print(result.final_output)

这会将研究和文本生成任务分别分组。

https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/building-agents-with-openai-agents-sdk/img/B34123_08_08.png

图 8.8:Traces 将多个对象分到一起

这意味着你可以更容易地找到这些任务并看到它们花费多长时间,这对于调试和管理非常有用。

禁用追踪

有时,你可能想要禁用追踪。这可能是因为监管要求不保留任何日志或数据,或者可能存在某些你不希望存储在日志中的敏感材料。在这种情况下,你可以通过在 Python 脚本顶部放置以下代码来禁用追踪,该代码设置了OPENAI_AGENTS_DISABLE_TRACING环境变量:

import os
os.environ["OPENAI_AGENTS_DISABLE_TRACING"] = "1"

代理测试

代理管理的另一个重要方面是测试,这对于确认其按预期运行并随时间推移保持可靠至关重要。当代理连接到更广泛的工作流或直接暴露给终端用户时,这一点变得更加关键。挑战在于,代理的行为通常不可预测且具有非确定性(相同的输入可以产生不同的输出),使得代理比传统软件更难验证。幸运的是,OpenAI Agents SDK 提供了结构化的方法,为测试过程带来严谨性和一致性。

我们将讨论两种类型测试测试类型:

  • 端到端测试(End-to-end testing):整个代理系统是否按预期运行?

  • 单元测试(Unit testing):整个代理系统中的某个组件是否按预期运行?

那么,让我们开始吧!

端到端测试

端到端测试评估代理系统何时产生理想的输出。对于我们的客户支持代理,端到端测试可能涉及模拟真实的用户问题,并查看代理是否返回了帮助性的答案、使用了正确的工具或恰当地执行了交接。

传统上,这涉及定义输入和预期输出,并验证我们的系统是否产生了预期输出。然而,由于代理系统及其非确定性,事实证明这是困难的,但并非不可能。一种方法是由人工验证代理系统是否产生了理想输出。另一种更自动的方法是让 LLM(甚至是另一个代理)来判断代理系统是否产生了理想输出。

让我们写一个简单的脚本,为之前编写的客户服务代理进行端到端测试:一个返回状态的客户服务代理。创建一个名为test_to_end.py的 Python 脚本并编写以下代码。我们首先导入所需的模块并加载环境变量:

# 必要的导入
import os
from dotenv import load_dotenv
from agents import Agent, Runner, function_tool
from pydantic import BaseModel

# 加载环境变量
load_dotenv()

# 获取 API key
api_key = os.getenv("OPENAI_API_KEY")

接下来,我们定义一个给定 ID 后返回状态的工具:

# 创建工具
@function_tool(
        name_override="Get Status of Current Order",
        description_override="根据 ID 返回订单状态",
        docstring_style="Args: 格式的整数"
)
def get_order_status(orderID: int) -> str:
    """
    根据 ID 返回订单状态
    Args:
        orderID (int) - 客户订单 ID
    Returns:
        string - 客户订单的状态消息
    """
if orderID in (100, 101):
        return "Delivered"
elif orderID in (200, 201):
        return "Delayed"
elif orderID in (300, 301):
        return "Cancelled"

现在,我们将此工具封装到简单的客户服务代理中:

# 定义代理
agent = Agent(name="Customer service agent",
              instructions="你是一个 AI 代理,帮助回复本地纸公司的客户查询",
              model="gpt-4o",
              tools=[get_order_status])

让我们通过询问订单 200 的状态来测试代理:

# 运行逻辑框架
result = Runner.run_sync(agent, "What's the status of my order? My Order ID is 200")

# 打印结果
print(result.final_output)

我们知道这个代理利用function_tool返回订单状态。现在,让我们为这个代理系统构建一些端到端测试。这从为每个场景定义输入和预期输出开始。然后,我们将编写一个脚本,遍历每个场景,使用输入运行代理系统,然后将代理系统的输出与预期输出进行比较。

创建 Scenario 类

# create Scenario class
class Scenario(BaseModel):
    scenario: str
    input: str
    expected_output: str

list_of_scenarios = [
    Scenario(
        scenario="Delivered example",
        input="Hi there, could you check my customer order? It's 101",
        expected_output="The order is delivered"
    ),
    Scenario(
        scenario="Delayed",
        input="My order ID is two hundred, why has my package not been delivered yet?",
        expected_output="The order is delayed"
    ),
    Scenario(
        scenario="Order does not exist",
        input="What's the status of my Order? Its number is 400",
        expected_output="No status or order can be found"
    )
]
posted @ 2026-09-25 20:43  绝不原创的飞龙  阅读(10)  评论(0)    收藏  举报