3.2 LangGraph
LangGraph是智能体框架中的资深成员,自2024年1月首次发布以来,它通过采用Pregel图结构解决了传统管道和链的非循环性问题。通过引入节点、边和条件边的概念,LangGraph使得在智能体中定义循环逻辑变得更加直观。它建立在LangChain的基础之上,利用该框架的对象和类型系统。
LangGraph智能体在结构上与基于代码的智能体相似,但其背后的实现逻辑却大相径庭。尽管LangGraph同样使用“路由器”模式来调用OpenAI模型并根据响应进行下一步操作,但技能之间的转换方式完全不同。
# 定义工具列表和模型
tools = [generate_and_run_sql_query, data_analyzer]
model = ChatOpenAI(model="gpt-4", temperature=0).bind_tools(tools)
# 创建智能体图
def create_agent_graph():
workflow = StateGraph(MessagesState)
# 创建工具节点
tool_node = ToolNode(tools)
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
# 添加边
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
)
workflow.add_edge("tools", "agent")
# 添加内存保存器
checkpointer = MemorySaver()
app = workflow.compile(checkpointer=checkpointer)
return app
在这个图中,我们定义了一个用于初始OpenAI调用的“agent”节点,以及一个用于工具处理步骤的“tools”节点。LangGraph提供了ToolNode内置对象,它接收一个可调用工具的列表,并根据ChatMessage响应触发这些工具,然后再次返回到“agent”节点。
# 定义是否继续的条件函数
def should_continue(state: MessagesState):
messages = state["messages"]
last_message = messages[-1]
if last_message.tool_calls:
return "tools"
return END
# 定义模型调用函数
def call_model(state: MessagesState):
messages = state["messages"]
response = model.invoke(messages)
return {"messages": [response]}
在每次调用“智能体”节点后,should_continue边决定是将响应返回给用户还是传递给ToolNode来处理工具调用。
在每个节点中,“state(状态)”存储来自OpenAI的消息和响应列表,类似于基于代码的智能体的方法。
LangGraph的挑战
使用LangGraph时遇到的挑战主要与Langchain对象的使用有关。
挑战1:函数调用验证
为了与ToolNode对象兼容,需要重构现有的Skill代码。ToolNode期望接收一个可调用函数列表,这要求技能定义为基本函数而非类方法,因为类方法的第一个参数是“self”。
挑战2:调试
在框架内进行调试可能比较困难,部分原因是错误消息可能不够明确,且抽象的概念使得查看变量值变得复杂。
LangGraph的优点
LangGraph的一个主要优点是其易用性。图形结构使得代码整洁且易于理解,尤其是对于具有复杂节点逻辑的智能体。此外,LangGraph简化了将LangChain中构建的现有应用程序迁移到新框架的过程。
总体而言,如果你完全遵循框架的规范,LangGraph将提供干净、高效的工作流程;如果你需要自定义或扩展框架的功能,可能需要准备面对一些调试挑战。
3.3 LlamaIdex Workflows
LlamaIndex Workflows是智能体框架领域的新晋成员,于今年夏季首次亮相。它旨在简化循环智能体的构建,并特别强调了对异步运行的支持。
Workflows的一些设计似乎是对LangGraph的直接响应,尤其是它采用事件驱动而非边和条件边的方法。Workflows通过步骤(类似于LangGraph中的节点)来组织逻辑,并在步骤间传递事件。
下面的代码定义了Workflows结构。与LangGraph类似,这是我准备状态并将技能附加到LLM对象的地方。
# 定义智能体工作流类
class AgentFlow(Workflow):
def __init__(self, llm, timeout=300):
super().__init__(timeout=timeout)
self.llm = llm
self.memory = ChatMemoryBuffer(token_limit=1000).from_defaults(llm=llm)
self.tools = [FunctionTool(
skill_map.get_function_callable_by_name(func),
metadata=ToolMetadata(name=func, description=skill_map.get_function_description_by_name(func))
) for func in skill_map.get_function_list()]
# 准备智能体上下文的步骤
@step
async def prepare_agent(self, ev: StartEvent) -> RouterInputEvent:
user_input = ev.input
user_msg = ChatMessage(role="user", content=user_input)
self.memory.put(user_msg)
chat_history = self.memory.get()
return RouterInputEvent(input=chat_history)
在Workflows中,我添加了一个额外的“prepare_agent”步骤来初始化智能体上下文。这个步骤根据用户输入创建ChatMessage并将其添加到工作流的内存中,避免了在循环执行时重复添加用户消息。
设置好Workflows后,我定义了路由代码:
# 定义路由步骤
async def router(self, ev: RouterInputEvent) -> ToolCallEvent | StopEvent:
messages = ev.input
if not any(isinstance(message, dict) and message.get("role") == "system" for message in messages):
system_prompt = ChatMessage(role="system", content=SYSTEM_PROMPT)
messages.insert(0, system_prompt)
with using_prompt_template(template=SYSTEM_PROMPT, version="v0.1"):
response = await self.llm.achat_with_tools(
model="gpt-4",
messages=messages,
tools=self.tools,
)
self.memory.put(response.message)
tool_calls = self.llm.get_tool_calls_from_response(response, error_on_no_tool_call=False)
if tool_calls:
return ToolCallEvent(tool_calls=tool_calls)
else:
return StopEvent(result=response.message.content)
以及工具调用处理代码:
# 定义工具调用处理步骤
async def tool_call_handler(self, ev: ToolCallEvent) -> RouterInputEvent:
tool_calls = ev.tool_calls
for tool_call in tool_calls:
function_name = tool_call.tool_name
arguments = tool_call.tool_kwargs
if "input" in arguments:
arguments["prompt"] = arguments.pop("input")
try:
function_callable = skill_map.get_function_callable_by_name(function_name)
except KeyError:
function_result = "Error: Unknown function call"
else:
function_result = function_callable(arguments)
message = ChatMessage(
role="tool",
content=function_result,
additional_kwargs={"tool_call_id": tool_call.tool_id},
)
self.memory.put(message)
return RouterInputEvent(input=self.memory.get())
这两个智能体看起来都比LangGraph智能体更类似于基于代码的智能体。这主要是因为Workflows将条件路由逻辑保留在步骤中,而不是保留在条件边中——第18-24行是LangGraph中的条件边,而现在它们只是路由步骤的一部分——而且LangGraph有一个ToolNode对象,它几乎可以自动执行tool_call_handler方法中的所有操作。
经过路由步骤,我很高兴看到的一件事是,我可以将我的SkillMap和基于代码的智能体中的现有技能与Workflows一起使用。这些不需要更改即可使用Workflows,这让我的生活变得更轻松。
Workflows的挑战
挑战1:同步与异步 异步执行虽然适合实时智能体,但同步智能体的调试更为直接。Workflows设计为异步,尝试强制同步执行非常困难。
挑战2:Pydantic验证错误 与LangGraph类似,围绕技能的Pydantic验证错误造成了困扰。幸运的是,Workflows能够很好地处理成员函数,这使得问题更容易解决。
Workflows的好处
构建Workflows智能体比LangGraph智能体更为直接,因为Workflows要求我自行编写路由逻辑和工具处理代码,而不是依赖内置函数。这使得Workflows智能体与基于代码的智能体非常相似。
Workflows的事件驱动架构为直接函数调用提供了一种有用的替代方案,尤其适用于复杂的异步应用程序。这种架构有助于清晰地管理多个异步触发的步骤和事件,使得Workflows成为一个轻量级且灵活的框架选择。
4 比较三种智能体框架
在评估无框架、LangGraph和LlamaIndex Workflows这三种智能体框架方法时,每种都有其独特的优势和适用场景。
无框架方法提供了最大的灵活性,因为所有的抽象都是由开发者自定义的,如上文提到的SkillMap对象。这种方法易于实现,但随着智能体复杂性的增加,如果没有强制的结构,代码的可读性和可维护性可能会受到影响。
LangGraph提供了清晰的结构,非常适合团队合作开发复杂的智能体。它为不熟悉智能体结构的开发者提供了一个良好的起点。然而,这种结构化的方法可能需要更多的调试工作,尤其是当开发者希望自定义超出框架预设的范围时。
LlamaIndex Workflows则介于两者之间,提供了基于事件的架构,这可能对需要处理多个异步步骤的项目特别有用。它比LangGraph更灵活,因为在使用LlamaIndex类型方面要求更少,为那些不完全依赖框架的应用程序提供了更大的自由度。
核心的决策因素可能是:“你已经在应用程序中使用了LlamaIndex或LangChain吗?”LangGraph和Workflows都与各自的底层框架紧密集成,因此,除非有明确的理由,否则切换到另一个智能体框架可能不会带来足够的额外好处。
无框架方法对于有足够自律来维护自定义抽象的开发者来说,始终是一个吸引人的选择。
4.1 帮助选择智能体框架的关键问题
为了帮助决定在下一个智能体项目中使用哪个框架,考虑以下问题:
你是否已经在项目的重要部分使用LlamaIndex或LangChain? 如果是,那么探索与这些框架集成的智能体框架可能是最佳选择。
你对常见的智能体结构熟悉吗,或者你需要指导来构建智能体? 如果你需要指导,Workflows可能是一个好选择。如果你更希望有一个结构化的起点,那么LangGraph可能更适合。
你的智能体是否需要从零开始构建? 框架的一个好处是它们提供了大量的教程和示例。对于纯代码智能体,这样的资源要少得多。
5 结论
选择智能体框架是影响生成式人工智能系统生产结果的众多决策之一。拥有坚实的基础和对LLM追踪的支持是至关重要的,同时保持对新的智能体框架、研究和模型的敏捷性,以应对技术的不断演进。