Ian Du
所有作品
Case Study · 04Evidence RAG

Citebase

我从 0 搭了一个团队知识库的 RAG,并查出问题进行升级迭代。

Key takeaway

这是个学习项目。第一版搭出来主要是为了学 RAG,跑通就行。但当我认真测量它到底好不好,带出了一个又一个问题:评测集有漏洞,于是我把评测重做了一遍;评测数据准确了,才看出检索的排序不行,于是又接上了 rerank;一路下来,每个改动都有数据支撑。

打开 Citebase看它怎么搭的 ↓部署在 Cloudflare Workers
00 · Origin

我做这个,是为了真正搞懂 RAG

我做 Citebase,是想真正搞懂 RAG。不是看几篇文章、抄段框架代码就算懂,而是自己从 0 把整条检索增强的链路搭出来,每一层的取舍都亲手踩一遍。

我只对齐一个原则:可查证。无来源就不作答,来源冲突就并列两个版本,答案里的每一句话都能点回原文的某一行。这一条不是后期补的功能,而是从第一层切分就开始铺垫的判断,后面很多技术选择,都是顺着它来的。

第一版搭通的过程里,每一层我都先把取舍想清楚,再决定怎么做,然后用真实数据跑一遍看证据。真正让我学到东西的是后来那一轮:我回头去认真测量它好不好,结果一个问题带出下一个,推着我把评测、检索、引用挨个重做。这一篇,讲的就是那次回炉。

如果一个 AI 只会说「据我所知」,我大概不敢拿它的答案去做事。要让人放心,它得能指着原文说:这句话,出自这里。
01 · The Product

它是什么:一个能交代来源的知识库

Citebase 是一个团队知识库问答系统。你问一个问题,它从你的文档里检索证据、用证据作答,并且把答案里的每一句话都标上来源:哪份文档、第几行。

点开一条引用,它会把你带回原文那几行被高亮的位置。检索不到足够证据时,它不会硬编,而是直接说「没有找到依据」。

答案

新员工激活的目标,是入职 7 天内交付第一个项目。onboarding.md · L43–L44

原文 · onboarding.mdL41–L46
41## Activation
42 
43New hires should ship their first
44project within 7 days of joining.
45 
46This is the primary activation KPI.
引用精确到行:切分时存下的起止行号,一路兑现成「点一下,看原文第几行」。中文问句也能落回英文原文,多语言向量负责跨过这道坎。

为什么从「行号」开始

引用大多只能精确到「某份文档」,是因为切分的时候就把文本块和原文的位置关系丢了。我反过来做:第一层切分就存下每块的起止行号,一路带到界面。引用能落到行,不是模型听话,是这条信息从头到尾没丢

02 · The Build

怎么搭的:从原文进,到带引用的答案出

把它拆成五层。从一份文档进去,到一句能追回原文的答案出来,中间每一层我都先问自己一个问题,再决定怎么做。

L1
切分Chunking

按标题把文档切块,并存下每块的起止行号,为后面把引用精确定位回原文埋好伏笔。

L2
向量入库Embeddings

用多语言模型 bge-m3 把每块编码进 Vectorize。选多语言,是因为问句可能是中文、语料是英文。

L3
混合检索Retrieval

dense 懂语义、BM25 懂精确关键词,两者盲区互补,用 RRF 按排名融合成一个结果。

L4
生成 + 校验Grounding

只用检索到的上下文作答;用代码逐条校验引用真的指向某个文本块,对不上就丢;无证据就明说没有。

L5
评测Eval

用一组带标准答案的问题量化 recall@5 和引用正确率,不靠感觉判断检索好坏。

+ 后来又在检索后加了一层精排、在生成后补了逐句核对,就是下面三节的事。

这是第一版,五层都跑通了。但这个项目真正的转折,在我跑通之后回头去量它好不好的时候,问题就是从那儿开始一个个冒出来的。
03 · The Eval

第一个问题:我给它打的分,是假的

评测我本来挺得意。八道题,每道标好答案该来自哪份文档,recall@5 跑出来 1.000。我差点把这个数写进简历。

盯久了不对劲。我总共就 8 份文档,每次取前 5 个,而每道题的正确来源常常就一两份。在 8 里挑 5,几乎不可能把那一两份漏光。这个 1.0 不是检索准,是题太松:它考的是「有没有把整份文档全漏掉」,那是地板,不是天花板。

一个不会失分的指标没用,它永远满分,也就永远不告诉我哪儿错了。于是评测整个重做:判分从「文档级」收到「段落 / 行号级」,再补上两件事:该拒答的时候它有没有拒答,答案里每句话是不是真有出处。

文档级 · 旧口径
1.000
recall@5

8 篇里取 5,正确文档几乎漏不掉,从不失分。

段落级 · 新口径
0.786
recall@5

判分收到具体行号。同一套检索,这才是真实的分。

该说没有时
6 / 6
拒答

查不到的题,它老实说没有,没一道硬编。

判分一收紧,recall 从假的 1.0 掉到真的 0.79。分变难看了,但它这才第一次有用,因为它终于会失分了。
04 · Rerank

第二个问题:评测变严了,才看出 RRF 不够

评测一变严,排序的毛病就藏不住了。召回是够的,正确证据基本都在前几名里;但它们排得不够靠前,尤其那道最难的「目标到底是 43% 还是 47%」,正确的两段一直挤不进前 5。

原来的检索(dense)把问题和文档各自编码成向量、最后比一下,快,但判相关性比较粗。我在后面接了一个 cross-encoder:它把问题和候选段落拼在一起读,判得准得多,代价是慢,所以只让它精排前面捞回来的十几个。

加完,衡量「正确证据排得多靠前」的 MRR 从 0.61 提到 0.70;那道矛盾题的正确两段,从挤不进前 5,变成稳稳排第一。

05 · Faithfulness

第三个问题:引用「合法」不等于「属实」

还有一处我原来没挡住。我会校验每条引用「格式对不对、指没指向一个真实存在的段落」,但这只能挡住瞎编的出处,挡不住另一种:它引了一段真实存在、却根本不撑这句话的原文。

所以我把答案拆成一句一句,每句都拿它引的原文去判:这段撑不撑得起这句话?撑不住的,在界面上标黄,明说「这句没有出处支撑」。校验从「引用合法」往前走了一步,到「引用属实」。

逐句核对

新员工激活的目标是 47%。okr-q2 · L18

这个目标适用于所有团队。这句话在引到的原文里找不到支撑

引用「合法」只保证它指向一个真实段落;这一步再判它「属实」:把答案拆成一句一句,撑不住的那句单独标出来。
06 · Trade-offs

搭的过程里,每一层都是一个取舍

粗排 vs 精排

dense 把问题和文档各自编码、最后比一下,快但粗;cross-encoder 把两者拼在一起读,准但慢。所以先用 dense 把候选都捞回来,再让 cross-encoder 只精排前十几个。

速度 vs 可信

宁可慢一点、宁可不答,也不让它一本正经地编。没有来源就停下。

语义 vs 关键词

dense 懂语义、BM25 懂精确关键词,两边盲区互补。但中文问句配英文语料时关键词只会灌噪声,所以按词表覆盖率自动决定要不要用 BM25。

裁决 vs 并列

来源互相矛盾时,并列两个版本、各自标好出处,而不是替用户武断挑一个。

07 · Reflection

回头这一轮,想明白的

搭之前我以为 RAG 就是「检索一下再喂给模型」。回头这一轮才让我明白,难的不是搭,是搭完之后怎么知道它到底好不好,而且我差点就信了自己给的一个满分。把评测改得会失分,后面的问题才一个接一个露出来:先是排序,再是出处对不对得上。

每一步我都没靠感觉,靠的是一个会失分的数字。这套「先别急着信自己的数」的习惯,我觉得比这个项目本身值钱。

知识库 AI 答得顺不算什么,得让用它的人敢信。这一轮我做的每件事,都是为了挣到这份信任。