粗排 vs 精排
dense 把问题和文档各自编码、最后比一下,快但粗;cross-encoder 把两者拼在一起读,准但慢。所以先用 dense 把候选都捞回来,再让 cross-encoder 只精排前十几个。
我从 0 搭了一个团队知识库的 RAG,并查出问题进行升级迭代。
这是个学习项目。第一版搭出来主要是为了学 RAG,跑通就行。但当我认真测量它到底好不好,带出了一个又一个问题:评测集有漏洞,于是我把评测重做了一遍;评测数据准确了,才看出检索的排序不行,于是又接上了 rerank;一路下来,每个改动都有数据支撑。
我做 Citebase,是想真正搞懂 RAG。不是看几篇文章、抄段框架代码就算懂,而是自己从 0 把整条检索增强的链路搭出来,每一层的取舍都亲手踩一遍。
我只对齐一个原则:可查证。无来源就不作答,来源冲突就并列两个版本,答案里的每一句话都能点回原文的某一行。这一条不是后期补的功能,而是从第一层切分就开始铺垫的判断,后面很多技术选择,都是顺着它来的。
第一版搭通的过程里,每一层我都先把取舍想清楚,再决定怎么做,然后用真实数据跑一遍看证据。真正让我学到东西的是后来那一轮:我回头去认真测量它好不好,结果一个问题带出下一个,推着我把评测、检索、引用挨个重做。这一篇,讲的就是那次回炉。
如果一个 AI 只会说「据我所知」,我大概不敢拿它的答案去做事。要让人放心,它得能指着原文说:这句话,出自这里。
Citebase 是一个团队知识库问答系统。你问一个问题,它从你的文档里检索证据、用证据作答,并且把答案里的每一句话都标上来源:哪份文档、第几行。
点开一条引用,它会把你带回原文那几行被高亮的位置。检索不到足够证据时,它不会硬编,而是直接说「没有找到依据」。
新员工激活的目标,是入职 7 天内交付第一个项目。onboarding.md · L43–L44
引用大多只能精确到「某份文档」,是因为切分的时候就把文本块和原文的位置关系丢了。我反过来做:第一层切分就存下每块的起止行号,一路带到界面。引用能落到行,不是模型听话,是这条信息从头到尾没丢。
把它拆成五层。从一份文档进去,到一句能追回原文的答案出来,中间每一层我都先问自己一个问题,再决定怎么做。
按标题把文档切块,并存下每块的起止行号,为后面把引用精确定位回原文埋好伏笔。
用多语言模型 bge-m3 把每块编码进 Vectorize。选多语言,是因为问句可能是中文、语料是英文。
dense 懂语义、BM25 懂精确关键词,两者盲区互补,用 RRF 按排名融合成一个结果。
只用检索到的上下文作答;用代码逐条校验引用真的指向某个文本块,对不上就丢;无证据就明说没有。
用一组带标准答案的问题量化 recall@5 和引用正确率,不靠感觉判断检索好坏。
+ 后来又在检索后加了一层精排、在生成后补了逐句核对,就是下面三节的事。
评测我本来挺得意。八道题,每道标好答案该来自哪份文档,recall@5 跑出来 1.000。我差点把这个数写进简历。
盯久了不对劲。我总共就 8 份文档,每次取前 5 个,而每道题的正确来源常常就一两份。在 8 里挑 5,几乎不可能把那一两份漏光。这个 1.0 不是检索准,是题太松:它考的是「有没有把整份文档全漏掉」,那是地板,不是天花板。
一个不会失分的指标没用,它永远满分,也就永远不告诉我哪儿错了。于是评测整个重做:判分从「文档级」收到「段落 / 行号级」,再补上两件事:该拒答的时候它有没有拒答,答案里每句话是不是真有出处。
8 篇里取 5,正确文档几乎漏不掉,从不失分。
判分收到具体行号。同一套检索,这才是真实的分。
查不到的题,它老实说没有,没一道硬编。
评测一变严,排序的毛病就藏不住了。召回是够的,正确证据基本都在前几名里;但它们排得不够靠前,尤其那道最难的「目标到底是 43% 还是 47%」,正确的两段一直挤不进前 5。
原来的检索(dense)把问题和文档各自编码成向量、最后比一下,快,但判相关性比较粗。我在后面接了一个 cross-encoder:它把问题和候选段落拼在一起读,判得准得多,代价是慢,所以只让它精排前面捞回来的十几个。
加完,衡量「正确证据排得多靠前」的 MRR 从 0.61 提到 0.70;那道矛盾题的正确两段,从挤不进前 5,变成稳稳排第一。
还有一处我原来没挡住。我会校验每条引用「格式对不对、指没指向一个真实存在的段落」,但这只能挡住瞎编的出处,挡不住另一种:它引了一段真实存在、却根本不撑这句话的原文。
所以我把答案拆成一句一句,每句都拿它引的原文去判:这段撑不撑得起这句话?撑不住的,在界面上标黄,明说「这句没有出处支撑」。校验从「引用合法」往前走了一步,到「引用属实」。
新员工激活的目标是 47%。okr-q2 · L18
这个目标适用于所有团队。这句话在引到的原文里找不到支撑
dense 把问题和文档各自编码、最后比一下,快但粗;cross-encoder 把两者拼在一起读,准但慢。所以先用 dense 把候选都捞回来,再让 cross-encoder 只精排前十几个。
宁可慢一点、宁可不答,也不让它一本正经地编。没有来源就停下。
dense 懂语义、BM25 懂精确关键词,两边盲区互补。但中文问句配英文语料时关键词只会灌噪声,所以按词表覆盖率自动决定要不要用 BM25。
来源互相矛盾时,并列两个版本、各自标好出处,而不是替用户武断挑一个。
搭之前我以为 RAG 就是「检索一下再喂给模型」。回头这一轮才让我明白,难的不是搭,是搭完之后怎么知道它到底好不好,而且我差点就信了自己给的一个满分。把评测改得会失分,后面的问题才一个接一个露出来:先是排序,再是出处对不对得上。
每一步我都没靠感觉,靠的是一个会失分的数字。这套「先别急着信自己的数」的习惯,我觉得比这个项目本身值钱。
知识库 AI 答得顺不算什么,得让用它的人敢信。这一轮我做的每件事,都是为了挣到这份信任。