RAG检索优化:混合检索与RRF融合算法实战 📅 发布时间:2026/9/1 9:48:56 👁 浏览次数: 现在很多团队做知识库问答第一步都会选 RAG 方案把文档切片、向量化、存入向量库用户提问时先检索再让大模型生成答案。这套流程听起来成熟但真正落地时很多人会卡在同一个环节——检索效果不稳定。问一些事实型、精确匹配类的问题比如合同编号、产品型号、API 名称向量检索的结果往往不尽如人意。问题出在哪又该怎么解决这篇就来完整梳理一套可落地的 RAG 问答系统技术方案重点讲稀疏向量检索和 RRF 倒数排名融合算法。1. 这篇文章真正要解决的问题先给一个清晰判断RAG 系统的效果上限很大程度上不取决于生成模型而取决于检索质量。大模型生成能力再强如果检索回来的上下文是错的、不全的、顺序混乱的答案就不可能准确。很多人把精力花在调 prompt、换更好的大模型上效果提升却很有限原因就在这里——上游检索阶段已经丢失了关键信息。具体到检索环节常见痛点有三类向量检索对精确词匹配不敏感。问“A100 显卡的显存是多少”如果知识库里写的是“NVIDIA A100 80GB”语义向量可能匹配上但如果文档里写的是“GA100 核心的 80GB 显存配置”向量检索有可能召回也有可能漏掉。对于人名、型号、编号、错误码这类强精确匹配场景纯向量检索的稳定性不够。单路召回漏检率高。只用向量检索会对语义相近但字面完全不同的内容失效只用关键词检索则无法处理同义改写、口语化表达。知识库内容越杂单路召回的问题越明显。多路召回后不知道怎么合并排序。很多团队已经意识到要同时用向量检索和关键词检索但两路结果返回后分数体系不一致向量余弦相似度和 BM25 得分根本不在一个量纲上没法直接相加排序。这篇文章要解决的就是这三件事讲清楚稀疏向量检索的原理以及它为什么能补上纯向量检索的短板。给出多路召回后的排序方案重点是 RRF 倒数排名融合算法的原理与实现。提供一套可运行的 RAG 问答系统代码包含文档加载、切片、索引、混合检索、重排序、大模型生成完整链路。适合正在做知识库问答、企业私有文档问答、智能客服或者准备从“Demo 级 RAG”往“可用级 RAG”迈进的开发者阅读。读完你应该能自己搭建一套检索质量明显更稳定的问答系统。2. 基础概念与核心原理2.1 什么是 RAGRAG 全称 Retrieval-Augmented Generation检索增强生成。核心思想很简单大模型的知识是训练时固化的无法覆盖私有数据和最新信息RAG 先从一个外部知识库中检索出与问题相关的文档片段再把这些片段作为上下文交给大模型生成答案。它解决的是大模型的“知识时效”和“私有知识”问题相比微调RAG 不需要重新训练模型知识更新只需更新检索库成本低、速度快、可解释性强。经典 RAG 链路分三个阶段索引阶段加载文档、清洗、切片、向量化、写入向量数据库。检索阶段用户提问后将问题向量化在向量库中做相似度检索召回 TopK 文档片段。生成阶段将召回结果组装成 prompt交给大模型生成最终答案。2.2 稠密向量检索与稀疏向量检索向量检索按向量表示方式主要分两类稠密向量检索和稀疏向量检索。稠密向量检索Dense Retrieval是目前 RAG 应用中最常见的方式。它用深度学习模型如 BGE、OpenAI Embedding 等把文本编码成一个固定维度的向量通常几百到几千维向量中的每个维度都有非零值所以叫“稠密”。检索时计算问题向量与文档向量的余弦相似度或内积返回最相似的 TopK。它擅长理解语义问“怎么申请退款”可以匹配到写“退换货流程”的文档即使两者字面完全不同。这是它最大的优势。但它有两个明显短板对专有名词、编号、精确短语不友好。向量空间里“iPhone 15 Pro Max”和“iPhone 15”距离很近但用户可能就需要精确那个型号。训练数据之外的领域效果不稳定。如果知识库非常垂直冷门通用 Embedding 模型的表征能力可能不足。稀疏向量检索Sparse Retrieval则是另一条路线。它把文本表示成一个高维稀疏向量大部分维度为 0只有少量维度有非零值。最经典的实现就是 BM25它基于词频和逆文档频率来给文本打分。Elasticsearch、OpenSearch 里的传统全文检索就是这个原理。稀疏向量检索擅长精确匹配文档里出现过“A100-80GB”你搜“A100-80GB”一定能命中。但它不理解语义换个说法就搜不到。两种检索方式本质上互补一个管语义泛化一个管精确命中。这也是为什么要做混合检索。这里要注意一个容易混淆的点稀疏向量不只有 BM25 一种形式还有 SPLADE、uniCOIL 这类学习型稀疏向量模型。它们的共同特点是向量维度等于词表大小且向量中大量维度为 0。实际工程中BM25 实现简单、无需训练、开箱即用是绝大多数团队的首选SPLADE 效果更好但需要额外部署模型成本更高。这篇文章以 BM25 作为稀疏检索主力因为它最稳定、最容易落地。2.3 混合检索混合检索Hybrid Search就是把稠密向量检索和稀疏向量检索结合起来两路并行召回再合并排序。它的动机很直接既然两种检索方式各有优劣那就都用上。用户问“支持哪些支付方式”向量检索能召回讲“付款渠道”的文档用户问“支持微信支付吗”BM25 能精确命中含“微信支付”字样的文档。两路结果取并集召回率显著提升。但问题随之而来两路检索的分数体系不一致不能直接相加。BM25 分数通常是 0 到十几的浮点数余弦相似度是 -1 到 1。直接加BM25 会完全主导排序做归一化又会丢失原始分布的区分度。这时候需要一种能融合异构分数的排序算法。2.4 RRF 倒数排名融合算法RRF全称 Reciprocal Rank Fusion倒数排名融合。它不直接操作原始分数而是把“排名”作为融合依据公式如下score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档d在第i路检索结果中的排名。k是平滑常数通常取 60。最终得分是所有检索路线的加权和按分数降序排列得到最终结果。这个公式的精妙之处在于它彻底绕开了不同检索方式分数不可比的问题。BM25 给文档打了 12 分也好向量相似度打了 0.7 也好在 RRF 里都不重要重要的是文档在各自结果列表里排第几。比如某文档在向量检索中排第 1在 BM25 中排第 10k 取 60那它的 RRF 得分就是 1/61 1/70 ≈ 0.0307。另一篇文档在两路中都排第 5得分为 1/65 1/65 ≈ 0.0308。可以看到RRF 鼓励那些在多个检索器中排名都靠前的文档胜出这也是它的核心价值多路共识优于单路高分。相比其他融合方式RRF 有几个工程优势无需调参k 取 60 在绝大多数场景表现良好。对分数分布不敏感天然适配异构检索器。实现成本极低几行代码就能完成。3. 环境准备与前置条件进入实战前先把环境准备好。本文的示例代码以 Python 为主用 Elasticsearch 作为检索后端因为它原生支持稠密向量、BM25 和 RRF版本请以实际项目为准本文重点演示通用思路。建议环境如下Python 3.10 以上Elasticsearch 8.x开启安全认证或本地 HTTP 模式均可一个 Embedding 模型可以使用开源的 BGE 系列也可以使用 OpenAI Embedding 接口一个大模型 API用于最终生成答案OpenAI 兼容接口即可示例文档准备几篇产品说明、FAQ 或技术文档安装依赖pip install elasticsearch pip install openai pip install sentence-transformers如果使用国产 Embedding 模型本地部署需要额外的 torch 和 transformers 依赖pip install torch pip install transformers启动 Elasticsearch 后确认服务可访问。如果是本地默认配置访问http://localhost:9200应返回节点信息。curl http://localhost:9200说明本文示例代码中的索引名、模型名称、API Key 均为占位符请按实际环境替换。版本差异可能带来 API 细节不同但核心流程是一致的。4. 核心流程拆解一个完整的 RAG 问答系统从文档到答案需要经历五个环节。4.1 文档加载与清洗这一步的目标是把原始文档变成可处理的纯文本。不同格式的文档需要不同的解析方式。PDF 用PyPDF2或pdfplumber。Word 用python-docx。HTML 用BeautifulSoup。Markdown 直接读取。清洗阶段要处理的内容包括去除页眉页脚、页码、水印。去除乱码字符、多余空行。统一换行符。表格数据转为 markdown 格式保留结构。这一步做不好后面切片和向量化的质量都会受影响。很多人忽略清洗直接把 PDF 内容切片结果向量库里塞满了无关的页眉信息检索干扰很大。4.2 文档切片切片是 RAG 里最容易被低估的环节。切片太大向量表示不精确检索噪声高切片太小语义不完整生成阶段上下文不足。经验参考常规文档每片 200 到 500 个字符带 50 到 100 字符的重叠。代码文档按函数或类切分。表格密集文档尽量保持表格完整。FAQ 类每条问答单独成片。切片策略没有绝对最优需要根据你的知识库类型做实验。这里的关键原则是保证每片语义自洽。宁可长一点不要把一个完整意思拆碎。4.3 Embedding 向量化将每个切片通过 Embedding 模型编码成向量。注意两个选择查询向量和文档向量是否用同一个模型。大多数场景下共用即可但有些模型专门优化了非对称检索可以按需要选择。向量维度要和你选的向量数据库支持一致。4.4 索引写入把切片的文本、向量、元数据写入 Elasticsearch。需要创建自定义索引映射指定 dense_vector 字段和 text 字段。同时启用 BM25 索引和向量索引。4.5 检索与生成用户提问时并行执行两路检索稠密向量检索将问题编码为向量在 dense_vector 字段上做 kNN 检索。稀疏 BM25 检索对问题文本做全文检索。两路结果经过 RRF 合并排序后取 TopK 作为上下文组装 prompt交给大模型生成答案。5. 完整示例与代码实现下面给出完整可运行的示例。示例使用 Elasticsearch 8.x 的 Python 客户端配合一个 OpenAI 兼容的 Embedding 接口和 Chat 接口。5.1 创建 Elasticsearch 索引# 文件路径create_index.py from elasticsearch import Elasticsearch ES_URL http://localhost:9200 INDEX_NAME rag_knowledge_base es Elasticsearch(ES_URL) # 索引映射同时包含稠密向量字段和文本字段 mapping { mappings: { properties: { content: { type: text, analyzer: ik_max_word }, content_vector: { type: dense_vector, dims: 768, index: True, similarity: cosine }, doc_id: { type: keyword }, chunk_id: { type: keyword } } } } if es.indices.exists(indexINDEX_NAME): es.indices.delete(indexINDEX_NAME) print(f索引 {INDEX_NAME} 已删除) es.indices.create(indexINDEX_NAME, bodymapping) print(f索引 {INDEX_NAME} 已创建)说明content字段使用 text 类型交给 BM25 做全文检索。分词器可按实际场景替换比如英文用 standard中文可以用 IK 分词器如果没有特殊分词需求也可以使用默认 standard。content_vector是 dense_vector 类型维度需要和 Embedding 模型输出维度一致本文示例 768 维如果是 OpenAI 的 text-embedding-3-small 则是 1536 维按实际模型修改。doc_id和chunk_id用于溯源和去重。5.2 文档切片与写入# 文件路径index_docs.py import hashlib from elasticsearch import Elasticsearch from openai import OpenAI ES_URL http://localhost:9200 INDEX_NAME rag_knowledge_base EMBEDDING_MODEL your-embedding-model es Elasticsearch(ES_URL) client OpenAI(api_keyyour-api-key, base_urlhttps://your-api-endpoint) def get_embedding(text: str) - list[float]: 调用 Embedding 接口生成向量 response client.embeddings.create(modelEMBEDDING_MODEL, inputtext) return response.data[0].embedding def chunk_text(text: str, chunk_size: int 300, overlap: int 50) - list[str]: 简单按字符数切片带重叠 chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) if end len(text): break start end - overlap return chunks def index_document(doc_id: str, content: str): 将文档切片并写入索引 chunks chunk_text(content) for idx, chunk in enumerate(chunks): # 生成向量 vector get_embedding(chunk) # 生成唯一 chunk_id chunk_id hashlib.md5(f{doc_id}-{idx}.encode()).hexdigest() # 写入索引 doc { content: chunk, content_vector: vector, doc_id: doc_id, chunk_id: chunk_id } es.index(indexINDEX_NAME, idchunk_id, documentdoc) print(f文档 {doc_id} 索引完成共 {len(chunks)} 个切片) if __name__ __main__: sample_text 微信支付是腾讯公司推出的第三方支付平台支持扫码支付、公众号支付、APP支付等多种方式。 接入微信支付需要先申请商户号并完成企业资质认证。 微信支付支持退款操作退款金额不能超过原订单金额。 扫码支付适用于 PC 网站、公众号、APP 等场景用户通过微信扫一扫完成支付。 index_document(doc_001, sample_text)这段代码的核心逻辑chunk_text按字符数切片带 50 字符重叠保证切片边界不会切断关键上下文。get_embedding调用远程 Embedding 接口你也可以换成本地模型推理。索引写入时同时保存原文、向量和元数据。5.3 混合检索与 RRF 实现这一步是核心。我们用 Elasticsearch 同时执行 kNN 检索和 BM25 检索再进行 RRF 融合。# 文件路径hybrid_search.py from elasticsearch import Elasticsearch from openai import OpenAI ES_URL http://localhost:9200 INDEX_NAME rag_knowledge_base EMBEDDING_MODEL your-embedding-model K 60 # RRF 平滑常数 es Elasticsearch(ES_URL) client OpenAI(api_keyyour-api-key, base_urlhttps://your-api-endpoint) def get_embedding(text: str) - list[float]: response client.embeddings.create(modelEMBEDDING_MODEL, inputtext) return response.data[0].embedding def rrf_fusion(results_list: list[list[dict]], k: int 60) - list[dict]: RRF 倒数排名融合算法 results_list: 多路检索结果列表每路是一个文档列表元素为 {_id: ..., _score: ..., content: ...} # 汇总每篇文档在每路中的排名 rank_map {} score_map {} for results in results_list: for rank, doc in enumerate(results): doc_id doc[_id] # RRF 分数累加 rank_map[doc_id] rank_map.get(doc_id, 0) 1.0 / (k rank 1) # 保留文档内容 score_map[doc_id] doc[_source][content] # 按 RRF 分数降序排序 sorted_docs sorted(rank_map.items(), keylambda x: x[1], reverseTrue) fused_results [] for doc_id, rrf_score in sorted_docs: fused_results.append({ _id: doc_id, rrf_score: rrf_score, content: score_map[doc_id] }) return fused_results def hybrid_search(query: str, top_k: int 5) - list[dict]: 混合检索向量检索 BM25 检索RRF 融合排序 # 生成查询向量 query_vector get_embedding(query) # 1. 稠密向量检索kNN knn_body { knn: { field: content_vector, query_vector: query_vector, k: top_k * 2, num_candidates: 100 }, size: top_k * 2 } knn_response es.search(indexINDEX_NAME, bodyknn_body) knn_results knn_response[hits][hits] # 2. BM25 全文检索 bm25_body { query: { match: { content: query } }, size: top_k * 2 } bm25_response es.search(indexINDEX_NAME, bodybm25_body) bm25_results bm25_response[hits][hits] # 3. RRF 融合排序 fused_results rrf_fusion([knn_results, bm25_results]) return fused_results[:top_k] if __name__ __main__: query 微信支付支持哪些方式 results hybrid_search(query, top_k5) print( 混合检索结果 ) for idx, doc in enumerate(results): print(fTop {idx 1} | RRF分数: {doc[rrf_score]:.6f}) print(doc[content][:100]) print(---)这里解释一下几个关键点kNN 检索num_candidates是候选集大小通常设置为k的 10 到 20 倍。它决定了向量检索的召回范围太大会慢太小会漏。BM25 检索直接用match查询走的就是稀疏检索的 BM25 打分逻辑。RRF 融合rank 1是因为 ES 的排名从 0 开始需要转为从 1 开始。k 取 60 是业界常用默认值对大多数场景稳定。5.4 组装 Prompt 并生成答案检索到相关文档后将文档内容组装进 Prompt调用大模型生成回答。# 文件路径rag_answer.py from hybrid_search import hybrid_search from openai import OpenAI client OpenAI(api_keyyour-api-key, base_urlhttps://your-api-endpoint) CHAT_MODEL your-chat-model def generate_answer(query: str, top_k: int 5) - str: 基于 RAG 链路生成答案 # 1. 混合检索 docs hybrid_search(query, top_ktop_k) # 2. 组装上下文 context \n\n.join([doc[content] for doc in docs]) # 3. 组装 Prompt prompt f请基于以下知识库内容回答问题。 知识库内容 {context} 用户问题{query} 要求 1. 如果知识库内容中不包含答案直接说“根据现有知识库无法回答”不要编造。 2. 答案要准确、简洁尽量引用知识库原文中的信息。 3. 如果答案涉及多个方面请分点说明。 回答 # 4. 调用大模型 response client.chat.completions.create( modelCHAT_MODEL, messages[ {role: user, content: prompt} ], temperature0.3 ) return response.choices[0].message.content if __name__ __main__: query 微信支付接入需要什么条件 answer generate_answer(query) print( RAG 回答 ) print(answer)这个 Prompt 里最重要的一句是“如果知识库内容中不包含答案直接说无法回答”。这不仅能减少幻觉还能帮你判断检索链路是否真的召回了有效文档——如果模型经常回答“无法回答”问题大概率出在检索侧而不是生成侧。5.5 纯 Python 实现 RRF 的简易版本如果不想依赖 Elasticsearch 原生 RRF也可以自己实现纯 Python 的融合逻辑。这个版本对任何检索器都适用。# 文件路径rrf_demo.py def reciprocal_rank_fusion(results_list, k60, top_n10): RRF 算法纯 Python 实现 参数: results_list: 多路检索结果每路为 [(doc_id, score), ...] k: RRF 平滑常数默认 60 top_n: 最终返回前 N 条 返回: [(doc_id, rrf_score), ...] fused_scores {} for results in results_list: for rank, (doc_id, _) in enumerate(results): # rank 从 0 开始转为从 1 开始 fused_scores[doc_id] fused_scores.get(doc_id, 0) 1.0 / (k rank 1) # 按分数降序排序 sorted_docs sorted(fused_scores.items(), keylambda x: x[1], reverseTrue) return sorted_docs[:top_n] if __name__ __main__: # 模拟两路检索结果 # 第一路向量检索返回 [(doc_id, cosine_similarity), ...] dense_results [ (doc_a, 0.91), (doc_b, 0.85), (doc_c, 0.78), (doc_d, 0.72) ] # 第二路BM25 检索返回 [(doc_id, bm25_score), ...] sparse_results [ (doc_d, 8.5), (doc_b, 7.2), (doc_e, 6.8), (doc_a, 6.1) ] fused reciprocal_rank_fusion([dense_results, sparse_results], top_n3) print( RRF 融合结果 ) for doc_id, score in fused: print(f{doc_id}: {score:.6f})运行这个示例你会看到doc_b和doc_d因为同时在两路中排名靠前最终排在前面。这就是 RRF “多路共识优先” 的直接体现。6. 运行结果与效果验证6.1 预期输出依次运行python create_index.py python index_docs.py python hybrid_search.py python rag_answer.py预期看到create_index.py输出索引创建成功。index_docs.py输出文档切片数和索引完成信息。hybrid_search.py输出混合检索结果每篇文档包含 RRF 分数和原文片段。rag_answer.py输出基于检索上下文生成的自然语言答案。6.2 怎么判断检索质量检索质量直接影响最终答案需要单独评估。常见做法是构建一组“问题-期望命中文档”的测试集然后计算召回率RecallTopK 结果中有多少比例包含了期望命中的文档。命中率Hit RateTopK 结果中是否至少包含一条期望文档。MRRMean Reciprocal Rank第一条期望文档出现的位置的倒数均值。手动快速验证的方法准备 10 到 20 个领域问题覆盖精确匹配、语义改写、多义词等类型。跑混合检索肉眼检查 Top 5 是否合理。对比单独向量检索和单独 BM25 的结果确认混合检索是否综合了两者优势。6.3 失败排查顺序如果运行失败或效果明显不对按以下顺序排查看 Elasticsearch 是否正常启动索引是否创建成功。看 Embedding 接口是否能正常返回向量维度是否和索引映射一致。看检索返回的hits.total是否为 0如果为 0 说明索引里没有写入数据。看 Prompt 中上下文字数是否过大如果超过模型上下文限制需要降低 TopK 或切片长度。7. 常见问题与排查思路问题现象可能原因排查方式解决方案索引创建失败dense_vector 维度与 Embedding 模型输出维度不一致检查模型输出维度对比索引映射中的 dims修改 dims 为实际模型维度检索返回 0 条结果索引中无数据或查询语法错误用es.count检查索引文档数重新执行索引写入脚本向量检索结果与问题无关Embedding 模型与知识库领域不匹配用几个已知问题测试向量相似度更换领域更匹配的 Embedding 模型BM25 中文匹配差中文未正确分词查看 analyze 接口的分词结果配置 IK 分词器或使用更适合中文的分词器RRF 融合结果不理想各路召回结果太少排名信息不充分检查每路检索的召回数量增大每路召回候选集数量如从 Top5 扩到 Top20答案经常说“无法回答”检索未召回正确答案直接打印检索结果人工判断是否有答案优化切片策略、增加 TopK、切换检索模型答案幻觉严重Prompt 约束不严或上下文混入无关信息检查 Prompt 中是否明确要求“不知道就说不知道”在 Prompt 中加入拒答指令降低 temperature这里重点说两个容易被忽略的坑第一个坑检索候选集过小。很多 RAG 框架默认每路只召回 Top 5RRF 融合时排名信息有限。实践中建议每路先召回 Top 20 到 Top 50融合后再取 Top 5 给大模型这样排名跨度更大RRF 的区分效果更好。第二个坑ES 8.x 版本差异。不同小版本的 kNN 查询语法有差异。有些版本用knn查询体有些版本用knninsidequery还有版本支持原生rrf参数。如果语法报错优先查官方文档对应版本的 API。本文示例的写法在 8.x 大部分版本可用但不保证所有版本完全兼容。8. 最佳实践与工程建议8.1 检索层面混合检索是基线不是终点。加上 RRF 融合后系统效果已经比纯向量检索稳定得多。但如果知识库量大、内容杂还需要引入重排序Rerank环节先用混合检索召回 Top 50再用 Cross-Encoder 重排序模型精细打分取 Top 5 给生成阶段。重排序模型比双塔 Embedding 更精准但速度慢适合做第二层精排。切片策略用数据说话。不要凭感觉定切片大小。准备一组验证集分别用 200 / 300 / 500 / 800 字切片跑检索对比 Recall 和 Hit Rate选出最稳定的方案。切片重叠建议保留它显著减少切在语义中间的损耗。8.2 工程层面检索链路要可观测。建议给每个检索请求加上 request_id记录每路的召回数量、RRF 分数、最终 TopK 内容和生成答案方便线上问题回溯。如果用户反馈答案不对靠日志可以直接定位是检索问题还是生成问题。知识库更新要有版本概念。生产环境不要直接覆盖索引建议用带版本号的别名类似rag_kb_v1、rag_kb_v2。更新完成后切换别名有问题可以快速回滚。保护 API Key。示例中把 API Key 直接写在代码里仅用于演示。实际项目中必须用环境变量或密钥管理系统保存禁止提交到 Git 仓库。8.3 安全与合规知识库中如果包含用户隐私或敏感数据向量本身也可能携带信息访问控制要在检索阶段就做而不是等生成后才过滤。对生产环境的索引删除操作务必先备份、在测试环境验证。示例中的delete调用仅适合开发环境。大模型生成结果需要人工抽检特别是金融、医疗等严肃场景不能完全自动发布。9. 总结与后续学习方向这篇把一个 RAG 问答系统的关键链路完整过了一遍。核心就一句话RAG 的效果瓶颈在检索检索的工程解法是混合召回混合召回的排序方案选 RRF 最省心。向量检索管语义泛化BM25 管精确命中RRF 把两路结果融合成一份可直接使用的排名成本低、效果稳、易实现。建议下一步按这个顺序实践先把示例代码在自己环境里跑通用你自己的文档建一个小索引试几个问题感受检索质量。构建一个 20 到 50 条的测试集量化对比纯向量、纯 BM25、混合RRF 三者的命中率和召回率你应该能看到明显的变化。如果效果还不够给链路加上完整的重排序阶段引入交叉编码器模型。再往后可以研究高级方向Agentic RAG让大模型自主决定何时检索、查什么、图增强 RAG、RAG 评估体系等。RAG 是一门工程实践远多于理论推演的技术最好的学习方式就是拿真实知识库反复调优。建议收藏这篇文章做项目时把它当作一份检索链路设计对照清单。