本文分为两个部分,两天内完成。
1 引言
随着人工智能领域的快速发展,智能体技术正逐渐成熟,有望在2024年实现自主系统在处理电子邮件、预订航班、数据管理等任务上的广泛应用。然而,要实现这一目标,我们仍需克服诸多挑战。开发者在构建智能体时,不仅要选择合适的模型和用例,还要决定使用哪个框架。是选择经过时间考验的LangGraph,还是尝试新兴的LlamaIndex工作流?或者,是否应该坚持传统,自行编写代码?
本文旨在简化这一决策过程。在过去几周,我对比了多个主流框架,并在每个框架中构建了相同的智能体,以便从技术角度评估它们的优缺点。
2 智能体测试背景
用于测试的智能体包括函数调用、工具或技能的集成、与外部资源的交互以及共享状态或内存的管理。
智能体具备以下功能:
从知识库中检索并回答问题。
与数据对话:针对LLM应用程序遥测数据提供答案。
数据分析:识别并分析遥测数据中的高级趋势和模式。
为实现这些功能,智能体配备了三项基础技能:使用产品文档的RAG、在跟踪数据库上生成SQL查询以及进行数据分析。智能体的用户界面采用简单的渐进式驱动设计,其本身被构建为一个聊天机器人。
3 基于代码的智能体(无框架)
在开发智能体时,一个可行的选择是完全绕过现有的框架,从头开始构建。这正是我在项目初期所采取的方法。
3.1 纯代码架构(Pure Code Architecture)
我构建的基于代码的智能体核心是一个由OpenAI驱动的路由器,它通过函数调用来决定使用哪种技能。一旦技能执行完毕,它会将控制权交回路由器,以便调用下一个技能或向用户返回响应。
智能体维护了一个持续更新的消息和响应列表,每次调用时都将这些信息传递给路由器,以保持对话的上下文连贯性。
# 定义路由器函数,用于处理消息并调用相应的技能
def router(messages):
# 检查消息列表中是否包含系统提示,如果没有则添加
if not any(
isinstance(message, dict) and message.get("role") == "system" for message in messages
):
system_prompt = {"role": "system", "content": SYSTEM_PROMPT}
messages.append(system_prompt)
# 使用OpenAI客户端发送消息并获取响应
response = client.chat.completions.create(
model="gpt-4", # 使用GPT-4模型
messages=messages, # 传递消息列表
tools=skill_map.get_combined_function_description_for_openai(), # 获取技能描述
)
# 将响应消息添加到消息列表中
messages.append(response.choices[0].message)
# 检查响应中是否包含工具调用
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
# 处理工具调用并递归调用路由器
handle_tool_calls(tool_calls, messages)
return router(messages)
else:
# 返回最终的响应内容
return response.choices[0].message.content
技能被定义在自己的类中(例如GenerateSQLQuery
),这些类被统一管理在SkillMap
中。路由器仅与SkillMap
交互,通过它来加载技能的名称、描述和可调用的函数。这种方法的设计理念是,向智能体添加新技能就像编写一个新的类并将其添加到SkillMap
的技能列表中一样简单,而无需修改路由器的代码。
# 定义SkillMap类,用于管理技能
class SkillMap:
def __init__(self):
# 初始化技能列表
skills = [AnalyzeData(), GenerateSQLQuery()]
self.skill_map = {}
# 将技能添加到技能映射中
for skill in skills:
self.skill_map[skill.get_function_name()] = (
skill.get_function_dict(),
skill.get_function_callable(),
)
def get_function_callable_by_name(self, skill_name) -> Callable:
# 根据技能名称获取可调用的函数
return self.skill_map[skill_name][1]
def get_combined_function_description_for_openai(self):
# 合并所有技能的描述,用于OpenAI客户端
combined_dict = []
for _, (function_dict, _) in self.skill_map.items():
combined_dict.append(function_dict)
return combined_dict
def get_function_list(self):
# 获取所有技能的名称列表
return list(self.skill_map.keys())
def get_list_of_function_callables(self):
# 获取所有技能的可调用函数列表
return [skill[1] for skill in self.skill_map.values()]
def get_function_description_by_name(self, skill_name):
# 根据技能名称获取技能描述
return str(self.skill_map[skill_name][0]["function"])
纯代码智能体的挑战与优势
挑战:
构建路由器系统提示是一个复杂的过程。例如,上述示例中的路由器需要自己生成SQL,而不是委托给特定的技能,这可能导致多轮调试的需要。
处理每个步骤的不同输出格式也是一个挑战。由于没有采用结构化输出,必须为路由器和技能中每个LLM调用的多种不同格式做好准备。
优势:
基于代码的方法提供了一个清晰的起点,有助于理解智能体的工作原理,而不必依赖于框架提供的现成教程。
尽管引导LLM的行为可能具有挑战性,但代码结构本身足够简单,易于使用,并且可能对某些特定用例非常有意义。(更多细节请参见下文的分析部分。)