03 - 事件驱动 Agent 循环
03 - 事件驱动 Agent 循环
学习目标
本章将带你深入 LlamaIndex 的 Agent 循环实现。你将学到:
- Workflow 引擎的事件驱动状态机架构
- @step 装饰器如何构建事件流图
- ReActAgent 的 Thought-Action-Observation 循环
- FunctionAgent 的原生 function calling 路径
- 并行工具调用的等待-聚合模式
- Context 状态管理机制
前置知识
- 事件驱动的 Agent 运行时 — 事件驱动架构基础
- 工具调用协议 — Function Calling 协议
项目实践
Agent 架构概览
LlamaIndex 的 Agent 系统建立在 Workflow 引擎之上。所有 Agent 类型继承自 BaseWorkflowAgent,它同时继承了 Workflow(事件驱动能力)和 Pydantic BaseModel(验证与序列化)。
Workflow 引擎:事件驱动状态机
与传统的 while True 循环不同,LlamaIndex 的 Agent 循环是事件驱动状态机。每个步骤用 @step 装饰器标记,引擎通过静态分析函数签名推断该步骤接受什么事件、产生什么事件。
核心步骤的事件流:
每个 @step 方法定义了一条状态转换边:输入事件类型 → 处理逻辑 → 输出事件类型。引擎将所有 @step 方法组装成有向图,事件沿着图的边流动。
ReActAgent:文本解析式推理循环
ReActAgent 实现了经典的 ReAct (Reasoning + Acting) 循环,基于文本格式解析。
循环流程:
- LLM 输入格式化:使用
ReActChatFormatter组合 system header、工具描述、对话历史、当前推理步骤 - LLM 调用:
llm.achat()获取响应 - 输出解析:
ReActOutputParser尝试匹配两种格式:Action:→ActionReasoningStep(需要调用工具)Answer:→ResponseReasoningStep(给出最终回答)
- 工具执行:如果是 Action,创建
ToolSelection并执行 - 观察注入:工具结果包装为
ObservationReasoningStep追加到推理链
推理步骤类型:
| 类型 | 触发条件 | 内容 |
|---|---|---|
ActionReasoningStep | LLM 输出包含 Action: | thought + 工具名 + 参数 |
ObservationReasoningStep | 工具执行完成 | observation(工具输出) |
ResponseReasoningStep | LLM 输出包含 Answer: | thought + 最终回答 |
ReAct 提示词模板(伪代码格式):
你是 designed to help with tasks using tools.
## 工具> Tool Name: tool_name> Tool Description: 工具描述> Tool Args: 参数 schema
## 输出格式Thought: <你的推理>Action: tool_nameAction Input: {"key": "value"}
Observation: tool_response
重复直到你能回答:Thought: 我不需要工具就能回答。Answer: <你的回答>FunctionAgent:原生 Function Calling
FunctionAgent 与 ReActAgent 的关键区别在于它依赖 LLM 的原生 function calling 能力,不需要文本格式化的 Thought-Action-Observation 循环。
前置条件:llm.metadata.is_function_calling_model 必须为 True。
take_step 流程(伪代码):
async def take_step(self, ctx, input): # 1. 组装输入 + scratchpad llm_input = build_input(ctx, input)
# 2. 调用 LLM 原生 function calling response = await llm.achat_with_tools( chat_history=scratchpad, tools=self.get_tools(input), allow_parallel_tool_calls=self.allow_parallel_tool_calls )
# 3. 从响应中提取 tool_calls tool_calls = llm.get_tool_calls_from_response(response)
# 4. 追加到 scratchpad scratchpad.append(response.message)
return AgentOutput(tool_calls=tool_calls)与 ReActAgent 对比:
| 维度 | ReActAgent | FunctionAgent |
|---|---|---|
| 工具调用协议 | 文本格式 (Thought-Action-Observation) | OpenAI function calling |
| 适用 LLM | 所有 LLM | 仅支持 function calling 的 LLM |
| 并行工具调用 | 不支持(串行解析) | 支持(allow_parallel_tool_calls) |
| 提示词开销 | 高(工具描述格式化为文本) | 低(JSON schema 传递) |
| 容错能力 | 中(LLM 格式错误时可重试) | 高(LLM 保证 JSON 格式) |
并行工具调用的等待-聚合模式
当 Agent 需要调用多个工具时,LlamaIndex 使用 ctx.collect_events() 实现并行调用和结果聚合:
关键设计:所有工具调用并行发出,collect_events 等待全部结果到达后才进入聚合。避免了传统循环中工具串行调用的延迟累积。
Context 状态管理
Context 对象提供 KV 存储 ctx.store,管理 Agent 的运行时状态:
memory:对话历史列表state:自定义状态字典max_iterations:最大迭代次数iteration_count:当前迭代计数器
early_stopping_method:达到最大迭代次数时的处理方式:
"force":抛出异常终止"generate":让 LLM 生成最终回答后正常结束
AgentWorkflow.from_tools_or_functions() 工厂
这个工厂方法根据 LLM 是否支持 function calling 自动选择 Agent 类型:
# 伪代码agent = AgentWorkflow.from_tools_or_functions( tools_or_functions=[search_tool, calculator], llm=my_llm # 如果 my_llm 支持 function calling → FunctionAgent,否则 → ReActAgent)问题与规避
| 陷阱 | 对策 |
|---|---|
| ReActAgent 的 LLM 输出格式不正确 | 在系统提示中明确 Thought/Action/Answer 格式;解析器会自动重试 |
| FunctionAgent 在不支持 function calling 的 LLM 上失败 | 使用 from_tools_or_functions() 工厂自动回退到 ReActAgent |
| 工具调用无限循环 | 设置 max_iterations 并选择 "generate" 作为 early_stopping_method |
| 并行工具调用的结果顺序不确定 | 不要在 Agent 逻辑中依赖工具执行顺序 |
| Context.store 在多 Agent 间泄漏状态 | 每个 Agent 使用独立的 memory 键,或手动管理 state 隔离 |
设计取舍
事件驱动 vs 简单 while 循环
| 维度 | 事件驱动 Workflow | 简单 while 循环 |
|---|---|---|
| 并发工具调用 | 天然支持 | 需要手动管理 asyncio.Task |
| 状态持久化 | Context 可序列化 | 需要手动序列化 |
| 可观测性 | 每步自动推送事件 | 需要手动添加日志 |
| 学习曲线 | 陡峭(理解事件流图) | 平缓(标准编程模式) |
| 灵活性 | 高(可组合、可路由) | 低(固定循环结构) |
为什么选择事件驱动:LlamaIndex 需要将 Agent 循环与 RAG 流水线、多 Agent 编排等复杂场景集成。事件驱动架构支持这些高级场景,代价是入门门槛更高。
参考来源
- LlamaIndex 官方文档:https://developers.llamaindex.ai/python/framework/
- Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models”, arXiv:2210.03629