RAG 检索工程
翻译说明:本文为英文报告 RAG Retrieval Engineering 的简体中文译本。为保证技术准确性和可执行性,产品名、论文名、模型名、缩写、API 名称与代码块均尽量保留原文;Mermaid 图中的说明文字已翻译为中文。
执行摘要
职位要求中的 “知识召回策略(如混合检索、Rerank 等)”,最直接对应的是 RAG 检索工程(RAG Retrieval Engineering)。这一方向有时也被称为 检索工程(Retrieval Engineering)、搜索相关性工程(Search Relevance Engineering),或 AI 应用工程中的检索层(Retrieval Layer)。
在实际系统中,这项工作位于企业原始数据与大语言模型(LLM)之间:它决定内容如何被分块、建立索引、过滤、检索、融合、重排、组装为上下文,并在生产环境中接受评测、观测与治理。因此,检索工程并不只是“向量搜索”,而是一门围绕有依据的 AI 应用,系统性优化 召回率、精确率、延迟、安全性与成本效率 的工程学科。
对于大多数团队,最稳健的默认架构是一个 两阶段或三阶段流水线:
- 使用基于倒排索引的词法检索(BM25)与基于 Embedding 和 ANN 的稠密检索,作为第一阶段召回层;
- 使用 RRF(Reciprocal Rank Fusion,倒数排名融合) 或归一化加权分数融合,将多路检索结果合并;
- 在生成模型组装上下文之前,对候选结果使用 交叉编码器重排模型(Cross-Encoder Reranker) 进行精排。
这一模式反复出现在主流平台文档和检索研究中,原因是两类方法具有明显的互补性:词法检索仍然擅长处理精确术语、ID、代码、版本号和低频词;稠密检索更擅长语义匹配和同义改写;重排则用于恢复最终候选集的排序精度。
如果你正在为 AI 应用工程岗位做准备,一条稳妥的学习顺序是:
先学习 Lucene、BM25 与分词;再学习 Embedding 与 ANN;然后学习混合检索与重排;最后学习评测、可观测性、ACL 过滤和治理。
跳过第一部分和最后一部分的团队,往往能做出看起来不错的演示系统,却会在真实企业流量、权限边界或成本约束下失败。
背景
检索增强生成(Retrieval-Augmented Generation,RAG)把 LLM 与外部知识连接起来,使模型能够依据预训练数据之外的语料库生成答案。AWS 和 Google 等机构对 RAG 的公开介绍,通常将其概括为“先检索,再生成”。但在生产系统中,这个简单的“检索”方框会扩展成一个大型子系统,其中包括:索引构建、检索编排、过滤、排序和上下文装载。
当职位描述明确提到 混合检索(Hybrid Retrieval) 和 重排(Rerank) 时,企业所说的“知识召回策略”,通常指的正是这个子系统。
从架构上看,检索工程位于 数据摄取(Data Ingestion) 与 生成/编排(Generation / Orchestration) 之间:
- 上游依赖文档解析、元数据提取、ACL 继承、文本分块、Embedding 生成和索引构建;
- 下游决定哪些证据会进入模型上下文、以什么顺序进入、是否存在冗余,以及受到哪些安全约束。
由于检索质量会强烈影响答案质量,真实生产环境中许多看似属于“LLM 质量”的问题,本质上其实是检索问题。
flowchart LR
A[原始数据源] --> B[解析与规范化]
B --> C[分块与元数据]
C --> D1[词法索引]
C --> D2[向量索引]
U[用户查询] --> Q[查询理解]
Q --> R1[BM25 检索]
Q --> R2[稠密检索]
R1 --> F[混合融合]
R2 --> F
F --> RR[重排器]
RR --> CA[上下文组装]
CA --> G[LLM 生成]
G --> O[日志、评测与反馈]
O --> Q
O --> C
可以把检索层理解为承担四类职责:
- 表示工程(Representation Engineering):分词、分析器、Embedding 和元数据;
- 候选生成(Candidate Generation):BM25、稀疏向量、稠密向量、ANN 和混合检索;
- 候选精炼(Candidate Refinement):重排、过滤、去重、多样化和上下文构建;
- 生产控制(Production Control):延迟预算、缓存、可观测性、评测、A/B 测试和治理。
这种拆分方式也与现代搜索引擎和向量系统的 API 设计相吻合:Elasticsearch Retriever、OpenSearch 混合检索流水线、Pinecone 混合搜索与重排、Qdrant 混合查询融合,以及 Milvus 的过滤和多索引搜索,都大体上把“广泛召回”与“精细化排序”区分为不同阶段。
核心技术
词法检索
词法检索仍然是企业级 RAG 的基础。基于 Lucene 风格全文搜索构建的系统依赖 倒排索引(Inverted Index):它把词项映射到包含这些词项的文档。OpenSearch 的概念文档明确以这种方式定义倒排索引;Elasticsearch 和 OpenSearch 的分析器则通过字符过滤器、分词器和 Token Filter 组成的流水线,把文本进一步拆分为词元。
这一点很重要,因为检索质量会受到分析器设计的显著影响。标识符、camelCase、源代码、法律条款、产品 SKU 和多语言文本,通常需要不同的分词策略。
在 Elasticsearch 中,BM25 是文本字段默认使用的相似度模型。Elastic 的文档与 BM25 技术解析指出,BM25 综合考虑以下因素:
- 词频(Term Frequency);
- 逆文档频率(Inverse Document Frequency);
- 文档长度归一化(Document-Length Normalization)。
其中,参数 k1 控制词频饱和程度,参数 b 控制文档长度归一化强度。
在实践中,当查询包含精确业务术语、低频名词、版本号、章节编号、错误消息或产品名称时,BM25 通常非常强。这也是为什么即便在零样本神经检索场景中,BEIR 仍然把 BM25 视为一个稳健基线。
一个重要的工程结论是:优秀的词法检索在选择任何 Embedding 模型之前就已经开始了,它首先取决于 字段设计 和 分析器设计。
- 长篇叙事文本可以使用标准分析器;
- ID 和关键词可能需要精确匹配字段;
- 代码或日志通常需要自定义分词;
- 多语言语料库经常需要语言专用分析器,或为同一内容建立多个索引字段。
如果一个检索系统在精确匹配意图上表现较差,问题常常出在分析器和字段映射,而不一定出在 Embedding 模型。
稠密检索与 ANN
稠密检索把文本表示为 Embedding,并根据向量相似度进行搜索,而不是依赖完全一致的词项重叠。Pinecone 的语义搜索介绍把稠密向量描述为高维空间中的点,语义相近的文本会彼此靠近;Elasticsearch 的稠密向量文档也使用同样的模式,在 dense_vector 字段上执行 kNN 搜索。
稠密检索的核心价值,是弥补 BM25 不擅长处理的同义改写和表达差异。
在大规模数据上,稠密检索通常是近似搜索,而不是穷举式精确搜索。随着语料规模增加,精确最近邻搜索的成本会迅速上升,因此生产系统通常使用 ANN(Approximate Nearest Neighbor,近似最近邻) 索引。
HNSW 的奠基论文提出了一种具有可控层级结构的图式 ANN 索引。今天,HNSW 仍然是现代向量数据库中部署最广泛的 ANN 算法之一。Milvus 把 HNSW 描述为支持从粗到细快速遍历的多层图结构;Qdrant 的搜索文档也将 HNSW 作为其核心向量搜索结构。
FAISS 是稠密相似度搜索研究和系统工程中最具代表性的底层工具包。Meta 与 Faiss 文档将其定义为一个用于高效稠密向量相似度搜索和聚类的库,具有以下能力:
- CPU 和 GPU 实现;
- 多种索引结构;
- 向量压缩;
- 参数调优;
- 支持可能无法完全装入内存的大规模集合。
近期的 Faiss 论文强调,Faiss 本身并不是一个分布式企业数据库;它是一个检索工具包,也经常作为更高层系统的底层引擎。
不同 ANN 家族对应不同的工程权衡:
- HNSW:通常具有优秀的召回率—延迟平衡,调参相对直接,但内存占用可能较高;
- IVF / PQ 类索引:通过粗粒度量化和压缩降低内存与成本,但通常需要更细致的调参,参数不当时可能损失检索质量。
OpenSearch 的 Faiss 产品量化文档指出,PQ 可以与基于 Faiss 的 HNSW 或 IVF ANN 一起使用。Elasticsearch 的稠密向量文档也提供了针对浮点向量的内置量化功能,这反映了整个行业通过量化降低向量搜索内存成本的趋势。
混合检索与结果融合
混合检索结合词法信号和语义信号,因为两类方法的失败模式具有互补性。Pinecone 的混合搜索指南把混合搜索定义为在一次请求中同时结合稠密向量和稀疏向量;Qdrant 和 OpenSearch 则支持把稠密检索与词法检索组合后再进行结果融合。
在企业语料库中,混合检索通常是第一个值得认真投入的基线。例如:
- 一名用户可能搜索:
SOX scope control 2.3 revision; - 另一名用户可能搜索:“我们如何限制财务审批权限?”
前者高度依赖精确词项,后者更依赖语义匹配,两者需要不同的检索信号。
最简单且稳健的融合方法是 RRF(Reciprocal Rank Fusion,倒数排名融合)。Elasticsearch 的 RRF 文档把它描述为一种无需调校不同相关性分数尺度、即可合并多个结果集的方法。其标准形式是:每个结果根据自身在各结果集中的排名,为最终分数贡献:
1 / (k + rank)
OpenSearch 的 Score Ranker Processor 也使用 RRF 来组合不同查询子句的结果。
RRF 之所以流行,是因为它避免了 BM25 分数与向量相似度分数之间脆弱的尺度校准。只要各路 Retriever 都能产生基本合理的排序,RRF 往往就能得到出乎意料地稳健的结果。
当分数校准可行时,加权融合(Weighted Fusion) 可能在稳定领域中超过简单 RRF。OpenSearch 的 Normalization Processor 和混合搜索指南描述了以下流程:
- 对各路分数进行归一化;
- 使用加权算术平均、调和平均或几何平均进行组合。
OpenSearch 的混合搜索技术文章还展示了词法信号与语义信号之间的显式加权算术融合。Qdrant 同样支持基于 RRF 与 DBSF 的排名融合,并允许通过自定义评分公式,在检索分数之上叠加时效性、流行度等业务逻辑。
因此,一个清晰的默认路线是:
- 从 BM25 + 稠密检索 + RRF 开始;
- 当你已经拥有领域标注数据,并且各路分数分布相对稳定时,再升级到 归一化加权融合;
- 如果需要组合两种以上的信号,例如 BM25、稠密检索、稀疏检索、时效衰减和权威度,应优先采用分阶段设计:先融合检索通道,再执行重排,最后应用业务规则。
| 方法 | 优势 | 劣势 | 最适合的场景 |
|---|---|---|---|
| 基于倒排索引的 BM25 | 精确术语、ID、低频稀疏词项;调优透明 | 不擅长同义改写和语义漂移 | 日志、代码、法律文档、商品目录、制度与搜索门户 |
| 稠密检索 | 语义匹配强;对同义改写具有较好鲁棒性 | 可能漏掉罕见精确词项和标识符密集型查询 | FAQ、帮助中心、叙事型文档、多语言语义搜索 |
| 混合检索 + RRF | 几乎不需要分数调校,是稳健默认基线 | 若后端未统一,可能需要同时运维两套系统 | 大多数企业级 RAG 系统 |
| 归一化加权混合检索 | 在有标注数据且经过调优时,可能超过 RRF | 对分布漂移更敏感 | 拥有稳定评测集的成熟领域 |
| 后期交互(Late Interaction) | 精度通常高于单向量稠密检索 | 存储和计算成本更高 | 高价值搜索和复杂问答任务 |
上表综合了 BEIR、各厂商混合搜索文档以及后期交互检索文献中报告的典型行为。
重排
重排是把宽泛候选池转换为精确最终候选集的精炼阶段。Pinecone 的重排文档把重排描述为一个两阶段过程:系统先检索出候选集合,再使用重排模型,根据查询与候选文档之间的相关性重新排序。
这是 RAG 中最标准的模式之一,因为第一阶段搜索的优化目标通常是速度和高召回率,而不是最终的排序精度。
最常见的重排器是 交叉编码器(Cross-Encoder)。Sentence Transformers 文档对双编码器和交叉编码器的差异做了清晰解释:
- 双编码器(Bi-Encoder):分别对查询和文档编码;
- 交叉编码器(Cross-Encoder):把查询—文档对联合输入模型,直接输出相关性分数。
交叉编码器通常更准确,但当候选集合很大时要慢得多。Elasticsearch 的语义重排文档也做出了同样区分,并指出其语义重排支持交叉编码器。因此,交叉编码器通常只应用于前 20~100 个候选结果,而不是整个语料库。
所谓 “类似重排的双编码器精炼”,本质上仍然是更强的第一阶段稠密检索:由于文档向量可以提前计算,它能够扩展到大型语料库;但在评分时,它无法对某个特定查询与某个特定段落之间进行完整的 Token 级交互建模。
一个实用的理解规则是:
- 双编码器用于扩展到大规模语料;
- 交叉编码器用于精炼候选短名单;
- ColBERT 等后期交互模型位于两者之间,通过更高存储开销换取更强的 Token 级匹配。
如果需要解释“语义搜索并不是单一技术”,ColBERTv2 和 LoTTE 评测体系是很有代表性的参考。
基于 LLM 的重排 也正在变得越来越实用,尤其是托管重排 API。Cohere 的官方文档将重排模型描述为:根据查询,对一组文本按语义相关性排序;Pinecone 也提供托管重排端点,接收查询与候选文档列表并返回新排序。
与自行训练重排器相比,这些方案在运维上更简单,但会引入 API 成本、网络延迟和数据治理问题,因此更适合小型候选集、原型验证或高价值工作流。
当系统需要在大规模流量下保持重排质量时,蒸馏(Distillation) 很重要。PairDistill 等近期研究表明,可以让更强的成对排序模型或 LLM 重排器监督更快的 Retriever,从而以更低的在线服务成本保留相当一部分重排质量。
从工业视角看,蒸馏是把“效果很好但太慢”转变为“效果足够好且成本可接受”的桥梁。
| 重排方式 | 准确性 | 延迟 | 基础设施负担 | 典型用途 |
|---|---|---|---|---|
| 交叉编码器 | 在候选短名单上通常最高 | 每个查询—文档对的延迟最高 | 中等 | 对 RAG/搜索的前 20~100 个候选进行重排 |
| 双编码器 | 低于交叉编码器 | 很低 | 低 | 第一阶段稠密检索 |
| LLM 重排 API | 较高,通常易于接入 | 中等到较高 | 运维负担低,但治理和 API 依赖较高 | 高价值搜索/RAG、原型开发 |
| 蒸馏后的 Retriever 或 Reranker | 中等到较高 | 低到中等 | 训练负担高 | 领域稳定、流量较大的成熟系统 |
查询理解、过滤、分块与上下文组装
查询理解是检索工程开始与 Agent 工程发生交叉的地方。
查询改写(Query Rewriting) 可以把信息不足、带有上下文依赖或对话式的用户输入,转化为更适合搜索的形式。Rewrite-Retrieve-Read 论文表明,改写查询本身能够提升检索增强 LLM 的性能。RaFe、DMQR-RAG 等较新的研究则进一步使用排序反馈或多样化的多查询改写。
在生产环境中,查询改写尤其适用于以下情况:
- 用户问题省略了关键信息;
- 当前问题依赖此前对话;
- 用户输入包含大量任务指令,但真正适合检索的关键词很少;
- 原始表述与知识库中的文档语言差异较大。
查询扩展(Query Expansion) 通过添加相关术语或伪文档提高覆盖率。Query2doc 表明,LLM 生成的伪文档可以改善稀疏检索和稠密检索。HyDE 则略有不同:它先生成一个“假设性的相关文档”,对该文档进行 Embedding,然后用这个向量作为检索查询。
当“相关性”更容易通过文档形式表达,而不是通过用户原始的短查询表达时,这两种方法都可能有效。HyDE 尤其适合零样本或标签稀缺环境。
在企业级 RAG 中,元数据过滤和 ACL 过滤不可妥协。Pinecone、Qdrant、Milvus、Elasticsearch 和 OpenSearch 都提供元数据或载荷过滤,但它们对真正访问控制的支持强度不同:
- Pinecone 和 Qdrant 支持通过元数据过滤缩小检索范围;
- Milvus 支持过滤搜索和标量谓词;
- Elasticsearch 和 OpenSearch 还可提供文档级与字段级安全控制,用于基于角色的读取隔离。
如果需求只是“显示 2025 年的产品文档”,元数据过滤可能已经足够;如果需求是“财务用户绝对不能检索到仅限 HR 的段落”,那么搜索引擎原生的访问控制就重要得多。
分块(Chunking) 经常被低估。近期研究显示,分块策略、块大小、重叠长度和上下文构建方式,都会可测量地影响 RAG 的可靠性。一项 2025 年研究比较了 Late Chunking 与 Contextual Retrieval;2026 年的研究则在工业配置下考察了块类型、块大小、重叠和上下文长度。
稳健结论并不是“存在一个适用于所有场景的块大小”,而是:分块应匹配 答案粒度 和 Retriever 的表示能力边界。
对于叙事型企业文档,一个常见起点是:
- 每块约 300~800 Token;
- 使用适度重叠;
- 再基于评测集调优。
对于代码、表格或结构化制度文档,结构感知分块通常优于简单的固定窗口。这个建议一部分来自相关研究,一部分是根据研究结果做出的工程推断。
即使检索结果“正确”,上下文组装仍然很重要。Lost in the Middle 研究表明,语言模型可能无法充分利用放在长上下文中部的证据。因此,检索工程师不应简单地按照原始分数顺序,把前 10 个 Chunk 全部塞进 Prompt。
更好的组装策略通常包括:
- 去重;
- 按文档分组;
- 把最高价值证据放在靠前位置;
- 交错安排不同来源的证据,保持多样性;
- 当新增上下文的边际效用下降时,积极截断。
这是检索工程与 Prompt Engineering 发生重叠的一个典型例子。
flowchart TD
Q[用户查询] --> U1[改写或扩展]
Q --> U2[实体与元数据提取]
U1 --> L1[词法检索]
U1 --> L2[稠密检索]
U2 --> F1[元数据或 ACL 过滤]
L1 --> H[混合融合]
L2 --> H
F1 --> H
H --> R[交叉编码器或 LLM 重排]
R --> D[去重、多样化与分组]
D --> P[Prompt 上下文打包器]
P --> M[LLM]
向量系统与产品选型
虽然 FAISS 是最值得理解的底层 ANN 库,但生产检索工程师通常需要在一体化搜索引擎和向量数据库之间做选择。
- Elasticsearch / OpenSearch:当全文搜索、混合搜索和安全控制需要统一在同一平台中时,通常最有优势;
- Qdrant:在现代混合检索、带过滤的向量检索和灵活查询融合方面表现突出;
- Milvus:适合需要开源分布式向量数据库,并希望支持多类 ANN 索引的团队;
- Pinecone:适合重视托管运维、Serverless 部署和托管推理服务的团队。
| 系统 | 类别 | 代表性能力 | 主要权衡 |
|---|---|---|---|
| FAISS | 库 | 丰富的 ANN 索引、GPU 支持、压缩能力、研究灵活性 | 本身不是完整的分布式企业数据库 |
| Elasticsearch | 搜索引擎 | BM25、稠密向量、RRF/线性 Retriever、安全控制、语义重排 | 运维和许可复杂度可能高于纯向量数据库 |
| OpenSearch | 搜索引擎 | 混合流水线、归一化加权、RRF、DLS/FLS | 某些领域的功能成熟度可能落后于或不同于 Elastic |
| Qdrant | 向量数据库/检索引擎 | 稠密—稀疏混合查询、Payload 过滤、公式评分、实时索引 | 与 Lucene 系技术栈相比,原生全文搜索积累较少 |
| Milvus | 分布式向量数据库 | HNSW/IVF/FLAT/SCANN/DiskANN、过滤搜索、平台文档中的 BM25 全文检索能力 | 需要学习和运维更多基础设施与架构组件 |
| Pinecone | 托管向量数据库 | Serverless 索引、混合搜索、元数据过滤、托管重排 | 存在托管服务依赖,需要严格控制成本 |
上表综合了各厂商官方文档以及 FAISS 的基础研究文献。
实现指南
一种务实的实现方式,是保留 两个检索面(Retrieval Surfaces):
- 一个词法检索面,运行在 Elasticsearch 或 OpenSearch 上;
- 一个向量检索面,可以运行在同一个引擎中,也可以使用 Qdrant、Milvus 或 Pinecone 等专用向量后端。
如果组织已经大量使用基于 Lucene 的搜索系统,那么先用 Elasticsearch 或 OpenSearch 同时承担词法检索和向量检索,通常是阻力最小的路径。
如果主要痛点是大规模语义检索,并希望快速实验,可以把 Qdrant 或 Pinecone 与 Lucene 系引擎组合使用:前者负责向量语义召回,后者负责词法召回和访问控制。
下面的示例使用:
- Python;
- LangChain 风格的应用组合方式;
- Qdrant 作为向量存储;
- OpenSearch / Elasticsearch 作为词法层;
- 交叉编码器 作为重排器。
包名和次要 API 会持续演进,尤其是在 LangChain 不同版本之间。上线前应锁定依赖版本,并使用当前 LangChain 文档,而不是归档的 v0.x 页面。LangChain v1 的迁移指南也明确说明,部分检索辅助组件已经移动位置或更改了导入路径。
示例:使用分块、Embedding 和 Qdrant 构建摄取流程
from typing import List
from dataclasses import dataclass
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from qdrant_client import QdrantClient, models
@dataclass
class ChunkedDoc:
id: str
text: str
metadata: dict
def chunk_documents(raw_docs: List[Document]) -> List[ChunkedDoc]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=600,
chunk_overlap=80,
separators=["\n\n", "\n", ". ", " ", ""],
)
out: List[ChunkedDoc] = []
for i, doc in enumerate(raw_docs):
pieces = splitter.split_text(doc.page_content)
for j, piece in enumerate(pieces):
meta = dict(doc.metadata)
meta["chunk_id"] = j
out.append(
ChunkedDoc(
id=f"{meta.get('doc_id', i)}:{j}",
text=piece,
metadata=meta,
)
)
return out
emb = OpenAIEmbeddings(model="text-embedding-3-large")
client = QdrantClient(url="http://localhost:6333")
collection_name = "kb_chunks"
dim = len(emb.embed_query("dimension probe"))
client.recreate_collection(
collection_name=collection_name,
vectors_config=models.VectorParams(size=dim, distance=models.Distance.COSINE),
)
def upsert_chunks(chunks: List[ChunkedDoc]) -> None:
texts = [c.text for c in chunks]
vecs = emb.embed_documents(texts)
client.upsert(
collection_name=collection_name,
points=[
models.PointStruct(
id=chunk.id,
vector=vec,
payload={"text": chunk.text, **chunk.metadata},
)
for chunk, vec in zip(chunks, vecs)
],
)
这是一条常见的稠密数据摄取流程:分块、生成 Embedding、附加元数据,然后 Upsert。Qdrant 文档强调 Payload 索引与过滤能力,这也是为什么元数据设计应该在项目早期完成,而不应在系统成型后再临时补上。
示例:使用 Elasticsearch 或 OpenSearch 构建词法索引
from opensearchpy import OpenSearch
os_client = OpenSearch(
hosts=[{"host": "localhost", "port": 9200}],
http_compress=True,
)
index_name = "kb_lexical"
mapping = {
"settings": {
"analysis": {
"analyzer": {
"default": {"type": "standard"}
}
}
},
"mappings": {
"properties": {
"title": {"type": "text"},
"body": {"type": "text"},
"doc_id": {"type": "keyword"},
"tenant_id": {"type": "keyword"},
"created_at": {"type": "date"},
"acl": {"type": "keyword"},
}
},
}
if not os_client.indices.exists(index=index_name):
os_client.indices.create(index=index_name, body=mapping)
def index_lexical(doc_id: str, title: str, body: str, tenant_id: str, acl: list[str]):
os_client.index(
index=index_name,
id=doc_id,
body={
"title": title,
"body": body,
"doc_id": doc_id,
"tenant_id": tenant_id,
"acl": acl,
},
refresh=True,
)
这段代码创建了一个支持 BM25 的词法检索面,并显式定义了租户隔离与 ACL 感知过滤所需的元数据字段。如果系统需要文档级或字段级安全,应优先使用搜索引擎原生控制,而不是只信任应用层过滤。
示例:混合检索与客户端 RRF
from collections import defaultdict
from typing import Any
def lexical_search(query: str, tenant_id: str, k: int = 20) -> list[dict[str, Any]]:
resp = os_client.search(
index=index_name,
body={
"size": k,
"query": {
"bool": {
"must": [{"multi_match": {"query": query, "fields": ["title^2", "body"]}}],
"filter": [{"term": {"tenant_id": tenant_id}}],
}
},
},
)
return [
{
"id": hit["_id"],
"text": hit["_source"]["body"],
"score": hit["_score"],
"source": "bm25",
}
for hit in resp["hits"]["hits"]
]
def dense_search(query: str, tenant_id: str, k: int = 20) -> list[dict[str, Any]]:
qvec = emb.embed_query(query)
points = client.query_points(
collection_name=collection_name,
query=qvec,
limit=k,
query_filter=models.Filter(
must=[models.FieldCondition(
key="tenant_id",
match=models.MatchValue(value=tenant_id)
)]
),
with_payload=True,
).points
return [
{
"id": str(p.id),
"text": p.payload["text"],
"score": p.score,
"source": "dense",
}
for p in points
]
def rrf_fuse(rankings: list[list[dict[str, Any]]], rank_constant: int = 60) -> list[dict[str, Any]]:
scores = defaultdict(float)
doc_store = {}
for ranking in rankings:
for rank, item in enumerate(ranking, start=1):
scores[item["id"]] += 1.0 / (rank_constant + rank)
doc_store[item["id"]] = item
fused = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return [{**doc_store[doc_id], "rrf_score": score} for doc_id, score in fused]
def hybrid_search(query: str, tenant_id: str, k: int = 30) -> list[dict[str, Any]]:
bm25_hits = lexical_search(query, tenant_id, k)
dense_hits = dense_search(query, tenant_id, k)
return rrf_fuse([bm25_hits, dense_hits])[:k]
如果后端已经原生支持 RRF 或混合检索流水线,应优先使用原生能力。Elasticsearch 提供 RRF Retriever,OpenSearch 则同时提供基于分数归一化的加权融合与基于排名的融合流水线。上面的客户端实现仍然适合作为概念基线和本地实验实现。
示例:使用交叉编码器进行重排
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def rerank(query: str, candidates: list[dict[str, Any]], top_n: int = 8) -> list[dict[str, Any]]:
pairs = [(query, c["text"]) for c in candidates]
scores = reranker.predict(pairs)
rescored = [{**c, "rerank_score": float(s)} for c, s in zip(candidates, scores)]
rescored.sort(key=lambda x: x["rerank_score"], reverse=True)
return rescored[:top_n]
Sentence Transformers 文档专门说明了交叉编码器在重排中的用法。Pinecone 和 Cohere 也提供等价的托管重排 API;如果不希望自行托管模型,可以把重排能力放在服务边界之后。
示例:使用 HyDE 风格的查询扩展
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
HYDE_PROMPT = """
Write a concise hypothetical passage that would answer the question below.
Do not explain what you are doing.
Question: {question}
"""
def hyde_expand(question: str) -> str:
return llm.invoke(HYDE_PROMPT.format(question=question)).content
def hyde_search(question: str, tenant_id: str, k: int = 20):
hypothetical_doc = hyde_expand(question)
qvec = emb.embed_query(hypothetical_doc)
# then use dense_search-like logic with qvec
这只是对 HyDE 思路的简化演示,不是完整的生产实现。其核心概念来自 HyDE 论文:先生成一个假设性的相关文档,对它进行 Embedding,再检索邻近的真实文档。
推荐技术栈
| 团队规模与约束 | 推荐技术栈 | 原因 |
|---|---|---|
| 小型团队,追求最快上线 | 只使用 OpenSearch 或 Elasticsearch;或者 Pinecone Serverless + 托管重排 | 组件更少;原生混合搜索或托管服务可以降低运维负担 |
| 中型团队,关注成本与可定制性 | Qdrant + OpenSearch/Elasticsearch + 本地交叉编码器 | 混合检索和过滤能力强;相比每次请求调用托管重排 API,更容易控制成本 |
| 企业团队,重视访问控制和审计 | 以 Elasticsearch 或 OpenSearch 为检索骨干,使用引擎原生 DLS/FLS;只有在规模或专业化需要时再增加向量后端 | 可在同一受治理平台中统一 BM25、混合搜索和访问控制 |
| 研究密集型团队或检索平台团队 | FAISS 和/或 Milvus + 自定义评测框架 | 对 ANN、压缩和大规模实验拥有最大控制力 |
评测与测试计划
离线评测是基础。对检索系统而言,最重要的指标包括:
- Recall@K;
- Precision@K;
- MRR;
- nDCG。
Pinecone 的离线评测指南与 Evidently 的排序指标指南概括了这些指标的意义:
- Recall@K 衡量前 K 个结果对所有相关文档的覆盖程度;
- Precision@K 衡量前 K 个候选结果中相关文档所占比例;
- MRR 奖励第一个相关结果更早出现;
- nDCG 在考虑排名位置的同时,衡量具有分级相关性标签的排序质量。
在实践中,第一阶段检索最核心的指标通常是 Recall@K;当系统加入重排后,nDCG@10 和 MRR@10 往往尤其有价值。
基准选择应与领域和语言环境匹配:
- BEIR:仍然是最重要的广泛零样本检索基准,适合在异构任务中比较稀疏、稠密、后期交互和重排方法;
- MIRACL:适合跨 18 种语言的多语言单语检索;
- MTEB:提供覆盖多种任务和语言的广泛 Embedding 评测框架;
- LoTTE:相较许多旧基准,更接近长尾主题检索;
- TREC RAG:提供面向端到端检索—生成系统的公开基准。
| 评测层级 | 要测试的问题 | 建议指标 | 示例数据集 |
|---|---|---|---|
| 第一阶段检索 | 系统能否至少召回相关 Chunk? | Recall@20、Recall@50 | 内部标注集、BEIR、MIRACL、LoTTE |
| 融合质量 | 混合检索是否优于单独使用 BM25 或稠密检索? | Recall@K、nDCG@10 | 内部标注集、BEIR |
| 重排质量 | 重排是否提升头部位置的精度? | MRR@10、nDCG@10、Precision@5 | 内部标注集、BEIR 子集 |
| 上下文组装 | Prompt 装载是否保留了可回答问题的证据? | 答案正确性、引用忠实度、上下文利用率 | 内部问答集、TREC RAG 风格设置 |
| 端到端产品质量 | 用户是否获得了更好的答案? | 人类偏好、有依据性、人工分流效果、延迟、成本 | 带标注的生产流量 |
在线测试不应从“随机把生产流量切换到新 Retriever”开始。一条更稳妥的上线顺序是:
- 离线检索评测;
- 影子流量(Shadow Traffic);
- 小流量切片 A/B 测试;
- 扩大上线范围。
LangSmith 等可观测性平台用于追踪和评测 Agent 与 RAG 行为;OpenTelemetry 则提供标准化的 Trace、Metric 和 Log 基础设施。两者结合,可以提供在真实流量下比较不同检索版本所需的最低限度观测能力。
一个实用的测试矩阵至少应覆盖:
- 单轮查询与多轮查询;
- 精确词项查询与同义改写密集型查询;
- 带租户过滤与不带过滤的查询;
- 短文档与长文档;
- 普通查询与对抗性查询。
每个切片不仅要追踪答案质量,还应追踪:
- 空结果率(Empty-Hit Rate);
- 过滤命中率(Filter Hit Rate);
- 重排器回退率(Reranker Fall-Through Rate);
- p95 延迟;
- 单次请求成本。
检索设计往往最先在这些运行指标上暴露问题。这个建议属于根据上述可观测性与评测工具做出的工程推断。
部署检查清单
上线前,首先验证 数据路径:
- 解析器是否保留文档边界与重要结构;
- Chunk ID 是否稳定;
- 元数据是否规范化;
- 与 ACL 相关的属性是否传播到检索层;
- 每个 Chunk 是否能够追溯到源文档和具体章节。
如果检索结果失去来源可追溯性,系统将很难调试。
然后验证 检索路径:
- 在启用重排之前,分别测量 BM25、稠密检索和混合检索基线;
- 使用明确的召回率—延迟目标调优 ANN 参数;
- 如果使用元数据过滤,应尽早为过滤字段建立索引;Qdrant 和 Pinecone 都强调,过滤性能取决于字段如何建模和建立索引;
- 如果使用 OpenSearch 加权融合,应把归一化与组合配置保存在版本化配置中,确保实验可复现。
接着验证 精度路径:
- 只对延迟预算允许的候选数量执行重排;
- 交叉编码器和托管重排 API 通常不适合超大候选集,这正是两阶段检索存在的原因;
- 如果需要更高吞吐量,应考虑蒸馏或压缩;
- 如果复杂段落上的质量不足,应先测试后期交互模型或更合理的分块,而不是无节制地扩大重排规模。
然后验证 运行路径:
- 增加查询缓存;
- 增加 Embedding 缓存;
- 在合适场景中增加响应缓存;
- 使用 OpenTelemetry 为端到端路径采集 Trace、Metric 和 Log;
- 建立检索专用仪表盘,追踪空结果率、重排队列时间、平均检索 Token 数量和单次请求成本。
Redis 将自身定位为高性能缓存和支持向量能力的数据系统,因此是实现热门查询缓存的一种实用途径。
最后验证 安全与治理路径:
- 对企业系统而言,单纯的元数据过滤通常不够;
- 在需要时使用文档级和字段级权限控制;
- 仔细测试多角色组合;
- 假设攻击者会尝试通过检索链进行提示词注入;
- OWASP LLM Top 10 把提示词注入、敏感信息泄露、供应链问题和数据投毒列为核心风险;
- NIST 的隐私与风险管理框架为 AI 系统的隐私和风险治理提供了更广泛的指导。
一份简明的发布清单如下:
- 至少为主要查询类型建立了带标签的评测集;
- 分别测量了 BM25、稠密检索、混合检索和重排基线;
- 使用对抗性用例测试了 ACL 或租户过滤;
- 为检索和重排设置了 p50、p95 与超时预算;
- 可观测性系统采集 Trace、Metric、Log、检索文档和成本;
- Retriever、融合模式和 Reranker 均具有回滚开关;
- 每次部署都记录分块版本和索引版本;
- 已记录数据保留、隐私和事故响应规则。
上述各项要么直接来自相关平台文档,要么是根据这些平台的能力与风险做出的保守生产工程推断。
推荐学习路线
第一步,在专门学习 RAG 之前,先学习 经典信息检索(Classical Information Retrieval):
- 倒排索引;
- 分词;
- 分析器;
- BM25;
- Lucene 的基本心智模型。
OpenSearch 的概念指南、Elasticsearch 的分析器与分词器文档,以及 Elastic 对 BM25 的解释,都是很好的实践起点,因为它们把教材中的信息检索概念连接到了生产搜索引擎。
第二步,学习 稠密检索与 ANN:
- 阅读 HNSW 论文;
- 阅读 Faiss 论文和当前 Faiss 文档;
- 研究 Milvus、Qdrant、Pinecone 和 Elasticsearch 如何落地 ANN、过滤、量化和混合搜索。
这个顺序能够同时建立算法视角和系统视角。
第三步,学习 混合检索与重排:
- 实现纯 BM25 基线;
- 实现纯稠密检索基线;
- 实现 BM25 + 稠密检索 + RRF;
- 加入交叉编码器重排;
- 最后再研究加权融合、DBSF、后期交互和蒸馏。
这个顺序与真实系统中的成熟度阶梯相符,也能避免过早优化。
第四步,学习 查询理解与分块:
- Query2doc;
- HyDE;
- Rewrite-Retrieve-Read;
- 近期分块评测研究。
在面试中,能够解释“何时应优先改进查询改写,何时应优先调整分块”的候选人,通常比只会罗列术语的人更突出,因为前者是在分析失败模式。
最后,学习 评测、可观测性与治理:
- 使用 BEIR、MIRACL、MTEB 和 TREC RAG;
- 使用 LangSmith 或同类平台配合 OpenTelemetry 为检索栈增加观测能力;
- 使用 OWASP 和 NIST 指南映射系统风险。
正是这一部分,把一个搜索演示系统转变为企业级 AI 应用。
结论
职位描述中突出显示的要求,明确属于更广义 AI 应用工程中的 RAG 检索工程。
它具体覆盖整个检索层:
- 词法检索;
- Embedding;
- ANN;
- 混合融合;
- 重排;
- 查询改写与扩展;
- 元数据与 ACL 过滤;
- 分块;
- 上下文组装;
- 评测;
- 可观测性;
- 治理。
如果岗位描述提到 混合检索 和 Rerank,它希望寻找的并不是一个只会调用向量数据库 API 的工程师,而是能够专业设计、评测和运维整个检索层的人。
截至 2026 年,最可靠的默认架构仍然是分阶段系统:
使用 BM25 + 稠密检索获得高召回率,使用 RRF 或加权融合完成混合排序,使用交叉编码器重排提升精度,执行严格的元数据/ACL 过滤,并建立纪律严明的离线与在线评测体系。
在此基础上,团队可以进一步进入后期交互、蒸馏、更丰富的查询理解和领域专用调优。但如果你能够解释这个基线为什么有效、如何测量它,以及如何安全地在生产环境中运行它,那么你已经具备了与该岗位要求直接相关的核心知识。