基于大模型的播客RAG系统搭建攻略(二):进阶之路与避坑指南

第一部分:https://onlymarshall.com/2025/01/05/build-personal-podcast-rag/

第三部分:https://onlymarshall.com/2025/01/26/build-personal-podcast-rag-part-3/

(注:本文在Gemini模型协助下完成。不得不感慨大模型很多时候思考的比人类周全,人类只要提供一些基本元素,大模型就可以把内容打磨地得非常完善)

上一篇我们介绍了播客搜索问答系统的基本架构和初步选型,特别是在语音转录方面的一些尝试和思考。今天我们继续深入,聊聊在系统搭建过程中遇到的更具体的技术挑战以及一些解决方案。

索引/检索的优化与挑战

上一篇我们提到索引和检索环节主要依赖文本向量模型和向量数据库。看似简单,但真正落地会遇到不少问题,尤其是在播客这种持续更新的内容源上。

增量更新:让知识库保持新鲜

播客的一大特点是持续更新。每周甚至每天都会有新的内容产生。如果每次更新都重新处理所有播客数据,那将是巨大的资源浪费。因此,增量更新是必须要考虑的。我们的目标是只处理新增的播客和更新的播客。这需要我们在“播客下载/爬虫”阶段做一些改进:

  • 记录已处理的播客和音频文件: 我们需要一个机制来记录哪些播客和对应的音频文件已经被处理过了。一个简单的做法是维护一个数据库(或者是简单的CSV文件),记录播客的feed URL以及每个episode的发布时间或唯一的GUID。
  • 对比更新时间或文件大小: 在爬取时,对于已经记录的播客,我们可以通过HEAD请求获取最新的episode信息,对比发布时间或GUID。对于新的episode,或者发布时间有更新的episode,再进行下载和后续处理。
  • 文件大小与HEAD请求的妙用: 除了对比发布时间,我们还可以利用HTTP HEAD请求来获取音频文件的大小。如果新爬取的音频文件大小与之前记录的不同,则说明文件可能被修改过,需要重新下载和处理。这对于一些播客会修改音频描述或者替换音频文件的情况很有用。HEAD请求相比于直接下载整个文件要高效得多,大大节省了网络资源。
  • 处理删除的播客: 有些播客可能会下架某集播客。我们需要定期检查已处理的播客列表,对比最新的feed内容,将已删除的播客集从向量数据库中移除,保持数据的一致性。
  • 保留时间:播客毕竟是音频文件,几百期的内容加起来也有几十个GB,如果总是保留所有的冷文件,特别是对于个人来说,存储的成本也没有必要。

有了以上这些功能,我们每天只要定时跑任务就可以自动完成对比、下载、转录、处理以及更新数据库的工作。

大模型转录的“后遗症”:重复文字问题

上一篇我们提到利用大模型的长上下文能力进行音频转录非常方便。但实际使用中,我们发现一个潜在的问题:重复文字和错误退出。

问题示例

尤其是在音频质量不高、语速较快或者多人对话的场景下,大模型有时会重复生成一些片段的文字。这可能是由于模型对音频的理解不够准确,或者在拼接长文本时出现偏差。

如何解决重复文字问题?

  • 检测错误并重新转录:这种情况有点像代码中因为并发造成的bug,而大模型运行本身也有一定的随机性,所以很大概率重新转录就可以解决这个问题。
  • 更精细的音频切分: 虽然我们避免了为了输入长度而进行的粗暴切分,但适当的、基于语义的切分仍然有必要。例如,可以根据静音段落或者明显的语意停顿进行切分,这样可以减少模型处理长音频的压力,降低重复的概率。
  • 后处理去重: 在得到转录文本后,可以进行一些后处理操作。例如,可以设定一个滑动窗口,如果窗口内的文本重复出现,则进行删除。当然,这需要谨慎操作,避免误删有效信息。
  • Prompt优化: 在给大模型下达转录指令时,可以加入一些提示,例如“请勿重复生成已出现过的文字”,虽然效果可能有限,但有时也能起到一定的作用。
  • 尝试不同的模型参数或模型: 不同的模型或者相同的模型在不同的参数设置下,表现可能会有差异。可以尝试调整解码策略或者使用更擅长处理此类问题的模型。

Text Embedding的中文选择:Gemini的踩坑

上一篇我们提到在文本向量模型上“跌了个跟头”。这里就来展开说说。

最初,我们可能自然而然地会选择一些通用的、在英文领域表现优秀的Embedding模型。既然Gemini也提供了文本Embedding,我们自然也选择这个模型。

但是在生成Embedding、存入数据库,再通过查询的Embedding进行检索之后,我发现了一个严重的问题:Gemini的文本模型似乎完全没办法理解中文

以下面的例子看,我们通过Gemini text-embedding-004模型,完全不同问题产生的embedding前10维完全一样。如果加上了一些英文字母的扰动,生成的结果就完全不一样。推测原因应该是tokenization的阶段把所有的非英文字符全部映射成了<UNK>,所以结果没有区别。

解决的方案也很简单:换成OpenAI的Embedding模型(text-embedding-3-small),因为使用了ChromaDB的代码,切换embedding function只要改动几行代码就可以实现。

后注:最终在某个文档页面发现了这个模型并不是多语言的版本,但似乎在GenAI embedContent的API里只能调用embedding-001和text-embedding-004,如果要调用多语言模型需要换到其他API。

Chunk大小的权衡

将长篇的转录文本切分成更小的chunk(块)是进行向量化的必要步骤。 Chunk的大小直接影响着检索的准确性和效率。

  • Chunk太小: 可能无法包含完整的语义信息,导致检索时丢失上下文,返回的结果不够相关。例如,一句关于“苹果公司新发布的手机”的信息被切分成“苹果公司”和“新发布的手机”两个chunk,单独检索“苹果公司”时,可能返回大量无关的结果。
  • Chunk太大: 虽然包含了更多的上下文,但会增加向量的计算成本和存储成本。同时,如果一个chunk内包含多个主题,可能会导致检索时返回与用户问题不太相关的部分。
  • Embedding模型限制:大部分embedding模型限制输入在几个K token左右,比如Gemini GenAI是输入上限2K,输出768维,OpenAI输入是8k,输出1536维

如何选择合适的Chunk大小?

这是一个需要根据实际播客内容和模型能力进行实验和调整的过程。一些可以参考的策略:

  • 基于句子或段落切分: 这是比较常用的方法,可以保证chunk的语义完整性。
  • 固定大小切分并加上重叠: 将文本按照固定token数量切分,并在相邻的chunk之间添加一定的重叠部分,以保留上下文信息。例如,一个chunk包含100个token,重叠20个token,那么下一个chunk从第81个token开始。
  • 利用语义切分工具: 一些工具可以根据语义相似性来切分文本,使得chunk内部的语义更加连贯。

在我们的实践中,我们尝试了不同的chunk大小,并结合向量检索的效果进行评估。 出于简单的考虑我们选择了固定大小切分并加上重叠。参数是2000个token一个chunk,重叠是200个token,通过tiktoken的包进行本地的计算(因为不知道embedding模型的tokenzier配置,所以这里只是近似)

小结

这篇博文我们深入探讨了播客RAG系统搭建过程中的一些关键技术点和挑战,包括增量更新、大模型转录的重复问题、中文Embedding模型的选择以及chunk大小的权衡。虽然已经有了大模型的加持可以很快编写核心骨架代码(大概降低了一半的时间),但是需求的不断迭代和看到结果后目标不停的调整,还是有相当大量的人为介入。编程门槛的降低,可以让非专业人士和专业人士摆脱一些低级的信息查找,把时间投入更有效率的地方。

后续我会整理一下代码之后发布开源(现在大部分都在notebook里)。

Leave a comment