DLAI-LlamaIndex-事件驱动笔记-全-

DLAI LlamaIndex 事件驱动笔记(全)

001:事件驱动的智能代理文档工作流简介 🚀

在本节课中,我们将学习如何构建基于事件驱动的智能代理文档工作流。这是一种建立在检索增强生成(RAG)系统之上的高级应用,能够自动化处理复杂的端到端文档任务。

概述

欢迎来到由LlamaIndex合作推出的“事件驱动的智能代理文档工作流”课程。本课程的讲师是LlamaIndex的开发者关系副总裁Lari Vos。

智能代理文档工作流是一种基于代理的应用程序。与仅能回答数据简单问题的RAG系统不同,智能代理工作流可以构建在RAG之上,以更复杂的方式处理输入文档。

智能代理工作流原理

在我们将要学习的架构中,代理会首先识别完成任务所需的信息,然后利用RAG检索相关材料,最后将收集到的信息组合成结构化的输出。

以下是两个具体示例:

  • 合同合规审查:代理可以解析合同,提取关键条款,并从法规要求知识库中匹配相关条款,最终生成一份合规性摘要。
  • 发票信息标准化:代理可以从发票中提取商品描述,利用RAG在产品目录中匹配最接近的产品代码,然后将标准化信息附加到该发票上。

在本课程中,您将把这类工作流应用于一个实际场景:构建一个能使用简历自动填写求职申请表的代理。

课程核心:LlamaIndex工作流抽象

Lari将指导您从头开始构建,使用的工具是LlamaIndex的工作流抽象。这是构建事件驱动系统的有效方法,也是构建高效代理集的关键设计模式。

LlamaIndex的工作流是一种事件驱动架构。您将把代理的逻辑封装在一系列步骤中,其中每一步都会发出事件以触发后续步骤。

您将学习如何在工作中实现:

  • 代码分支和循环。
  • 创建并发事件。
  • 在给定步骤收集多个事件。

实践项目:分步构建表单填写代理

您将应用上述概念,逐步构建您的表单填写代理:

  1. 设置RAG能力:首先,设置代理的RAG功能,以解析给定的简历、加载到向量数据库并创建查询引擎。
  2. 解析申请表:让代理解析求职申请表,将空白处转换为一连串问题,并发送给RAG流程处理。
  3. 迭代与反馈:为代理提供的答案提供反馈,并共同迭代改进。您将通过文本,乃至语音的方式与代理沟通反馈。

总结

本节课我们一起学习了事件驱动智能代理文档工作流的基本概念、优势及其应用场景。我们了解到,这是一种超越基础RAG的、能够自动化复杂文档处理任务的高级模式。通过LlamaIndex的工作流抽象,我们可以以事件驱动的方式设计和构建这样的代理。

许多人为本课程的创作做出了贡献,特别感谢来自LlamaIndex的Logan Markrovitch和DeepLearning.AI的Hout Salami。

事件驱动工作流是一个非常重要的设计模式,越来越多的企业正在使用它来设计由大语言模型驱动的应用程序。希望您能享受学习这些概念的过程。接下来,让我们进入下一个视频,开始动手实践吧!😊

002:什么是智能代理文档工作流 🧠

在本节课中,我们将学习智能代理文档工作流的基本概念。这包括RAG(检索增强生成)、代理和工作流。你将了解RAG如何帮助回答关于数据的问题,工作流如何为代理规定数据流,以及事件驱动的文档处理如何增强RAG。让我们开始吧。

概述 📋

今天要介绍的是智能代理文档工作流。你将构建一个简单的示例。这是一种基于RAG(如ChatGPT)构建LLM应用的新范式,旨在解决RAG的局限性,并通过应用代理策略超越这些限制。

但这里有很多陌生的术语。什么是RAG?什么是代理策略?在开始之前,我们先来定义这些概念。

什么是RAG? 🔍

RAG代表检索增强生成。RAG是对LLM一个基本局限性的回应。LLM在大量数据上训练,但并未在你的特定数据上训练。通常,当你解决问题时,不仅需要通用知识,还需要处理你的私有数据。

为了让LLM回答关于你数据的问题,你必须将数据提供给LLM。但这遇到了LLM的另一个基本限制:上下文窗口。你一次只能给LLM提供有限的数据。即使最强大的LLM一次也只能处理大约一百万个标记的信息,而你的组织数据量可能远超于此,达到数千万甚至数亿。

因此,你必须选择性地向LLM提供数据。这就带来了一个挑战:如何选择数据?你希望给LLM提供最相关的数据。

事实证明,产生LLM的相同技术也产生了称为嵌入模型的东西,这是解决该问题的部分方案。嵌入模型将数据字符串编码成称为向量的数字数组。所有可能向量的集合被称为向量空间。这些向量编码了你所编码数据的含义。

然后,你可以将这些向量存储在数据库中。现在,如果你有一个关于数据的问题,你可以将你的问题通过相同的嵌入模型运行,你的问题也会被编码成一个向量。嵌入模型的魔力在于,因为它们编码含义,所以你的问题和回答你问题的数据是关于相似事物的,它们具有相似的含义。因此,它们在向量空间中数学上是彼此接近的。

你可以使用数据库来搜索附近的向量。这就是当你有一个查询时,如何找到相关数据提供给LLM的方法。

然后,你可以将你的查询和相关数据(称为上下文)一起提供给LLM,并要求它使用上下文来回答问题。我们将这一步称为生成,因为它正在生成答案。而我们搜索相关数据的步骤称为检索。因此,合起来就是检索增强生成RAG

RAG是一种极其强大的技术,但它确实有一些局限性。其中之一是处理复杂或多部分问题。

RAG的局限性与解决方案 ⚙️

这个局限性是合理的,因为RAG基于搜索。如果你有一个包含多个部分的问题,RAG会同时搜索你的嵌入以寻找许多东西。因此,它得到的结果会不那么集中。你会得到大量结果,但它们可能不包含你需要的所有信息。

这个问题的解决方案是将你的复杂问题分解成许多更简单的问题。每个更简单的问题将获得一组更集中、更全面的搜索结果。这对LLM来说是一个很好的任务,因为它们擅长查看一个复杂问题并将其分解为更小的问题。在另一端,它们可以获取一堆简单问题的答案,并将它们综合成一个连贯的答案。

这就是你今天要做的事情。而你将通过构建一个代理来实现。

什么是代理? 🤖

我之前提到过代理和代理策略。那么,什么是代理?这是一个相当模糊的术语。在LlamaIndex,当我们说代理时,我们指的是一段半自主软件。它可以被赋予工具和一个目标,并且会找出如何解决问题,而无需被给予实现该目标的具体、逐步的指令。

这与传统编程非常不同,在传统编程中,每一步都是精确定义的。在LlamaIndex中,构建代理的方式是使用工作流

什么是工作流? 🔄

工作流是LlamaIndex中代理系统的构建模块。它们是一个基于事件的系统,允许你定义一系列由事件连接的步骤,并在步骤之间传递信息。正如你将看到的,你可以创建相当复杂的工作流,包含分支、循环和并行执行,以实现你需要的任何任务。

工作流为你的代理提供结构,其精细程度或宽松程度可根据需要而定。一些代理框架根本没有结构,这可能导致混乱的结果。另一些则采用基于图的方法,这使得循环和其他结构更加困难。我认为工作流提供了两全其美的方案。

智能代理文档工作流 📄

这让我们来到了智能代理文档工作流ADW。我们已经讨论了基础知识:RAG、工作流和代理。智能代理文档工作流是一种通过将代理工作流应用于实际问题,并将其构建到更大的软件中,来解决实际业务问题的软件构建方式。

ADW建立在RAG的强大功能之上。但与主要处理简单问题的RAG不同,智能代理文档工作流处理复杂问题,并产生结构化、具体的输出,而不仅仅是简单的英文答案。

总结 🎯

在本节课中,我们一起学习了智能代理文档工作流的核心概念。我们了解了RAG如何通过检索和生成来回答关于私有数据的问题,以及它在处理复杂问题时的局限性。我们探讨了代理作为半自主软件的概念,以及工作流如何为代理提供结构化的执行路径。最后,我们定义了智能代理文档工作流,它是一种结合了RAG、代理和工作流优势,用于解决复杂业务问题并产生结构化输出的高级应用范式。在下一课中,我们将开始动手构建。

003:构建工作流 🛠️

在本节课中,我们将学习构建一系列由简到繁的工作流,以掌握其基本概念。我们将从一个简单的、由一系列触发事件的步骤组成的工作流开始,然后逐步为其添加分支和循环逻辑,实现并发执行,并最终学习如何在特定步骤收集不同类型的事件。

概述

工作流本质上是常规的Python类,由一系列步骤定义。每个步骤接收特定类型的事件,并发出特定类型的事件。接下来,让我们开始构建第一个工作流。

导入与基础设置

首先,我们需要导入必要的库。我们将使用OpenAI,因此需要设置API密钥。同时,我们还需要导入一些特殊事件类以及工作流的核心模块。

import os
from llama_index.core.workflow import (
    StartEvent,
    StopEvent,
    step,
    Context,
    Event,
)
from llama_index.core.workflow.visualization import draw_all_possible_flows
from IPython.display import display, HTML
import asyncio
import random
import time

# 设置OpenAI API密钥
os.environ["OPENAI_API_KEY"] = "your-api-key-here"

构建单步工作流

我们从一个最简单的单步工作流开始。它接收一个StartEvent(启动事件),并发出一个StopEvent(停止事件)。

class MyWorkflow:
    @step
    async def my_step(self, ev: StartEvent) -> StopEvent:
        # 这是一个异步函数,可以暂停和恢复,允许其他任务同时运行。
        # 这在后续实现并行执行时会很有用。
        return StopEvent(result="Workflow completed.")

# 实例化并运行工作流
async def run_workflow():
    workflow = MyWorkflow(timeout=10, verbose=False)
    result = await workflow.run()
    print(result)

# 在Jupyter Notebook中可以直接运行
# await run_workflow()

# 在普通Python脚本中,需要这样运行:
# asyncio.run(run_workflow())

可视化工作流

工作流的一个强大功能是内置的可视化工具。我们可以生成一个交互式的HTML图表来查看工作流的结构。

# 生成可视化图表
draw_all_possible_flows(MyWorkflow, "simple_workflow.html")

# 在Notebook中显示图表
with open("simple_workflow.html", "r") as f:
    html_content = f.read()
display(HTML(html_content))

如上所示,可视化图表展示了从StartEventmy_step,再到StopEvent的简单流程。

构建多步工作流

单步工作流功能有限。要创建多步工作流,我们需要定义可以触发其他步骤的自定义事件。

以下是创建一个三步工作流的步骤:

  1. 定义自定义事件类:它们必须继承自Event基类。
  2. 定义工作流类:使用@step装饰器定义每个步骤,并指定其接收和发出的事件类型。
# 1. 定义自定义事件
class FirstEvent(Event):
    pass

class SecondEvent(Event):
    pass

# 2. 定义三步工作流
class MyMultiStepWorkflow:
    @step
    async def step1(self, ev: StartEvent) -> FirstEvent:
        print("Step 1 executed.")
        return FirstEvent()

    @step
    async def step2(self, ev: FirstEvent) -> SecondEvent:
        print("Step 2 executed.")
        return SecondEvent()

    @step
    async def step3(self, ev: SecondEvent) -> StopEvent:
        print("Step 3 executed.")
        return StopEvent(result="Multi-step workflow completed.")

# 运行工作流
async def run_multi_step():
    workflow = MyMultiStepWorkflow()
    result = await workflow.run()
    print(result)

# await run_multi_step()

可视化这个工作流,你会看到一条清晰的路径:StartEvent -> step1 -> FirstEvent -> step2 -> SecondEvent -> step3 -> StopEvent

实现循环逻辑

仅仅顺序执行还不够灵活。工作流允许我们实现循环和分支逻辑。为了实现循环,我们可以创建一个循环事件,并让某个步骤在满足条件时发出该事件,从而触发自身或其他步骤再次执行。

以下是实现随机循环的示例:

# 定义循环事件
class LoopEvent(Event):
    pass

class LoopingWorkflow:
    @step
    async def step1(self, ev: StartEvent | LoopEvent) -> FirstEvent | LoopEvent:
        # 随机决定是继续执行还是循环
        if random.randint(0, 1) == 0:
            print("A bad thing happened. Looping back.")
            return LoopEvent()  # 发出循环事件,触发step1再次执行
        else:
            print("A good thing happened. Moving to next step.")
            return FirstEvent()

    @step
    async def step2(self, ev: FirstEvent) -> StopEvent:
        print("Step 2 executed.")
        return StopEvent(result="Looping workflow completed.")

# 运行多次以观察随机循环
# await LoopingWorkflow().run()

在可视化图中,你可以看到从step1出发的一个箭头指向FirstEvent(继续执行),另一个箭头指回step1自身(形成循环)。

实现分支逻辑

与循环类似,我们也可以实现分支逻辑,让工作流根据条件执行不同的路径。

以下是创建一个二分支工作流的示例:

# 定义分支相关事件
class BranchA1Event(Event):
    pass
class BranchA2Event(Event):
    pass
class BranchB1Event(Event):
    pass
class BranchB2Event(Event):
    pass

class BranchingWorkflow:
    @step
    async def step1(self, ev: StartEvent) -> BranchA1Event | BranchB1Event:
        if random.randint(0, 1) == 0:
            print("Taking Branch A.")
            return BranchA1Event()
        else:
            print("Taking Branch B.")
            return BranchB1Event()

    @step
    async def step_a1(self, ev: BranchA1Event) -> BranchA2Event:
        print("Executing Step A1.")
        return BranchA2Event()

    @step
    async def step_a2(self, ev: BranchA2Event) -> StopEvent:
        print("Executing Step A2.")
        return StopEvent(result="Branch A completed.")

    @step
    async def step_b1(self, ev: BranchB1Event) -> BranchB2Event:
        print("Executing Step B1.")
        return BranchB2Event()

    @step
    async def step_b2(self, ev: BranchB2Event) -> StopEvent:
        print("Executing Step B2.")
        return StopEvent(result="Branch B completed.")

# 可视化分支工作流(无需实例化)
draw_all_possible_flows(BranchingWorkflow, "branching_workflow.html")

可视化图表将清晰地展示从step1分出的两条独立路径。

实现并发执行

对于耗时的任务,并发执行可以显著提高效率。工作流通过Context对象和send_event方法支持并行执行。

Context对象是工作流中所有步骤可访问的共享内存。通过它,一个步骤可以并行发出多个事件。

以下是一个并行执行三个查询任务的示例:

class Step2Event(Event):
    def __init__(self, query: str):
        self.query = query

class ParallelFlow:
    @step
    async def step1(self, ev: StartEvent, ctx: Context) -> None:
        # 使用 ctx.send_event 并行发出三个事件
        await ctx.send_event(Step2Event("Query 1"))
        await ctx.send_event(Step2Event("Query 2"))
        await ctx.send_event(Step2Event("Query 3"))
        # 注意:这里没有return事件,流程由step2触发的事件驱动

    @step
    async def step2(self, ev: Step2Event) -> StopEvent:
        # 模拟一个耗时任务
        wait_time = random.randint(1, 5)
        print(f"Processing {ev.query}, waiting {wait_time} seconds...")
        await asyncio.sleep(wait_time)
        print(f"Finished {ev.query}")
        # 第一个完成的step2会触发StopEvent,导致整个工作流停止
        return StopEvent(result=ev.query)

# 运行并发工作流
# result = await ParallelFlow().run()
# print(f"The first query to finish was: {result}")

需要注意的是,在这个例子中,第一个完成的step2发出的StopEvent会立即终止整个工作流。

收集并行事件的结果

如果我们希望收集所有并行任务的结果,而不是只取第一个,可以使用Context对象的collect_events方法。

collect_events会等待指定数量、指定类型的事件全部到达后,才返回一个包含这些事件的列表。

以下是收集三个相同类型事件结果的示例:

class Step3Event(Event):
    def __init__(self, query: str):
        self.query = query

class ConcurrentCollectWorkflow:
    @step
    async def step1(self, ev: StartEvent, ctx: Context) -> None:
        await ctx.send_event(Step2Event("Query 1"))
        await ctx.send_event(Step2Event("Query 2"))
        await ctx.send_event(Step2Event("Query 3"))

    @step
    async def step2(self, ev: Step2Event) -> Step3Event:
        wait_time = random.randint(1, 3)
        await asyncio.sleep(wait_time)
        return Step3Event(query=ev.query)

    @step
    async def step3(self, ev: Step3Event, ctx: Context) -> StopEvent | None:
        # 收集3个Step3Event类型的事件
        events = await ctx.collect_events(Step3Event, count=3)
        if events is None:
            # 如果还没收集齐,返回None,步骤会再次被触发
            print("Not all events received yet.")
            return None
        else:
            # 收集齐了,处理结果
            results = [e.query for e in events]
            print(f"All queries received: {results}")
            return StopEvent(result=results)

# 运行收集工作流
# result = await ConcurrentCollectWorkflow().run()
# print(f"Final result: {result}")

收集不同类型的事件

collect_events方法同样可以用于收集多种不同类型的事件。你需要指定要收集的每种事件类型及其数量。

以下是收集三种不同类型事件结果的示例:

# 定义多种事件类型
class StepAEvent(Event):
    pass
class StepACompleteEvent(Event):
    def __init__(self, data: str):
        self.data = data
class StepBEvent(Event):
    pass
class StepBCompleteEvent(Event):
    def __init__(self, data: str):
        self.data = data
class StepCEvent(Event):
    pass
class StepCCompleteEvent(Event):
    def __init__(self, data: str):
        self.data = data

class MultiTypeCollectWorkflow:
    @step
    async def step1(self, ev: StartEvent, ctx: Context) -> None:
        # 并行发出三种不同类型的事件
        await ctx.send_event(StepAEvent())
        await ctx.send_event(StepBEvent())
        await ctx.send_event(StepCEvent())

    @step
    async def step_a(self, ev: StepAEvent) -> StepACompleteEvent:
        await asyncio.sleep(random.randint(1, 2))
        return StepACompleteEvent(data="Something A-ish")

    @step
    async def step_b(self, ev: StepBEvent) -> StepBCompleteEvent:
        await asyncio.sleep(random.randint(1, 2))
        return StepBCompleteEvent(data="Something B-ish")

    @step
    async def step_c(self, ev: StepCEvent) -> StepCCompleteEvent:
        await asyncio.sleep(random.randint(1, 2))
        return StepCCompleteEvent(data="Something C-ish")

    @step
    async def step_final(self,
                         ev: StepACompleteEvent | StepBCompleteEvent | StepCCompleteEvent,
                         ctx: Context) -> StopEvent | None:
        # 收集三种不同类型的事件各一个
        # 注意:顺序决定了返回列表中的事件顺序
        events = await ctx.collect_events(StepCCompleteEvent, StepACompleteEvent, StepBCompleteEvent, count=1)
        if events is None:
            return None
        else:
            results = [e.data for e in events]
            print(f"Collected results in specified order: {results}")
            return StopEvent(result=results)

# 运行并可视化这个更复杂的工作流
# draw_all_possible_flows(MultiTypeCollectWorkflow, "multi_type_workflow.html")
# result = await MultiTypeCollectWorkflow().run()

实现事件流式输出

对于运行时间较长的智能体,向用户实时反馈进度非常重要。工作流支持通过Context对象的write_event方法将事件流式传输回用户。

以下是一个集成LLM并流式返回其生成内容的示例:

from llama_index.llms.openai import OpenAI

# 定义流式相关事件
class FirstEvent(Event):
    pass
class SecondEvent(Event):
    pass
class TextEvent(Event):
    def __init__(self, delta: str):
        self.delta = delta
class ProgressEvent(Event):
    def __init__(self, message: str):
        self.message = message

class StreamingWorkflow:
    @step
    async def step1(self, ev: StartEvent, ctx: Context) -> FirstEvent:
        # 流式发送一个进度事件
        await ctx.write_event(ProgressEvent(message="Step 1 is happening..."))
        return FirstEvent()

    @step
    async def step2(self, ev: FirstEvent, ctx: Context) -> SecondEvent:
        llm = OpenAI(model="gpt-4o-mini")
        # 异步流式调用LLM
        response = await llm.astream_complete("Write a very short haiku about programming.")
        async for chunk in response:
            # 将LLM返回的每个数据块包装成TextEvent并流式发出
            if chunk.delta:
                await ctx.write_event(TextEvent(delta=chunk.delta))
        return SecondEvent()

    @step
    async def step3(self, ev: SecondEvent, ctx: Context) -> StopEvent:
        await ctx.write_event(ProgressEvent(message="Step 3 is happening..."))
        return StopEvent(result="Streaming complete.")

async def run_and_stream():
    workflow = StreamingWorkflow()
    # 获取工作流运行句柄,而不是直接等待结果
    handle = workflow.run()
    # 从句柄获取事件流
    async for ev in handle.stream_events():
        # 过滤并处理我们关心的事件类型
        if isinstance(ev, ProgressEvent):
            print(f"[Progress] {ev.message}")
        elif isinstance(ev, TextEvent):
            # end='' 确保输出在同一行
            print(ev.delta, end='', flush=True)
    # 所有事件流式传输完毕后,获取最终结果
    final_result = await handle
    print(f"\n[Final Result] {final_result}")

# 运行流式工作流
# await run_and_stream()

运行上述代码,你将看到“Step 1 is happening...”立即打印,然后LLM生成的诗歌会以词块的形式逐渐出现,最后显示“Step 3 is happening...”和最终结果。

总结

在本节课中,我们一起学习了LlamaIndex工作流的核心构建方法。我们从最简单的单步工作流开始,逐步掌握了如何定义事件和步骤、构建多步顺序流程、实现循环与分支逻辑以提高灵活性、利用并发执行来提升效率、收集并行或不同类型任务的结果,以及最终通过流式输出来改善长时间运行任务的用户体验。这些基础概念为我们下一节课将RAG(检索增强生成)集成到工作流中打下了坚实的基础。

004:为代理添加RAG能力

概述

在本节课中,我们将学习如何为智能代理添加检索增强生成(RAG)能力。我们将使用LlamaParse解析一份简历文档,将其信息加载到向量存储中,并构建一个能够查询这些信息的RAG工具。最后,我们会将这个工具封装成一个可复用的工作流。

启用嵌套异步与导入依赖

为了确保本教程中的代码正常运行,我们需要启用嵌套异步功能。

首先,导入必要的库和模块。

# 导入所需库
import asyncio
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_parse import LlamaParse

接着,启用嵌套异步。

# 启用嵌套异步
import nest_asyncio
nest_asyncio.apply()

我们还需要设置API密钥,包括OpenAI的API密钥和Llama Cloud的API密钥(用于LlamaParse服务)。你可以在 llamaindex.ai 免费获取Llama Cloud的API密钥。

使用LlamaParse解析简历

LlamaParse是一个高级文档解析器,能够读取PDF、Word、PowerPoint和Excel等文件,并将复杂文档中的信息提取成易于大语言模型理解的形式。

它的一个强大功能是,你可以告知它正在解析的文档类型,以便它能更智能地提取内容。在本例中,我们将告诉它正在解析一份简历。

以下是解析文档的步骤。

首先,导入LlamaParse并初始化解析器。

# 初始化LlamaParse解析器
parser = LlamaParse(
    api_key="你的LlamaCloud_API_KEY",
    result_type="markdown", # 输出格式可以是markdown、text等
    parsing_instruction="这是一份简历。请将相关事实整理在一起,并使用标题和项目符号格式化。"
)

然后,使用解析器加载我们的假简历文件。

# 解析简历文档
documents = parser.load_data("./fake_resume.pdf")

解析完成后,documents 是一个文档数组。我们可以查看解析出的内容。

# 查看解析结果(例如,查看数组中的第三个文档)
print(documents[2].text)

解析器会以Markdown格式输出简历内容,包含项目标题、公司名称以及用项目符号列出的工作职责描述。

构建向量存储索引

上一节我们成功解析了简历文档,本节我们将把这些文档转换成可搜索的向量形式。

我们需要使用一个嵌入模型将文本转换为向量。这里我们使用OpenAI提供的 text-embedding-3-small 模型。

首先,导入嵌入模型和向量存储索引类。

# 设置嵌入模型
embed_model = OpenAIEmbedding(model="text-embedding-3-small")

接着,使用解析出的文档创建向量存储索引。

# 从文档创建向量存储索引
index = VectorStoreIndex.from_documents(
    documents,
    embed_model=embed_model
)

现在,我们有了一个包含简历信息的索引。为了进行查询,我们需要基于这个索引创建一个查询引擎。

创建查询引擎并进行测试

索引构建完成后,我们可以创建一个查询引擎来回答关于简历的问题。

首先,初始化我们将要使用的大语言模型(LLM)。这里我们使用 gpt-4o-mini,因为它速度快且成本低。

# 初始化LLM
llm = OpenAI(model="gpt-4o-mini")

然后,从索引创建查询引擎。我们可以设置 similarity_top_k 参数,它决定了引擎在回答问题时返回的最相关上下文片段的数量。

# 创建查询引擎
query_engine = index.as_query_engine(
    llm=llm,
    similarity_top_k=5
)

现在,我们可以用查询引擎来提问了。

# 向查询引擎提问
response = query_engine.query("申请人的姓名是什么?他最近的一份工作是什么?")
print(response)

查询引擎会返回答案,例如:“申请人的名字是Sarah Chen,她最近的工作是TechFlow Solutions公司的高级全栈开发工程师。”

持久化与加载向量存储

为了在后续会话中复用向量存储,我们需要将其保存到磁盘。

保存索引非常简单,只需调用 persist 方法并指定存储目录。

# 将索引持久化到磁盘
index.storage_context.persist(persist_dir="./storage")

当需要再次使用时,我们可以从存储目录加载索引。

# 从磁盘加载索引
storage_context = StorageContext.from_defaults(persist_dir="./storage")
loaded_index = load_index_from_storage(storage_context)

加载后,可以像之前一样创建查询引擎并进行查询。

# 使用加载的索引创建查询引擎
loaded_query_engine = loaded_index.as_query_engine(llm=llm)
response = loaded_query_engine.query("申请人的姓名是什么?")
print(response)

将RAG功能封装为代理工具

我们已经有了一个可工作的RAG管道,现在将其封装成一个工具,以便智能代理可以调用它。

这需要两个新类:FunctionToolFunctionCallingAgent

首先,定义一个执行RAG查询的普通Python函数。为函数提供描述性的名称、输入输出类型以及详细的文档字符串非常重要,因为这些元数据会被提供给LLM,帮助它决定是否以及如何使用这个工具。

def query_resume(query: str) -> str:
    """
    根据简历内容回答问题。
    参数:
        query (str): 关于申请人简历的问题。
    返回:
        str: 基于简历信息生成的答案。
    """
    response = query_engine.query(query)
    return str(response)

然后,使用 FunctionTool.from_defaults 方法将这个函数转换为代理可用的工具。

from llama_index.core.tools import FunctionTool

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-llmidx-evtdvn/img/725831658f122b151a7f425149a61914_24.png)

# 将函数转换为工具
rag_tool = FunctionTool.from_defaults(fn=query_resume)

现在,我们可以创建一个使用此工具的函数调用代理。

from llama_index.core.agent import FunctionCallingAgent

# 创建代理
agent = FunctionCallingAgent.from_tools(
    tools=[rag_tool],
    llm=llm,
    verbose=True # 设置为True以查看代理的思考过程
)

创建代理后,我们可以与它对话。

# 向代理提问
response = agent.chat("申请人有多少年工作经验?")
print(response)

verbose=True 时,控制台会输出详细的步骤信息,包括代理何时调用 query_resume 工具、传递了什么参数以及最终的回答。

构建可复用的RAG工作流

最后,我们将之前的所有步骤整合封装成一个整洁、可复用的工作流类。这个工作流类会处理文档解析、索引创建/加载以及查询响应。

以下是工作流类的定义。

from llama_index.core.workflow import (
    StartEvent, StopEvent, step, Context
)
from pathlib import Path

# 定义工作流事件
class QueryEvent:
    def __init__(self, question: str):
        self.question = question

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-llmidx-evtdvn/img/725831658f122b151a7f425149a61914_34.png)

# 定义RAG工作流类
class RAGWorkflow:
    def __init__(self, storage_dir: str = "./storage"):
        self.storage_dir = storage_dir
        self.llm = None
        self.query_engine = None

    @step
    async def setup(self, ctx: Context, event: StartEvent):
        """工作流设置步骤:初始化LLM,加载或创建索引。"""
        # 从事件中获取简历文件路径
        resume_file = event.resume_file
        self.llm = OpenAI(model="gpt-4o-mini")

        storage_path = Path(self.storage_dir)
        # 检查存储目录是否存在
        if storage_path.exists():
            # 从磁盘加载索引
            storage_context = StorageContext.from_defaults(persist_dir=self.storage_dir)
            index = load_index_from_storage(storage_context)
        else:
            # 解析文档并创建新索引
            parser = LlamaParse(api_key="你的API_KEY", result_type="markdown")
            documents = parser.load_data(resume_file)
            embed_model = OpenAIEmbedding(model="text-embedding-3-small")
            index = VectorStoreIndex.from_documents(documents, embed_model=embed_model)
            # 持久化新索引
            index.storage_context.persist(persist_dir=self.storage_dir)

        # 创建查询引擎
        self.query_engine = index.as_query_engine(llm=self.llm, similarity_top_k=5)
        # 触发查询事件
        await ctx.emit(QueryEvent(question=event.query))

    @step
    async def ask_question(self, ctx: Context, event: QueryEvent):
        """执行查询并返回答案。"""
        response = self.query_engine.query(event.question)
        await ctx.set_result(response)
        await ctx.emit(StopEvent())

# 实例化并运行工作流
workflow = RAGWorkflow()
# 假设我们有一个启动事件,包含简历文件路径和查询问题
start_event = StartEvent(resume_file="./fake_resume.pdf", query="申请人掌握哪些编程语言?")
result = asyncio.run(workflow.run(start_event))
print(result)

运行这个工作流,它会快速返回答案,因为如果索引已存在,它会直接从磁盘加载,而无需重新解析文档。

注意:当前工作流存在一个小缺陷。如果你第二次运行时传入一份不同的简历文件,代码会发现磁盘上已存在旧的索引,因此不会解析新的简历。目前你不需要修复它,但可以思考一下如何改进。

总结

本节课中,我们一起学习了如何为智能代理构建RAG能力。我们使用LlamaParse解析了简历文档,将信息嵌入到向量存储中,并创建了查询引擎。接着,我们将RAG查询功能封装成一个代理可以调用的工具。最后,我们将所有组件整合到一个可复用、事件驱动的工作流中。这为创建能够处理更复杂任务的智能代理系统奠定了坚实的基础。在下一课中,我们将为代理赋予更复杂的任务。

005:表单解析与智能代理工作流 🧩

在本节课中,我们将学习如何让智能代理处理更复杂的任务。具体来说,我们将指导代理解析一份职位申请表,将解析出的表单内容转化为一系列简单问题,并利用这些问题来查询RAG(检索增强生成)管道。我们将再次使用LlamaParse来提取需要填写的字段,然后生成对RAG系统的查询。


导入必要库与设置

首先,我们需要导入必要的库并进行基础设置。这与上一节课的步骤类似。

import os
from llama_parse import LlamaParse
from openai import OpenAI
# 假设有辅助函数来获取API密钥
from helpers import get_openai_key, get_llamaparse_key

我们还需要设置Ancchio(假设这是一个工作流管理工具)并获取API密钥。

import ancchio
openai_key = get_openai_key()
llamaparse_key = get_llamaparse_key()

执行以上代码以完成初始化。


解析申请表

在上一节课中,我们使用LlamaParse来解析简历并包含了解析指令。这次我们将做类似的事情,但指令会更高级:让模型读取一份申请表,并将其转换为一个需要填写的字段列表,并以JSON对象的形式返回。

首先,设置我们的解析器。我们要求结果类型为Markdown,但这次我们提供两套指令:内容指导指令和格式指令。

parser = LlamaParse(
    api_key=llamaparse_key,
    result_type="markdown",
    parsing_instructions="这是一份职位申请表。请创建所有需要填写的字段列表。",
    formatting_instructions="仅返回字段的项目符号列表。"
)

现在,我们可以调用load_data方法来加载我们的模拟申请表文件。

documents = parser.load_data("./fake_application_form.pdf")
parsed_result = documents[0].text
print(parsed_result)

如你所见,模型严格遵循了指令,输出了一个仅包含表单字段的项目符号列表。


将列表转换为JSON

大语言模型(LLM)的一个有用功能是能将人类可读的格式(如此列表)转换为机器可读的格式。我们将在这里实现这一点,要求模型将列表转换为一个包含字段数组的JSON对象。

为此,我们需要一个LLM。

client = OpenAI(api_key=openai_key)

我们将给LLM一个提示词和我们的字段列表。

prompt = f"""
这是一个已解析的表单,数据如下:
{parsed_result}
请仅返回JSON格式。
"""
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": prompt}]
)
json_output = response.choices[0].message.content
print(json_output)

你可以看到它返回了JSON。现在我们可以解析这个JSON并以编程方式打印字段列表。

import json
fields_data = json.loads(json_output)
fields_list = fields_data.get('fields', [])
print(fields_list)

完美。我们现在获得了一个清晰的字段数组,可以在后续的编程工作中使用。


将解析器集成到工作流中

现在,让我们将上一节课构建的工作流与这个新的解析器结合起来。我们需要两个新事件:parse_form事件和query事件。

以下是新的工作流代码,它更长一些,我们将逐行讲解。

class RAGWorkflow:
    def __init__(self, storage_dir, llm, query_engine):
        self.storage_dir = storage_dir
        self.llm = llm
        self.query_engine = query_engine
        self.context = {}  # 用于在步骤间共享数据

    def setup(self):
        # 触发解析表单事件
        self.emit_event("parse_form")

    def parse_form(self, event_data):
        # 这是我们上面编写的解析代码
        parser = LlamaParse(
            api_key=llamaparse_key,
            result_type="markdown",
            parsing_instructions="这是一份职位申请表。请创建所有需要填写的字段列表。",
            formatting_instructions="仅返回字段的项目符号列表。"
        )
        documents = parser.load_data("./fake_application_form.pdf")
        parsed_result = documents[0].text

        # 转换为JSON
        prompt = f"这是一个已解析的表单,数据如下:\n{parsed_result}\n请仅返回JSON格式。"
        response = self.llm.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )
        json_output = response.choices[0].message.content
        fields_data = json.loads(json_output)
        self.context['fields'] = fields_data.get('fields', [])
        print(f"解析出的字段: {self.context['fields']}")

        # 为每个字段触发查询事件
        for field in self.context['fields']:
            self.emit_event("query", {"field": field, "query": f"候选人关于'{field}'的信息是什么?"})
        self.context['total_fields'] = len(self.context['fields'])

工作流的其余部分暂时保持不变,我们稍后再处理。让我们执行这个工作流看看效果。

workflow = RAGWorkflow(storage_dir="./storage", llm=client, query_engine=None)
workflow.setup()
# 假设有一个事件循环来驱动工作流

它执行了与工作流外部代码完全相同的操作,这正是我们需要的。现在,工作流知道了它需要为哪些字段寻找答案。


实现并发查询与答案聚合

在下一轮迭代中,我们可以为每个字段触发一个query事件,它们将被并发执行。还记得在第2课中我们讨论过并发步骤吗?

我们要做的修改是:

  1. 为从表单中提取的每个问题生成一个query事件。
  2. 创建一个fill_in_application步骤,它将接收所有问题的回答,并将它们聚合成一个连贯的响应。
  3. 添加一个response事件,将查询结果传递给fill_in_application

让我们看看具体实现。首先,定义我们的事件,这里有一个新事件response

以下是更新后的工作流核心部分:

    def ask_question(self, event_data):
        # 模拟查询RAG管道并获取答案
        field = event_data['field']
        query = event_data['query']
        # 这里应调用实际的query_engine
        # simulated_answer = self.query_engine.query(query)
        simulated_answer = f"这是关于{field}的模拟答案。"
        # 触发响应事件
        self.emit_event("response", {"field": field, "answer": simulated_answer})

    def fill_in_application(self, event_data):
        # 收集所有响应事件
        responses = self.collect_events("response", self.context['total_fields'])
        if not responses:
            return  # 尚未收集到所有响应

        # 将所有回答整理成列表,传递给LLM
        answers_list = [f"{r['field']}: {r['answer']}" for r in responses]
        prompt = f"请根据以下信息回答申请表上的问题:\n" + "\n".join(answers_list)
        final_response = self.llm.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        ).choices[0].message.content

        print("最终填表结果:")
        print(final_response)
        # 触发停止事件
        self.emit_event("stop")

collect_events方法(假设在工作流引擎中实现)会等待特定数量的事件被触发。我们将之前存储的字段总数total_fields保存在上下文中,以便跨步骤使用。

现在,让我们执行这个完整的工作流。

# 传入模拟简历和申请表路径
workflow.run(resume_path="./fake_resume.pdf", form_path="./fake_application_form.pdf")

这次可能会花费一点时间,因为它需要询问一系列问题。

完成之后,你可以看到LLM已经解释了它的操作,并给出了所有字段及其答案的编号列表。你的工作流提取了表单中的所有字段,并为它们生成了合理的答案。

不过,有些字段的答案可能还有改进空间,例如“作品集”字段。在原始文档中,作品集是一个链接,而这里它试图列出作品集内容。我们将在下一节课中利用反馈机制来尝试修正这个问题。


总结

在本节课中,我们一起学习了如何扩展智能代理的能力:

  1. 解析复杂表单:使用LlamaParse和高级指令从申请表中提取结构化字段列表。
  2. 格式转换:利用LLM将人类可读的列表转换为机器可读的JSON格式。
  3. 集成与并发:将解析步骤集成到事件驱动的工作流中,并为每个字段触发并发查询。
  4. 答案聚合:通过收集所有并发查询的响应,并使用LLM合成最终的回答来填写申请表。

通过以上步骤,我们构建了一个能够自动解析表单、生成问题、查询知识库并汇总答案的智能代理工作流。在下一节课中,我们将为这个代理添加反馈机制,使其能够根据我们的意见进行修正并重新尝试。🚀

006:人机协同反馈循环 🎯

概述

在本节课中,我们将学习如何为上一节创建的自动表单填写代理添加人机协同(Human-in-the-Loop)反馈机制。我们将修改工作流,使其能够接收人类反馈,并根据反馈改进答案,从而生成更准确的结果。


工作流重构:分离解析与生成

上一节我们介绍了如何自动识别表单字段并生成答案。本节中,我们来看看如何将表单解析与问题生成步骤分离,以便支持多次反馈循环。

为了实现人机协同,我们需要使用两个新的事件:input_requiredhuman_response。它们专门设计用于允许工作流暂停并接收外部输入。

由于现在可能需要循环多次提问,我们不需要每次都解析表单。因此,我们将重构工作流,将原来的单一步骤拆分为多个步骤。这种重构在创建更复杂的工作流时非常常见。

新的 generate_questions 步骤将由两个事件触发:

  1. 表单解析器触发的 generate_questions 事件。
  2. 获取反馈后触发的 feedback 事件(即我们的循环)。

然后,我们将发出一个 input_required 事件。在同一流程中,工作流将暂停,等待 human_response 事件(即外部输入)。最后,使用 LLM 解析反馈,并决定是继续输出结果,还是需要循环回去重新生成答案。

让我们开始吧。首先,导入必要的库和设置 API 密钥。

import asyncio
# ... 其他必要的导入
# 设置 API 密钥


定义新的事件与工作流

现在,像往常一样设置我们的事件。这里新增了一个 feedback 事件。

# 定义事件类
class GenerateQuestionsEvent:
    pass
class InputRequiredEvent:
    pass
class HumanResponseEvent:
    pass
class FeedbackEvent:
    pass
# ... 其他事件

让我们逐步分析新的工作流。

# 工作流定义示例
@workflow
async def my_workflow(context):
    # 1. 设置步骤(与之前相同)
    # 2. 解析表单步骤(已修改)
    #    解析表单后,将字段存入上下文,并发送 GenerateQuestionsEvent
    # 3. 生成问题步骤(新增逻辑)
    #    可由 GenerateQuestionsEvent 或 FeedbackEvent 触发
    #    检查是否有反馈,并据此修改问题
    #    为每个字段触发查询事件
    # 4. 收集答案步骤
    # 5. 发出 InputRequiredEvent,等待人类反馈
    # 6. 获取反馈步骤,接收 HumanResponseEvent
    #    使用 LLM 判断反馈内容
    #    若反馈为“OK”,则停止;若为其他,则触发 FeedbackEvent 循环

初始的设置和表单解析逻辑基本保持不变。关键变化在于,我们将表单解析与问题生成分离。解析表单得到字段列表后,将其存入上下文,然后发送 GenerateQuestionsEvent

generate_questions 步骤可以由 GenerateQuestionsEvent 触发,也可以由后续的 FeedbackEvent 触发。


实现生成问题与收集答案

generate_questions 步骤中,我们首先从上下文中获取需要填写的字段列表。

以下是该步骤的核心逻辑:

# 伪代码逻辑
fields_to_fill = context.get(‘fields_to_fill’)
total_fields = len(fields_to_fill)
context.set(‘total_fields‘, total_fields)

for field in fields_to_fill:
    # 检查是否存在反馈,若有则附加到问题中
    question = f“What is the {field}?”
    if hasattr(event, ‘feedback’) and event.feedback:
        question += f“ Note: {event.feedback} (This may not be relevant to this field.)”
    # 触发查询事件
    emit(QueryEvent(question=question, field=field))

与之前一样,我们存储字段总数,以便 collect_events 步骤知道需要等待多少个答案。

ask_questions 事件保持不变。但在 fill_application_form 步骤中,我们现在会发出一个 input_required 事件。我们仍然等待收集所有答案事件,完成后将其转换为问题列表。新的操作是,我们将这个问题集存入上下文以备后用,然后发出 input_required 事件。

这个事件会向人类发送一条消息,展示当前表单填写情况,并请求人类给出反馈。


处理人类反馈

在名为 get_feedback 的下一步中,我们接收一个 human_response 事件。这与之前的步骤不同,之前的步骤总是由前一步骤的事件触发。

现在工作流中出现了一个“间隙”:input_required 事件不是由工作流内部触发的,它需要被外部捕获;同样,human_response 事件也不是在工作流内部发出的,它必须来自工作流外部。

一旦我们获得反馈,就会要求 LLM 判断反馈是“好”(一切正常)还是“坏”(需要修改)。如果 LLM 认为一切正常,它将发出“OK”信号,工作流将停止。如果 LLM 发出“feedback”信号,那么我们将触发一个 feedback 事件,从而开始循环。

那么,我们如何实际获取反馈呢?实际上,方法你已经知道了。input_required 事件是事件流中的一个事件,就像之前发送的 progresstext 事件一样。你可以用同样的方式拦截它,并使用上下文上的 send_event 方法发回一个 human_response 事件。

所有这些都发生在执行工作流的代码中。

# 执行工作流并处理反馈的示例
async def run_workflow():
    handler = await my_workflow.run()
    async for event in handler.stream_events():
        if isinstance(event, InputRequiredEvent):
            # 打印当前表单状态给用户看
            print(“Current form answers:“, context.get(‘questions’))
            # 获取键盘输入作为反馈
            response = input(“Please provide feedback (or type ‘OK’ if fine): “)
            # 将反馈作为 HumanResponseEvent 发送回工作流
            await handler.send_event(HumanResponseEvent(response=response))
        # ... 处理其他事件
    await handler

当我们看到 input_required 事件时,会打印出表单内容供用户查看,然后使用 input() 方法获取键盘反馈。一旦在 response 变量中获得反馈,我们就使用 send_event 方法将其作为 human_response 事件发送回去,然后等待处理程序继续。


整合反馈以改进答案

目前,我们的工作流可以接收反馈并循环,但还没有找到整合反馈的方法,所以它只会以完全相同的方式重复操作。现在,让我们进一步修改,以便实际利用生成的反馈做一些有用的事情。

这涉及到检查是否存在反馈,并将其附加到问题中。在这个简化的例子中,我们将把反馈附加到每个问题上(以防相关)。更复杂的代理可能只将反馈应用到相关的字段上。

以下是修改后的 generate_questions 步骤逻辑:

# 修改后的生成问题逻辑
if hasattr(event, ‘feedback’) and event.feedback:
    feedback_text = event.feedback
    for field in fields_to_fill:
        modified_question = f“What is the {field}? Note: {feedback_text} (This may not be relevant to this field.)”
        # 使用修改后的问题触发事件
else:
    # 使用原始问题触发事件

现在,当工作流再次运行时,LLM 将看到附加了反馈的问题,从而知道如何调整答案。

例如,第一次运行时,“项目组合”字段可能生成了一段描述。在收到“组合应该是一个URL”的反馈后,第二次运行时,LLM 会看到这个提示,从而为该字段生成一个链接。


总结

本节课中,我们一起学习了如何为 LlamaIndex 智能代理工作流添加人机协同反馈循环。我们重构了工作流,引入了 input_requiredhuman_response 事件来接收外部输入,并修改了问题生成逻辑以整合人类反馈。现在,我们的代理能够理解自然语言反馈,并根据反馈迭代改进其输出,从而生成更准确、更符合要求的表单填写结果。这体现了 LLM 作为人类助手、增强而非取代人类工作的强大能力。

007:使用语音交互的智能代理工作流 🎤

概述

在本节课中,我们将学习如何为智能代理添加语音交互功能。我们将把之前基于文本的反馈机制,升级为通过自然语言语音进行交互。你将学会如何集成音频处理模型,并创建一个能够“听懂”用户语音指令的智能代理工作流。


回顾与过渡

上一节我们介绍了如何通过文本为代理提供反馈。本节中,我们来看看如何让代理接收并处理用户的语音反馈。我们将为代理添加多模态能力,使其能够捕获用户说出的音频反馈。

首先,让我们通过可视化工具回顾一下目前已构建的工作流。

以下是所有必要的导入语句和云服务密钥配置。

# 导入必要的库和配置密钥
import ...
api_key = "your_api_key_here"

这是我们上一课使用的完整工作流代码。

# 完整的Warag工作流定义
workflow = ...

执行上述代码后,我们使用可视化工具来查看工作流结构。

工作流相当复杂,因此我们需要稍微缩小视图。这个可视化非常有趣,因为它清晰地展示了我们遵循的完整“快乐路径”。

流程从开始、设置、解析表单、生成问题、提问、填写申请表开始。接着是一个外部步骤,用于获取人类反馈。获得反馈后,你可以选择停止,或者完成循环并再次执行。


从文本到语音的转变

现在,我们来做一个更有趣的改动:将文本反馈改为用户大声说出的实际语音。

为了实现这一点,我们将使用OpenAI的另一个模型:Whisper。LlamaIndex内置了使用Whisper将音频文件转录为文本的方法。

以下是一个函数,它接收一个文件并使用Whisper返回文本。

def transcribe_audio(file_path):
    """
    使用Whisper模型将音频文件转录为文本。
    参数:
        file_path: 音频文件的路径。
    返回:
        转录后的文本字符串。
    """
    # 加载文件
    audio_file = load_file(file_path)
    # 实例化Whisper阅读器
    whisper_reader = WhisperReader()
    # 从文档中获取文本
    document = whisper_reader.load_data(audio_file)
    text = document[0].text
    return text

这与我们之前用于RAG的文档类型相同。在使用它之前,我们需要从麦克风捕获一些音频,这涉及一些额外的步骤。

首先,我们创建一个回调函数,将数据保存到一个全局变量中,以便在获得转录结果后有地方存储它。

transcription_global = None

def store_transcription(text):
    global transcription_global
    transcription_global = text

创建音频捕获界面

接下来,你将使用Gradio。Gradio有特殊的组件,可以在笔记本内渲染,用于创建从麦克风捕获音频的界面。

当音频被捕获时,它会调用transcribe_speech函数处理录制的数据,并调用store_transcription存储结果。

以下是定义输入、输出并运行该函数的代码。

import gradio as gr

def transcribe_speech(audio):
    # 此处是音频转录逻辑
    text = transcribe_audio(audio)
    store_transcription(text)
    return text

# 定义Gradio界面
inputs = gr.Audio(source="microphone", type="filepath")
outputs = gr.Textbox(label="转录文本")
interface = gr.Interface(fn=transcribe_speech, inputs=inputs, outputs=outputs)
interface.launch()

在Gradio中,你进一步定义了一个包含此麦克风输入和输出的可视化界面,然后启动它。我们创建了一个Blocks界面,放入一些标签页,然后启动它。

Gradio为我们设置了这个非常酷的界面,它可以监听我们的麦克风。让我们来试试。

我们说:“Hey, computer, can you hear me?”,然后提交。它正确地转录了音频。很好。

Gradio非常强大,只需几行代码就能完成所有这些并打印出转录文本。

让我们确保转录结果已保存在之前设置的全局变量中。

print(transcription_global)

结果在那里。我们将再次运行Gradio,因此最好关闭正在使用的Gradio界面,否则你的Gradio界面会相互冲突。


构建转录处理器类

现在,我们将创建一个全新的类:TranscriptionHandler。我将逐步讲解它的功能。

首先,我们创建一个队列来保存我们的转录值。每次我们录制一些内容时,都会使用store_transcription方法将其放入队列。

import queue
import threading
import time

class TranscriptionHandler:
    def __init__(self):
        self.queue = queue.Queue()
        self.interface = None

    def store_transcription(self, text):
        """将转录文本存入队列"""
        self.queue.put(text)

    def create_interface(self):
        """创建Gradio录音界面"""
        # ... 界面创建逻辑,与之前类似,但调用self.store_transcription
        def gradio_transcribe(audio):
            text = transcribe_audio(audio)
            self.store_transcription(text)
            return text
        # 创建并启动Gradio界面
        self.interface = gr.Interface(...)
        self.interface.launch(inbrowser=True, share=False)

    def get_transcription(self):
        """从队列中获取转录文本,每0.5秒检查一次"""
        while True:
            try:
                # 非阻塞方式从队列获取
                text = self.queue.get_nowait()
                # 关闭界面
                if self.interface:
                    self.interface.close()
                return text
            except queue.Empty:
                # 队列为空,等待0.5秒再检查
                time.sleep(0.5)

create_interface方法与之前使用的界面和转录逻辑相同,只是它将结果存储在队列中而不是全局变量里。你可以看到它调用了我们在这里定义的self.store_transcription

然后我们像之前一样启动转录界面。但我们做的新事情是,每0.5秒轮询一次,等待有内容进入队列。这个while True循环将永远继续,每次休眠0.5秒。但如果队列中有内容,它会注意到,并关闭界面,然后返回结果。

现在你有了一个转录处理器,你可以在工作流中获取人类输入时使用它,而不是键盘接口。


集成语音反馈到工作流

我们完全不需要为了使其工作而更改工作流。因此,和之前一样,我们设置工作流,传入我们的模拟简历和模拟申请表,然后等待一个input_required事件。

现在,我们不再获取键盘输入,而是调用我们的转录处理器。一旦转录处理器给我们提供了转录文本,我们就将其作为human_response事件发送出去。

到了关键时刻,让我们试试看。

我做了个改动,但没有告诉你:为了让我们更清楚地了解底层发生了什么,我让它打印出它在思考时得到的每一个问题和答案。

你可以在这里看到它正在这样做。

好的,它已经问完了所有问题。

现在它启动了一个Gradio界面,向我们征求反馈。

从问题的答案中我们可以看到,它犯了和之前一样的错误:项目组合是候选人做过的项目列表,而不是一个URL。

因此,让我们使用语音反馈来告诉它我们需要更改这一点。

我们说:“The portfolio field should be a URL.”

我们提交。

LLM读取了该转录文本,并正确地判定我给出了一些反馈。

现在你可以看到我们的问题和答案正在生成。每一个都附加了这个额外的反馈字段。每个问题都得到了相同的反馈:“portfolio字段应该是一个URL”,而这实际上只对其中一个问题有用。你可以思考一下,如果在一个更生产化的应用中,你会如何改进这一点。

它再次问完了所有问题,并启动了另一个Gradio界面征求我们的反馈。

让我们告诉它这次做得很好,因为它确实做得很好。项目组合是一个URL,正如我们所要求的。

我们说:“That‘s great. Good job.”

LLM正确地将我的积极反馈解释为一切正常,并输出了表单。


总结 🎉

恭喜你!你已经成功创建了一个能够响应人类语音反馈的AI代理。

本节课中,我们一起学习了:

  1. 回顾复杂工作流:通过可视化工具理解了代理的完整执行路径。
  2. 集成语音识别:利用OpenAI的Whisper模型将音频转换为文本。
  3. 构建交互界面:使用Gradio快速创建麦克风音频捕获界面。
  4. 创建处理器类:设计TranscriptionHandler类来管理音频输入队列和界面生命周期。
  5. 无缝替换反馈方式:将原有的文本输入节点替换为语音输入节点,而无需改动核心工作流逻辑。
  6. 实现语音交互循环:代理能够接收语音反馈,理解其内容,并根据反馈调整其行为,完成多轮交互。

你现在拥有一个可以通过自然语言对话进行指导和修正的智能文档处理代理了。

008:总结与展望 🎉

在本节课中,我们将对之前学习的事件驱动智能代理文档工作流进行总结,并展望未来的应用方向。


上一节我们详细探讨了代理如何响应人类反馈以完善文档。现在,我们来对整个课程的核心内容进行回顾。

恭喜你,现在你已经掌握了如何设计一个代理工作流,该工作流能够填写文档并响应人类反馈,从而生成更准确的已填写表单。

我期待看到你将独立构建出怎样的应用。😊


本节课中我们一起学习了构建事件驱动智能代理文档工作流的完整流程。从理解基础概念,到设计代理逻辑,再到实现反馈循环以提升输出准确性,你已经掌握了创建能够动态处理文档的智能系统的关键知识。希望你能将这些知识应用到实际项目中,创造出有价值的解决方案。

posted @ 2026-03-26 08:11  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报