深入解析 LlamaIndex Workflows:基于 Workflows 构建 ReAct 模式 AI 智能体
LlamaIndex 推出了新特性 Workflows,这是一种事件驱动、用于构建复杂 AI 工作流应用的新方法。在本文中,我们将学习如何基于 Workflows 来构建一个 ReAct 模式的 AI 智能体。尽管 LlamaIndex 框架中已经提供了开箱即用的 ReActAgent 组件,但通过 Workflows 从零构建 ReAct 智能体,可以更深入地了解其内部原理,在未来帮助实现更底层、更灵活的控制能力。
01. ReAct Agent 再回顾
ReAct(Reasoning + Acting)模式的 AI 智能体采用迭代式的推理到行动的工作流程,旨在应对更复杂的人工任务和问题。它通过将推理步骤与实际行动相结合,使得智能体可以逐步理解任务、采取行动,并观察行动获得的新信息以推理后续步骤。过程大致如下:
- 推理:智能体会分析任务与环境、推理步骤、决定下一步行动。
- 行动:调用外部工具,如搜索、执行计算、与外部 API 交互等。
- 观察并循环:观察行动结果,推理后续步骤,调整策略,直至任务完成。
ReAct 模式具备很好的动态性,使得 AI 能够应对复杂和未知情况,适用于更开放性的问题和探索性的任务,展现出更高的自主决策智能。

ReAct Agent 基本构成
一个标准的 ReAct Agent 通常包含以下核心组件:
- LLM(大语言模型):负责生成推理和行动指令。
- Memory(记忆模块):存储历史对话和上下文信息。
- Tools(工具集):提供外部能力的接口。
- Parser(解析器):将 LLM 的输出解析为可执行的计划或动作。
02. 设计 ReAct Agent 工作流
根据 ReAct 智能体的基本思想,其工作流中最核心的步骤(step)应该包括:
- 输入处理:将输入问题(或任务)、已有对话、工具信息、已经获得的信息(即已调用工具的返回内容)等输入 LLM,让 LLM 推理下一步动作。
- 判断结束:如果此时 LLM 可以回答,则直接给出答案,结束流程。
- 工具规划:如果此时 LLM 无法回答,则给出使用工具的信息(工具名、输入参数等)。
- 工具执行:如果需要使用工具,则根据第 3 步给出的信息进行工具调用,并获得返回。
- 循环迭代:回到第一步,进行迭代,直到在第 2 步能够完成任务。
基于 LlamaIndex Workflows 开发 ReAct Agent 的工作流程图如下所示,展示了从用户输入到最终输出的完整闭环。

03. 基于 Workflows 实现 ReAct Agent
下面我们将参考官方样例,详细讲解如何使用 Python 代码实现这一逻辑。
【定义 Event】
参考上面的工作流图,我们需要定义几个需要的 Event 类型来传递状态和数据:
from llama_index.core.llms import ChatMessage
from llama_index.core.tools import ToolSelection, ToolOutput
from llama_index.core.workflow import Event
import os
class PrepEvent(Event):
pass
class InputEvent(Event):
input: list[ChatMessage]
class ToolCallEvent(Event):
tool_calls: list[ToolSelection]
class FunctionOutputEvent(Event):
output: ToolOutput
【ReAct Agent 初始化】
工作流初始化,主要是为了给智能体准备必备的'工具',最重要的就是智能体需要的几大件:LLM 大模型、Memory 记忆、以及可以使用的 Tools 工具。
from typing import Any, List
from llama_index.core.agent.react import ReActChatFormatter, ReActOutputParser
from llama_index.core.agent.react.types import ActionReasoningStep, ObservationReasoningStep
from llama_index.core.llms.llm import LLM
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.core.tools.types import BaseTool
from llama_index.core.workflow import Context, Workflow, StartEvent, StopEvent, step
from llama_index.llms.openai import OpenAI
class ReActAgent(Workflow):
def __init__(
self,
*args: Any,
llm: LLM | None = None,
tools: list[BaseTool] | None = None,
extra_context: str | None = None,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
tools or []
self.tools = tools or []
self.llm = llm or OpenAI()
self.memory = ChatMemoryBuffer.from_defaults(llm=llm)
.formatter = ReActChatFormatter(context=extra_context )
.output_parser = ReActOutputParser()
.sources = []
这里有两个关键的辅助工具:
- formatter:用来把保存在 memory 中的对话历史以及推理历史,格式化成 LLM 输入的消息格式(通常是一个包含 role 与 content 属性的对象列表);还要附加上引导 LLM 进行思考的系统指令。
- out_parser:解析 LLM 输出的解析器。在 ReAct 模式下,LLM 的输出可能是类似 Thought...Action...Action Input...这样的推理结果,需要对这样的输出进行解析,以决定下一步是使用工具还是输出答案。
【用户输入消息处理:new_user_msg】
这是一次性的步骤,简单的把输入问题/任务放入 memory 即可:
@step
async def new_user_msg(self, ctx: Context, ev: StartEvent) -> PrepEvent:
"""
流程入口:接受用户输入,并放置到 Memory 中;并触发下一步
"""
self.sources = []
user_input = ev.input
user_msg = ChatMessage(role="user", content=user_input)
self.memory.put(user_msg)
await ctx.set("current_reasoning", [])
return PrepEvent()
【LLM 输入准备:prepare_chat_history】
在这个步骤中,利用上面初始化的 formatter,把对话历史与推理历史格式化,用来输入给 LLM 做推理。注意在一次任务中,这个步骤有可能会被多次循环调用,除非输入问题被 LLM 直接回答(无需借助工具)。
@step
async def prepare_chat_history(
self, ctx: Context, ev: PrepEvent
) -> InputEvent:
"""
将对话与推理历史组装成 LLM 的输入消息列表 (通常是角色 + 内容)。
推理历史包括:
1. LLM 输出的推理结果 (直接回答问题、需要工具调用、观察工具调用结果后可以回答)
2. 工具调用的结果
"""
chat_history = self.memory.get()
print(f'\n------------当前消息历史------------')
for idx, message in enumerate(chat_history, start=1):
print(f'\n{idx}. {message}')
current_reasoning = await ctx.get("current_reasoning", default=[])
print('\n-------------当前推理历史------------')
for idx, reasoning in enumerate(current_reasoning, start=1):
print(f'\n{idx}. {reasoning}')
llm_input = self.formatter.format(
self.tools, chat_history, current_reasoning=current_reasoning
)
return InputEvent(input=llm_input)
【LLM 调用:handle_llm_input】
使用上一步骤准备的输入内容,调用 LLM,并解析结果。以决定下一步动作(返回不同的事件),具体可以参考下面的代码及注释:
@step
async def handle_llm_input(
self, ctx: Context, ev: InputEvent
) -> ToolCallEvent | StopEvent:
"""
调用 LLM;
解析输出结果,获得推理结果;
判断是结束(可以回答问题), 还是需要调用工具;
"""
chat_history = ev.input
response = await self.llm.achat(chat_history)
try:
reasoning_step = self.output_parser.parse(response.message.content)
(await ctx.get("current_reasoning", default=[])).append(
reasoning_step
)
if reasoning_step.is_done:
self.memory.put(
ChatMessage(
role="assistant", content=reasoning_step.response
)
)
return StopEvent(
result={
"response": reasoning_step.response,
"sources": [*self.sources],
"reasoning": await ctx.get(
"current_reasoning", default=[]
),
}
)
elif isinstance(reasoning_step, ActionReasoningStep):
tool_name = reasoning_step.action
tool_args = reasoning_step.action_input
return ToolCallEvent(
tool_calls=[
ToolSelection(
tool_id="",
tool_name=tool_name,
tool_kwargs=tool_args,
)
]
)
except Exception as e:
(await ctx.get("current_reasoning", default=[])).append(
ObservationReasoningStep(
observation=f"There was an error in parsing my reasoning: "
)
)
PrepEvent()
【工具调用:handle_tool_calls】
这是智能体使用外部工具(Tools)的关键步骤。根据上一步骤 LLM 输出的工具调用需求,调用外部工具(可能有多次调用),并把返回结果放在推理历史中,用于下一次迭代。
@step
async def handle_tool_calls(
self, ctx: Context, ev: ToolCallEvent
) -> PrepEvent:
"""
工具调用,将调用结果作为 LLM 的观察对象;
并将观察内容添加到推理历史
"""
tool_calls = ev.tool_calls
tools_by_name = {tool.metadata.get_name(): tool for tool in self.tools}
for tool_call in tool_calls:
tool = tools_by_name.get(tool_call.tool_name)
if not tool:
(await ctx.get("current_reasoning", default=[])).append(
ObservationReasoningStep(
observation=f"Tool {tool_call.tool_name} does not exist"
)
)
continue
try:
tool_output = tool(**tool_call.tool_kwargs)
self.sources.append(tool_output)
(await ctx.get("current_reasoning", default=[])).append(
ObservationReasoningStep(observation=tool_output.content)
)
except Exception as e:
(await ctx.get("current_reasoning", default=[])).append(
ObservationReasoningStep(
observation=f"Error calling tool {tool.metadata.get_name()}: {e}"
)
)
return PrepEvent()
【测试实现的 ReAct Agent】
现在,整个 ReAct Agent 的工作流就完成了,过程还是比较简单清晰的。当然这里也会利用到一些 LlamaIndex 提供的组件,比如用来封装 LLM 推理结果的 xxxReasoningStep 组件等。
我们来测试这个 ReAct Agent 组件,准备两个模拟工具(利用 LlamaIndex 中的 FunctionTool 快速构造基于函数的工具),然后创建一个 Agent:
from llama_index.core.tools import BaseTool, FunctionTool
def send_email(subject: str, message: str, email: str) -> None:
"""用于发送电子邮件"""
print(f"邮件已发送至 {email},主题为 {subject},内容为 {message}")
tool_send_mail = FunctionTool.from_defaults(fn=send_email,name='tool_send_mail',description='用于发送电子邮件')
def query_customer(phone: str) -> str:
"""用于查询客户信息"""
result = f"该客户信息为:\n姓名:张三\n积分:50000 分\n邮件:[email protected]"
return result
tool_customer = FunctionTool.from_defaults(fn=query_customer,name='tool_customer',description='查询客户信息,包括姓名、积分与邮件')
agent = ReActAgent(
llm=OpenAI(model="gpt-4o-mini"), tools=[tool_send_mail,tool_customer], timeout=120, verbose=True
)
现在调用这个 Agent,我们发出一个比较复杂的请求,来看看会发生什么:
async def main():
ret = await agent.run(input="给客户 13688888888 发电子邮件,通知他最新的积分")
print(ret["response"])
if __name__ == "__main__":
import asyncio
asyncio.run(main())
这里的任务很显然需要借助两次工具调用,一次查询客户信息,一次发送邮件,我们从输出中来观察最后一次的迭代信息。
注意到这里的推理历史,完整的反应了 LLM 的'思考'过程:首先需要查询客户信息(调用 tool_customer);然后观察到客户信息(工具调用结果);判断需要发送邮件(调用 tool_send_mail);最后观察返回结果后结束流程。这是一个完整的符合 ReAct 模式(推理 - 行动 - 观察)的工作流,也证明了这里基于 Workflows 构建的 ReAct Agent 的可用性。
最佳实践与扩展建议
在实际生产环境中部署此类智能体时,建议关注以下几点:
- 内存管理:随着对话轮数增加,Memory 可能会变得过大。建议定期清理旧对话或使用滑动窗口机制保留最近 N 条记录。
- 超时控制:设置合理的
timeout 参数,防止智能体陷入死循环或长时间等待工具响应。
- 错误处理:在工具调用环节增加重试机制,对于网络波动或 API 暂时不可用的情况提供容错能力。
- 日志监控:开启
verbose=True 模式有助于调试,但在生产环境应接入结构化日志系统以便追踪链路。
04. 结束语
至此我们对 LlamaIndex 所推出的新特性 Workflows 已经有了较为全面的认识。Workflows 是一个与 LangChain 的 LangGraph 相似的另一种智能体开发底层框架,两者都面向复杂的智能体/RAG 应用工作流,但又采取了不同的设计思想。至于哪个更好或许是见仁见智的问题,但对于大量 LLM 应用的开发者来说,的确又多了一个强大的工具。相信随着后续的迭代,Workflows 也会越来越强大,为构建更复杂的 AI 应用提供支持。