介绍
由 LLMs经历了起起落落。从 2023 年 AutoGPT 和 BabyAGI 的病毒式演示到今天更精致的框架,AI Agent的概念——LLMs自主执行端到端任务的 LLM——既引起了人们的想象力,也引起了怀疑。
为什么重新引起人们的兴趣?LLMs 在过去 9 个月中进行了重大升级:更长的上下文窗口、结构化的输出、更好的推理和简单的工具集成。这些进步使构建可靠的AI Agent应用程序比以往任何时候都更加可行。
在这篇博客中,我们将探讨用于构建代理应用程序的三个流行框架:LangGraph、CrewAI 和 OpenAI Swarm。通过使用 Agentic Finance Assistant 的动手示例,我们将重点介绍每个框架的优势、劣势和实际用例。
什么是AI Agent
AI Agent是由大型语言模型 (LLMs,可以独立地与其环境交互并实时做出决策。与传统的 LLM的结构是刚性的预定义管道(例如 A → B → C),AI Agent工作流引入了一种动态和自适应的方法。AI Agent利用工具(支持与环境交互的功能或 API)根据上下文和目标决定下一步。这种灵活性使代理能够偏离固定序列,从而实现更自主、更高效的工作流程,以适应复杂和不断发展的任务。
但是,这种灵活性也带来了一系列挑战:
跨任务管理状态和内存。
编排多个子AI Agent及其通信架构。
确保工具调用可靠,并在出现复杂错误情况时进行处理。
大规模处理推理和决策。
为什么我们需要AI Agent 框架
从头开始构建AI Agent并非易事。LangGraph、CrewAI 和 OpenAI Swarm 等框架简化了流程,使开发人员能够专注于他们的应用程序逻辑,而不是重新发明状态管理、编排和工具集成的轮子。
在其核心,代理框架提供
- 定义代理和工具的简单方法
- 编排机制
- 状态管理
- 支持更复杂应用程序的其他工具,例如:持久层(内存)、中断等
我们将在以下部分中逐一介绍这些。
代理框架简介
我们选择了 LangGraph、CrewAI 和 OpenAI Swarm,因为它们代表了代理开发的最新思想流派。下面是一个快速概述:
LangGraph
LangGraph :顾名思义,LangGraph 将图形架构视为定义和编排代理工作流的最佳方式。与早期版本的 LangChain 不同,LangGraph 是一个设计精良的框架,具有许多为生产环境构建的健壮且可定制的功能。但是,对于某些使用案例,它有时比所需的要复杂得多,并且可能会产生额外的开销。
CrewAI
相比之下,CrewAI 的入门要简单得多。它带有直观的抽象,可帮助您专注于任务设计,而不是编写复杂的编排和状态管理逻辑。但是,权衡是它是一个高度固执己见的框架,并且以后更难定制。
OpenAI Swarm
OpenAI 将一个轻量级、极简主义框架描述为“教育性”而不是“生产就绪”。OpenAI Swarm 几乎代表了一个“反框架”——将许多功能留给开发人员来实现,或者让强大的 LLMs 自己弄清楚。我们相信,它可能非常适合那些目前使用案例简单的人,或者那些希望将灵活的代理工作流程集成到现有 LLM。
其他值得注意的框架
LlamaIndex 工作流
一个事件驱动框架,在概念上非常适合许多代理工作流。但是,就目前而言,我们发现它仍然需要开发人员编写大量样板代码才能使其正常工作。LlamaIndex 团队正在积极改进 Workflow 框架,我们希望他们尽快创建更多高级抽象。
AutoGen
AutoGen 是 Microsoft 为多代理对话编排开发的框架,已被用于各种代理用例。从早期版本的错误和反馈中吸取教训,AutoGen 团队正在将 v0.2 完全重写(从 v0.2 重写到 v0.4)到事件驱动的编排框架。
构建财务助理 AI Agent
为了对这些框架进行基准测试,我们利用每个框架构建了相同的代理财务助手。此处提供了所构建应用程序的完整代码:Relari 代理示例。
我们希望AI Agent处理复杂的查询,例如:
- Spirit Airlines 的财务状况与竞争对手相比如何?
- 从财务角度来看,Apple 表现最好的产品线是什么?他们在他们的网站上营销什么?
- 找到一些市值低于 50 亿美元但收入同比增长超过 20% 的消费类股票
为了实现这些目标,我们通过 FMP API 为AI Agent系统提供对金融数据库的访问,并为研究 Internet 内容提供 Internet 访问。
在构建AI Agent 应用程序时,我们首先需要采取的选择之一是架构。有几种架构,每种架构都有其优点和缺点。在下图中,LangGraph 总结了一些流行的架构(您可以在此处阅读有关架构选择的更多信息:多代理架构)。
我们为此应用程序选择了 Supervisor 架构,用于教育目的。因此,我们将创建一个 Supervisor Agent,其任务是决定将哪个子代理委派给任务,以及三个具有工具访问权限的子AI Agent:一个金融数据AI Agent、一个 Web 研究AI Agent和一个摘要AI Agent。
让我们探讨一下每个框架如何处理AI Agent创建、工具集成、编排、内存和人机交互。
1. 定义代理和工具
我们首先看看如何定义常规AI Agent,如 Financial Data Agent、Web Research Agent 和 Output Summarizing Agent,并在每个框架中声明其相关工具。Supervisor Agent 是一个扮演编排角色的特殊代理,因此我们将在 Orchestration 部分中介绍。
LangGraph
创建简单的工具调用Agent的最简单方法是使用预构建的 create_react_agent 函数,如下所示,我们可以在其中提供我们希望此代理操作的工具和提示。
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# Below is one example of a tool definition
@tool
def get_stock_price(symbol: str) -> dict:
"""Fetch the current stock price for a given symbol.
Args:
symbol (str): The stock ticker symbol (e.g., "AAPL" for Apple Inc.).
Returns:
dict: A dictionary containing the stock price or an error message.
"""
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": "Unable to fetch stock price."}
# Below is one example of a simple react agent
financial_data_agent = create_react_agent(
ChatOpenAI(model="gpt-4o-mini"),
tools=[get_stock_price, get_company_profile, ...],
state_modifier="You are a financial data agent responsible for retrieving financial data using the provided API tools ...",
)
在 LangGraph 中,一切都被结构化为一个图形。实用程序函数create_react_agent创建一个简单的可执行图形,其中包含 agent 节点和 tool 节点。
代理充当决策者,动态确定要调用哪些工具,并评估它是否有足够的信息来转换为 end 状态。
在图中,实线表示确定性边缘(Tool 节点必须始终返回到代理),而虚线表示条件边缘,其中 LLM 驱动的Agent正在决定下一步的去向。
节点和边是图的基础构建块。我们稍后将在编排部分看到,此图可以表示为更大、更复杂的图中的节点。
CrewAI
CrewAI 的代理定义以代理和任务之间的关系(代理应该完成什么)为中心。
对于每个代理,我们必须定义其角色、目标和背景故事,并指定它有权访问的工具。
from crewai import Agent, Task
financial_data_agent = Agent(
role="Financial Data Agent",
goal="Retrieve comprehensive financial data using FMP API that provide the data needed to answer the user's query",
backstory="""You're a seasoned financial data gatherer with extensive experience in
gathering financial information. Known for your precision
and ability to find the most relevant financial data points using
FMP API that provides financial data on public companies in the US""",
tools=[
StockPriceTool(),
CompanyProfileTool(),
...
]
)
然后我们必须创建需要AI Agent执行的任务。Task 必须包含描述和expected_output。
gather_financial_data = Task(
description=("Conduct thorough financial research to gather relevant financial data that can help "
"answer the user query: {query}. Use the available financial tools to fetch accurate "
"and up-to-date information. Focus on finding relevant stock prices, company profiles, "
"financial ratios, and other pertinent financial metrics that answer the user's query: {query}."),
expected_output="A comprehensive set of financial data points that directly address the query: {query}.",
agent=financial_data_agent,
)
这种为 LLMs提供了一个清晰一致的框架,确保代理和任务定义明确。虽然这种方法有助于保持重点和连贯性,但有时会让人感觉僵硬或重复,尤其是在重复定义角色、目标、背景故事和任务描述时。
可以使用 @tool 装饰器集成工具,类似于 LangGraph 中的方法。值得一提的是,或者,我们可以扩展 BaseTool 类,这将是一种更健壮的强制工具输入模式方法,这要归功于 Pydantic 模型的使用(LangGraph 也支持这种方法)。
class StockPriceInput(BaseModel):
"""Input schema for stock price queries."""
symbol: str = Field(..., description="The stock ticker symbol")
class StockPriceTool(BaseTool):
name: str = "Get Stock Price"
description: str = "Fetch the current stock price for a given symbol"
args_schema: Type[BaseModel] = StockPriceInput
def _run(self, symbol: str) -> dict:
# Use FMP API to fetch the stock price of the given symbol
OpenAI Swarm
Swarm 采取了不同的方法:OpenAI 建议将系统提示中的流程构建为“例程”(在指令中),代理为完成任务所遵循的预定义步骤或指令集,而不是在代码中明确定义推理流程。这是可以理解的,因为他们更希望开发人员更多地依赖遵循指令的模型能力,而不是在代码中定义自定义逻辑集。我们发现这种方法在使用更强的 LLM,它能够跟踪 Routines 的推理。
对于工具,我们可以直接作为工具引入。
from swarm import Agent
financial_data_agent = Agent(
name="Financial Data Agent",
instructions="""You are a financial data specialist responsible for retrieving financial data using the provided API tools.
Your tasks:
Step 1. Given a user query, use the appropriate tool to fetch relevant financial data
Step 2. Read the data and make sure they can answer the user query. If not, modify the tool input or use different tools to get more information.
Step 3. Once you have gathered enough information, return only the raw data obtained from the tool. Do not add commentary or explanations""",
functions=[
get_stock_price,
get_company_profile,
...
]
)
2. 编排
现在,我们来看看每个框架的核心部分,了解它们如何将多个子Agent组合在一起。
LangGraph
langgraph 的核心是基于图的编排。我们首先创建 Supervisor 代理,它充当路由器,其唯一任务是分析情况并决定下一步要呼叫哪个代理。执行代理本身只能将结果传回 supervisor 代理。
LangGraph 需要 state 的明确定义。‘AgentState’ 类有助于定义跨不同代理的通用状态架构。
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
next: str
对于每个代理,我们通过将状态包装在一个节点中来与状态进行交互,该节点将代理输出转换为一致的消息架构。
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"
)
]
}
现在,我们已准备好定义Agent本身。
class RouteResponse(BaseModel):
next: Literal[OPTIONS]
def supervisor_agent(state):
prompt = ChatPromptTemplate.from_messages([
("system", ORCHESTRATOR_SYSTEM_PROMPT),
MessagesPlaceholder(variable_name="messages"),
(
"system",
"Given the conversation above, who should act next?"
" Or should we FINISH? Select one of: {options}",
),
]).partial(options=str(OPTIONS), members=", ".join(MEMBERS))
supervisor_chain = prompt | LLM.with_structured_output(RouteResponse)
return supervisor_chain.invoke(state)
定义 supervisor agent 后,我们将每个 agent 添加为节点,将所有执行逻辑添加为 edge,从而将 agent 工作流定义为图形。
在定义边时,我们有两种可能性:规则边或条件边。当我们想要确定性过渡时,使用常规边缘。例如,Financial Data Agent 应始终将结果返回给 Supervisor_Agent 以决定后续步骤。
当我们希望 LLM(例如,Supervisor Agent 决定它是否有足够的数据来发送到 Output_Summerizing_Agent 或返回 Data 和 Web Agent 以获取更多信息。
from langgraph.graph import END, START, StateGraph
def build_workflow() -> StateGraph:
"""Construct the state graph for the workflow."""
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
)
workflow.add_edge("Output_Summarizing_Agent", END)
workflow.add_edge(START, "Supervisor_Agent")
return workflow
这是生成的图形。
CrewAI
与 LangGraph 相比,CrewAI 抽象出了大部分编排任务。
与 Langgraph 类似,我们首先创建 supervisor agent。请注意 allow_delegation 标志,它允许代理将任务传递给其他代理。
upervisor_agent = Agent(
role="Financial Assistant Manager",
goal="Leverage the skills of your coworkers to answer the user's query: {query}.",
backstory="""You are a manager who oversees the workflow of the financial assistant,
skilled in overseeing complex workers with different skills and ensuring that you can answer the user's query with the help of the coworkers.
You always try to gather data using the financial data agent and / or web scraping agent first.
After gathering the data, you must delegate to the output summarizing agent to create a comprehensive report instead of answering the user's query directly.""",
verbose=True,
llm=ChatOpenAI(model="gpt-4o", temperature=0.5),
allow_delegation=True,
)
接下来,我们使用 Crew 将代理聚集在一起。请务必在此处选择 Process.hierarchical 以允许主管Agent委派任务。在幕后,主管Agent获取用户查询并将其转换为任务,然后找到关联的代理来执行这些任务。另一种选择是不使用管理器代理,如果我们想创建一个更具确定性的流程,其中任务将按顺序执行,则执行 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 Swarm
Swarm 编排使用一种非常简单的策略 - 切换。核心思想是创建一个 transfer function,它使用另一个代理作为工具。
这无疑是最干净的方法。这些关系隐含在 transfer 函数中。
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="""You are a supervisor agent responsible for coordinating the Financial Data Agent, Web Researcher Agent, and Summarizing Agent.
Your tasks:
1. Given a user query, determine which agent to delegate the task to based on the user's query
2. If the user's query requires financial data, delegate to the Financial Data Agent
3. If the user's query requires web research, delegate to the Web Researcher Agent
4. If there's enough information already available to answer the user's query, delegate to the Summarizing Agent for final output.
Never summarize the data yourself. Always delegate to the Summarizing Agent to provide the final output.
""",
functions=[ # Agent as a tool
transfer_to_financial_data_agent,
transfer_to_web_researcher,
transfer_to_summarizer
]
)
这种方法的一个缺点是,随着应用程序的增长,代理之间的依赖关系更难跟踪。
3. 内存
内存是有状态代理系统的关键组件。我们可以区分两层内存:
- 短期记忆允许Agent保持多轮次/多步骤执行
- 长期记忆允许Agent在会话中学习和记住偏好
这个主题可能会变得非常复杂,但让我们看一下每个框架中可用的最简单的内存编排。
LangGraph
LangGraph 区分线程内(单个对话线程内的内存)和跨线程(跨对话的内存)。
为了节省线程内存,LangGraph 提供了 MemorySaver() 类,该类将图形的状态或对话历史记录保存到检查点程序中。
from langgraph.checkpoint.memory import MemorySaver
def build_app():
"""Build and compile the workflow."""
memory = MemorySaver()
workflow = build_workflow()
return workflow.compile(checkpointer=memory)
要将代理执行与内存线程相关联,请将配置与 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() # Can be a DB-backed store in production use
user_id = "user_0"
store.put(
user_id,
"current_portfolio",
{
"portfolio": ["TSLA", "AAPL", "GOOG"],
}
)
CrewAI
毫不奇怪,CrewAI 采用了一种更简单但更严格的方法。开发人员端所需要做的就是将 memory 设置为 true。
finance_crew = Crew(
agents=[financial_data_agent, web_researcher_agent, summarizing_agent],
tasks=[gather_financial_data, gather_website_information, summarize_findings],
process=Process.hierarchical,
manager_agent=supervisor_agent,
memory=True, # creates memory databases in "CREWAI_STORAGE_DIR" folder
verbose=True, # necessary for memory
)
它在幕后的作用非常复杂,因为它会创建一些不同的内存存储:
- 短期记忆:它使用 OpenAI 嵌入创建一个 ChromaDB 向量存储,用于存储代理执行历史记录。
- 最新记忆:SQLite3 db,用于存储最近的任务执行结果。
- 长期记忆:SQLite3 db 来存储任务结果,请注意,任务描述必须完全匹配(相当严格)才能检索长期记忆
- 实体记忆:提取关键实体并将实体关系存储到另一个 ChromaDB 向量库中。
OpenAI Swarm
Swarm 使用简单的无状态设计,没有任何内置内存功能。OpenAI 如何思考内存的一个参考可以在其有状态的 Assistant API 中看到。每个对话都有一个短期记忆thread_id,而每个助手都有一个可以与长期记忆关联的assistant_id。
也可以集成第三方内存层提供商,例如 mem0 或实现我们自己的短期和长期。
4. 人机协同
尽管我们希望代理是自主的,但许多代理旨在与人类交互。例如,客户支持代理可以在整个执行链中向用户询问信息。人类还可以充当审计员或指南,以实现更无缝的人机代理协作。
LangGraph
LangGraph 允许我们在图中设置断点,如果我们想在摘要器构建最终输出之前添加人工检查点,如下所示。
workflow.compile(checkpointer=checkpointer, interrupt_before=["Output_Summarizing_Agent"])
然后,Graph 将执行,直到到达中断点。然后,我们可以实施一个步骤来获取用户输入,然后再继续绘制图表。
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread, stream_mode="values"):
print(event)
try:
user_approval = input("Do you want to go to Output Summarizer? (yes/no): ")
except:
user_approval = "yes"
if user_approval.lower() == "yes":
# If approved, continue the graph execution
for event in graph.stream(None, thread, stream_mode="values"):
print(event)
else:
print("Operation canceled by user.")
CrewAI
CrewAI 允许人类通过在代理初始化中设置 human_input=True 标志来向代理提供反馈。
然后,代理将在执行后暂停,并要求用户输入有关其操作和结果的自然语言反馈(见下文)。
但是,它不支持更多自定义的人机交互。
OpenAI Swarm
Swarm 没有任何内置的 human-in-the-loop 函数。但是,在执行过程中添加人工输入的最简单方法是将 human 添加为 Tool 或 AI 代理可以转移到的代理。
我们的建议
我们在流程图中总结了我们的建议,以帮助您决定从哪个框架开始。