基于大模型的播客RAG系统搭建攻略(一):缘起,架构和选型

声明:本文仅介绍如何通过技术手段搭建系统,请遵守各个播客的用户协议。

第二部分:https://onlymarshall.com/2025/01/11/build-personal-podcast-rag-part-2/

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

缘起

开始听播客是在2016年初,那个时候刚拥有了人生第一辆车,也搬到了郊区自己的房子里,有比较固定的时间和空间。不知道是什么原因知道了在喜马拉雅上遇到了机核网的播客,那会儿已经开播了好几年,如获至宝,在两个月内把Gadio Pro栏目(特别是麦教授和四十二参与的)从第一期开始听完。之后随着机核的发展,转战机核App,直到2020年播客爆发,App也换成了小宇宙(意外发现就在五角场创智天地的前公司马路对面),形成了固定的收听习惯,订阅几十个中外栏目。

订阅的这些播客栏目,有些还是有相当的信息量。因为产品的音频形态,信息检索比较麻烦,有些播客即便刚刚听过,想回过头来找忘记或者漏掉的信息,也和很难定位到具体的时间点位重听。或者新接触一个播客,想知道以前聊过的内容里有没有自己感兴趣的话题可以快速加入收听列表。

架构

信息检索系统与搜索引擎

信息检索本质上可以分解成三个模块:

  • 下载/爬虫
  • 处理/索引
  • 检索/响应服务

传统的搜索引擎,这三部分对应了:

  • 爬虫系统:按照超链接把互联网的网页(文档)下载到本地
  • 索引系统:清洗数据,提取关键词,建立倒排索引(关键词->文档),索引更新
  • 检索系统:查询分析、排序结果文档

之所以要构建这三个系统,特别是爬虫和索引系统,主要原因在于整个互联网数据量太大,因此需要提前把文本内容下载并存储成检索友好的格式,这样用户通过关键词搜索就可以在几百毫秒之内得到结果。

Source: ByteByteGo https://blog.bytebytego.com/p/ep104-how-do-search-engines-work

RAG

随着大模型的出现,语义搜索也有了新的思路。这两年比较流行的方案是RAG(Retrieval Augementation Generation,检索增强生成,一个很拗口的名字)。我们也将采用这个方案。

RAG主要影响索引系统和检索系统,尤其是后者。传统的信息检索系统通过关键词匹配的方式搜索,直接把结果文档推给用户(再加上匹配到的关键词以及上下文高亮),但用户还需要自行处理加工。用户本质上是带着问题来的,比如“鸡蛋会增加胆固醇吗?”,“如何申请延迟提交报税表格”,关键词查询往往离最后的答案还有一定距离。

RAG利用大模型出色的语义处理能力,通过向量搜索(因为查询不再是关键词而是一个问题,传统的关键词查找加上倒排索引无法完成任务)查找到相关的文档,然后交给大模型去回答问题。RAG可以胜任语义搜索/问答的任务,但相对地使系统开销大大增加(10x),响应速度明显变慢(10x)。

来源:https://www.gptsecurity.info/2024/05/26/RAG/

播客搜索/问答架构

回到一开始的问题,我们搭建的系统也是由这几个模块组成:

  • 播客下载/爬虫:通过公开的RSS爬取播客音频和介绍信息;
  • 播客转录:把音频和介绍信息发送给语音到文字的引擎,变成文字稿,同时将文字稿分割处理向量化,放进向量数据库;
  • 播客搜索:搭建RAG系统,把问题转成向量进行向量搜索得到相关的文字稿,再把文字稿作为上下文和问题一起发送给大模型,得到最终回答并返回给用户。

选型

开发工具

最重要的开发工具是用来生成代码的大模型,这个年代做这种原型开发似乎已经没人从零开始写代码,尤其是还需要很多查找文档或者调试的工作。这次开发没有用很火的Cursor(舍不得花20刀月费)或者Github Copilot(公司有,但自己的设备也要花钱),而是体验阶段的Gemini 2.0 Flash Thinking Experimental搭配Google AI Studio。另外只是自己使用,暂时没有上线的需求,所以只在Jupyter Notebook里运行。

Google AI Studio写需求生成代码

语音转录

一开始想直接用OpenAI的whisper,价格如果嫌贵(一分钟$0.006刀,对应1集1个小时$0.36,100集要36刀也不是很便宜),也可以考虑本地host(但没有具体benchmark手头的4080可以跑多快)。主要的限制有两个:

  • 模型比较旧,处理prompt(比如时间轴,嘉宾识别)的能力不强;
  • 最大处理能力25MB,一个小时的音频大概是50~70MB,需要把音频切块。可以使用mp3lst等开源工具在段落切分(根据dB),但是不同音频的切分threshold可能不同,切分如果太细也会影响转录的精度。

前期工具调研在这一步卡了几天,后来想到可以直接用长上下文的大模型进行转录。尝试了市面上主流的几个模型以后,发现Google Gemini Flash 1.5非常合适:

  • 转录效果不错,可以基本正确生成时间轴,识别讲话的嘉宾;
  • 上下文窗口足够大,1M token可以容纳超过5个小时的音频(最大支持9.5小时);
    • 免费!免费档每天1500次配额,一集播客大概1-3次请求即可完成转录,转录一集大概3分钟完成,正好足够单线程处理);后面的收费档,一集一小时播客的成本大概$0.18(更正应为 $0.018,比官方价格贵因为上下文长且有好几次的对话),是whisper的一半(更正:应为5%),如果使用1.5 Flash 8B价格还能再减半(效果差点)。

解决了输入长度的问题,下一步卡在了输出这一步。为了避免单次请求占用过多的GPU资源,大部分模型的输出卡在了4k/8k,大概只够放半个小时的音频转录内容。难道又要回到音频切分的老路?其实解决办法很简单,在与大模型对话中继续提示:“请根据上面已经生成的内容继续生成转录文字稿。”,就会在上次停下的地方继续转录,直到完成所有的转录。这也是大模型编程神奇的地方:通过自然语言下发指令,而不是严格定义的编程规格/接口。

利用大模型的长上下文来做转录固然比较方便,但是这会导致需要下一次请求附带整个对话(之前转录的内容加上音频),成本直接翻N倍(如果是N次输出)。如果考虑成本的话,进行切分再拼起来是更好的选择。

索引/检索

这块没有特别的要求,基本上各厂商的文本对话大模型和文本向量模型就能满足需求。不过后面在文本向量模型上跌了个大跟头,后面的文章细说。

本地向量数据库选用了ChromaDB,直接让大模型替我做了选择。

附录:问问小宇宙

小宇宙官方也有类似的应用问问小宇宙,UI挺特别的,但是似乎产品一直没有迭代,召回也比较一般(似乎是基于每篇播客的AI总结做的,因此漏掉很多细节)。

构建失败

Joel著名的12条测试里有两条与构建相关:

  • Can you make a build in one step?
  • Do you make daily builds?

这两条相信大部分有些规模的公司都能够做到。不过,有每日构建,就必然伴随着构建失败,特别是在多人协作的环境,由于代码依赖、开发人员失误(比如只签入了一部分代码)、代码合并等等原因,连续好几天构建都失败的情况也偶会发生。
失败的代码构建让人万分沮丧,首先打击了士气。没有人打算让伟大的项目在第一步就失败,就像70圈的F1在热身圈就冲出赛道退出比赛一样。另外还造成了大量的浪费,由于构建失败,同一天签入的代码就没法在第一时间得到测试,加大了项目后期测试的压力。而当你下载了最新代码,却发现连编译链接都无法通过,那估计你也没兴趣写代码了。
正如bug不可避免一样,构建失败也是不可避免的(特别是对于像C/C++这种有大量预编译的语言),即使构建成功与否是非常容易验证的,非黑即白。所以构建失败往往也被认为是最愚蠢的错误。不过再愚蠢的错误,也必须正面对待,而不是对犯错的哥们嘲笑或者怒吼了事。
对付一件坏事,基本上两种办法:要么预防发生,要么减小影响。

预防:

1. 降低构建的复杂度:太大的构建模块和构建链/工具链、太长的构建时间、过多的代码依赖、紧缺的构建服务器资源都会让开发人员倾向于减少构建的次数。我也不止一次看到,由于最后只改动了几个字符,开发人员懒得重新构建造成的构建失败。每个人都会偷懒(不是有句话说“偷懒就是美德”?),但如果降低偷懒的动机,偷懒自然会减少。合理的模块划分、分布式版本系统的使用(大大加快checkout时间)、更强的构建机器(CPU、内存,特别是SSD)、高效的构建方法(怎么着也得并行构建)。如果一次构建只需要15分钟而不是3个小时,没有人愿意冒风险悍然签入代码。经常是还有2个小时就下班但构建需要3个小时的情况下。
2. 惩罚:虽然嘲笑或者怒吼解决不了问题,不过一些善意的小惩罚还是可以接受的。微软90年代初的标准是5美元一次,按照收入比换算的话在国内大概20RMB应该还是合理的范围。不过国人似乎不喜欢现金,那么改成请大家吃零食也是个不错的主意。另外一个方法就是把某个恶搞标志放到TA桌子上,直到下一个人破坏了构建才能送出去。

减小影响:

3. 提高构建频率:把每日一次构建改成每日两次构建可以提早一半的时间发现问题。如果把构建频率提高到每天4-6次,并禁止在测试发布版构建和其上一次构建之间的签入任何代码(除非是修复破坏构建的代码),这样就保证至少有4-6小时的时间修复失败构建。(这个时间应该足够修复所有构建相关问题)。当然如果需要构建的版本特别多(特别是多平台发布和多分支维护的情况下),在可利用的资源条件下无法做到这个标准,可以采取差异化方案。主要工作版本和发布平台(即签入最频繁的构建)增加构建频率,几天才有一次代码签入的分支可以只做每日构建。这样就可以确保最值得关注的地方得到最多的资源。
4. 签入队列:签入的代码不马上进入开发的主分支,而是进入一个签入队列。每天签入队列里的改动要通过构建后才能进入主分支,如果当中有破坏构建的签入,则当天的所有签入都会被hold住直到问题解决。这样的代价是复杂性增加了,另外每日测试和最新的代码会有一天的延迟,不利于问题的及早发现(不过问题仍然可以追溯到某天签入代码的范围里)。
5. 专人负责:代码能否构建是软件的第一要素,这么重要的东西当然值得指派专人(或者是一个委员会)来负责。国际化团队需要在每个点(或者相近的时区)配备负责人,确保破坏构建的签入能够早几个小时被定位,甚至代替责任者修复一些愚蠢的问题。每当构建失败发生,委员会成员都会收到通知邮件,这时候他(们)应该停止手上的所有工作来解决这个问题,并在第一时间找到责任人通知其修复。如果暂时没法找到,也可以通知其peer或者在找到一两个相关reviewer的情况下自行修复。(建议与第6、8条配合使用就不会找不到责任者)
6. 自动构建:如果每次代码的签入都能够伴随一次完整的自动化构建,构建期间锁住代码库,并在确认构建结果后才允许正式签入,这样我们基本就不会碰到构建失败。这种方法其实是提高构建频度的极致。当然对于大型开发团队来说这并不现实,也可以做一些妥协,比如自动构建期间放弃代码库的锁定。毕竟由于多人同时签入代码导致的失败构建,所有人都可以理解的,而这种情况也相对较少。
7. 停止生产线:这条是丰田的做法。出现构建失败后停止所有的代码签入(当然修复构建的除外),直到下一次成功构建。期间构建负责人介入并定位问题(或者责任人自己认领)。这样有利于隔离问题,避免连续的构建失败。而夜间测试至少也有一个新的版本可以利用。
8.  签入时间限制:这条是从《微软的秘密》中看到的,所有人在下午2点之后不能签入代码。每天2点开始每日构建,一旦出现什么问题可以有人当场fix而不是将问题拖到第二天而浪费一整晚测试的宝贵时间。但对于国际化开发的团队显然不合适,实际一点的做法是要求所有人必须在下班前3小时之前做好当天的代码签入。另外,国际化开发的团队也总是有每日构建及测试的固定时间,一般会是在岸团队的夜里(想不到onshore的翻译了),可以规定在每日测试使用的版本构建前3小时内不允许的签入,而在这个构建前安排一个小的构建进行验证。总的原则是保证构建能够成功。
9. 签入前的公示:也是从《微软的秘密》里偷来的,特别是对于大规模的签入。公示可以是发邮件,也可以是在某个系统里登记一下。而每个人在签入之前也检查一下邮件或者系统。
小结一下减小影响的办法:及早发现、及早修复、隔离错误、拉长签入流水线。
最后是书托时间。关于软件开发的优秀实践,《微软的秘密》有很多很好的例子,虽然里面的有些相对现在流行的“敏捷开发”有些老土(书里写的基本上是92-95年的微软),但由于这么多年来软件开发的效率没什么本质上的提高,所以这些实践到现在仍然适用。