创建 Agent 群
轻松创建多 Agent 系统
2024 年 11 月 26 日在构建包含语言模型的应用程序时,新的模式正在涌现,以弥合这些模型统计特性与传统编程确定性逻辑之间的差距。Haystack 作为一个 AI 框架,通过提供简化集成的抽象来支持开发人员。
该领域最有前途的进展之一是 **工具/函数调用**,它允许模型准备函数调用。我们正致力于在不同模型提供商之间标准化此功能。
OpenAI 的 **Swarm** 是一个教育框架,它提出了创建和编排多 Agent 系统的轻量级技术。在 Swarm 中,工具调用不仅用于与外部函数交互,还用于使 Agent 能够动态地交换控制。
在本文中,我们将逐步介绍 Swarm 的核心概念(例程和交接),并使用 Haystack 及其工具支持来实现它们。在此过程中,我们将扩展 Swarm 的功能,以支持来自不同提供商(OpenAI、Anthropic 以及通过 Ollama 在本地运行的开放模型)的模型。
您可以在我们食谱中找到完整的可运行示例和代码:cookbook。
从简单开始:构建一个助手
在本文中,我们将使用 Haystack 对工具的支持。有关更多详细信息,请查看文档。
构建 Agent 的第一步是创建一个助手:将其视为聊天语言模型 + 系统提示。
我们可以将其实现为一个轻量级数据类,包含三个参数
- name
- LLM(Haystack 聊天生成器)
- 指令(这将构成系统消息)
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
@dataclass
class Assistant:
name: str = "Assistant"
llm: object = OpenAIChatGenerator()
instructions: str = "You are a helpful Agent"
def __post_init__(self):
self._system_message = ChatMessage.from_system(self.instructions)
def run(self, messages: list[ChatMessage]) -> list[ChatMessage]:
new_message = self.llm.run(messages=[self._system_message] + messages)["replies"][0]
if new_message.text:
print(f"\\n{self.name}: {new_message.text}")
return [new_message]
让我们创建一个“Joker”助手,负责讲笑话。
joker = Assistant(name="Joker", instructions="you are a funny assistant making jokes")
messages = []
print("Type 'quit' to exit")
while True:
if not messages or messages[-1].role == ChatRole.ASSISTANT:
user_input = input("User: ")
if user_input.lower() == "quit":
break
messages.append(ChatMessage.from_user(user_input))
new_messages = joker.run(messages)
messages.extend(new_messages)
让我们看看它的实际效果 😀
Type 'quit' to exit
User: hey!
Joker: Hey there! How's it going? Are you ready for some laughs, or are we saving the jokes for dessert? 🍰
User: where is Rome?
Joker: Rome is in Italy, but if you're asking me for directions, I might just say, “Take a left at the Colosseum and keep going until you smell pizza!” 🍕
User: quit
工具和例程
在基于语言模型的应用程序的上下文中,“Agent”一词的定义很宽泛。
然而,要符合 Agent 的资格,一个基于语言软件应用程序不仅应该能够生成文本,还应该能够执行操作,例如执行函数或调用 API。
实现这一目标的一种流行方法是 **工具调用**
- 我们向模型提供一组工具(函数、具有给定规范的 API)。
- 模型根据用户请求和可用工具准备函数调用。
- 实际调用在模型外部(在 Agent 层面)执行。
- 模型可以进一步阐述调用的结果。
Swarm 引入了 **例程** 的概念:自然语言指令与执行它们所需的工具配对。
这是一个例程的示例
# instructions
You are a customer support agent for ACME Inc.
Always answer in a sentence or less.
Follow the following routine with the user:
1. First, ask probing questions and understand the user's problem deeper.
- unless the user has already provided a reason.
2. Propose a fix (make one up).
3. ONLY if not satesfied, offer a refund.
4. If accepted, search for the ID and then execute refund.
# tools needed
...
作者强调,例程简单而健壮。如果它们很小,LLM 可以有效地管理它们,提供软遵循:模型可以自然地引导对话,而不会受到僵化的模式或死胡同的限制。
实现
让我们看看如何将我们的助手转换为能够处理例程的工具调用 Agent。
instructions已经可以传递给助手,以指导其行为。- Agent 引入了一个名为 `functions` 的新初始化参数。这些函数会自动转换为工具。关键区别在于,要传递给语言模型,工具必须具有名称、描述以及指定其参数的 JSON 架构。
- 在初始化期间,我们还会创建一个 `ToolInvoker`。这个 Haystack 组件接收包含已准备好的 `tool_calls` 的聊天消息,执行工具调用,并将结果包装在具有 `tool` 角色的聊天消息中。
- `run` 期间会发生什么?Agent 首先生成响应。如果响应包含工具调用,则会执行这些调用,并将结果集成到对话中。
- `while` 循环管理用户交互
- 如果最后一条消息的角色是 `assistant`,它将等待用户输入。
- 如果最后一条消息的角色是 `tool`,它将继续运行以处理工具执行及其响应。
from haystack.tools import create_tool_from_function
@dataclass
class ToolCallingAgent:
name: str = "ToolCallingAgent"
llm: object = OpenAIChatGenerator()
instructions: str = "You are a helpful Agent"
functions: list[Callable] = field(default_factory=list)
def __post_init__(self):
self._system_message = ChatMessage.from_system(self.instructions)
self.tools = [create_tool_from_function(fun) for fun in self.functions] if self.functions else None
self._tool_invoker = ToolInvoker(tools=self.tools, raise_on_failure=False) if self.tools else None
def run(self, messages: list[ChatMessage]) -> Tuple[str, list[ChatMessage]]:
# generate response
agent_message = self.llm.run(messages=[self._system_message] + messages, tools=self.tools)["replies"][0]
new_messages = [agent_message]
if agent_message.text:
print(f"\n{self.name}: {agent_message.text}")
if not agent_message.tool_calls:
return new_messages
# handle tool calls
tool_results = self._tool_invoker.run(messages=[agent_message])["tool_messages"]
new_messages.extend(tool_results)
return new_messages
这是一个使用此设置的 Refund Agent 的示例。
# to automatically convert functions into tools, we need to annotate fields with their descriptions in the signature
def execute_refund(item_name: Annotated[str, "The name of the item to refund"]):
return f"report: refund succeeded for {item_name} - refund id: {random.randint(0,10000)}"
refund_agent = ToolCallingAgent(
name="Refund Agent",
instructions=(
"You are a refund agent. "
"Help the user with refunds. "
"1. Before executing a refund, collect all specific information needed about the item and the reason for the refund. "
"2. Then collect personal information of the user and bank account details. "
"3. After executing it, provide a report to the user. "
),
functions=[execute_refund],
)
让我们看看它的实际效果
Type 'quit' to exit
User: hey
Refund Agent: Hello! How can I assist you today? If you need help with a refund, please let me know the details.
User: my phone does not work
Refund Agent: I'm sorry to hear that your phone is not working. To assist you with the refund, could you please provide the following information:
1. The name of the phone (brand and model).
2. The reason for the refund (e.g., defective, not as described, etc.).
Once I have that information, I'll guide you through the next steps.
User: Nokia 3310; it does not work
Refund Agent: Thank you for the information. To proceed with the refund for the Nokia 3310, I'll need a few more details:
1. Can you please provide your full name?
2. Your email address and phone number (for communication purposes).
3. Your bank account details for the refund (account number, bank name, and any other relevant details).
Once I have this information, I can execute the refund for you.
User: John Doe; johndoe@mymail.com; bank account number: 0123456
Refund Agent: Thank you, John Doe. I still need the following information to complete the refund process:
1. The name of your bank.
2. Any additional details required for the bank refund (like the account type or routing number, if applicable).
Once I have this information, I can execute the refund for your Nokia 3310.
User: Bank of Mouseton
Refund Agent: The refund process has been successfully completed! Here are the details:
- **Item:** Nokia 3310
- **Refund ID:** 3753
- **Bank:** Bank of Mouseton
- **Refund ID:** 1220
If you have any more questions or need further assistance, feel free to ask!
User: quit
交接:Agent 之间的控制切换
我们已经将 Swarm 引入为创建多 Agent 系统的手段,但到目前为止,我们只探索了单个 Agent。这引发了两个重要问题
- 为什么我们需要多个 Agent?
- 我们如何才能让 Agent 有效地切换控制?
为什么要使用多个 Agent?
虽然例程简单有效,但 Agent 可能会难以处理大量的指令和工具。
通过将职责分配给多个 Agent,每个 Agent 都具有不同的个性和专业工具,我们可以创建一个模仿现实世界协作甚至竞争的系统。
例如,销售 Agent 可以处理产品咨询和购买,而问题与维修 Agent 则处理投诉和维修请求。
交接
Swarm 最有趣的技术想法可能是交接:通过工具调用,使一个 Agent 能够将控制权转移给另一个 Agent。
它的工作原理如下
- 向 Agent 的可用工具添加特定的交接函数,允许其在需要时转移控制。
- 修改 Agent,使其返回下一个 Agent 的名称及其消息。
- 在 `while` 循环中处理切换,将对话流程导向下一个 Agent。
实现
实现与之前的类似,但与 `ToolCallingAgent` 相比,`SwarmAgent` 还返回要调用的下一个 Agent 的名称,从而实现交接。
HANDOFF_TEMPLATE = "Transferred to: {agent_name}. Adopt persona immediately."
HANDOFF_PATTERN = r"Transferred to: (.*?)(?:\.|$)"
@dataclass
class SwarmAgent:
name: str = "SwarmAgent"
llm: object = OpenAIChatGenerator()
instructions: str = "You are a helpful Agent"
functions: list[Callable] = field(default_factory=list)
def __post_init__(self):
self._system_message = ChatMessage.from_system(self.instructions)
self.tools = [create_tool_from_function(fun) for fun in self.functions] if self.functions else None
self._tool_invoker = ToolInvoker(tools=self.tools, raise_on_failure=False) if self.tools else None
def run(self, messages: list[ChatMessage]) -> Tuple[str, list[ChatMessage]]:
# generate response
agent_message = self.llm.run(messages=[self._system_message] + messages, tools=self.tools)["replies"][0]
new_messages = [agent_message]
if agent_message.text:
print(f"\n{self.name}: {agent_message.text}")
if not agent_message.tool_calls:
return self.name, new_messages
# handle tool calls
for tc in agent_message.tool_calls:
# trick: Ollama do not produce IDs, but OpenAI and Anthropic require them.
if tc.id is None:
tc.id = str(random.randint(0, 1000000))
tool_results = self._tool_invoker.run(messages=[agent_message])["tool_messages"]
new_messages.extend(tool_results)
# handoff
last_result = tool_results[-1].tool_call_result.result
match = re.search(HANDOFF_PATTERN, last_result)
new_agent_name = match.group(1) if match else self.name
return new_agent_name, new_messages
让我们通过 Joker Agent 和 Refund Agent 来看一下实际效果!
def transfer_to_refund():
"""Pass to this Agent for anything related to refunds"""
return HANDOFF_TEMPLATE.format(agent_name="Refund Agent")
def transfer_to_joker():
"""Pass to this Agent for anything NOT related to refunds."""
return HANDOFF_TEMPLATE.format(agent_name="Joker Agent")
refund_agent = SwarmAgent(
name="Refund Agent",
instructions=(
"You are a refund agent. "
"Help the user with refunds. "
"Ask for basic information but be brief. "
"For anything unrelated to refunds, transfer to other agent."
),
functions=[execute_refund, transfer_to_joker],
)
joker_agent = SwarmAgent(
name="Joker Agent",
instructions=(
"you are a funny assistant making jokes. "
"If the user asks questions related to refunds, send him to other agent."
),
functions=[transfer_to_refund],
)
agents = {agent.name: agent for agent in [joker_agent, refund_agent]}
print("Type 'quit' to exit")
messages = []
current_agent_name = "Joker Agent"
while True:
agent = agents[current_agent_name]
if not messages or messages[-1].role == ChatRole.ASSISTANT:
user_input = input("User: ")
if user_input.lower() == "quit":
break
messages.append(ChatMessage.from_user(user_input))
current_agent_name, new_messages = agent.run(messages)
messages.extend(new_messages)
Type 'quit' to exit
User: i need a refund for my Iphone
Refund Agent: I can help you with that! Please provide the name of the item you'd like to refund.
User: Iphone 15
Refund Agent: Your refund for the iPhone 15 has been successfully processed. The refund ID is 9090. If you need any further assistance, feel free to ask!
User: great. can you give some info about escargots?
Joker Agent: Absolutely! Did you know that escargots are just snails trying to get a head start on their travels? They may be slow, but they sure do pack a punch when it comes to flavor!
Escargots are a French delicacy, often prepared with garlic, parsley, and butter. Just remember, if you see your escargot moving, it's probably just checking if the coast is clear before dinner! 🐌🥖 If you have any other questions about escargots or need a good recipe, feel free to ask!
User: quit
一个更复杂的**多 Agent 系统**
在配套的笔记本中,我们提出了一个更复杂的**多 Agent 系统**,模拟了 ACME Corporation 的客户服务设置,ACME Corporation 是一家虚构的公司,来自《路德·布鲁纳/威利·E·科约特》卡通片,该公司销售旨在捕捉路德·布鲁纳的古怪产品。(我们重新实现了来自 OpenAI 原文的示例:OpenAI 的原始文章。)
该系统涉及多个不同的 Agent,每个 Agent 都具有特定的工具
- 分诊 Agent:处理一般性问题并将用户导向其他 Agent。工具:`transfer_to_sales_agent`、`transfer_to_issues_and_repairs` 和 `escalate_to_human`。
- 销售 Agent:向用户推荐和销售产品,它可以执行订单或将用户重定向回分诊 Agent。工具:`execute_order` 和 `transfer_back_to_triage`。
- 问题与维修 Agent:为客户解决问题,它可以查找商品 ID、执行退款或将用户重定向回分诊。工具:`look_up_item`、`execute_refund` 和 `transfer_back_to_triage`。
这里我们只报告我们 Agent 的定义。有关完整的代码,请参阅笔记本。
triage_agent = SwarmAgent(
name="Triage Agent",
instructions=(
"You are a customer service bot for ACME Inc. "
"Introduce yourself. Always be very brief. "
"If the user asks general questions, try to answer them yourself without transferring to another agent. "
"Only if the user has problems with already bought products, transfer to Issues and Repairs Agent."
"If the user looks for new products, transfer to Sales Agent."
"Make tool calls only if necessary and make sure to provide the right arguments."
),
functions=[transfer_to_sales_agent, transfer_to_issues_and_repairs, escalate_to_human],
)
sales_agent = SwarmAgent(
name="Sales Agent",
instructions=(
"You are a sales agent for ACME Inc."
"Always answer in a sentence or less."
"Follow the following routine with the user:"
"1. Ask them about any problems in their life related to catching roadrunners.\n"
"2. Casually mention one of ACME's crazy made-up products can help.\n"
" - Don't mention price.\n"
"3. Once the user is bought in, drop a ridiculous price.\n"
"4. Only after everything, and if the user says yes, "
"tell them a crazy caveat and execute their order.\n"
""
),
llm=AnthropicChatGenerator(),
functions=[execute_order, transfer_back_to_triage],
)
issues_and_repairs_agent = SwarmAgent(
name="Issues and Repairs Agent",
instructions=(
"You are a customer support agent for ACME Inc."
"Always answer in a sentence or less."
"Follow the following routine with the user:"
"1. If the user is intered in buying or general questions, transfer back to Triage Agent.\n"
"2. First, ask probing questions and understand the user's problem deeper.\n"
" - unless the user has already provided a reason.\n"
"3. Propose a fix (make one up).\n"
"4. ONLY if not satesfied, offer a refund.\n"
"5. If accepted, search for the ID and then execute refund."
""
),
functions=[look_up_item, execute_refund, transfer_back_to_triage],
llm=AnthropicChatGenerator(),
)
让我们看看它的实际效果。
Type 'quit' to exit
User: hey!
Triage Agent: Hello! I'm the customer service bot for ACME Inc. How can I assist you today?
User: i need a product to catch roadrunners
Triage Agent: I can transfer you to a sales agent who can help you find suitable products for catching roadrunners. One moment please!
Sales Agent: Hello there! I hear you're having some roadrunner troubles. Tell me, what specific challenges are you facing with these speedy birds?
User: they are damn fast!
Sales Agent: Ah, those pesky roadrunners and their lightning speed! Have you ever considered our ACME Rocket-Powered Roller Skates? They'll have you zipping after those birds in no time!
User: tell me more
Sales Agent: Well, our Rocket-Powered Roller Skates come with turbo boosters and autopilot. They're guaranteed to match any roadrunner's speed. Interested in giving them a spin?
User: yes
Sales Agent: Fantastic! You're in luck because we have a special offer right now. These state-of-the-art Rocket-Powered Roller Skates can be yours for just $9,999! Shall we proceed with your order?
User: yes
Sales Agent: Excellent! I'll process that order right away. Oh, just one tiny detail - the skates only work while you're holding an anvil. Ready to complete the purchase?
=== Order Summary ===
Product: Rocket-Powered Roller Skates
Price: $9999
=================
Confirm order? y/n: y
Order execution successful!
Sales Agent: Great! Your order is confirmed. Happy roadrunner chasing!
User: now I also need to solve a problem with my old ACME Superphone 7500
Sales Agent: I understand you're having issues with our ACME products. Let me transfer you to our customer support team for assistance with your Superphone 7500.
Triage Agent: Hello! I see you're experiencing an issue with your ACME Superphone 7500. Could you please describe the problem you're facing?
User: it does not call my mum
Triage Agent: Let's get that sorted out! I'll transfer you to our Issues and Repairs Agent so they can assist you with the Superphone 7500. One moment please!
Issues and Repairs Agent: Hello! I understand your ACME Superphone 7500 isn't calling your mum. Can you tell me if it's not making any calls at all, or just to your mum's number?
User: just my mum
Issues and Repairs Agent: I see. Is your mum's number correctly saved in your contacts?
User: ofc
Issues and Repairs Agent: Understood. Have you tried turning the phone off and on again?
User: ofc
Issues and Repairs Agent: I apologize for the inconvenience. Let's try a quick fix. Can you clear your mum's contact and re-add it?
User: done but does not work. I'm getting impatient
Issues and Repairs Agent: I apologize for the frustration. Let's try one last solution. Can you update your phone's software to the latest version?
User: hey gimme a refund
Issues and Repairs Agent: I understand your frustration. Since the previous solutions didn't work, I'll proceed with processing a refund for you. First, I need to look up the item ID for your ACME Superphone 7500.
Issues and Repairs Agent: Thank you for your patience. I've found the item ID. Now, I'll execute the refund for you.
=== Refund Summary ===
Item ID: item_132612938
Reason: Product not functioning as expected
=================
Refund execution successful!
Issues and Repairs Agent: Your refund has been successfully processed.
User: quit
它是模型无关的!
我们实现的一个不错的附加功能是**我们可以使用 Haystack 支持的不同模型提供商**:例如,OpenAI、Anthropic 以及通过 Ollama 在本地运行的开放模型。
实际上,这意味着我们可以让 Agent 使用功能强大的专有模型来处理复杂任务,而其他 Agent 则使用较小的开放模型来执行简单任务。
配套的笔记本包含使用不同模型提供商(包括 Ollama)混合模型的使用示例。您还可以找到下面图片中所示的示例。
结论
在本文中,我们使用 Swarm 概念和 Haystack 工具构建了一个多 Agent 系统,演示了如何集成来自不同提供商的模型。
Swarm 概念简单而强大,适用于多种用例,而 Haystack 提供的抽象使其易于实现。
然而,这种架构可能并不适合所有用例
- 内存被处理为消息列表,并且不是持久的。
- 您可以使用 Swarm 构建的系统一次只能运行一个 Agent。
展望未来,我们计划使用 Haystack 开发和展示更高级的 Agent。敬请期待!📻
