我这个月把B站收藏夹里四百多个吃灰视频,变成了一套能直接对话的知识库。说白了就是:把收藏的视频字幕、弹幕、简介全部拉下来,做成分片、向量化,再交给大模型,你可以直接用自然语言问它“哪个视频讲过Nginx反向代理怎么配”,它给出答案的同时,还会告诉你出自哪个视频、大约在第几分钟。整套流程用开源工具和Python就能跑通,个人玩成本几乎为零。这件事解决的真实痛点是:收藏夹只能按标题模糊搜索,根本没法按语义找内容,视频里全是干货,但你检索不到。适合收藏重度用户、知识管理爱好者,以及想从零手搓一套RAG系统实战的开发者。
1. 别急着写代码:一个收藏夹为什么值得做成知识库
1.1 收藏夹的本质:重收藏、轻检索
几乎所有B站用户都有同一个病:看到好视频先收藏,然后就没有然后了。收藏夹膨胀到几百个视频之后,它本质上已经不是一个“知识库”,而是一堆按时间排序的链接。你需要找某条内容时,只能靠标题关键词碰运气,而视频真正有价值的部分是在音频和画面里,搜索引擎既钻不进去,也不会帮你做语义匹配。
我自己就是因为一次痛苦的经历才下决心做这件事。当时想找一个之前收藏过的、讲系统dmesg日志排查的视频,标题里既没有“dmesg”也没有“排查”,我只记得博主当时用了一个很有意思的案例。结果翻收藏夹翻了半小时,最后放弃了。那半小时的时间,如果交给一套语义检索系统,大概不到一分钟就能给出答案。
把收藏夹变成知识库,本质上是做三件事:把视频里的内容变成文本,把文本组织成可检索的索引,再通过对话式交互把结果交还给你。B站天然具备这个条件——大量视频自带CC字幕或AI字幕,弹幕也在,元数据也在,相当于平台已经帮我们做了音视频转写,我们要做的只是把这些素材结构化。
1.2 RAG到底是什么:给大模型递参考资料的机制
这个项目最核心的技术就是RAG,全称Retrieval-Augmented Generation,检索增强生成。名字很唬人,原理其实很朴素:大模型不是所有知识都知道,尤其不知道你收藏夹里这些具体视频讲了什么。但你可以在它回答问题之前,先从你的库里检索出最相关的几个片段,把这些片段作为参考资料塞进上下文,让它“开卷作答”。
打个生活比方。你请了一个学识渊博但容易胡编乱造的助手,你不能指望他什么都凭空知道。于是你先翻了一遍书架,把相关的那几页折好角,放到他桌上,明确告诉他:回答只能依据这几页内容,找不到就说不知道。这一整套“翻书架、折角、递材料、监督作答”的过程,就是RAG。
那为什么不是微调?微调意味着要让大模型把收藏夹里的内容背进参数,成本高、周期长、每次收藏新视频还得重新训练,个人项目根本划不来。RAG则是把知识放在外部“资料库”里,收藏了新视频只需要更新索引,模型本身不用动。再加上这个场景的典型问题都是“某视频里提过什么”、“怎么做某件事”,答案往往锚定在一段具体内容上,检索的精准度比模型“硬背”可靠得多。
那为什么不用纯关键词搜索?关键词搜索在今天的信息环境里基本已经被语义搜索碾压了。你说“NGINX 502排查”,收藏的视频标题里可能只有“网关排错实录”,两种表述没有共同词,但语义高度相关。RAG靠向量检索可以跨词面找到相关内容,这正是收藏夹检索最需要的。
2. 系统架构:从收藏夹到可对话知识库的完整链路
2.1 五层架构与数据流动
整个系统我按数据流向拆成五层,任何环节出了问题都可以单独定位。一开始没按这个思路做,导致后面调试时到处抓瞎,这里直接把成熟的分层结构写出来。
第一层是数据获取层。负责调用B站的开放接口,拿到收藏夹列表、视频基本信息、字幕、弹幕。这层解决的问题只有一个:如何稳定、合规地把用户自己账号下的收藏数据取下来。
第二层是解析清洗层。B站返回的字幕是JSON,弹幕是XML,夹杂大量重复、无意义内容。这层要做的是把它们解析成结构化文本,保留时间戳、视频标题、UP主等关键元数据,同时过滤噪声。
第三层是索引构建层。把清洗后的文本切分成固定片段,用Embedding模型转成向量,写入向量数据库。这层决定了后面检索质量的上限,也是全项目踩坑最多的环节。
第四层是检索对话层。收到你的问题时,先转成向量,到向量库里召回候选片段,再拼装上下文,交给大模型生成回答。为了让回答有出处,检索层还要承担来源标记的工作。
第五层是交互展示层。一个最简单的Gradio页面,输入框、输出框、来源列表,就足够用了。
每层之间通过标准化的数据格式传递。前一层输出的是“一条带元数据的文本记录”,后一层只依赖这种结构,不关心数据来源是字幕还是弹幕。这种松耦合设计让我后面想加评论、加视频简介,都只动了第一层和第二层,索引层和对话层完全不用改。
2.2 模块选型:为什么是字幕、ChromaDB、本地Embedding
选型是整个项目里最值得动脑的部分,因为每一步都有替代方案,但替代方案往往意味着更多的成本或更差的效果。
数据源我优先选字幕,而不是自己跑语音识别。B站视频很大一部分带AI字幕或CC字幕,这些字幕虽然不是100%准确,但足以支撑语义检索。如果自己跑ASR转写,四百个视频意味着几十小时的GPU算力和一堆需要处理的音频文件,个人项目很容易卡在这一步就弃坑。我实测下来,B站AI字幕对技术类视频的准确率足够高,错误主要出现在专业名词和英文缩写上,但RAG检索并不要求文本完美,只要能召回正确片段就行。
向量数据库我选了ChromaDB,而不是Milvus或FAISS。核心原因是个人项目的规模决定了一切:按我的估算,几百个视频分块后也就是几千条向量,ChromaDB作为嵌入式数据库,零部署成本,支持持久化,API简洁,完全够用。FAISS虽然快但它本质上是一个索引库,还要自己管理元数据存储和持久化,Milvus则是更重的基础设施,适合多用户和生产环境,个人项目用了反而添乱。
Embedding模型我选了本地的bge-m3。原因很简单:数据隐私好、中文效果好、不用为每次Embedding调用付费。这套系统全程不依赖外部API也能跑通,就算哪天不想接大模型API了,本地跑一个小参数量的对话模型,配bge-m3依然能形成一个完全离线闭环,这是我很看重的独立性。
2.3 数据量预估:先算清楚再动手
动手之前我做了一个粗算,这一步帮我在选型上省了很多弯路。假设平均每个视频30分钟,字幕按5分钟一个片段切,一个视频大概6个chunk;四百个视频约2400个chunk,每个chunk五百字左右,全库总文本量一百二十万字上下。2400条向量哪怕每个向量1024维,存储开销也不到20MB。这个量级,ChromaDB跑检索单次响应在几十毫秒级别,几乎不需要优化。
如果收藏夹有几千个视频,chunk数量大概到三万左右,ChromaDB依然能顶住。但如果你要处理几万个视频、百万级chunk,就必须升级到Milvus这类分布式向量库,同时还要考虑分片和索引压缩。这个估算过程解释了为什么我敢用轻量方案:不是因为它听起来简单好用,而是规模预算告诉我“够用”。
3. 数据采集实战:收藏夹、字幕、弹幕一个都别少
3.1 拿到收藏夹全量视频列表
B站有一个公开的收藏夹内容接口,只要带上登录后的Cookie就能拿到自己收藏夹下的视频列表。接口路径是/x/v3/fav/resource/list,关键参数是media_id、pn(页码)、ps(每页数量)。media_id可以从收藏夹页面URL里直接看到,pn从1开始翻页,ps最大只能取20,所以视频多了要循环翻页。
下面是我跑通的简化代码,重点在请求头里的Cookie和Referer,少了后面两个,接口大概率返回需要登录的错误码。
import requests def fetch_fav_list(media_id, cookie, max_pages=None): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://www.bilibili.com/", "Cookie": cookie, } page = 1 videos = [] while True: resp = requests.get( "https://api.bilibili.com/x/v3/fav/resource/list", headers=headers, params={ "media_id": media_id, "pn": page, "ps": 20, "platform": "web", }, timeout=10, ) data = resp.json() if data.get("code") != 0: break items = data["data"]["medias"] for item in items: videos.append({ "bvid": item["bvid"], "title": item["title"], "author": item["upper"]["name"], "duration": item["duration"], "intro": item.get("intro", ""), "fav_time": item["fav_time"], }) # 判断是否还有下一页 if data["data"].get("has_more") == 0 or (max_pages and page >= max_pages): break page += 1 return videos注意返回结构里intro就是视频简介,它的文本质量比字幕更干净,我后面把它作为高优先级检索来源。这个接口每页只给20条,四百个视频要翻20次,但接口响应很快,控制好频率十几秒就能全部拉完。
3.2 字幕抓取:B站提供的现成文本,不用白不用
字幕获取是整个项目里最核心的一步,因为它直接决定了知识库的“内容质量”。B站的字幕分为CC字幕(UP主上传或平台生成的)和AI字幕(视频AI自动识别的),两者在接口返回里都有,优先选lan为“zh-CN”且subtitle不为空的那条。
流程有两步。第一步拿bvid换cid,一个视频的唯一标识;第二步带着cid去字幕接口拉字幕列表。这里我用一个非常经典的B站播放器信息接口/x/player/v2,返回结构里data.subtitle.subtitles就是可用字幕列表。每项里有字幕的下载地址和语言代码。
def fetch_subtitle(bvid, cid, headers): player_url = "https://api.bilibili.com/x/player/v2" resp = requests.get( player_url, headers=headers, params={"bvid": bvid, "cid": cid}, timeout=10, ) data = resp.json() subtitles = data["data"]["subtitle"]["subtitles"] if not subtitles: return None # 优先中文 for sub in sorted(subtitles, key=lambda x: x["lan"] != "zh-CN"): sub_resp = requests.get(sub["subtitle_url"], headers=headers, timeout=10) return sub_resp.json() # body是字幕片段列表 return None字幕接口返回的JSON结构有点像这样:body是一个数组,每个元素包含from(开始时间,单位秒)、to(结束时间)、content(文本)。我需要把浮点时间转成MM:SS格式存起来,比如from=754.32转成12:34,这样后面在对话回答里能直接给出“在第12分34秒提到”。
这里有个特别容易踩的坑:部分字幕URL是带签名的,直接用浏览器里的Cookie不一定能下载,需要复用浏览器请求头里的完整Cookie和UA,并且某些URL还要求带Referer为视频页面地址。我最初测试时复制了零散的Cookie,结果字幕拉下来一堆401错误。解决办法是打包一套完整的请求头,尤其别漏掉浏览器默认带的Accept、Accept-Language这些字段。
3.3 弹幕:价值与噪声的平衡
弹幕是B站内容的特色,但对于RAG知识库来说,它的定位比较尴尬。弹幕能反映视频里的“梗”和用户关注点,可绝大多数弹幕是“哈哈哈哈”“前方高能”“666”这种噪声,直接进索引不仅帮不上忙,还会把向量空间搅乱。
我试过两种处理方式。一种是全量抓取弹幕放进索引,结果检索召回经常出现莫名其妙的“名场面”片段,对正经问题帮助极小。第二种是加一层清洗规则:过滤掉长度小于4个字符的弹幕、去除纯“哈”和“6”的重复文本、过滤带举报率高的屏蔽词,时间轴上按弹幕密度聚簇,密度大的地方代表高能片段,可以作为辅助标签加进元数据。
最终我的取舍是:弹幕不进向量索引,但会把高能弹幕所在的时间点存成“高能标记”,作为字幕片段元数据的一个字段。这样既不影响检索精度,又能让对话系统在回答“这个视频哪里最好笑”这类主观问题时,给出有弹幕依据的建议。如果想省事,直接丢弃弹幕也完全不影响核心功能的闭环。
3.4 采集合规与风控避坑
先说一句重要的:整个项目只应该采集你自己账号下能看到、你自己收藏的公开数据,而且只用于个人学习研究,不要公开传播抓取的内容。B站的服务条款对批量抓取有限制,这里强调几件事。
一是Cookie有效期很短。B站登录态通常几天就会失效,实测中收藏夹接口经常返回-101(未登录)或-412(请求被拦截)。遇到这种情况不用慌,重新打开浏览器登录一次,把新的Cookie替换到配置里即可。我建议把Cookie写进配置文件,不要反复硬编码在代码里。
二是请求频率一定要克制。翻页接口可以循环调用,但我额外加了0.5秒到1秒的随机延时,批量字幕下载时每两个请求之间sleep 0.3秒。这样跑下来四百个视频大概耗时二十分钟,虽然慢一点,但换来的是稳定的成功率。千万不能不加延时并发猛拉,那样很容易触发风控,连正常网页访问都可能被临时限制。
三是字幕URL的签名时效。有些字幕链接在获取后短时间内有效,不要先把列表存成JSON、过了好几个小时再去下载,那样大概率会遇到签名过期。正确做法是:拿到字幕URL后立刻下载内容并落盘。
4. 从字幕片段到向量索引:分块、Embedding、向量库的选型与硬经验
4.1 字幕清洗与结构化:时间戳是灵魂
如果说采集是体力活,清洗就是真正的技术活。B站字幕的原始JSON虽然工整,但直接拿来做索引很不靠谱。首先是大量字幕内容是重复的,尤其是AI字幕经常在一两秒内重复输出同一句话;其次是单条字幕文本太短,很多时候只有几个字,单独向量化几乎没有语义信息。
我的清洗管线按顺序做四件事:按视频ID和字幕ID去重;把时间上连续、内容语义相近的短句合并;过滤掉包含“字幕由AI生成”这类纯声明文本;最后统一编码格式、去掉空行。
清洗时一定要把时间戳保留下来,这是这个项目的关键设计。原因在于知识库最终要给用户可验证的答案,如果只返回“来自某视频”而没有具体时间点,用户仍然不知道去哪看原始内容。所以我的每条清洗结果都会写成一个字典结构:{"video_title": "...", "video_url": "...", "author": "...", "start_time": "12:34", "text": "..."},后面向量化和检索都带着这份元数据走。
4.2 分块策略:按时间窗口聚合,而不是一刀切
分块是RAG项目里决定性的一环,我在这上面做过好几轮实验。最开始我图省事,直接按字幕条一条一条向量化,结果检索效果非常差:用户的问题是“如何排查CPU负载异常”,召回的字幕全是零散句子,什么“这个命令看一下”“然后分析这里”,上下文根本不够大模型回答。
后来我改成了按时间窗口聚合。具体做法是定一个窗口长度,比如5分钟,把落在同一个窗口里的所有字幕文本合并成一个大片段;相邻窗口之间再叠加一定重叠,比如30秒,防止知识点正好被窗口边界切断。这样做的依据是:视频的一个完整知识讲解通常持续几十秒到几分钟,单独一句话不足以回答问题,而5分钟的窗口基本能覆盖一个知识点的完整表述。
我给的窗口参数可以用一个简单规则来调整:如果你的收藏夹以长视频为主,窗口可以放到8到10分钟;如果以短视频为主,窗口缩到2到3分钟。重叠比例一般取窗口的10%到20%,太大浪费token,太小容易切掉上下文。
分块之后还有一个关键步骤:片段级去重。由于视频可能重复引用同一段落文本,尤其是AI字幕会在不同句间重复,我计算了一下约10%的片段属于重复内容。去重方法很简单,对片段文本取哈希,只保留第一次出现的版本;更精细一点可以做MinHash去重,但对个人项目来说哈希已经够了。
4.3 Embedding模型与向量数据库选型
Embedding的任务是把文本变成一组浮点数向量,语义相近的文本在向量空间里距离也近。我选用的是BAAI/bge-m3,支持中文,输出1024维向量。这类模型可以通过sentence-transformers库一条命令加载,而且本地推理速度很快,我笔记本CPU跑几百个片段也就是十几秒的事。
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-m3") def embed_texts(texts): # normalize_embeddings会让后续余弦相似度计算更稳定 return model.encode(texts, normalize_embeddings=True, show_progress_bar=True)向量库我用ChromaDB,因为它就是一个Python库,不需要单独启动服务。下面是写入索引的示例,注意我额外加了一个where过滤器元数据,方便后续只检索某位UP主的视频。
import chromadb client = chromadb.PersistentClient(path="./bili_kb") collection = client.get_or_create_collection( name="bili_fav", metadata={"hnsw:space": "cosine"}, ) collection.add( ids=[str(i) for i in range(len(chunks))], documents=[c["text"] for c in chunks], metadatas=[{ "video_title": c["video_title"], "video_url": c["video_url"], "author": c["author"], "start_time": c["start_time"], } for c in chunks], embeddings=embed_texts([c["text"] for c in chunks]), )如果没条件跑本地模型,也可以用OpenAI的Embedding接口或者云厂商的中文向量模型,效果都很好,但每次调用都要花钱。本地模型的好处是一次加载、无限次使用,而且数据不出机器,对个人知识库这种敏感场景更友好。
4.4 索引构建与增量更新
索引构建不是只跑一次就完事,因为你的收藏夹会持续更新。我需要考虑增量更新。最简单的方式是给每条向量设一个“视频BVID+片段序号”的ID,每次构建时先查一下已存在的ID集合,只对新增视频做向量化,已存在的片段直接跳过。
这里有一个细节很容易被忽略:ChromaDB的collection.get()可以传ids去重判断,但如果视频被删除、或UP主改了标题,旧片段会残留。我因此加了一个“全量重建”的开关,每周跑一次完整清空重建,增量更新则每天跑一次。重建的代价其实不高,按前面估算的全库两千多个chunk,重构索引加向量化十分钟内就能完成,完全能接受。
向量化结果的缓存也很重要。我踩过一次坑:有一次清理了数据库,第二天增量更新时发现Embedding模型重新加载花了好几分钟,之后还重复跑了所有旧片段。解决办法是把Embedding结果按片段哈希存在本地文件里,重跑时用哈希文件直接跳过已经算过的部分,这样增量更新几乎能做到秒级。
5. 检索与对话模块:让回答有据可查
5.1 检索链路:召回、过滤、重排
检索链路是整个RAG系统给大模型“递资料”的关键环节。我的实现分四步:先对用户问题做Embedding,然后从向量库召回候选片段,再做来源和相似度过滤,最后对候选做重排,只保留最相关的内容进入Prompt。
召回阶段,我把n_results设置成8到10个,因为候选稍微多一点,才能覆盖不同类型的问题。但召回多了会出现相似片段扎堆的问题,比如同一个视频的两个相邻窗口都命中了,造成上下文重复。我的过滤逻辑是:同一个视频最多保留两个候选片段,优先保留相似度高的;如果两个候选来自同一个视频且时间重叠,只保留时间靠前或文本更完整的那一个。这样做能显著降低大模型读重复内容的比例。
相似度阈值也很重要。按我的测试,bge-m3在余弦相似度0.35以下基本就是无关内容,0.5以上才比较可靠。实际阈值定在0.45比较稳妥,既能过滤噪声,又不会漏掉部分表达不同的答案。
重排阶段我尝试过两种方案。简单方案是按相似度从高到低排序,直接取前3到5条,效果已经不错;进阶方案是用bge-reranker这类重排模型,它能计算“问题-片段”的语义匹配分数,比向量相似度更准。个人项目我不建议一开始就上重排模型,先用简单的排序跑通闭环,效果不够再上重排,避免引入额外模型的复杂度。
5.2 提示词设计:把参考片段变成有出处的回答
RAG的最终效果一半取决于检索质量,另一半取决于Prompt怎么写。我的核心要求是:回答必须基于参考片段,禁止编造;回答必须带来源;如果没有相关内容,直接承认不知道。
我用的系统提示词大致长这样:
你是一个基于用户私人收藏视频内容的知识助手。你会收到用户的问题和若干参考片段,每个片段包含视频标题、UP主、视频URL和出现时间。 回答要求: 1. 优先依据参考片段回答,不要使用片段之外的其他知识。 2. 如果参考片段中没有答案,明确回复“收藏夹的视频里没有找到相关内容”。 3. 回答必须简洁,保留关键步骤和细节。 4. 每段关键结论后面,标注来源,格式为:来自《视频标题》约12:34。 5. 多个片段信息冲突时,如实列出冲突。 用户问题:{question} 参考片段: {context}这套提示词里最有用的就是“格式为:来自《视频标题》约12:34”,它强制大模型把答案来源写清楚,而不是笼统说“我了解到”。用户拿到时间点之后可以回到B站看原视频验证,这种可溯源体验是纯生成模型给不了的。
片段拼装也有一些讲究。我的做法是把候选片段按相似度从高到低排列,每条以“标签”开头,例如[片段1] 来自《xxx》UP主xxx,约12:34:具体内容。这样大模型在阅读时能自然把来源和时间点绑定到内容上,输出引用时也不太容易出错。
5.3 模型部署与对话界面集成
LLM的选型我给两条路线。一条是直接调用云端大模型API,比如你常用的OpenAI兼容接口,优点是无脑、回答质量高,缺点是要联网、按token付费。另一条是本地部署,例如用Qwen系列或ChatGLM系列,跑在显存足够的显卡上,隐私性好,但回答质量和推理速度会受硬件限制。
我做的时候是先接API跑通闭环,确定检索和提示词都调好了,再切到本地模型。这样可以避免一上来就在本地模型上调试,结果分不清到底是检索问题还是模型能力问题。
界面用了Gradio,两百行代码就搞定。界面核心就三样:输入框、输出区、来源表格。Gradio可以直接接收向量库检索结果,把来源列表渲染成表格,用户体验比纯命令行好太多。如果愿意花些时间,用Streamlit做也行,两者差别不大,看个人习惯。
下面是简单的对话接口示例逻辑,不是完整项目,但能表达清楚结构:
def answer(question): query_vec = embed_texts([question]) hits = collection.query( query_embeddings=query_vec, n_results=8, ) # 过滤、重排、组装context context = assemble_context(hits) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"问题:{question}\n参考片段:\n{context}"}, ] reply = llm_chat(messages) return reply, hits6. 常见问题与排查技巧实录
6.1 高频问题速查表
我把实际运行中遇到的高频问题整理成了下表,方便直接对照排查。
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 收藏夹接口返回-101 | Cookie过期或未登录 | 重新登录并更新Cookie,检查请求头是否完整 |
| 收藏夹接口返回-412 | 请求频率过快或风控拦截 | 降低并发,加随机延时,检查Referer和UA |
| 字幕列表为空 | 视频本身没有CC/AI字幕 | 跳过该视频,或后续用ASR转写,不影响整体 |
| 字幕下载401 | 字幕URL的签名或Referer缺失 | 携带完整浏览器请求头,并立即下载 |
| 检索结果全是噪声 | 文本清洗不彻底或分块窗口过小 | 清理重复弹幕字幕,加大分块窗口并增加重叠 |
| 回答引用错误视频 | 候选片段扎堆同一视频且上下文重复 | 每个视频限制最多两个候选,增加相似度阈值 |
| 回答明显在编造 | Prompt没强调“仅依据片段” | 在系统提示词里明确禁止使用片段外知识 |
| 本地Embedding加载慢 | 模型文件大,未做缓存 | 启动时只加载一次,向量结果按哈希缓存到本地 |
| 新收藏的视频查不到 | 索引未更新 | 运行增量更新脚本,只对新视频做向量化 |
6.2 我踩过的几个大坑
第一个坑是字幕文本直接入索引,没有做分片合并。最开始每个字幕条单独作为一篇文档,结果检索出来的都是残缺句子。这个问题花了我两个晚上才意识到,核心现象是“检索命中的内容看起来相关,但大模型回答缺上下文”。后来翻看了十几个视频的字幕,才确定是分块粒度的问题。
第二个坑是B站收藏夹接口的“has_more”字段判断。这个字段在最后一页时会返回0,但某次网络请求超时导致它返回了None,我的循环判断while data["data"]["has_more"] == 1直接崩了。后来我改成先判断data和data["data"]是否存在,再加get默认值,代码健壮了很多。
第三个坑是Embedding模型的归一化。ChromaDB默认用余弦相似度,但如果embedding前不设置normalize_embeddings=True,向量长度不一致时计算出来的余弦值会有点漂,具体表现是阈值怎么调都不稳定。补上归一化之后,阈值一下变得好使了。
第四个坑是上下文超长。收藏夹里有些长视频,5分钟窗口文本可能达到一千多字,一次塞进五个片段再让大模型回答,后端很容易报超限。我的对策是拼装Prompt时先估算token,超过最大值就只保留相似度最高的前三段,再不行就用摘要压缩。这个策略保证系统在极限情况下也不会直接崩。
6.3 效果调优经验:从答非所问到有理有据
如果你搭完发现回答还是不太靠谱,先不要怀疑大模型,按顺序排查三条链路。
先查索引质量:随机抽十个视频的字幕,看清洗后文本里有没有大段重复、时间戳错乱或乱码。索引是原料,原料不干净,后面再怎么调都是白搭。
再查分块参数:用同一批视频分别用不同的窗口大小跑一遍检索,人工对比召回片段的合理性。我自己从每字幕块切分改成5分钟窗口之后,检索准确率提升非常明显;之后又把窗口调到4分钟,发现对短视频更友好,最终定在5分钟。
最后才调Prompt:把system prompt里的“必须依据片段回答”改成“只能依据片段回答”,把“尽量简洁”改成“保留关键步骤”,每次只改一处,观察输出变化。这种单变量调试法虽然慢,但能确凿地定位是哪个环节拉低了效果。
调优到后期,我会做一个小型评测集:准备二十个关于收藏夹内容的模拟问题,每个问题手工标一个期望答案片段。每次改动后跑一遍评测集,看命中率变化。这套方法虽然粗糙,但比感觉靠谱得多,能让我知道自己的系统在一点点变好。
我个人在做这个项目时还有一个额外的收获:以前收藏夹里的视频对我来说只是“鬼知道我什么时候会看”的负担,现在它变成了一个能反复查询的资料库。偶尔随手问一句“我之前收藏过讲Shell数组的视频吗”,然后顺着答案跳回原视频,那种“原来我当时存了这个”的惊喜感,比收藏动作本身更让人踏实。
再分享一个可以立刻上手的扩展方向:当你把字幕、弹幕、简介、收藏时间都结构化之后,这套系统理论上还可以做“收藏夹自动分类”“按UP主聚合问答”“视频要点周报”。收藏夹不再是吃灰列表,而是一份可以持续挖掘的个人数据资产。对于刚入门RAG的朋友,我建议先拿一个几十个视频的收藏夹跑通最小闭环,再逐步加功能,不要一上来就追求完美基建。数据会给你最大的正反馈。