1. 收藏夹里的六百个视频,为什么我决定做一个问答系统
先说一个我自己的尴尬事。某天晚上我想找半年前看过的一个关于注意力机制的讲解视频,印象里UP主拿Transformer铺开讲了一整期,当时觉得讲得特别好,顺手点了收藏,还发了条弹幕说“期末就靠这期了”。结果期末真正到跟前,我在收藏夹里翻了整整二十分钟,愣是没找到那期视频。收藏夹里躺着的几千条视频,标题五花八门,有“必看!!!”有“看完这条你也能上手”,有“呕心沥血整理”,光凭标题完全看不出里面讲的是什么。
一个很反直觉的事实是:B站收藏夹是我过去几年积累知识密度最高的地方,但它几乎是不可检索的。首页推荐、搜索、稍后再看,平台给的都是“别人想看什么”,不是“我收藏了什么、收藏的东西之间有什么联系”。我需要的不是再看一遍视频,而是能针对收藏夹里的内容提问——比如“我收藏过的视频里,有哪些讲过Embedding和相似度计算的区别”“关于RAG的架构演进,我收藏过的视频都提到了哪些关键节点”——然后系统直接基于这些视频帮我组织答案。
这就是Bilibili RAG这个项目要解决的事情。RAG,全称Retrieval-Augmented Generation,检索增强生成,核心思路很简单:先从一个私有的知识源里把相关内容检索出来,再把这些内容作为上下文喂给大语言模型,让它基于这些材料生成回答。本质上就是给语言模型外挂了一个“可检索的笔记本”,它不凭空编造,而是先翻笔记再开口。而我要做的,就是把B站收藏夹——包括视频的字幕、弹幕、评论、简介——变成这个“笔记本”,最后输出一个能对话的本地知识库系统。
这个项目适合谁参考?如果你也攒了几百上千条收藏夹内容却很少回去看,如果你在看完某个技术视频后想快速回忆其中的关键结论,或者你本身对RAG技术栈想跑通一条完整的落地方案,这篇文章都值得你看完。前面是动机,后面全部是实操,包括数据怎么抓、文本怎么清洗、切块怎么切、检索怎么调优,以及我踩过的几个坑。
2. 整体链路拆解:从UP主视频到可回答的索引
在做任何一个RAG项目前,我建议先画一张数据流图,弄清楚自己到底在搬运什么。这个项目我把它拆成了五个环节,缺一个都不行。
第一环节是数据采集。B站收藏夹里的视频,可用的文本来源有几种:视频标题、UP主简介、视频分P标题、字幕(CC字幕或UP主上传的字幕文件)、弹幕、评论。这里面质量差异非常大。标题和简介信息量太少,弹幕噪声太大,评论内容漂移严重,真正高密度的是字幕。但字幕又不一定每个视频都有,所以我最后按优先级设计了采集策略:有字幕用字幕,没字幕用语音转写,实在不行才靠简介加弹幕兜底。这个顺序不是随便拍的,是整个项目里我对内容质量最重要的一个判断。
第二环节是内容清洗。B站字幕有双重压缩的问题:一是B站自己的CC字幕经常缺句缺段,二是有些字幕文件本身是二次压制过的,里面混着日文韩文碎片、广告、UP主口播的“关注三连”这些垃圾。清洗的时候要做去重、去噪、去无意义符号,还要做到“句级的连续性还原”——因为字幕是一句一句分离的,要拼回成连贯的段落,才能被后续的切分器正确理解。
第三环节是切分与向量化。这是RAG质量的地基,也是很多人做得最粗的一步。字符合并的问题在于视频这种内容天然地按主题分段,一段三分钟的讲解可能围绕一个公式展开。我后来用了层级化切分方案:先把同一期视频的字幕按语义边界切成长文本块,再在每个文本块内部做更细的段落切分。向量化用了一个常规的文本嵌入模型,模型参数不大,正常消费级显卡就能跑,或者直接走本地向量库。这个细节我放到第四节展开说。
第四环节是索引与检索。这一步我在Milvus里建了collection,字段包括视频ID、视频标题、UP主信息、文本块内容、时间戳、嵌入向量。查询的时候先做向量相似度检索,拿回Top-K候选,再做重排。这一步决定了用户提问“我收藏夹里哪些视频讲过不协调的Embedding对齐”,系统能不能答到点子上,而不是给你返回一堆字面上沾边但内容无关的视频。
第五环节是生成。生成端我用的是支持本地部署的对话模型,配合自定义的系统提示词。提示词里明确了回答规则:优先基于检索到的文本块作答,回答标注信息来源——哪一期视频、什么时间段的内容——如果检索内容不足,必须明确说“知识库中未找到相关内容”,而不是强行编造。这一步看着简单,其实直接影响用户体验。第一次跑通的时候,模型把我的提示词当耳边风,问什么都是一本正经地胡说八道。后来加了一个强约束判断逻辑:如果检索结果的相关分很低,就先一步拦截,直接告诉用户“这个问题的答案不在收藏夹里”,这才算真正闭环。
3. 数据获取与内容清洗:字幕、弹幕和评论的取舍
3.1 采集策略:优先字幕,兜底弹幕
接上节,我在数据采集里遇到的第一件事是:B站不像普通网站,它的页面是动态渲染的,接口也有风控和签名机制。如果你直接拿requests去抓,很大概率抓回来一堆空壳HTML。业内常用的做法是调用公开的非官方接口封装库,或者自己带上cookie和签名参数去请求。我的方案是先用浏览器登录态把cookie导出,然后在代码里配置请求头,模拟真实的播放器环境。这里有个容易翻车的细节:B站的接口风控会检查请求频率,短时间高频请求会触发验证码甚至封禁,所以采集端必须带限速和重试机制。我用了一个很保守的策略:单个视频的元信息请求间隔至少两秒,弹幕专人通道的间隔保持五秒以上,一批采集跑下来稳得多。
字幕采集是另一个关键分支。B站存在两种字幕:一种是UP主手动上传的CC字幕或JSON字幕,另一种是AI生成的自动字幕。自动字幕虽然能覆盖绝大多数视频,但错别字和漏句特别多。我在实测里发现,技术类视频的AI字幕准确率还算能看,但遇到专业名词密集的讲解视频,漏句率可能接近一成。所以采集顺序我调整为:先检查视频是否有CC字幕文件,没有就触发离线语音转写流程,把这个视频的音轨抽出来,用本地语音识别模型转一遍,再用字幕原来的句子边界做对齐。这里我用了ffmpeg抽音频,转写速度大概是一比三,一个十五分钟的视频五分钟转完,完全可接受。
3.2 清洗才是真功夫
采集下来的原始字幕直接进知识库,答案质量会很惨。我踩过一次印象很深的坑:某期视频字幕里反复出现“那个那个”“就是就是”这类口语填充词,文本块切出来之后向量化,语义被严重稀释,导致检索时经常把面无表情的口水话当成高相关文本返回。清洗这步我分了三道:
第一道是符号层清洗。去掉所有非中英文和数字的杂项符号,统一引号、括号的格式,把全角和半角数字做归一。第二道是口语层清洗:去掉重复的语气词、口播的“关注三连”“一键三连”“本期视频”等模板话,但要注意不能过度删除——有些口头表达本身承载了句义,比如“这个方法是……但有个问题”里的转折词不能丢。第三道是结构层清洗:把弹幕里的重复刷屏内容去重(比如满屏的“哈哈哈哈”只保留一条),把评论里的楼主楼层关系去掉,只保留有效文本。最后我会按视频ID做一次段落合并,把同一话题的连续句子拼接成逻辑连贯的大段落,再送去切分。
这里要给一个明确的建议:弹幕在大多数场景下不值得进知识库。我曾尝试把弹幕作为补充信号,结果检索时经常被“前方高能”“名场面”这类情绪弹幕带偏,还增加了不少无效文本。弹幕唯一用得上的场景是热度聚合分析——比如统计某期视频里弹幕高频词,辅助判断观看者关心什么,但那是另一个项目的事了。评论我的策略是只保留楼中楼主层且超过二十字的认真评论,它们的补充信息价值比弹幕高,但也只作为字幕的辅助,绝不喧宾夺主。
4. 切分与向量化:决定问答质量的第一道分水岭
4.1 先讲清楚切分为什么难
做RAG的人最容易犯的一个错,是拿通用文档的切分逻辑直接套到视频字幕上。通用文档切断句可以靠句号和换行,但视频字幕是“句子碎片”组成的——一个字幕块可能只有四五个字“这个公式”,下一个字幕块是“我们代入数据”,搁在一起才构成一个完整语义单元。如果按固定字符数硬切,很容易把一个完整的论证过程劈成两半,向量检索的时候就什么都匹配不上。
我采用的方案是两层切分。第一层做“语义段落识别”:“这个公式”和“我们代入数据”之间如果没有明显的停顿或话题切换,就合并到同一个语义段落。我试过用标点符号加停顿时间戳作为判断依据,更稳。第二层再对语义段落做定长切分,但切分边界尽量落在句子边界上,而不是硬凑字符数。最终文本块大小的选择上,我取了一个比较折中的值——单个文本块控制在五百个字符以内。为什么是五百?因为向量检索的精度和文本块的语义聚焦度强相关,块太小检索回来的是碎片,块太大语义被稀释。五百这个值是在我自己的收藏集上对比过几百次检索效果后选出来的,我建议你做项目时也花时间做一组类似的对比测试,而不是拍脑袋定死参数。
4.2 向量化和重排:本地模型该怎么选
向量化这块,市面上可选模型确实不少。但你要考虑自己的硬件和运行场景。B站收藏夹的知识库是个人本地服务,不需要走云端API,更看重的是稳定可控。我选的嵌入模型是常规开源文本向量模型,输出维度768,足够表达视频字幕这种中等复杂度的语义。你可能会问,为什么不用更大的模型?因为在本项目的场景里,大模型带来的精度提升非常有限,但显存占用和推理延迟直接翻倍。收藏夹这个场景的文本本身就是口语化的、碎片化的,模型再大也补不齐源头的信息损耗。我的判断是:嵌入模型做到“够用且快”,把更多资源留给后面的重排和生成。
重排器我叫它“最后一个守卫”。向量检索拿回Top-50的候选文本块,质量参差不齐,真正相关的可能只有七八条。重排器的作用是对这五十条重新打分排序,把真正贴合查询语义的往前顶。我用的是一种轻量的交叉编码器模型,输入是用户查询和文本块拼接起来的一对文本,输出相关性分数。这一步比单靠向量相似度精多了,但计算代价高一些——不过放心,只在Top-50上重排,消耗完全可以接受。我实测下来,加了重排之后,回答命中率大概提升了十几个百分点,最直接的表现就是用户问“收藏夹里有哪些视频讲过向量数据库的索引结构”,返回的不再是讲数据库概述的视频,而是真正展开讲HNSW、IVF的段落。
5. 检索增强生成:混合检索与调优经验
5.1 关键词和向量双轨检索
向量检索有个天生的盲区——它对专有名词、缩写、型号这类“精确关键词”不敏感。比如你问“P40显卡”,向量检索可能返回一堆讲显卡架构的内容,而不是真正提到“P40”这个型号的片段。我在项目里补了一条BM25全文检索轨道,和向量轨道并行跑,最终用RRF(Reciprocal Rank Fusion)把两路结果合并排序。原理不复杂:每个文档在两路结果里都有一个排名,RRF把每个排名的倒数相加,排名越靠前分越高,最后统一按综合分取前几十条,再进重排器。
实测下来,混合检索对这个场景非常关键。字幕文本里有大量代码片段、模型名称、版本号这类精确信息,BM25对这些是字面量匹配,一抓一个准;向量检索则擅长抓“语义相似但字面不同”的内容,比如“脑内筛选”和“心理过滤”这种表达。两路一合,覆盖面宽多了。RRF的权重我只做了最朴素的均权融合,没有做复杂的学习式排序,因为在对个人开发者的单机场景下,复杂排序带来的提升不值得那个维护成本。
5.2 不解决问题:提示词与拦截策略
生成侧我用了本地部署的开源对话模型,上下文长度只开了默认窗口。为什么这么吝啬?因为RAG场景里塞给模型的上下文远不是“越长越好”。如果两路检索加上重排后拿回十五条文本块全灌进上下文,模型反而会迷茫,分不清哪条是核心材料。我最终的做法是:重排后的Top-5文本块作为主要上下文,其他的只保留标题和摘要作为参考信息。每组检索回来的文本还带着它们对应的视频标题、UP主ID、时间戳这些元数据,一并交给模型,这样模型回答时能准确标注“根据某UP主一期关于某某的视频内容”。
最值得分享的是我在提示词之外加的一道硬拦截。单纯在系统提示词里写“请基于检索内容回答,不要编造”,模型的服从性时好时坏。所以我加了逻辑判断层:从重排器拿回来的最高分文本块,如果得分低于预先设定的阈值,直接判定“本次检索没有找到靠谱材料”,返回给用户的回答是“知识库中没有找到相关内容,请换一种问法或搜索其他关键词”,而不是让模型硬着头皮生成。这个设计看着简单,但它是用户体验质变的关键。一个负责任的RAG系统,最重要的能力之一是知道自己“不知道什么”。后来我把这个逻辑写成了可配置的阈值项,跑了多组测试之后定下默认值。B站收藏夹这种高噪声数据源,宁可少答,不要错答,否则用户问十次有两次是幻觉回答,这个系统就没有任何信任可言了。
调优过程中我做了个小实验,把收藏夹里的视频按“编程”“人工智能基础”“数据分析”“效率工具”四类分别建了子索引。检索时先判断用户提问属于哪一类,只在该类索引里检索。效果提升了,但代码复杂度也上去了。最后我放弃了自动分类硬路由,改成了轻提示:在用户输入query时先用模型做一个粗分类提示,把类别标签拼进检索逻辑。这样既保证检索粒度,又不用维护复杂的分类服务。这个变通的思路值得借鉴:一个RAG系统里,不是所有模块都要做到极致复杂,把复杂留给真正影响体验的环节。
5.3 对话式追问的设计
“收藏夹变成可对话的知识库”这句话里的关键在于“可对话”。不只是用户问一句、系统答一句,还要能针对上一轮的回答继续追问。我的设计是让每一轮历史对话也参与检索:拼接最近两个轮次的问题和回答摘要,一起生成向量去做检索。这样用户问“那HNSW是什么”,系统能顺着上一轮“向量数据库索引”的话题,直接定位到收藏夹里讲HNSW的具体视频段落,而不是重新泛泛地搜一遍。
这个方案带来的问题也很典型:历史对话的加入会增加检索噪声。我试过拼接全部历史对话,结果显示系统开始答非所问——因为历史会话里包含了太多和当前问题无关的上下文。最后我只保留当前轮次加最近一轮的问答对,效果最好。做类似项目的人可以直接抄这个思路,但要根据自己的使用习惯微调历史轮次的数量。
6. 落地过程中的坑与躲坑姿势
单机RAG系统里,最容易翻车的往往不是模型本身,而是那些“看似不起眼的基础设施”。我总结几个自己踩过的和看朋友踩过的坑。
6.1 文本块归属信息丢失
这是我踩过最深的一个坑。最初我向量化文本块时只管嵌入向量,忘了把视频ID、时间戳、所在分P这些归属字段一并存进向量库。后来检索到一条很好的文本块,模型引用了它,但是回答里完全无法标注来源——因为真是找不回这条文本到底来自哪个视频了。重新找回来源的办法只有重新遍历字幕文件,恶心得要命。改造方案是把元数据全部作为标量字段存进Milvus的同一行记录,检索时随向量一起取出来。从第一天起,我就在写入这一步强制要求:每个文本块必须携带完整血缘信息。
6.2 重复内容对索引的污染
收藏夹里的视频不是没有重叠的,恰恰相反,同一个知识点往往被多个UP主反复讲。索引里如果放着几乎一模一样的文本块,向量检索会把“相似度高”误当成“相关内容质量高”,排名全被重复内容占掉,反而把信息密度最高的独有文本挤下去了。解决思路是加一步去重:对写入向量库的文本块做一次向量去重,相似度超过一个高阈值的文本块就不重复入库了,仅在元数据里关联多个视频ID。这样既能提醒用户“这个内容有三个视频讲过”,又不会让索引爆炸。
6.3 字幕的时间轴错位
有些视频的字幕文件是错帧的——字幕文本和音频里的句意对不上。这直接导致我最初做“按时间戳跳转视频”的功能时,用户点击引用的时间戳,跳到的画面内容压根不是那段字幕对应的内容。排查下来发现问题出在采集流程:部分视频的CC字幕是UP主新建的,时间轴和视频正片存在偏移。我的处理方法是做音频指纹对齐:抽取几个关键句的音频片段,和字幕文本的预期时间做比对,计算出一个偏移量统一纠正。这一步逻辑不复杂但非常有用,跳转功能的准确性一下就上来了。
6.4 本地部署的显存焦虑
我跑这套系统的时候,嵌入模型和一个重排器外加对话模型同时加载,三块模型压在显存里,单张普通显卡很快就吃紧。我最后采取的做法是把三块模型拆到两个进程里跑:对话模型单独占一个进程,嵌入模型和重排器共享另一个进程,用模型调度顺序串行执行。稍微牺牲一点并发能力,但换来了稳定运行。如果你也用类似方案,强烈建议先用脚本统计三个模型各自的峰值显存,再做进程分配,别等到OOM了再拍大腿。
6.5 增量更新比全量重建难得多
收藏夹是会变的,今天又收藏了两个视频,明天取消了一个收藏,系统不可能每次变动都全量重建索引。我实现了一个比较轻的增量管道:定期扫描收藏夹变动,把新增视频做“采集—清洗—切分—向量化—入库”全链路处理,删除的视频则按视频ID批量删除向量库中对应记录。这里要注意的一个细节是:B站视频存在被UP主删稿或设私的情况,原链接失效,但你的索引里还留着旧内容。我的做法是每周做一次链接健康检查,失效视频及时标记下线,避免以后检索出一个“不存在”的来源。
7. 一个比想象中好用的本地知识库
项目跑到今天,我已经习惯了遇到问题先去问收藏夹:某期讲分布式一致性的视频到底提没提过Raft的选主时间;半年前看过的某个手写Transformer的视频里,梯度裁剪的具体数值是多少。系统不仅能回答这些问题,还能顺着话题牵出我压根忘了收藏过的相关视频,这是最惊喜的地方——检索的过程中,它像是帮我重新翻了一遍收藏夹。
回顾整个项目的投入,真正耗费时间的不是模型调参,而是数据链路的地基工程:采集、清洗、元数据设计、增量更新,每一步都在为最终的问答质量买单。如果你也想做类似的系统,我最大的建议是:先别急着追求花哨的模型和复杂的Agent流程图,把“一条文本从视频字幕走到向量库”的链路跑通跑稳,再逐步叠加对话、重排、分类那些能力。一条烂数据流上喂再好的模型,产出的也是烂答案。B站收藏夹这个场景特别适合做RAG练手,因为数据自带时间的价值沉淀,你积累的每一期视频都是别人没有的私有知识,而检索它们的快乐,也是搜索引擎给不了的。