LlamaIndex Workflows 开源使用指南 *(注:如果需要更偏向口语/视频标题的风格,也可以译为:**开源项目怎么用 LlamaIndex Workflows**)*

open-source入门18 分钟阅读2026/7/11

上个月,我正在开发一个客服智能体,它需要完成三件事:从数据库查询订单详情、在知识库里搜索排障步骤,以及决定是否要把问题升级转给人工。我一开始用的是标准的链式(chain-based)方法,按线性顺序把各个步骤串联起来。在一切顺利的“理想路径”下,它跑得挺好。但一旦我需要让智能体回头重试——比如客户的第一个排障步骤没管用,我们需要换第二种方案时——整个流程就崩了。有向无环图(DAG)用来做流水线很棒,但对于需要推理、分支和循环的智能体来说,简直是噩梦。

就在那时,我偶然发现了 LlamaIndex Workflows。传统的基于图的编排需要你显式地把节点连在一起,而 Workflows 不同,它使用的是事件驱动架构。各个步骤(Steps)负责发送和接收事件,框架则根据这些事件来决定接下来运行什么。听起来这正是我需要的,所以我花了一个周末用它重构了我的客服智能体。以下是我学到的东西。

核心概念:步骤与事件

Workflows 建立在两个基础模块之上:步骤事件。步骤就是一个加了 @step 装饰器的函数,它会执行一些操作并发送事件。事件则是类型化的数据类,负责在步骤之间传递信息。神奇之处在于,步骤之间不需要互相认识——它们只需要知道自己接收什么事件、产生什么事件就行了。

这跟基于图的方法相比是一个巨大的转变。在 DAG 中,你需要手动定义边:“节点 A 连接到节点 B”。而在事件驱动的 Workflow 中,步骤 A 发出一个 LookupResultEvent,任何接收 LookupResultEvent 作为输入的步骤都会自动收到它。这让分支和循环变得非常自然,不再是生硬地强加了。

环境配置

首先,安装必要的包。你需要 LlamaIndex 核心库和 workflow 工具包:

pip install llama-index-core>=0.11.16 llama-index-utils-workflow

我还推荐安装 workflow 渲染工具——它能生成 HTML 格式的流程可视化图,对调试非常有帮助:

pip install llama-index-utils-workflow

至于大语言模型(LLM),这里我使用 OpenAI,因为它是默认选项,但你也可以随时换成其他提供商。只需设置你的 API 密钥:

import os
os.environ["OPENAI_API_KEY"] = "sk-your-key-here"

如果你想使用像 Ollama 这样的本地或开源模型,可以通过 Settings 对象进行配置:

from llama_index.core import Settings
from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="llama3", request_timeout=120.0)

构建工具调用智能体

让我带你过一遍我实际构建的智能体。我希望这个 Workflow 能够:

  1. 接收用户查询
  2. 判断是需要查询订单信息还是搜索知识库
  3. 执行相应的工具
  4. 评估结果,然后要么直接回复,要么循环回去执行更多操作

第一步:定义事件

事件是 Workflow 的骨干。每个事件都携带着步骤间需要传递的特定数据。以下是我定义的内容:

from llama_index.core.workflow import Event

class UserQueryEvent(Event):
    query: str

class ToolCallEvent(Event):
    tool_name: str
    tool_args: dict

class ToolResultEvent(Event):
    tool_name: str
    result: str

class ResponseEvent(Event):
    response: str

这里的类型声明非常重要。Workflows 使用这些类型来将事件路由到正确的步骤。我一开始犯了个错,用了普通的字典而不是类型化的事件,结果路由彻底崩了。

第二步:定义工具

我创建了两个简单的工具——一个用于查询订单,一个用于搜索知识库:

from llama_index.core.tools import FunctionTool

def lookup_order(order_id: str) -> str:
    """Look up order details by order ID."""
    # 实际场景中,这里会查询数据库
    orders = {
        "ORD-123": "Order shipped on Jan 15, tracking: 1Z999AA10123456784",
        "ORD-456": "Order processing, estimated ship date: Jan 20",
    }
    return orders.get(order_id, "Order not found")

def search_knowledge_base(query: str) -> str:
    """Search the knowledge base for troubleshooting steps."""
    # 实际场景中,这里会查询向量存储
    kb = {
        "reset password": "Go to Settings > Account > Reset Password",
        "wifi connection": "1. Restart router 2. Forget network 3. Reconnect",
    }
    for key, value in kb.items():
        if key in query.lower():
            return value
    return "No relevant articles found"

order_tool = FunctionTool.from_defaults(fn=lookup_order)
kb_tool = FunctionTool.from_defaults(fn=search_knowledge_base)

第三步:构建 Workflow

现在到了重头戏。Workflow 类将你的步骤组合在一起,并管理共享上下文:

from llama_index.core.workflow import Workflow, Context, StartEvent, StopEvent, step
from llama_index.core.llms import LLM

class SupportAgentWorkflow(Workflow):
    def __init__(self, llm: LLM, **kwargs):
        super().__init__(**kwargs)
        self.llm = llm

    @step
    async def decide_tool(self, ctx: Context, ev: StartEvent | UserQueryEvent) -> ToolCallEvent | ResponseEvent:
        """Decide which tool to call based on the query."""
        query = ev.get("query", "")
        
        # 将查询存入上下文,留待后用
        await ctx.set("query", query)
        
        # 询问 LLM 该使用哪个工具
        prompt = f"""You are a support agent. Given this user query, decide what to do.

Available tools:
- lookup_order: Look up order details (requires order_id)
- search_knowledge_base: Search for troubleshooting steps (requires query)

User query: {query}

Respond with either:
- TOOL: lookup_order|order_id_value
- TOOL: search_knowledge_base|query_value
- RESPONSE: your direct response if no tool is needed"""

        response = await self.llm.acomplete(prompt)
        text = str(response).strip()
        
        if text.startswith("TOOL:"):
            parts = text.split("|", 1)
            tool_name = parts[0].replace("TOOL:", "").strip()
            tool_arg_value = parts[1].strip() if len(parts) > 1 else ""
            
            if tool_name == "lookup_order":
                return ToolCallEvent(tool_name=tool_name, tool_args={"order_id": tool_arg_value})
            elif tool_name == "search_knowledge_base":
                return ToolCallEvent(tool_name=tool_name, tool_args={"query": tool_arg_value})
        
        elif text.startswith("RESPONSE:"):
            return ResponseEvent(response=text.replace("RESPONSE:", "").strip())
        
        # 兜底回复
        return ResponseEvent(response="I'm not sure how to help with that. Could you rephrase?")

    @step
    async def execute_tool(self, ctx: Context, ev: ToolCallEvent) -> ToolResultEvent:
        """Execute the selected tool."""
        if ev.tool_name == "lookup_order":
            result = lookup_order(**ev.tool_args)
        elif ev.tool_name == "search_knowledge_base":
            result = search_knowledge_base(**ev.tool_args)
        else:
            result = "Unknown tool"
        
        return ToolResultEvent(tool_name=ev.tool_name, result=result)

    @step
    async def evaluate_result(self, ctx: Context, ev: ToolResultEvent) -> ResponseEvent | UserQueryEvent:
        """Evaluate tool result and decide if we need more action."""
        query = await ctx.get("query")
        
        prompt = f"""You are a support agent. Based on the tool result, decide if you can answer the user's query.

User query: {query}
Tool used: {ev.tool_name}
Tool result: {ev.result}

If the result is sufficient, respond with RESPONSE: followed by your answer.
If you need to try another tool or approach, respond with RETRY: followed by a refined query."""

        response = await self.llm.acomplete(prompt)
        text = str(response).strip()
        
        if text.startswith("RESPONSE:"):
            return ResponseEvent(response=text.replace("RESPONSE:", "").strip())
        elif text.startswith("RETRY:"):
            new_query = text.replace("RETRY:", "").strip()
            return UserQueryEvent(query=new_query)
        
        return ResponseEvent(response=ev.result)

    @step
    async def format_response(self, ctx: Context, ev: ResponseEvent) -> StopEvent:
        """Format and return the final response."""
        return StopEvent(result=ev.response)

这里信息量比较大,让我拆解一下关键部分:

  • StartEventStopEvent 是内置的事件类型。StartEvent 负责启动 Workflow,StopEvent 负责结束它。
  • 类型提示中的 | 操作符至关重要。当一个步骤可以返回多种事件类型时,框架会根据这些类型注解来正确路由事件。decide_tool 可以返回 ToolCallEventResponseEvent,框架会把它们分别送到对应的下一步。
  • Context 是一个共享状态对象。我用它来存储原始查询,以便后续步骤可以访问。这比把状态塞进每一个事件里传参要优雅得多。
  • 循环发生在 evaluate_result 中。如果 LLM 认为工具返回的结果不够好,它就会发出一个 UserQueryEvent,这个事件会被路由回 decide_tool。这就是在 DAG 中极其痛苦才能实现的循环行为。

第四步:运行

from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o-mini")
workflow = SupportAgentWorkflow(llm=llm, verbose=True)

result = await workflow.run(query="Where is my order ORD-123?")
print(result)
# 输出: "Your order ORD-123 was shipped on January 15th. 
#          Your tracking number is 1Z999AA10123456784."

设置 verbose=True 会在执行时打印出每一步,这对调试来说简直是无价之宝。你可以清楚地看到发出了哪些事件,以及哪些步骤接收了它们。

可视化 Workflow

我最喜欢的功能之一就是 HTML 渲染。定义好 Workflow 后,你可以生成一张可视化的流程图:

from llama_index.utils.workflow import draw_all_possible_flows

draw_all_possible_flows(workflow, filename="support_agent.html")

在浏览器中打开这个 HTML 文件,你会看到一张清晰的图表,展示了 Workflow 中所有可能的路径,包括循环。当我试图向同事解释这个流程时,这个功能帮我省了好几个小时。

让我意外的地方

在开发过程中,有几件事让我踩了坑:

类型提示不是可选的。 我一开始试图省略步骤返回值的联合类型提示,以为框架能自己推断路由。它不能。整个路由机制完全依赖于这些类型注解。如果你不明确指定 decide_tool 返回的是 ToolCallEvent | ResponseEvent,框架根本不知道该把事件发到哪里。

Context 只支持异步。 ctx.set()ctx.get() 方法都是协程。我好几次忘了加 await 关键字,结果得到了让人摸不着头脑的报错。一定要记住:await ctx.set("key", value)

事件命名对可读性至关重要。 当你的 Workflow 变大时,会有很多事件类型。我一开始用了类似 StepOneEvent 这种泛泛的名字,很快就晕头转向了。换成 ToolCallEventEvaluationNeededEvent 这种描述性的名字后,一切都清晰多了。

实用建议

  1. 从简单开始,然后再加循环。 先跑通一条直线路径(查询 → 工具 → 响应),验证没问题了,再加循环逻辑。在一个连线性都没跑通的 Workflow 里调试循环,那滋味绝对酸爽。

  2. 开发时开启 verbose=True 控制台输出会显示每一次事件发送和步骤执行。在生产环境中为了性能可以关掉它。

  3. 保持事件精简。 事件应该只携带下一步所需的数据。我一开始把整个对话历史都塞进了事件里,导致 Workflow 又慢又难调试。共享状态应该用 Context 对象来存。

  4. 单独测试步骤。 既然步骤本质上就是异步函数,你完全可以创建相应的事件并直接调用该步骤来进行独立测试。这比每次测试都要跑一遍整个 Workflow 轻松多了。

诚实的局限性

Workflows 并不是万能的。对于简单的线性流水线(摄入 → 文档分块 → 嵌入 → 存储),传统的流水线更简单直接。只有当你涉及分支、循环或条件逻辑时,事件驱动的开销才是划算的。

文档还在不断完善中。我遇到了一些边界情况——比如如何处理多个步骤发出相同事件类型的情况——需要去翻源码。不过社区很活跃,LlamaIndex 的 Discord 频道帮了不少忙。

另外,要注意循环能力意味着你需要防范死循环。我在上下文中加了一个重试计数器,用来限制 evaluate_result 可以循环回去的次数。如果不加这个,一个犯迷糊的 LLM 可能会无限转圈。

总结

LlamaIndex Workflows 精准解决了我面临的问题:构建一个需要推理、行动,并且在搞不定时能回头重试的智能体。对于智能体系统,事件驱动架构感觉非常自然,这是 DAG 永远做不到的。如果你在构建任何超越简单链式调用的东西——尤其是涉及工具调用、多步推理或人机协同(human-in-the-loop)模式的系统——Workflows 绝对值得深入研究。只要记住:留意你的类型提示,限制好你的循环。

相关 Agent

M

Meta AI

Meta AI 是一个开源AI平台,用于研究和开发先进的语言模型及生成式AI工具。

了解更多 →