随着大型语言模型的快速发展,构建基于LLM驱动的自治代理(autonomous agents)已经成为一个备受关注的话题。仅在过去一年中,就出现了许多基于这一理念的新技术和框架。
本文将探索微软开源的Agent框架:Autogen。它弥补了此类新技术未能解决的空白:允许多个Agent协作以实现共同目标。它在LLM之上添加了最小但重要的功能,以支持多个代理的初始化和协作,允许在多个Agent之间单聊以及群聊。
但作为一个仍处于早期阶段的框架,通过API把Autogen集成到实际的生产环境中仍然是一个挑战,如Web APP。由于缺乏成熟的文档或资源,在Agent通信流中需要一些变通方法。
因此,在本文中,我们将讨论Autogen Agent连接到API的详细过程。
一、Autogen介绍
在之前的LLM之Agent(七)| AutoGen介绍文章中介绍过Autogen,它是一个基于LLM的Agent通信框架,支持创建具有不同人物角色的代理。然后,这些代理可以进行单聊;也可以进行群聊,每个代理轮流发言。
Autogen提供了一些具有不同功能的内置代理类型,例如:
- User Proxy Agent:可以检索用户输入并执行代码;
- Assistant Agent:默认的系统消息代理,该消息允许Agent充当完成任务的助理;
- Conversable Agent:在user proxy agents和assistant之间构建会话能力。
虽然Autogen主要支持OpenAI LLM,如GPT-3.5和GPT-4来创建代理,但用户也可以与本地或其他托管LLM一起使用。
二、Autogen群聊
Autogen中的群聊功能允许多个代理在群设置中进行协作,主要特点如下:
- 每个代理都可以看到组中其他代理发送的所有消息;
- 一旦启动,群聊将继续,直到满足其中一个终止条件。(例如:代理在回复中使用终止消息,用户选择退出聊天,达到群的最大聊天次数等)
- 每个群聊都有一个管理代理,负责监督消息广播、发言人选择和聊天终止。
- Autogen在每轮聊天中选择下一位发言人,目前支持四种方法
-manual:要求用户手动选择下一个发言人
-random:随机选择下一个发言人
-round robin:使用循环方法选择下一个发言人
-auto:让LLM选择下一个有聊天历史记录作为上下文的发言人
这些特性使Autogen群聊成为Agent协作的理想选择。然而,如果想控制Agent在此环境中更多的协作方式时,也会带来很多挑战。
三、使用Autogen开发应用程序
目前,Autogen旨在作为一种工具使用,用户可以完全了解不同代理之间的所有内部通信。这使得将Autogen集成到用户不应该知道这些信息的应用程序中成为一项棘手的工作。
例如,如果您构建了一个系统,其中多个代理共同担任销售助理,那么在决定对用户查询的最终响应之前,您可能不想公开他们是如何在内部规划和选择销售策略的。您也可能不想让用户暴露在这种内部沟通的复杂性中。
除此之外,在尝试将Autogen代理系统与API集成时,我们还面临以下问题:
- Autogen主要是一个CLI工具。(例如:它将代理消息打印到CLI,并提示用户通过CLI提供反馈);
- Autogen无法在没有明确用户输入的情况下提供一致的方式来结束特定的聊天序列。
但好消息是,我们可以使用Autogen已经支持的某些定制来解决这些问题。我们能够将Autogen集成到API中。
下面通过旅游代理系统来演示一下如何将AutoGen与API进行集成:
四、基于Autogen的旅游代理系统
该系统将由两个Autogen Assistant Agent和一个User Proxy Agent构建,在群聊中这些代理人中的每一位都有以下职责:
- Tour Agent:主代理决定如何响应用户查询以及在生成对用户的最终响应之前应收集的信息;
- Location Researcher:旅游代理的助理,在通过SERP API查询谷歌地图的函数调用的帮助下进行位置研究。它使代理商能够研究用户心目中与目的地相关的景点、餐厅、住宿等;
- User Proxy:代理群聊天中用户的代理。
由于本教程依赖于OpenAI和SERP API,因此您需要每个服务的API密钥来尝试本示例。
4.1 Autogen config
首先,定义AutoGen的配置:
config_list = [{
'model': 'gpt-3.5-turbo-1106',
'api_key': os.getenv("OPENAI_API_KEY"),
}]
4.2 Assistant Agents
然后,创建两个助理代理:Tour Agent和Location Researcher。
Tour Agent是一个简单的Assistant Agent,具有一个自定义的系统提示,用于描述其角色和职责,它指定代理应如何将TERMINATE添加到针对用户的最终回答的末尾。
tour_agent = AssistantAgent(
"tour_agent",
human_input_mode="NEVER",
llm_config={
"config_list": config_list,
"cache_seed": None
},
system_message="You are a Tour Agent who helps users plan a trip based on user requirements. You can get help from the Location Researcher to research and find details about a certain location, attractions, restaurants, accommodation, etc. You use those details a answer user questions, create trip itineraries, make recommendations with practical logistics according to the user's requirements. Report the final answer when you have finalized it. Add TERMINATE to the end of this report."
)
另一方面,在创建Location Researcher时,定义一个函数,它可以调用并执行来搜索谷歌地图。将在下一节中介绍该函数的实际schema和具体实现。下面代码片段显示了如何通过自定义提示将它们附加到Assistant Agent。
location_researcher = AssistantAgent(
"location_researcher",
human_input_mode="NEVER",
system_message="You are the location researcher who is helping the Tour Agent plan a trip according to user requirements. You can use the `search_google_maps` function to retrieve details about a certain location, attractions, restaurants, accommodation, etc. for your research. You process results from these functions and present your findings to the Tour Agent to help them with itinerary and trip planning.",
llm_config={
"config_list": config_list,
"cache_seed": None,
"functions": [
SEARCH_GOOGLE_MAPS_SCHEMA,
]
},
function_map={
"search_google_maps": search_google_maps
}
)
4.3 User Proxy
然后,创建User Proxy。尽管User Proxy在该代理系统中没有发挥积极作用,但它对于接受用户消息和在向用户发送响应之前检测何时结束对用户查询的回复序列至关重要。
def terminate_agent_at_reply(
recipient: Agent,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
config: Optional[Any] = None,
) -> Tuple[bool, Union[str, None]]:
return True, None
user_proxy = UserProxyAgent(
"user_proxy",
is_termination_msg=lambda x: "TERMINATE" in x.get("content", ""),
human_input_mode="NEVER",
code_execution_config=False
)
user_proxy.register_reply([Agent, None], terminate_agent_at_reply)
我已经向User Proxy注册了一个新的回复函数,它只返回True和None输出。要了解如何结束聊天序列的,必须了解Autogen是如何使用回复功能的。
当代理生成回复时,Autogen依赖于注册到代理的回复函数列表。它接受这个列表中的第一个函数,如果它能生成最终的回复,则返回True,reply。如果返回False,表示函数无法生成回复,将转移到列表中的下一个函数。
Autogen支持代理不同的回复方式,例如请求人工反馈、执行代码、执行函数或生成LLM回复。
当我将terminate_agent_at_reply注册为回复函数时,它会被添加到此列表的开头,并成为第一个被调用的回复函数。由于默认情况下返回True、None,这将阻止用户代理发送自动回复或使用其他回复功能生成LLM回复。使用“None”作为回复会阻止群聊继续进行更多的聊天回合。
4.4 Group chat
最后,我将创建允许所有这些代理进行协作的群聊和管理代理。
group_chat = GroupChat(
agents=[self.user_proxy, self.location_researcher, self.tour_agent],
messages=[],
allow_repeat_speaker=False,
max_round=20
)
group_chat_manager = GroupChatManager(
self.group_chat,
is_termination_msg=lambda x: "TERMINATE" in x.get("content", ""),
llm_config={
"config_list": config_list,
"cache_seed": None
}
)
在这里,我允许群聊使用默认的发言人选择方法,auto,因为这个用例没有其他合适的选项。我还设置了聊天管理器的is_terminate_msg参数,以检查消息内容中是否存在terminate。
那么,当我已经为用户代理使用了以前的terminate_at_agent_reply函数时,为什么我要在这里设置另一个终止条件呢?
如果LLM在Tour agent的最终回答后选择用户代理以外的代理作为下一个发言人,它应该起到故障保护的作用。
4.5 把上述功能合并到一个class里
现在,我可以把所有这些代理逻辑放在一个类中,还介绍了一种方法来接受来自API的用户消息,并在回复序列之后发送最终回复。
import os
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager, Agent
from typing import Optional, List, Dict, Any, Union, Callable, Literal, Tuple
from dotenv import load_dotenv
from functions import search_google_maps, SEARCH_GOOGLE_MAPS_SCHEMA
load_dotenv()
config_list = [{
'model': 'gpt-3.5-turbo-1106',
'api_key': os.getenv("OPENAI_API_KEY"),
}]
class AgentGroup:
def __init__(self):
self.user_proxy = UserProxyAgent(
"user_proxy",
is_termination_msg=lambda x: "TERMINATE" in x.get("content", ""),
human_input_mode="NEVER",
code_execution_config=False
)
self.user_proxy.register_reply([Agent, None], AgentGroup.terminate_agent_at_reply)
self.location_researcher = AssistantAgent(
"location_researcher",
human_input_mode="NEVER",
system_message="You are the location researcher who is helping the Tour Agent plan a trip according to user requirements. You can use the `search_google_maps` function to retrieve details about a certain location, attractions, restaurants, accommodation, etc. for your research. You process results from these functions and present your findings to the Tour Agent to help them with itinerary and trip planning.",
llm_config={
"config_list": config_list,
"cache_seed": None,
"functions": [
SEARCH_GOOGLE_MAPS_SCHEMA,
]
},
function_map={
"search_google_maps": search_google_maps
}
)
self.tour_agent = AssistantAgent(
"tour_agent",
human_input_mode="NEVER",
llm_config={
"config_list": config_list,
"cache_seed": None
},
system_message="You are a Tour Agent who helps users plan a trip based on user requirements. You can get help from the Location Researcher to research and find details about a certain location, attractions, restaurants, accommodation, etc. You use those details a answer user questions, create trip itineraries, make recommendations with practical logistics according to the user's requirements. Report the final answer when you have finalized it. Add TERMINATE to the end of this report."
)
self.group_chat = GroupChat(
agents=[self.user_proxy, self.location_researcher, self.tour_agent],
messages=[],
allow_repeat_speaker=False,
max_round=20
)
self.group_chat_manager = GroupChatManager(
self.group_chat,
is_termination_msg=lambda x: "TERMINATE" in x.get("content", ""),
llm_config={
"config_list": config_list,
"cache_seed": None
}
)
def process_user_message(self, message: str) -> str:
self.user_proxy.initiate_chat(self.group_chat_manager, message=message, clear_history=False)
return self._find_last_non_empty_message()
def _find_last_non_empty_message(self) -> str:
conversation = self.tour_agent.chat_messages[self.group_chat_manager]
for i in range(len(conversation) - 1, -1, -1):
if conversation[i].get("role") == "assistant":
reply = conversation[i].get("content", "").strip()
reply = reply.replace("TERMINATE", "")
if reply:
return reply
return "No reply received"
@staticmethod
def terminate_agent_at_reply(
recipient: Agent,
messages: Optional[List[Dict]] = None,
sender: Optional[Agent] = None,
config: Optional[Any] = None,
) -> Tuple[bool, Union[str, None]]:
return True, None
这里,每当代理组接收到用户消息时,用户代理都会启动与组管理器的聊天,其中clear_history=False,可以保留以前回复序列的历史记录。
回复序列结束后,find_last_non_empty_message会从聊天记录中找到旅行社代理发送的最后一条非空消息,并将其作为答案返回。该函数在寻找应该返回答案时,会考虑到与代理回复和回复序列的一些不一致性。
4.6 API endpoint
现在,我将创建API端点来接收FastAPI用户查询。
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Dict
from agent_group import AgentGroup
class ChatRequest(BaseModel):
session_id: str
message: str
app = FastAPI()
sessions: Dict[str, AgentGroup] = {}
@app.post("/chat")
def chat(request: ChatRequest):
session_id = request.session_id
message = request.message
if session_id not in sessions.keys():
sessions[session_id] = AgentGroup()
agent_group = sessions[session_id]
reply = agent_group.process_user_message(message)
return {"reply": reply, "status": "success"}
4.7 函数调用
最后,回过头来处理在前一步中遗漏的内容:Location Researcher使用的函数调用的模式和实现。
import os
from serpapi import GoogleSearch
from dotenv import load_dotenv
from typing import Dict
load_dotenv()
SEARCH_GOOGLE_MAPS_SCHEMA = {
"name": "search_google_maps",
"description": "Search google maps using Google Maps API",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A concise search query for searching places on Google Maps"
}
},
"required": ["query"]
}
}
def search_google_maps(query):
params = {
"engine": "google_maps",
"q": query,
"type": "search",
"api_key": os.getenv("SERP_API_KEY")
}
results = _search(params)
results = results["local_results"]
top_results = results[:10] if len(results) > 10 else results
data = []
for place in top_results:
data.append(_populate_place_data(place["place_id"]))
return data
def _populate_place_data(place_id: str):
params = {
"engine": "google_maps",
"type": "place",
"place_id": place_id,
"api_key": os.getenv("SERP_API_KEY")
}
data = _search(params)
return _prepare_place_data(data["place_results"])
def _prepare_place_data(place: Dict):
return {
"name": place.get("title"),
"rating": place.get("rating"),
"price": place.get("price"),
"type": place.get("type"),
"address": place.get("address"),
"phone": place.get("phone"),
"website": place.get("website"),
"description": place.get("description"),
"operating_hours": place.get("operating_hours"),
"amenities": place.get("amenities"),
"service_options": place.get("service_options")
}
def _search(params: Dict[str, str]):
search = GoogleSearch(params)
results = search.get_dict()
return results
search_google_maps函数获取代理提交的搜索查询,并将其发送到SERP的google maps API。然后,它使用位置ID检索结果中前10个位置的更多详细信息。最后,它使用这些详细信息创建一个简化的对象并将其发送回。
4.8 测试
终于到了运行这个应用程序的时候了,看看它的效果如何。
我正在尝试一个例子,用户发送一个请求来计划去巴厘岛旅行的行程。
Create a week long itinerary to Ubud, Bali for myself for May 2024. I’m going solo and I love exploring nature and going on hikes and activities like that. I have a mid-level budget and I want the itinerary to be relaxing not too packed.
旅行社代理考虑了这一请求,并多次联系位置研究员,以获取岛上景点、餐厅和住宿选择的详细信息。
旅行社代理根据这些信息提供最终答案。然后,用户对这个答案给出更多的反馈,使用更多的请求继续进行聊天。
五、改进和定制
在上述的Tour Agent系统中,成功地配置了Autogen,以更好地适应基于API的应用程序,在该应用程序中,内部代理通信对用户是隐蔽的。然而,它仍然有一些缺点和不可预测的行为。
例如:
- 我假设Tour Agent已经收集了所有所需的详细信息,并在用户代理被选为下一位发言人时完成了对用户查询的最终回答。考虑到使用LLM来选择下一个说话者的不可预测性,情况可能并不总是如此。
- 之所以选择订单代理,是因为发言人并不总是一致的。有时,在用户查询后,位置研究员会被选为第一个发言人,而不是旅行社代理,等等。
尽管这些缺点对于这样的用例来说并不重要,但对于另一个场景来说可能会有所不同。随着群聊中代理数量的增加,这些问题也变得更加明显。
但是,如果你想为代理群聊带来更多的可预测性和一致性,仍然有一些方法可以定制Autogen的内置行为。
5.1 修改群聊发言人选择提示
Autogen GroupChat类包含一个select_speaker_msg方法,可以覆盖该方法以指定如何管理发言人选择。
这是原始提示(供参考)。
def select_speaker_msg(self, agents: List[Agent]) -> str:
"""Return the system message for selecting the next speaker. This is always the *first* message in the context."""
return f"""You are in a role play game. The following roles are available:
{self._participant_roles(agents)}.
Read the following conversation.
Then select the next role from {[agent.name for agent in agents]} to play. Only return the role."""
您可以更新此信息,具体说明在何种情况下选择哪种代理,以使演讲者选择更加一致。
5.2 使用图模型强制执行更严格的说话者转移路径
此笔记本[2]显示了如何将所有允许的说话者转换路径定义为有向图,并覆盖GroupChat的select_speaker方法来强制执行该行为。例如,对于这个用例,可以创建这样一个简单的图。
这些只是几个例子。最终,适合您的解决方案将取决于您的特定需求。你还必须在提示和路由方面发挥创意,以确保所有代理的行为都符合你的意愿。
参考文献:
[1] https://levelup.gitconnected.com/harnessing-the-power-of-autogen-multi-agent-systems-via-api-integration-edb0b9651608
[2] https://github.com/microsoft/autogen/blob/main/notebook/agentchat_graph_modelling_language_using_select_speaker.ipynb