首页 / 文章

使用LangGraph的YouTube问答修正型RAG流程中出现的问题。

分块、嵌入限制、LangGraph状态、异步处理问题及转录速度:在构建YouTube问答机器人时发现的错误及其解决方法。

发布者:REACTAPP.TOP阅读约 7 分钟1520 词

来源: Building a Corrective RAG Pipeline for YouTube Q&A: What Broke and Why?

改进型的RAG系统是一种检索增强生成框架,与普通版本相比多了三个组成部分:

  1. 一个评估模型,用于判断每个检索到的片段是否与用户的问题相关。
  2. 一个查询重写器,用于重新构建问题以便更精准地检索信息。
  3. 当检索到的内容均不相关时,可启用网页搜索作为备用方案。

这些技术背后的项目是一个用于YouTube视频的问答机器人。用户只需提供视频URL和问题,它就会给出答案。该系统还能处理没有字幕的视频,这一点非常重要,因为LangChain的YouTube加载器只有在存在字幕时才能正常工作。其处理流程为:下载视频,使用语音转文字模型(Whisper)将其转录为文本,将文本嵌入到模型中,把向量存储在ChromaDB中,最后通过LangGraph图结构来回答问题。

由于该任务涉及状态跟踪、重试和备用方案,LangGraph比LangChain链或一堆if-else分支更适用。通过节点、边以及共享状态对象来处理任务,还能揭示RAG应用的实际运作方式。过程中的诸多错误带来的收获比成功更多,这些错误被汇总在下方。

分块

在任何RAG系统中,若分块步骤出错,往往不会产生明显问题。常见的方法有:

  1. 递归字符分割法会遍历一系列分隔符,不断切割文本,直到各部分符合大小限制。
  2. 语义分块法会根据含义对句子进行分组,例如使用BERT风格的模型计算句子相似度,然后将最相似的句子合并在一起。
  3. 文档特定分割法则遵循源文本的格式,这类方法多用于代码处理。
  • 分层式的父子分块方式会保留较大的父片段,并从中分割出较小的子片段。
  • 最初,语义分块似乎是自然的选择。视频涵盖各种 imaginable的主题,因此按意义分组显得合乎逻辑。但随后出现了一个奇怪的现象:对于时长在十分钟以内的文字记录,分块工具往往只会生成一个片段。深入分析后找到了原因——短视频通常会围绕一个主题进行深入探讨,而当整段文本都涉及同一主题时,语义分块就没什么可分割的。

    接着采用了递归字符分割方法,但又出现了第二个错误:将chunk_overlap的值设为零。虽然这样分出的片段本身还算不错,但结果却变得更差。由于没有重叠部分,检索到的片段无法体现前文内容,导致生成模型缺乏关于该片段所指内容的上下文,常常会将不同细节混为一谈。

    最终采用的方案是带有实际重叠的递归分割方式,且不针对该用例进行语义分块。该方法能够处理长短不同的视频,并生成质量更好的字幕片段。在包含10个查询、k值为5的小型测试集上,该流程的平均互反排名达到了0.81。由于样本量极小,应将其视为大致参考指标而非基准值。

    选择嵌入模型

    该机器人需要处理各种类型的视频,因此涉及的主题涵盖各个领域。当视频超出特定领域的训练范围时,专用嵌入模型可能会迅速失效。考虑到通用性以及有限的硬件条件,最终选择了all-MiniLM-L6-v2——这是一种快速、轻量级的模型,在几乎所有场景下都能达到不错的效果。

    问题在于输入长度有限。该模型仅能处理约256个标记,超过此范围的文本会被截断,这意味着长文本只能使用其前256个标记进行嵌入。这样一来会导致数据无声丢失且嵌入结果不均匀,进而影响应用的质量。

    解决办法是将chunk_size设置在模型允许的范围之内。按字符分割时,可依据“一个标记大约对应四个字符”的经验法则来控制长度,避免超过标记限制。例如将分割阈值设为800个字符,大约相当于200个标记,这样是合适的。

    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,      # Notice: 800 characters limit here (~200 tokens)
        chunk_overlap=200,
        length_function=len,
        is_separator_regex=False,
    )
    docs = text_splitter.create_documents([text])
    sentence_transformer = HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2"
    )
    vector_store = Chroma(
        collection_name=get_config("vectorDB_collection_name"),
        embedding_function=sentence_transformer,
    )
    

    解决了两种最常见的检索问题后,接下来的问题则与 LangGraph 本身相关。

    LangGraph 状态与 ChromaDB

    LangGraph 应用中的每个节点都通过状态对象来共享数据。每个节点都会接收该状态并对其进行更新。定义状态有两种常见方式:

    1. TypedDict,一种用于字典的类型提示方案。它能进行静态检查,但无法在运行时进行验证。
    2. Pydantic,可在运行时验证数据,并在类型不匹配时抛出错误。

    当状态为 Pydantic 的 BaseModel 时,所有属性都会被验证。如果将 ChromaDB 客户端这类复杂对象放入其中,会立即出现错误。在状态类中设置 arbitrary_types_allowed=True 即可让自定义类型顺利通过验证。

    还有一个更严重的问题存在。LangGraph中的诸如检查点器、持久化内存以及LangSmith可观测性功能有时会序列化并复制状态。简单类型可以顺利序列化,但ChromaDB实例却可能引发序列化错误。序列化只是将数据转换为可存储或传输的形式而已。

    更安全的设计是将ChromaDB完全置于状态之外。这样架构也更清晰,因为数据库资源不应与普通数据混杂在一起。需要使用数据库的节点仍然可以通过传递给config参数来访问它:

    app.aupdate_state(
        config={
            "configurable": {
                "thread_id": thread_id,
                "recursion_limit": 30,
                "vector_store": vector_store,      # Pass the Vector DB here
            }
        }
    )
    

    异步执行与 input() 陷阱

    LangGraph 的一大优势在于支持异步执行。它为同步调用提供了对应的异步版本:用 ainvoke() 代替 invoke(),用 aupdate_state() 代替 update_state()。借助这些函数,你可以编写异步节点,从而打造响应更快的应用程序。

    破坏这一优势的最简单方法就是在节点内部调用 input()。它会阻塞执行过程以等待用户输入,因此即便是在完全异步的应用中也会失去异步带来的好处。

    解决办法是在节点外部收集用户输入,然后通过 aupdate_state() 将其传递给图结构:

    await app.aupdate_state(
        config={
            "configurable": {"thread_id": thread_id, "recursion_limit": 30}
        },  # adjust as needed
        values={
            "user_query": user_query  # Send the user inputs using the 'values' property
        },
    )
    

    本地转录或云端转录

    转录是整个项目的起点,但也成了瓶颈。最初的尝试使用了本地语音转文本模型 whisper-small,但存在两个问题:

    1. 准确性。该模型适用于对硬件要求较低的本地环境,但其准确率与其他选项相比仅处于中等水平。
    2. 延迟。在资源有限的条件下,生成转录文本需要很长时间。测试设备上的实时系数约为1.03,这意味着转录时间与视频时长相同。对于超过五分钟的视频,用户必须等待这么长时间才能得到初步结果。

    实时系数是指转录时间除以音频时长。由于在转录完成之前问答功能无法使用,任何高于1的数值都会影响用户体验。

    这些数据并非机密信息,因此通过 Groq API 转移到云服务具有充分的合理性。这解决了两个问题:由于 Groq 使用的是 whisper-large-v3-turbo 模型,而在大多数情况下其性能优于 whisper-small,因此准确性得到了提升;同时更强大的云端硬件将实时处理时间缩短到了约 0.15,延迟也有所改善。

    这一选择的前提是数据不属于敏感信息。对于私人或机密资料,决策方式则会大不相同。

    快速检查清单

    以上经验可归纳为在信任此类处理流程之前应进行的几项检查:

    • 查看典型的短文本转录会生成多少个片段。无论分割器如何承诺,若片段数为 1,则属于警示信号。
  • 确认分割后的数据块大小在转换为标记后仍低于嵌入模型的输入限制。截断操作不会引发错误,因此需要主动进行检查。
  • 单独读取几段获取到的数据块。如果某段脱离上下文就毫无意义,说明重叠部分过小。
  • 将客户端及向量存储等连接对象置于图状态之外,通过配置进行传递。
  • 在指责框架运行缓慢之前,先检查图节点中是否存在input()之类的阻塞调用或同步网络请求。
  • 在实际运行转录任务的机器上测量其实时处理能力,再判断相关数据是否适合使用托管服务。
  • 未涵盖的内容

    这些笔记仅涵盖了在首次使用 LangGraph 时遇到的一些问题。还有其他主题需要单独探讨:系统提示词、对系统及各个组件的评估、REST 接口的构建,以及云部署。