1. 结果
这套系统要回答的问题很简单:用户问一句投资相关的话,系统能不能从资料里找到对的段落,并且只依据这些段落回答。
经过四组对比实验,最终方案在 40 条有答案的测试题上,前 5 条命中正确段落的比例从 0.675 提升到 0.850;前 5 条里命中正确书目的比例达到 95%;8 条知识库里没有答案的问题全部正确拒答,没有一条编造。
| 环节 | 选择 | 效果 |
|---|---|---|
| 切块 | 512 字平铺 | 在效果和成本之间最平衡 |
| 嵌入模型 | bge-m3(1024 维) | 中文、英文、跨语言都靠前 |
| 查询处理 | 多 query 改写 + RRF | 召回提升的主要来源 |
| 排序 | 可选 cross-encoder 重排 | 把最相关的证据推到第一位 |
| 拒答 | 分数门控 + LLM 审核 | 不该答的题不乱答 |
2. RAG 是怎么工作的
RAG(检索增强生成)不是“把文档丢给大模型”,而是一条流水线。每一环都有不同的做法,选错了,后面再强的模型也救不回来。
文档:切块 → 向量化 → 索引
问题:改写 → 检索(向量 + 关键词)→ 重排 → 生成 → 带引用的回答
下面按这条链路,逐个说明有哪些选择、各自的取舍。
2.1 文档怎么切
模型一次能读的文字有限,长文档必须先切成块(chunk)。切法直接决定检索能不能命中:
- 固定长度切块:每 512 或 1024 个字切一段,简单、块数可控,是最常见的基线。
- 按句切块:以句号、问号为边界,语义更完整,但块的长短不一。
- 父子块:把文档切成大块(父块)和小块(子块),检索时用小块匹配、返回大块作为上下文。想法是“用小粒度找准位置,用大粒度提供上下文”。
- 按结构切块:按章节、条款、表格切,最贴合年报这类结构化文档,但依赖解析质量。
块越小,定位越准,但上下文越碎;块越大,上下文越完整,但容易混入无关内容。这是切块的核心矛盾。
2.2 文本怎么变成向量
检索依赖嵌入模型(embedding model)把文字变成向量。向量越能表达语义,找得越准。这次比较了四类模型:
| 模型 | 维度 | 特点 |
|---|---|---|
| bge-small-zh-v1.5 | 512 | 本地 ONNX,轻量,中文够用,英文和跨语言较弱 |
| multilingual-e5-small | 384 | 更小,英文表现好,跨语言一般 |
| bge-m3 | 1024 | 本地 ONNX,多语言,长文本,跨语言最强 |
| text-embedding-v4 | 1024 | 云端 API,中文强,不占本地算力 |
维度不是越高越好:维度高通常更准,但索引更慢、存储更大。选模型要看语料是中文、英文,还是中英混合。
2.3 怎么检索
有了向量,检索有三种基本方式:
- 向量检索:比语义相似度,能处理“换个说法”的问题。
- 关键词检索(BM25):比词面匹配,专有名词、数字、条款号很准。
- 混合检索:两路都跑,按权重合并。本项目用“语义三成、关键词七成”,因为投资文本里术语和数字的权重很高。
混合检索的关键不是“都开”,而是权重。权重偏向量,同义改写更容易命中;偏关键词,精确术语更稳。
2.4 问题怎么改写
用户的一句话往往不是最好的检索词。查询改写让模型把问题变成多条 query:
- 去掉口语的原始问法;
- 只保留术语的关键词版;
- 英文翻译(资料里有英文原版书);
- 多跳问题再加 1–2 条子问题。
每条 query 各检索一次,再用 RRF(Reciprocal Rank Fusion)按排名融合。RRF 只看“排第几”,不看分数绝对值,所以不同 query 的结果可以直接合并。
2.5 找到后怎么排序
向量检索返回的是“大致相关”的候选。重排模型(reranker)把问题和每个候选放在一起做一次精细判断,重新打分。它通常能把最相关的证据推到第一位,但计算量比向量检索大,适合作为可选的第二阶段。
2.6 没有答案怎么办
知识库里没有答案时,系统应该明确说“资料中没有足够信息”,而不是硬凑。两种做法:
- 分数阈值:相似度低于某个值就拒答。简单,但不同检索模式的分数不可比。
- LLM 审核:让模型读前 5 条,判断“这些材料够不够回答问题”。更准,但多一次模型调用。
3. 实验怎么做的
3.1 语料
- 六本投资经典:三本英文教材(财务分析、财务会计、公司金融)+《证券分析》《海龟交易法则》《股票大作手回忆录》。
- 30 份 A 股年报:10 家公司、2022–2024 年,共 8,205 页,覆盖白酒、新能源、银行、汽车、地产、保险、医药、家电、建材、电子十个行业。
两套语料回答的是不同问题:经典书籍考“概念和推理”,年报考“数字、表格和具体公司”。
3.2 测试题
48 条问题:40 条有答案、8 条无答案;有答案里 9 条是“中文提问、答案在英文书里”,用来考察跨语言检索。后来做拒答实验时,又补了 20 条知识库外的问题(公司财报、行情、宏观),合计 28 条无答案。
3.3 指标
| 指标 | 含义 |
|---|---|
| Recall@5 | 前 5 条里有没有正确答案 |
| MRR | 第一条正确答案排得多靠前 |
| nDCG@5 | 相关结果有没有排在前面 |
| Top-1 | 第一条是不是相关 |
| 文档级召回 | 前 5 条有没有命中正确的书 |
所有对比都在同一套题上做,一次只改一个变量。
4. 实验一:切块方式
先固定嵌入模型和检索参数,只改切块。
| 切块方式 | 块数 | Recall@5 | MRR | Top-1 |
|---|---|---|---|---|
| PDF 512 平铺 | 3,948 | 0.625 | 0.588 | 0.550 |
| TXT 512 平铺 | 3,189 | 0.675 | 0.633 | 0.600 |
| TXT 512 父子块 | 33,169 | 0.675 | 0.619 | 0.575 |
| TXT 1024 平铺 | 1,648 | 0.700 | 0.646 | 0.625 |
| TXT 1024 父子块 | 33,155 | 0.675 | 0.650 | 0.625 |
(PDF 与 TXT 的解析方式不同,两者不是严格对照;切块结论只看 TXT 内部。)
三个观察:
- 512 到 1024 有小幅提升:Recall +1 条、Top-1 +1 条,因为块更大,上下文更完整。
- 父子块与同尺寸平铺持平:512 父子块和 512 平铺同分,1024 父子块的排序优势在平铺 1024 里也能拿到。收益来自“块变大”,不是父子机制本身。
- 成本差距很大:父子块有 33,000 多个子块,平铺 1024 只有 1,648 块。
最终选 512 平铺:效果接近最优,索引和检索成本最低,后续换模型、改参数也最容易。
5. 实验二:嵌入模型
5.1 纯向量对比
先排除关键词检索的干扰,只用向量做 top-5 检索:
| 模型 | 维度 | Recall@5 | 中文 | 英文 | 跨语言 | MRR |
|---|---|---|---|---|---|---|
| bge-small-zh-v1.5 | 512 | 0.475 | 0.727 | 0.333 | 0.000 | 0.433 |
| multilingual-e5-small | 384 | 0.650 | 0.773 | 0.889 | 0.111 | 0.602 |
| bge-m3 | 1024 | 0.800 | 0.773 | 0.889 | 0.778 | 0.714 |
| text-embedding-v4 | 1024 | 0.750 | 0.864 | 0.778 | 0.444 | 0.695 |
结论很清晰:
- bge-m3 综合最强,整体召回和跨语言都领先,适合中英混合的书籍语料。
- text-embedding-v4 中文最强,但跨语言只有 0.444,且走云端 API。
- e5-small 只解决英文,英文 0.889,跨语言只有 0.111,适合纯英文场景。
- bge-small 最轻,但纯向量召回明显偏低,主要靠关键词检索兜底。
没有“全面碾压”的模型,只有适合语料的模型。这也是为什么先做纯向量对比:混合检索会掩盖模型之间的差距。
5.2 接入混合检索后
把 bge-small 换成 bge-m3,切块和其他参数不变:
| 方案 | Recall@5 | MRR | Top-1 |
|---|---|---|---|
| bge-small 直接检索 | 0.675 | 0.633 | 0.600 |
| bge-m3 直接检索 | 0.725 | 0.696 | 0.675 |
| bge-small + 多 query | 0.850 | 0.708 | 0.625 |
| bge-m3 + 多 query | 0.850 | 0.740 | 0.650 |
直接检索时,bge-m3 多命中 2 条、MRR 提升 6.3 个百分点;叠加查询改写后,召回都到 0.850,bge-m3 继续在排序上领先。原因是混合检索的关键词权重已经补掉了部分语义差距,模型的优势主要体现在“把对的排得更前”。
最终选 bge-m3:多语言能力和排序质量都更好,索引成本可以接受。
6. 实验三:查询改写 + 多 query + RRF
这是提升最大的一环。做法是让模型把一个问题改写成 3–5 条 query,各检索一次,再融合。
| 模式 | Recall@5 | MRR | 跨语言 Recall |
|---|---|---|---|
| 直接检索 | 0.675 | 0.633 | 0.000 |
| 只用英文翻译 | — | — | 0.667 |
| 多 query(不含英文) | 0.800 | 0.676 | 0.222–0.556 |
| 多 query + RRF | 0.850 | 0.708 | 0.667 |
- 召回从 0.675 到 0.850,相当于 40 条题里多命中 7 条。
- “换一种中文说法 + 术语关键词”贡献了主要增益;英文那一路把跨语言从 0 拉到 0.667。
- RRF 主要改善排序:同一批候选,MRR 从 0.676 升到 0.708。
多 query 的代价是检索次数变成 3–5 倍。实测可以通过缓存和并发把延迟压到可接受范围。
7. 实验四:重排
在多 query 的候选池上再加一层 cross-encoder 重排,只比较排序质量:
| 排序方式 | MRR | Top-1 |
|---|---|---|
| 多 query + RRF | 0.690 | 0.625 |
| 每路分别重排后融合 | 0.715 | 0.675 |
| 并集候选 + 原问题重排 | 0.750 | 0.725 |
重排不改变“能找到什么”,但明显改变“先看到什么”:Top-1 从 0.625 提到 0.725,MRR 从 0.690 提到 0.750。对于生成式问答,第一位往往就是被引用的证据,所以这个提升很直接。
由于重排计算量较大,最终把它定位为可选层:对延迟不敏感的场景开启,交互场景可以先用多 query 的结果。
8. 最终架构
四组实验的结论合成一条链路:
| 层 | 最终选择 | 参数 |
|---|---|---|
| 解析 | PDF / TXT | 版面解析后统一为纯文本 |
| 切块 | 固定长度平铺 | 512 字 |
| 嵌入 | bge-m3 | 1024 维,本地 ONNX |
| 关键词 | BM25 | 与向量按 0.3 / 0.7 加权 |
| 改写 | LLM 多 query | 3–5 条 |
| 融合 | RRF | k = 60,取 top 5 |
| 重排 | bge-reranker-base int8 | 可选 |
| 拒答 | 分数门控 + LLM 审核 | 0.45 |
| 生成 | LLM | 仅用 top 5,[n] 引用 |
8.1 效果
检索侧(默认不开重排):
| 指标 | 结果 |
|---|---|
| Recall@5(段落级) | 0.850 |
| 文档级 Recall@5 | 0.950 |
| MRR | 0.740 |
| 跨语言 Recall | 0.778 |
生成侧(32 题:24 有答案 + 8 无答案):
| 指标 | 结果 |
|---|---|
| 支撑度(0–2) | 1.96 |
| 相关性(0–2) | 2.00 |
| 引用质量(0–2) | 1.96 |
| 无答案题正确拒答 | 8/8 |
| 编造事实 | 0 |
也就是说:材料足够时,回答基本都能在原文找到依据,引用对得上;材料不足时,系统会明确说“资料中没有足够信息”,而不是猜一个答案。
8.2 在系统中的落地
这条链路已经接进产品运行时,不在实验脚本里:core/rag_pipeline.py 负责改写、多路检索、RRF、重排与拒答判定,core/rerank.py 负责 bge-reranker-base int8 的 ONNX 推理。公司分析与 /api/rag 都走同一条链路,「分析依据与过程」会显示 N 条 query、是否重排、是否审核。
两个后端的分工要说清楚:混合检索的加权由后端负责——RAGFlow 后端提供 bge-m3 + BM25(0.3 / 0.7);本地后端只有 bge-small 向量检索,链路里其余环节两端一致。所有增强都有开关,任一步失败自动退回单 query 检索,不阻断一次分析;重排模型不存在时直接跳过。
9. 一句话总结
RAG 的效果不取决于“用了多大的模型”,而取决于每个环节有没有选对:切块要匹配语料,嵌入模型要匹配语言,查询改写负责召回,重排负责排序,拒答负责边界。这四件事做对,检索召回可以从 0.675 提到 0.850,并且让系统在该说“不知道”的时候真的说“不知道”。