用三种不同框架构建的同一款带有代理协调功能的财务应用程序之间的深入比较。
由ChatGPT生成的图片。提示为:多代理合作系统。
我们将要讨论的内容- 什么是代理? 我们深入探讨代理的定义及其与AI管道和独立的大规模语言模型(LLM)的区别。
- 使用3个流行的代理框架构建的实际示例:LangGraph、CrewAI 和 OpenAI Swarm,完整代码见这里。
- 我们何时该使用哪种框架的建议。
- 接下来的期待: Part II 的预览,我们将探讨这些框架的调试性和可观测性。
简介
由LLM驱动的自主代理经历了起起落落。从2023年AutoGPT和BabyAGI的火爆演示,到如今更成熟的框架和系统,能自主完成端到端任务的代理这一概念既激发了人们的想象力,也引起了人们的质疑。
为什么会有重新燃起的兴趣?在过去的9个月里,大规模语言模型(LLMs)有了显著的进步:更长的上下文长度、结构化的输出、更出色的推理能力以及简单的工具接入。这些进步使得构建可靠的代理应用比任何时候都更可行。
在这次博客文章里,我们将探讨 三个流行的框架 用于构建代理型应用:LangGraph , CrewAI ,和 OpenAI Swarm。通过一个动手实践的示例,我们将展示每个框架的优势和劣势,以及它们的实际应用场景。
代理是什么?代理是由大型语言模型(LLM)驱动的高级系统,能够自主地与其环境互动并实时做出决定。与传统的LLM应用结构化为僵化的预定义管道(例如,A → B → C)不同,代理工作流程引入了一种动态和适应性方法。代理利用工具——允许与环境互动的功能或API——根据上下文和目标决定接下来的行动。这种灵活性使代理能够脱离固定流程,从而实现更自主和高效的流程,适应复杂和不断变化的任务。
不过,这种灵活也带来了一些挑战:
- 管理跨任务的状态和内存资源
- 协调多个子代理及其通信方式
- 确保工具调用的可靠性并妥善处理可能出现的复杂错误情况
- 在大规模情况下处理推理和决策
从零开始构建代理程序并不是一件容易的事。像 LangGraph、CrewAI 和 OpenAI Swarm 这样的框架简化了这个过程,让开发者可以专注于应用的逻辑,而不是重新创造这些领域的轮子,比如状态管理、编排和工具集成。
代理框架的核心在于
- 一种简单的方式来定义代理和工具
- 一个编排机制
- 状态管理
- 额外的工具,以支持更复杂的应用,如:持久层、异常处理等
我们将在接下来的部分中逐一讲解这些内容。
让我们来看看代理框架我们选择了LangGraph、CrewAI和OpenAI Swarm,因为它们代表了代理开发领域的最新思想潮流。这里做一下简要介绍。
- LangGraph: 如其名称所示,LangGraph 使用图架构作为定义和编排代理工作流程的最佳方式。与早期版本的 LangChain 不同,LangGraph 是一个设计完善的框架,具有许多稳健且可定制的功能,适用于生产环境。然而,对于某些用例来说,它有时会比实际需要的更复杂,并会增加额外的负担。
- CrewAI: 相比之下,CrewAI 更容易上手。它提供直观的抽象,帮助你专注于任务设计,而不是编写复杂的编排和状态管理逻辑。虽然它是一个高度有倾向性的框架,后期定制可能会更困难。
- OpenAI Swarm: 一个轻量级、极简的框架,OpenAI 描述它为“教育用途”而非“生产就绪”。OpenAI Swarm 几乎不提供任何框架,许多功能需要开发者自行实现或由强大的 LLM 自行解决。我们认为它可能非常适合那些目前拥有简单用例的人,或希望将敏捷代理工作流程集成到现有 LLM 管道中的人。
截至2024年11月为止的GitHub Stars数量对比。
其他值得一提的框架
- LlamaIndex 工作流程: 一个基于事件驱动的框架,在概念上非常适合多种代理工作流程。然而,我们发现它仍需开发人员编写大量样板代码才能正常使用。LlamaIndex 团队正在积极改进工作流框架,希望他们尽快开发更高层次的抽象。
- AutoGen: 微软开发的多代理对话编排框架,已被用于各种代理应用场景。吸取了早期版本的教训和用户反馈,AutoGen 团队正在进行从 v0.2 到 v0.4 的全面重写,转变为一个基于事件驱动的编排框架。
为了对这些框架进行基准测试,我们制作了一个相同的代理金融助手,并使用每个框架构建。完整代码可在Relari Agent Examples获取。
让代理处理复杂的查询,例如:
- Spirit Airline的财务状况与竞争对手相比如何?
- 苹果公司哪条产品线在财务上表现最好?他们在网站上宣传什么?
-
请帮我找到一些市值低于50亿美元且年同比增长率超过20%的消费类股票
这里是一个例子,我们希望助理能够给出的完整回答。
Relari 提供的图片:示例回复截图(Agentic Finance Assistant)
为了完成这些任务,我们通过FMP API让代理系统可以访问金融数据库,并通过互联网帮助代理系统查找相关信息。
在开发代理型AI应用时,我们首先需要选择的是架构。有多种架构,每个架构都有自己的优缺点。在下面这张图中,列出了几种流行的架构,这些都是LangGraph列出的(您可以在那里了解更多关于架构选择的信息:多代理架构)。
LangChain提供的图片:多代理架构
我们选择了Supervisor架构作为这个应用的学习目的。所以我们将创建一个Supervisor代理,其职责是决定把任务交给哪个子代理,以及三个具有工具访问权限的子代理:一个财务数据代理,一个网络搜索代理,和一个总结器代理。
图片由 Relari 提供。这是 Agentic Finance Assistant 的架构。
让我们来了解一下每个框架在创建代理、工具集成、协作、记忆存储和人机互动方面分别是怎么做的。
1. 定义代理和工具这两个概念我们首先看看如何定义这些常规代理,如金融数据代理、网络研究代理和输出汇总代理,并在每个框架中声明其关联的工具。监督代理是一个特殊的代理,它起到协调作用,因此我们将在协调部分详细介绍。
语言图谱 (LangGraph)创建一个简单的工具调用代理程序最简单的方法是使用预构建的 create_react_agent
函数,如下,我们可以在其中提供此代理将要使用的工具和提示。
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# 以下是一个工具定义的示例
@tool
def get_stock_price(symbol: str) -> dict:
"""获取给定股票代码的当前股价。
参数:
symbol (str): 股票代码(例如,"AAPL" 表示苹果公司)。
返回:
dict: 包含该股票价格或错误信息的字典。
"""
base_url = "https://financialmodelingprep.com/api/v3/quote-short"
params = {"symbol": symbol, "apikey": os.getenv("FMP_API_KEY")}
response = requests.get(base_url, params=params)
if response.status_code == 200:
data = response.json()
if data:
return {"price": data[0]["price"]}
return {"error": "无法获取该股票价格。"}
# 以下是一个简单的 React 代理示例
financial_data_agent = create_react_agent(
ChatOpenAI(model="gpt-4o-mini"),
tools=[get_stock_price, get_company_profile, 等],
state_modifier="你是一个金融数据代理,负责使用提供的 API 工具来获取金融数据...",
)
在LangGraph中,一切都是以图的形式组织的。实用函数 create_react_agent
创建了一个简单的可执行图,其中包含代理节点(agent节点)和工具节点(tool节点)。
图片来自 Relari。一张简单的工具调用 React 代理的图。
代理充当决策者,根据情况决定使用哪些工具,并评估是否有足够的信息来过渡到__end__
。
图中,实线表示确定性的边(Tool节点总是返回给代理),而虚线表示条件性的边,这些由LLM驱动的代理正在决定下一步去哪里的方向。
节点和连接线是图的基础组成部分。在后面的章节中我们将会看到,这个图可以在一个更大、更复杂的图中作为一个节点来表示。
请提供待审译文和原文。
船员AI系统
CrewAI的代理定义围绕着智能体和任务(代理人完成的工作)之间的关系。
对于每一个代理,我们必须定义其角色
,目标
,以及背景故事
,并指定它能使用的工具。
from crewai import Agent, Task
financial_data_agent = Agent(
role="财务数据代理人",
goal="使用FMP API检索全面的财务数据,以便为用户提供回答查询所需的信息",
backstory="""简而言之,你是一位经验丰富的财务数据收集者,以精准和能够利用FMP API找到最相关的美国上市公司财务数据而闻名""",
tools=[
StockPriceTool(),
CompanyProfileTool(),
...
]
)
然后我们就需要创建一个任务,让代理去执行。这个任务需包括描述
和预期输出
。
gather_financial_data = Task(
description=("进行详细的财务研究,收集与回答用户查询相关的财务数据:{query}。使用可用的财务工具获取准确且最新的信息。尤其关注找到相关的股价、公司概况、财务比率以及其他与用户查询相关的财务指标。"),
expected_output="一套全面覆盖的财务数据,直接针对查询:{query}。",
agent=financial_data_agent,
)
这种为LLM构建提示的结构化方法提供了一个清晰且一致的框架,确保了代理和任务的定义明确,。虽然这种方法有助于保持焦点和连贯,但有时会感觉过于僵硬或冗长,特别是在需要反复定义角色、目标、背景故事和任务描述时。
工具可以使用@tool装饰器进行整合,与LangGraph中的方式类似。值得一提的是,我们也可以通过继承BaseTool类来扩展工具类,这种方法使用Pydantic模型更为强大,能够更有效地强制执行工具输入模式规范,同样得到LangGraph的支持。
class StockPriceInput(BaseModel):
"""股票价格查询的输入模式描述."""
symbol: str = Field(..., description="股票符号")
class StockPriceTool(BaseTool):
name: str = "获取股票价格"
description: str = "获取给定股票符号的当前价格"
args_schema: Type[BaseModel] = StockPriceInput
def _run(self, symbol: str) -> dict:
# 通过FMP API获取给定股票代码的最新价格由于提供的 XML 标签内没有源文本或翻译内容,无法进行评估和编辑。请提供需要分析的文本。
开放AI群
Swarm 采用了一种不同的方法:不再像以前那样在代码中明确地定义推理流程,而是 OpenAI 建议将流程结构化为系统提示中的“Routine”(即 instructions
中的预定义步骤或指令)。这很自然,因为 OpenAI 更希望开发者更多地依赖模型遵循指令的能力,而不是在代码中定义自定义逻辑集。我们认为这种方法在与更强大的 LLM 一起工作时简单且有效,这样的 LLM 能够跟踪和遵循 Routine。
工具可以直接带进来。
from swarm import Agent
financial_data_agent = Agent(
name="Financial Data Agent",
instructions="""你是一位金融数据专员,负责利用提供的API工具获取金融数据。
你的任务:
步骤1. 根据用户的需求,使用合适的工具获取相关的金融数据
步骤2. 阅读数据并确保它们能回答用户的问题。如果不能,修改工具的输入或更换其他工具获取更多信息。
步骤3. 收集到足够信息后,仅返回从工具获取的原始数据,不要添加任何评论或解释。""",
functions=[
get_stock_price,
get_company_profile,
……
]
)```
请提供需要审查和改进的文本内容。
#2. 交响乐编排
我们现在来看看每个框架究竟是怎么把多个子代理整合在一起的。
## 语言图谱
langgraph的核心是图形编排。我们首先创建监督者,它就像个路由器,唯一的工作就是分析情况并决定调用哪个下个代理。执行者们只能将结果回传给监督者。
LangGraph 需要明确地定义状态。`AgentState` 类帮助定义不同代理之间的通用状态定义。
定义一个名为AgentState的类,其中包含带有operator.add注解的BaseMessage序列作为消息列表,以及一个表示后续操作的字符串next。
对于每个智能体,我们通过将其状态封装在一个节点中来进行交互,该节点将智能体输出信息转换为一致的消息格式。
async def financial_data_node(state):
result = await financial_data_agent.ainvoke(state)
return {
"messages": [
AIMessage(
content=result["messages"][-1].content, name="Financial_Data_Agent"
)
]
}
我们现在准备好定义这个智能体了。
class RouteResponse(BaseModel):
下一步: Literal[OPTIONS]
def supervisor_agent(state):
prompt = ChatPromptTemplate.from_messages([
("system", ORCHESTRATOR_SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="messages"),
(
"system",
"根据上面的对话,接下来应该由谁行动呢?或者我们该结束了?从以下选项中选择一个选项:{options}",
),
]).partial(options=str(OPTIONS), members=", ".join(MEMBERS))
supervisor_chain = prompt | LLM.with_structured_output(RouteResponse)
return supervisor_chain.invoke(state)
定义了监督代理之后,我们将代理工作流定义为一个图结构,通过将每个代理作为节点,并将所有执行逻辑作为边。
在定义边时,我们有两种选择:**常规**边或**条件性**边。常规边用于我们希望实现确定性转换的情况。例如,金融数据代理应始终将结果返回给监督代理,以确定下一步行动。
当我们希望让LLM选择路径时(例如,Supervisor Agent 决定是否有足够的数据直接发送给 Output Summarizing Agent,还是返回 Data and Web Agents 获取更多数据),就会使用条件边(condition edge)。
```python
from langgraph.graph import END, START, StateGraph
def build_workflow() -> StateGraph:
"""构建工作流的状态图."""
workflow = StateGraph(AgentState)
workflow.add_node("Supervisor_Agent", supervisor_agent)
workflow.add_node("Financial_Data_Agent", financial_data_node)
workflow.add_node("Web_Research_Agent", web_research_node)
workflow.add_node("Output_Summarizing_Agent", output_summarizing_node)
workflow.add_edge("Financial_Data_Agent", "Supervisor_Agent")
workflow.add_edge("Web_Research_Agent", "Supervisor_Agent")
conditional_map = {
"Financial_Data_Agent": "Financial_Data_Agent",
"Web_Research_Agent": "Web_Research_Agent",
"Output_Summarizing_Agent": "Output_Summarizing_Agent",
"FINISH": "Output_Summarizing_Agent",
}
workflow.add_conditional_edges(
"Supervisor_Agent", lambda x: x["next"], conditional_map
) # 为 'Supervisor_Agent' 添加条件边
workflow.add_edge("Output_Summarizing_Agent", END) # 将 'Output_Summarizing_Agent' 与结尾连接
workflow.add_edge(START, "Supervisor_Agent") # 将开始与 'Supervisor_Agent' 连接
return workflow
以下就是图。
图片来源:Relari。展示 Agentic Finance Assistant 的图表。
船员AI:与 LangGraph 不同,CrewAI 更简化了大部分的编排工作。
supervisor_agent = Agent(
role="财务助理主管",
goal="利用同事们的技能来协助回答用户的问题: {query}.",
backstory="""你是一位监督财务助理日常工作流程的经理,擅长管理具有不同技能的复杂员工,并确保能通过同事的帮助来回答用户的问题。
你总是先尝试通过财务数据代理和/或网页抓取代理来收集数据。
收集数据后,你需要委托输出总结的代理来生成一份全面的报告,而不是直接回答用户的问题。""",
verbose=True,
llm=ChatOpenAI(model="gpt-4o", temperature=0.5),
allow_delegation=True,
)
类似于Langgraph,我们首先创建一个监督代理程序。请注意,allow_delegation
这个标志允许代理将任务委派给其他代理。
接下来我们使用 Crew
来聚集这些代理。在这里选择 Process.hierarchical
非常关键,这样可以让监督代理能够委派任务。在幕后,监督代理会将用户查询转化为具体任务,接着找到合适的代理来完成这些任务。如果不使用管理代理,而是选择 Process.sequential
,那么任务将按顺序进行,形成一个确定的过程。
finance_crew = Crew(
agents=[
financial_data_agent,
web_scraping_agent,
output_summarizing_agent
],
tasks=[
gather_financial_data,
gather_website_information,
summarize_findings
],
process=Process.hierarchical,
manager_agent=supervisor_agent,
)
OpenAI群
蜂群调度使用了一个非常简单的策略——轮换。核心思想是创建一个转移机制,利用另一个代理。
这无疑是最简洁的方法。关系通过转移函数隐含。
def transfer_to_summarizer():
return summarizing_agent
def transfer_to_web_researcher():
return web_researcher_agent
def transfer_to_financial_data_agent():
return financial_data_agent
supervisor_agent = Agent(
name="Supervisor",
instructions="""你是一个主管代理,负责协调金融数据代理、网络调研代理和摘要代理。
你的任务:
1. 根据用户查询的内容决定将任务委托给哪个代理
2. 如果用户的查询需要金融数据,委托给金融数据代理
3. 如果用户的查询需要网络调研,委托给网络调研代理
4. 如果有足够信息来回答用户的查询,委托给摘要代理提供最终输出。
永远不要自己总结数据。始终委托给摘要代理来提供最终输出。
""",
functions=[ # Agent可用的工具
transfer_to_financial_data_agent,
transfer_to_web_researcher,
transfer_to_summarizer
]
)
这种方法的一个问题是,随着应用规模的扩大,各代理间的依赖关系越来越难以追踪。
3. 回忆记忆是带有状态的代理系统中的一个关键组成部分。我们可以将记忆分为两个层次:
- 短期记忆使代理能够支持多轮/多步骤操作,
- 长期记忆使代理能够学习并记住不同会话中的偏好。
这个话题可能会变得非常复杂,但让我们来看一看每个框架中提供的最简单的内存编排。
语言图谱LangGraph 区分 (单线程) 内的记忆和 (跨线程) 的记忆,其中单线程指的是单个对话线程内的记忆,而跨线程指的是跨多个对话线程的记忆。
为了节省会话内存,LangGraph 提供了 MemorySaver() 类来将图的状态及对话历史保存到 checkpointer
中。
从langgraph.checkpoint.memory导入MemorySaver
def 构建():
"""构建并编译工作流."""
内存 = MemorySaver()
工作流 = build_workflow()
# 构建并编译工作流
return 工作流.compile(checkpointer=内存)
为了将代理执行与内存线程(memory thread)关联,传递一个带有 thread_id
的配置。这会指示代理使用哪个线程的内存检查点。例如:
config = {"configurable": {"thread_id": "1"}} # 初始化配置
app = build_app() # 构建应用
await run(app, input, config) # 启动应用并传入输入和配置
为了节省线程间的内存,LangGraph 让我们可以把内存存到 JSON 数据存储里。
from langgraph.store.memory import InMemoryStore
store = InMemoryStore() # 这在生产环境中可以是一个由数据库支持的存储
user_id = "user_0"
store.put(
user_id,
"current_portfolio",
{
"portfolio": ["TSLA", "AAPL", "GOOG"],
}
)
CrewAI(船员AI系统)
果然,CrewAI 采取了一种更简单但更死板的方法。开发人员只需将 '记忆' 参数设置为 true。
finance_crew = Crew(
agents=[financial_data_agent, web_researcher_agent, 摘要代理],
tasks=[gather_financial_data, gather_website_information, 汇总发现],
process=Process.hierarchical,
manager_agent=主管代理,
memory=True, # 会在 "CREWAI_STORAGE_DIR" 文件夹中创建记忆数据库
verbose=True, # 开启详细输出,以便记忆功能运作
)
它背后做的事情非常复杂,它创建了几个不同类型的内存存储。
- 短期记忆:它使用OpenAI嵌入创建一个ChromaDB向量存储,来记录代理执行的历史。
- 最近的记忆:使用SQLite3数据库存储最近的任务执行结果。
- 长期记忆:使用SQLite3数据库存储任务结果。需要注意的是,为了检索长期记忆,任务描述必须完全匹配(相当严格)。
- 实体记忆:提取关键实体,并将实体关系存储到另一个ChromaDB向量存储中。
图片由 Relari 制作。CrewAI 自动生成的内存存储数据库系统。
开放AI团队Swarm 使用一个简单的无状态设计,自身并没有内置任何记忆功能。OpenAI 对记忆的看法的一个例子可以在其有状态的 Assistant API 中找到。每个对话都有一个 thread_id 用于短期记忆,而每个助手则有一个 assistant_id 用于长期记忆。
也可以引入第三方内存层提供商,例如mem0这样的提供商,或者实现我们自己的短期和长期内存层功能。
4. 人机协作尽管我们希望代理能够自己做决定,但实际上很多代理是设计来与人类互动的。
例如,客服代理可以在整个流程的各个阶段向用户询问信息。此外,人类也可以充当审计员或引导者,从而使合作更加顺畅。
LangGraph[语言图谱 (yǔyán túpǔ)]如果我们希望在摘要生成器构建最终输出之前添加一个人工干预点,LangGraph 允许我们在图中设置断点位置,如下面的例子所示。
工作流编译(检查点=检查点, interrupt_before=[输出总结代理])
图形会一直执行直到遇到断点。然后我们可以添加一步,在继续执行图之前获取用户的输入。
# 运行图直到第一次中断或结束
for event in graph.stream(initial_input, thread, stream_mode="values"):
print(event)
try:
user_approval = input("您想继续进入输出汇总器吗?(yes/no): ")
except Exception as e:
user_approval = "yes"
if user_approval.lower() == "yes":
# 如果获得批准,继续图的执行
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
else:
print("操作已由用户取消。")
CrewAI
CrewAI 让人类在初始化智能体时设置 human_input=True
即可,从而提供反馈。
图片由 Relari 绘制。消息由 CrewAI 生成,请求人类提供输入。
执行完后,代理会暂停并请用户提供对其行为和结果的自然语言反馈(见下)。
不过,它不支持更多的人工干预互动。
开放AI群Swarm 并没有内置任何人类干预的功能。然而,最简单的方法是在执行过程中添加人类干预,即将人类作为工具或作为 AI 代理可以传递的任务。
功能差异要点总结一下,我们在三个不同框架下构建相同的应用程序的发现的。
Relari 制作的图片。功能对比概述。
我们的推荐款我们将推荐总结成流程图,以帮助您决定从哪个框架开始入门,从而更好地作出选择。
图片由 Relari 制作。代理框架的决策树。
接下来会是什么呢?
这篇博客文章专注于构建第一个版本的智能代理。这些智能代理根本不是高性能或可靠的。
下一个博客将专注于改进和迭代这些代理程序,这通常构成了AI系统生产中的大部分工作。
- 定义成功标准和指标
- 进行使用不同框架构建的代理的性能基准测试
- 深入探究质量和可靠性
- 针对提示、工具、推理和架构进行有针对性的改进
我们将使用 Relari 最新开发的一些最新代理评价、模拟和可观测性工具,深入研究如何将这些代理转变为可投入生产的系统。
别错过,快来关注我们哦!
共同学习,写下你的评论
评论加载中...
作者其他优质文章