RAG Q&A

使用方式:面试时先答 结论,再按 流程/原因/取舍 展开。加粗部分是建议必须说到的关键词。

你在项目中采用的 RAG 向量数据库是什么?

我们生产方案里主选 Milvus 2.x,同时保留 PostgreSQL pgvector 作为 MVP 或轻量部署阶段的备选。

核心原因是 DevMate 面向公司内部约 1000 名用户,规划知识库规模在 100 万到 300 万 chunk,向量数据量大概 100GB 到 500GB。这个规模下需要考虑 集群化、水平扩容、索引性能和高并发检索,所以生产阶段更适合用 Milvus。

在 RAG 链路里,Milvus 不单独工作,而是和 Elasticsearch 组成 混合检索

用户问题 -> Query Rewrite -> 权限过滤 -> Elasticsearch BM25 + Milvus 向量召回 -> bge-reranker 重排 -> 组装上下文 -> 大模型生成带引用答案

面试可以总结为:Milvus 负责语义召回,Elasticsearch 负责精确关键词检索和过滤,reranker 负责排序质量,MySQL 负责元数据和权限。

为什么选这个做向量数据库?你有什么考虑?

我主要从 规模、性能、扩展、安全和工程边界 五个角度考虑。

第一,数据规模比较大。DevMate 不是简单 Demo,而是百万级 chunk、百 GB 级向量数据,Milvus 更适合做大规模 ANN 检索和集群化部署。

第二,在线链路对延迟敏感。知识检索 QPS 目标约 30-60,普通问答 QPS 约 20-40,峰值有 100-150 路并发会话。向量召回后还要 rerank 和模型生成,所以召回层必须稳定,不能成为瓶颈。

第三,架构上符合混合检索。企业研发知识里既有 DTS 编号、接口名、错误码、模块名 这类精确关键词,也有自然语言描述的问题。Milvus 做语义召回,Elasticsearch 做精确匹配,两者结合比单一检索更稳。

第四,权限过滤要前置。Milvus 中会冗余保存项目、密级、状态、版本等轻量过滤字段,检索时做 pre-filter,避免无权限内容进入候选集。

第五,系统边界更清晰。MySQL 管元数据和权限,Milvus 管向量,ES 管关键词索引,MinIO 管原文。这样方便独立扩容、监控和排障。

一句话总结:选 Milvus 不是因为流行,而是因为 DevMate 是千人级、百万 chunk 级、需要长期扩展的企业 RAG 平台。

pgvector 为什么被淘汰了?

准确说 pgvector 不是被淘汰,而是从生产主选降级为 MVP 或小规模场景的备选

pgvector 的优点是 简单、部署成本低、能复用 PostgreSQL,早期验证 RAG 链路很合适。但 DevMate 生产阶段有几个压力:

  • 数据量压力:100 万到 300 万 chunk,向量数据 100GB 到 500GB。
  • 并发压力:检索 QPS 和会话并发都不低,向量召回不能拖慢整条 RAG 链路。
  • 职责过重:PostgreSQL 如果同时承担元数据、权限、审计和大规模向量检索,OLTP 和向量计算会互相影响。
  • 扩展边界不清晰:后续文档量和检索量增长时,Milvus 可以单独扩容,pgvector 更容易和业务库绑定在一起。

面试可以这样答:早期用 pgvector 快速验证,生产用 Milvus 保证规模、性能和隔离性。pgvector 不是不好,而是不适合作为这个规模下的最终主检索引擎。

你们的混合检索是怎么做的?

我们的混合检索是 Elasticsearch BM25 + Milvus 向量检索 + bge-reranker-v2-m3 重排

完整流程可以按七步讲:

  1. Query Rewrite:做同义词扩展、缩写还原、业务术语扩展、关键词提取。
  2. 权限过滤:根据用户部门、项目、角色、文档 ACL、密级生成过滤条件。
  3. 并行召回:ES 走 BM25,适合编号、接口名、错误码;Milvus 走向量检索,适合语义描述。
  4. 候选融合:两路 TopK 合并、去重,并结合文档类型、版本、更新时间、是否废弃等元数据初步调权。
  5. Rerank 精排:用 bge-reranker-v2-m3 判断问题和候选 chunk 的真实相关性。
  6. 上下文组装:取 TopN,做引用编号、同源合并、去重、截断、保留表格和代码块。
  7. 生成答案:大模型基于上下文生成答案,并展示引用来源。

关键点是:BM25 解决精确匹配,向量检索解决语义召回,reranker 解决排序质量,权限过滤保证安全,引用组装保证可追溯。

你们的 TopK 和 TopN 是怎么取的?取多少?

这个参数不是写死的,会按场景配置和调优。默认设计是兼顾 召回率、延迟和 token 成本

普通问答场景:

  • ES BM25 召回 TopK 30-50
  • Milvus 向量召回 TopK 30-50
  • 融合去重后保留 50-80 个候选进入 rerank。
  • rerank 后最终取 TopN 5-8 个 chunk 进入大模型上下文。

复杂问题或深度研究场景:

  • 两路召回可以扩大到 TopK 80-100
  • 融合后控制在 100-150 个候选内。
  • 每轮取 TopN 10-20 个证据片段,但会分步骤处理,配合上下文压缩。

动态调整规则:

  • 问题包含 DTS、接口名、错误码时,提高 BM25 权重
  • 用户自然语言描述现象时,提高 向量召回权重
  • 召回分数集中时减少 TopN,证据不足时扩大召回。
  • 高频 FAQ 可以走缓存或较小 TopK。

面试总结:普通问答两路各 30-50,融合后 50-80 进 rerank,最终 5-8 个 chunk 进上下文;复杂任务会扩大召回,并通过配置中心和评测结果持续调参。

你们是怎么验证这个召回率的?

我们主要用 离线黄金问答集 + 线上反馈回放 两条线验证。

离线评测:

  • 建设 黄金问答集,来源包括高频问题、DTS 问题单、需求平台、Wiki/W3、新人常问问题、故障复盘等。
  • 每条样本包含:用户问题、标准答案、标准证据文档、关键 chunk、文档版本和权限范围。
  • 分阶段看指标:BM25 是否召回、向量是否召回、融合后是否保留、rerank 后是否进入最终 TopN。
  • 重点指标是 Recall@5/10/20、MRR、nDCG、TopN 命中率

生成质量评估:

  • RAGAS + 自研脚本 看答案忠实度、上下文相关性、答案完整性和引用准确性。
  • 如果证据召回了但答案错了,多半是 Prompt 或生成问题;如果证据没进 TopN,就是检索、融合或 rerank 问题。

线上闭环:

  • 记录完整 trace:原 query、改写 query、BM25 结果、向量结果、rerank 排名、最终上下文、引用、答案、用户反馈。
  • 用户点踩后回放链路,判断是 没召回、召回没排上、排上没用、文档过期 哪一类问题。
  • 高频失败问题进入黄金问答集,形成 线上反馈 -> 人工标注 -> 离线评测 -> 参数优化/文档治理 -> 回放验证 的闭环。

还要强调一点:权限也是召回评测的一部分。有权限用户要能召回,无权限用户不能召回,不能为了召回率牺牲安全。

你们切割 chunk 是怎么切割的?

我们的 chunk 切割不是固定长度硬切,而是 结构感知 + 语义边界 + 少量重叠 + 元数据继承

流程如下:

  1. 文档解析清洗:解析 Wiki、DTS、需求文档、Markdown、Word、PDF、Excel 等,清理页眉页脚、重复导航和无意义模板。
  2. 按结构切分:优先按标题层级、章节、接口、问答对、步骤来切,保持一个 chunk 在同一主题下。
  3. 按语义补切:章节过长时,按自然段、列表项、步骤、问题答案对继续切,不在句子、表格、代码块中间硬截断。
  4. 控制长度:普通文本目标约 500-800 中文字300-500 token,最大一般不超过 800-1000 token
  5. 保留 overlap:相邻 chunk 保留约 50-100 token,避免边界信息丢失。
  6. 特殊结构保护:表格保留表头,代码按函数/类/配置段切,FAQ 保持“问题 + 答案”成对。
  7. 元数据继承:每个 chunk 带上来源、标题路径、版本、更新时间、密级、ACL、是否废弃等。

面试总结:先解析清洗,再按标题章节切,再按语义段落补切,对表格和代码块做结构保护,最后绑定权限、版本、来源等元数据。

你们用这种方案切割出来的 chunk 不会太大吗?

不会,因为我们不是只按章节切,而是有 二次语义补切和长度上限

需要澄清一点:“按章节切”不是把完整章节直接作为一个 chunk,而是把章节标题当作 主题边界和上下文路径。真正生成 chunk 时,还是会在章节内部按段落、列表、步骤、问答对继续切。

常见策略是:

  1. 章节用于确定主题边界。
  2. 段落是主要切分粒度。
  3. 句子用于过长段落的兜底拆分。
  4. 多个过短句子会合并,避免切得太碎。
  5. 相邻 chunk 保留少量 overlap。

不同内容会有不同策略:

  • 普通说明文档:控制在 500-800 中文字
  • 接口文档:按接口、参数、返回值、错误码、示例切。
  • 故障复盘:按背景、影响范围、时间线、根因、解决方案、预防措施切。
  • 表格:按主题或行组切,保留表头。
  • 代码:按函数、类、配置段切,避免截断代码块。

关键取舍是:chunk 太小会丢上下文,chunk 太大会降低召回精度、增加 rerank 和 LLM 成本。我们通过结构切分、长度上限和 overlap 在两者之间平衡。

你知道你们线上有多少 chunk 吗?

方案里的规划规模是 100 万到 300 万 chunk,向量数据量预计 100GB 到 500GB

如果面试官问“线上实际多少”,可以稳妥回答:目前我掌握的是方案规划和容量设计口径,按生产目标做的是百万级 chunk 架构;真实线上数量会通过索引后台看 document_count、chunk_count、Milvus collection count、ES index doc count,并监控 ES 和 Milvus 的数量一致性。

可以补一句:我在设计上不是按小 Demo 做的,而是按百万级 chunk 做容量规划、索引异步化和检索扩展。

你们 embedding 是怎么做的?

embedding 分为 文档侧离线/异步 embedding查询侧实时 embedding

文档侧流程:

数据同步 -> Parser 解析清洗 -> Chunk 切分 -> 元数据增强 -> RocketMQ 投递任务 -> 批量调用 bge-m3 生成向量 -> 写入 Milvus -> 元数据写 MySQL -> 关键词索引写 ES -> 原文写 MinIO

这样做的好处是文档入库、向量生成和索引构建不会阻塞在线问答链路,可以通过 MQ 做 削峰、重试、死信队列和失败告警

查询侧流程:

用户问题 -> Query Rewrite -> bge-m3 生成 query embedding -> Milvus 向量召回 -> 与 ES 结果融合 -> rerank

工程细节:

  • chunk embedding 时会拼接 标题路径、模块名、正文,避免短 chunk 语义不足。
  • 文档内容会计算 hash,没变化就不重复 embedding。
  • 每个 chunk 记录 chunkId、documentId、版本号、embedding 模型版本
  • 模型升级时支持灰度重建索引,先评测再切换。

面试总结:文档 embedding 是入库阶段异步批量做的,查询时只对用户问题实时生成 query embedding。

元数据为什么要写入 MySQL?不能直接在向量数据库里吗?

可以在 Milvus 里存一部分过滤字段,但不能只依赖向量数据库保存全部元数据。我们的设计是:MySQL 是元数据权威源,Milvus 只冗余检索需要的轻量字段。

原因:

  • 业务管理需要:文档来源、版本、状态、作者、部门、产品线、解析状态、索引状态等更适合 MySQL 管。
  • 权限和审计需要强记录:文档 ACL、项目权限、密级、权限变更需要可追溯。
  • 向量库职责单一:Milvus 适合向量召回,不适合做复杂后台查询、跨表关联、审计报表。
  • 索引一致性需要追踪:MySQL 可以记录 ES documentId、Milvus vectorId、embedding 版本、失败原因。
  • 过滤字段可冗余:Milvus 保存 knowledgeBaseId、projectId、securityLevel、status、version 等字段用于 pre-filter。

一句话总结:MySQL 管业务元数据、权限、状态和索引映射;Milvus 管向量和必要过滤字段;ES 管关键词;MinIO 管原文。

那你们检索的时候,怎么用元数据过滤去提高向量数据库的检索效率呢?

核心思路是 过滤前置、过滤下推、应用层二次校验

流程如下:

  1. 用户登录后,权限服务提供用户的部门、项目、角色、知识库范围、密级范围等。
  2. RAG 服务结合问题意图,识别产品线、模块、版本、文档类型、时间范围等条件。
  3. 把高选择性字段下推到 Milvus 和 ES,例如 knowledgeBaseId、projectId、sourceType、productLine、version、securityLevel、status、isDeprecated。
  4. Milvus 只在过滤后的向量子集里做 ANN 检索,减少扫描范围。
  5. ES 也带同样的 filter 做 BM25 召回,减少无关候选。
  6. 候选返回后,再基于 MySQL 权威元数据或权限缓存做二次校验,防止权限同步延迟。

示例:用户问“A 产品 2.3 版本最近 DTS 问题集中在哪些模块”,系统会下推 productLine=A、version=2.3、sourceType=DTS、status=ACTIVE、securityLevel<=用户密级、projectId in 用户项目列表

面试总结:把权限、知识库、项目、产品线、版本、文档类型、状态等高选择性字段冗余到检索索引中,在召回阶段做 pre-filter,既提速又防止越权。

原文写入 MinIO 的作用是什么?

MinIO 主要保存 原始文档、附件和解析中间结果,用于追溯、重建和审计。

它的作用可以从五点讲:

  • 保留原始证据:RAG 回答引用 chunk,但用户需要能回到原文。
  • 支持重新解析和重新 embedding:切分策略、清洗规则、embedding 模型升级时,可以从 MinIO 重新处理。
  • 保存大文件:PDF、Word、Excel、图片、日志包不适合直接放 MySQL、ES 或 Milvus。
  • 方便排障审计:通过 chunkId -> documentId -> objectKey 找到原始内容,判断问题出在原文、解析、切分、召回还是生成。
  • 支持版本留存:文档更新后保留历史对象,方便解释旧答案为什么引用旧版本。

总结:MySQL 保存 objectKey 和元数据,MinIO 保存原文和附件,ES/Milvus 负责检索。

你们 embedding 的模型选用的是什么?

embedding 模型选用 bge-m3,rerank 模型选用 bge-reranker-v2-m3

bge-m3 主要做两件事:

  • 文档侧 embedding:文档解析、切分、元数据增强后,异步生成 chunk 向量写入 Milvus。
  • 查询侧 embedding:用户问题经过 Query Rewrite 后,生成 query 向量做语义召回。

选择它是因为 DevMate 的文档以中文研发资料为主,但混合了大量 英文缩写、接口名、错误码、配置项、代码片段和内部术语。bge-m3 对中文和多语言检索比较友好,适合中英文混合场景。

面试总结:bge-m3 负责召回阶段的语义匹配,bge-reranker-v2-m3 负责候选精排。

为什么选择这个模型?你有了解过其他 embedding 模型吗?

选择 bge-m3 主要看 中文效果、多语言能力、私有化部署、reranker 配套和成本平衡

关键原因:

  • DevMate 文档是中文为主、中英文混合,bge-m3 对这类场景比较适配。
  • bge-m3 和 bge-reranker-v2-m3 可以组成完整的 召回 + 精排 方案。
  • 企业内部研发资料有安全要求,embedding 模型需要支持 私有化部署
  • 百万级 chunk 需要考虑批量向量化和模型升级成本,bge-m3 在效果和工程成本之间比较均衡。

了解过的其他模型:

  • text2vec / m3e:中文上手快,适合 Demo 或小规模知识库,但复杂中英文混合和生态配套相对弱一些。
  • GTE 系列:通用检索能力不错,可以作为候选评测对象。
  • E5 / multilingual-e5:多语言效果成熟,但通常要遵循 query/passsage 输入格式,工程适配稍多。
  • 商业 embedding API:效果和接入便利性不错,但企业敏感文档不适合外发。
  • 轻量小模型:成本低,但百万级企业知识库更看重召回质量和稳定性。

面试总结:最终不是拍脑袋选模型,而是会用黄金问答集比较 Recall@K、MRR、nDCG、中文/中英混合效果、推理成本和部署复杂度。当前方案里 bge-m3 更符合企业内网和中文研发知识库场景。

你们 rerank 是怎么做的?

rerank 放在 混合召回之后、上下文组装之前,目标是从“可能相关”的候选里选出“最值得给大模型看的证据”。

流程:

  1. ES BM25 和 Milvus 向量检索分别召回 TopK。
  2. 按 chunkId 合并去重,保留原始分数、文档类型、版本、更新时间、密级、是否废弃等元数据。
  3. 过滤无权限、废弃、状态异常的 chunk,对过期文档降权。
  4. bge-reranker-v2-m3 做 Cross Encoder 精排,把“用户问题 + 候选 chunk”成对输入,得到相关性分数。
  5. 结合业务规则做最终排序,例如版本是否匹配、文档是否最新、是否同产品线、是否高质量 FAQ。
  6. 普通问答取 TopN 5-8 个进入上下文,复杂问题取 10-20 个并分步骤处理。
  7. 上下文组装时做引用编号、同源合并、去重截断、表格/代码块保护。

为什么要 rerank:第一阶段召回追求覆盖率,可能会带入噪声;LLM 上下文有限,不能把所有候选都塞进去。rerank 的核心价值是提升最终证据片段的相关性,减少无关内容干扰大模型。

降级策略:如果 rerank 服务不可用,会临时退回到 BM25 分数 + 向量分数 + 元数据权重 的融合排序,并记录告警和 trace,保证问答链路可用。

后续有新增文档的时候,要怎么做?

新增文档走 增量入库流程,不会手工直接写向量库。

整体链路:

数据源同步 -> 原文入 MinIO -> 元数据入 MySQL -> 解析清洗 -> chunk 切分 -> RocketMQ 投递 embedding 任务 -> 写入 Milvus 和 Elasticsearch -> 更新索引状态

关键步骤:

  1. 发现新增文档:从 Wiki、W3、DTS、需求平台、代码仓库、运维平台或人工上传入口同步,方式可以是 Webhook、消息订阅、定时扫描。
  2. 保存原文和元数据:原文写 MinIO,MySQL 记录 documentId、sourceId、objectKey、hash、版本、作者、部门、密级、ACL 等。
  3. 解析和清洗:不同格式走不同 Parser,保留标题、段落、表格、代码块、列表等结构。
  4. chunk 切分和元数据增强:每个 chunk 绑定 documentId、标题路径、sourceType、产品线、版本、密级、ACL、状态等。
  5. 异步 embedding 和索引构建:RocketMQ 投递任务,bge-m3 生成向量写 Milvus,文本和关键词写 ES,映射关系写 MySQL。
  6. 状态和失败处理:记录 PARSED、CHUNKED、EMBEDDED、INDEXED、FAILED 等状态,失败重试,多次失败进死信队列。

特别要强调权限:新增文档入库时必须同步 ACL、密级、项目、部门等权限元数据,不能出现先全员可见、后补权限的窗口。

面试总结:新增文档先保存原文和元数据,再解析清洗、结构化切分、异步 embedding,最后分别写入 MySQL、ES、Milvus 和 MinIO;全流程有状态记录、失败重试、死信队列和权限同步。

内容变更怎么做?

内容变更采用 hash 判断 + 版本化重建 + 索引无空窗切换

流程:

  1. 发现变更:通过 Webhook、源系统事件、定时扫描或人工上传发现文档更新。
  2. hash 对比:和 MySQL 中上一次 hash 比较;hash 不变就跳过 embedding,只同步状态或时间戳。
  3. 生成新版本:hash 变化时生成 documentVersion / chunkVersion,新原文写入 MinIO。
  4. 重新解析切分:中小文档默认整篇重建;超大文档后续可以做 chunk 级 diff,只处理变化段落。
  5. 先建新索引:新版本 ES 文档和 Milvus 向量都写成功后,再把旧 chunk 标记为 INACTIVE / DEPRECATED。
  6. 后台清理旧数据:物理删除延迟异步做,保留审计窗口和回滚能力。

如果只是权限、密级、项目归属变化,正文没变,就 不需要重新 embedding。只更新 MySQL 权威元数据,并同步 ES/Milvus 的过滤字段,让权限尽快生效。

面试总结:内容变更先做 hash 判断,没变就跳过 embedding;变了就生成新版本并重建索引。为了避免检索空窗,通常先建新索引再下线旧 chunk;权限变更只更新元数据和过滤字段,不重新生成向量。

如果检索时的 TopN 不理想,你有什么方案吗?

TopN 不理想不能只靠把 TopN 调大解决。正确做法是先用 trace 回放定位问题,再针对性优化。

排查顺序:

  1. 正确文档没被召回:是 query rewrite、BM25、向量召回、TopK 或过滤条件问题。
  2. 召回了但排得靠后:是融合排序、元数据权重或 rerank 问题。
  3. 进入 TopN 但答案错:是 Prompt、上下文组装或模型生成问题。
  4. 版本不对或文档过期:是版本字段、更新时间、废弃状态和排序权重问题。
  5. chunk 不完整或噪声多:是切分策略、overlap、标题路径拼接问题。
  6. 权限过滤误伤:是 ACL、项目、密级、过滤字段同步问题。

优化手段:

  • 优化 Query Rewrite:补充同义词、缩写、模块名、错误码、DTS/需求编号识别。
  • 调整召回参数:普通问答 TopK 从 30-50 提到 50-80,复杂问题提高到 80-100。
  • 动态 TopN:简单事实问答取 3-5,复杂分析取 10-20,并按 rerank 分数断崖提前截断。
  • 调整融合权重:编号精确命中、版本匹配、近期文档、高质量 FAQ 加权;废弃文档降权。
  • 优化 rerank 输入:给 reranker 拼接标题路径、来源、版本等上下文,减少重复 chunk。
  • 改进 chunk 策略:调整长度、overlap,对表格、FAQ、代码、接口文档使用专门规则。
  • 补充评测集:把失败 query 加入黄金问答集,用 Recall@K、MRR、nDCG、TopN 命中率验证。

面试总结:TopN 不理想时,先用 trace 定位是召回、融合、rerank、chunk、权限还是数据质量问题;再通过 query rewrite、扩大 TopK、动态 TopN、调整融合权重、优化 rerank 输入和改进 chunk 策略解决。不要盲目只把 TopN 调大,否则会增加 token 成本,还可能把噪声带给大模型。

完整的 RAG 线上流程是什么?需要考虑哪些异常情况和解决方案?

完整 RAG 流程可以分成两条主链路:离线知识入库链路在线问答检索链路。离线链路负责把企业内部知识变成可检索、可追溯、可权限控制的索引;在线链路负责在用户提问时完成权限过滤、混合召回、重排、上下文组装和答案生成。

一、离线知识入库链路

离线入库流程是:

1
2
3
4
5
6
7
8
9
数据源接入
-> 原文保存
-> 元数据入库
-> 解析清洗
-> chunk 切分
-> 权限和业务元数据增强
-> embedding 生成
-> ES / Milvus 索引写入
-> 状态更新和质量校验

第一步是数据源接入。Indexer 从 Wiki、W3、DTS、需求平台、代码仓库、CI/CD、CronSchedule、运维平台、人工上传文档等来源同步数据。接入方式包括消息订阅、Webhook、定时增量扫描和后台手动上传。

线上可能出现的问题:

  • 源系统失败或超时:重试、限流、熔断、断点续扫。
  • 数据重复推送:用 sourceId、documentId、hash 做幂等。
  • 源系统字段不完整:进入待处理队列,标记为 METADATA_INCOMPLETE,后台补录。
  • 数据源格式不统一:为不同 sourceType 配置不同 parser 和字段映射。

第二步是原文保存。新增文档的原文、附件、日志包、解析中间结果会写入 MinIO。MySQL 只记录 objectKey、hash、大小、格式、来源、版本等元数据。

线上可能出现的问题:

  • MinIO 写入失败:任务重试,失败后进入死信队列,不继续构建索引。
  • 文件过大:限制上传大小,超大文件拆分或走异步解析。
  • 原文损坏或格式异常:标记 PARSE_FAILED,并保留失败原因。
  • 文档更新后需要追溯历史:MinIO 保留版本对象,MySQL 记录版本映射。

第三步是元数据入库。document_meta 保存文档来源、标题、作者、部门、产品线、版本、密级、ACL、状态、更新时间、hash、MinIO objectKey 等信息。document_chunk 保存 chunkId、documentId、chunkIndex、标题路径、ES documentId、Milvus vectorId、embedding 版本、索引状态等。

线上可能出现的问题:

  • 元数据和索引不一致:以 MySQL 为权威源,定时校验 ES count、Milvus count 和 document_chunk 状态。
  • 权限字段缺失:不允许进入 ACTIVE 状态,避免默认全员可见。
  • 版本混乱:使用 documentVersion 和 chunkVersion,检索默认只查 ACTIVE 最新版本。

第四步是解析清洗。Parser 对 Markdown、HTML、PDF、Word、Excel、接口文档、DTS 单据、论坛帖子等做解析,清理页眉页脚、重复导航、广告模板、无意义噪声,同时保留标题、段落、表格、代码块、列表、链接等结构。

线上可能出现的问题:

  • PDF 扫描件无法抽取文本:进入 OCR 或人工处理流程。
  • 表格解析错位:使用表头保护和行组切分,必要时保留原表格截图或原文件引用。
  • 代码块被错误切断:代码类文档按函数、类、配置段落切分。
  • 清洗过度导致内容丢失:保留原文,清洗规则版本化,可重新解析。

第五步是 chunk 切分。工程上不是整章直接入库,而是章节确定主题边界,段落作为主要切分粒度,句子作为过长段落的兜底切分。普通 chunk 目标大约 300-500 token,最大约 800-1000 token,相邻 chunk 保留少量 overlap。

线上可能出现的问题:

  • chunk 太小:语义不完整,增加 overlap、标题路径拼接或合并短句。
  • chunk 太大:噪声过多,降低最大长度,按段落/步骤/列表继续切。
  • 表格丢上下文:每个表格 chunk 保留表头和标题路径。
  • FAQ 被拆散:问题和答案成对切分。
  • 代码语义断裂:按函数、类、接口或配置段切分。

第六步是权限和业务元数据增强。每个 chunk 继承 document_meta 的 ACL、密级、项目、部门、产品线、版本、来源类型、更新时间、是否废弃等字段。MySQL 保存完整元数据,ES 和 Milvus 冗余检索过滤所需的轻量字段。

线上可能出现的问题:

  • 权限变更延迟:权限变更事件优先级高于普通索引任务,快速更新 ES/Milvus 过滤字段。
  • 无权限文档被召回:候选返回后再用权限缓存或 MySQL 做二次校验。
  • 文档废弃但仍被召回:status/isDeprecated 下推过滤,并对废弃文档降权或不召回。

第七步是 embedding 和索引构建。Indexer 把任务投递到 RocketMQ,异步消费者调用 bge-m3 生成 chunk 向量,写入 Milvus;关键词、标题、正文、元数据写入 Elasticsearch;原文保存在 MinIO;状态和映射关系写入 MySQL。

线上可能出现的问题:

  • embedding 服务超时:批量大小降级、重试、限流,必要时暂停低优先级任务。
  • RocketMQ 积压:扩容 consumer,按知识库或来源分队列,优先处理权限变更和高优先级文档。
  • ES 写入成功但 Milvus 失败:document_chunk 标记 PARTIAL_INDEXED,后台补偿任务重建缺失索引。
  • 向量模型升级:embeddingVersion 版本化,灰度重建索引,用黄金问答集评估后再切流。

二、在线问答检索链路

在线问答流程是:

1
2
3
4
5
6
7
8
9
10
11
12
用户提问
-> 鉴权和限流
-> 会话上下文加载
-> 意图识别
-> Query Rewrite
-> 权限与元数据过滤
-> BM25 + 向量混合召回
-> 结果融合
-> rerank 精排
-> TopN 上下文组装
-> LLM 生成
-> 引用展示和反馈记录

第一步是鉴权、限流和审计。Gateway 校验登录态,获取用户身份、部门、角色、项目权限、可访问知识库和密级范围,同时初始化 traceId。对用户、部门、场景做 QPS、并发和 token 配额限制。

线上可能出现的问题:

  • 用户无登录态:直接拒绝,不进入检索。
  • 高并发冲击:Gateway 限流,普通问答优先于深度研究任务。
  • 权限服务异常:使用短期权限缓存;缓存不可用时拒绝检索,不放开权限。

第二步是会话上下文加载。Conversation 服务加载历史消息、摘要快照和用户偏好,但不会把大量历史原文直接塞入检索 query。长上下文会压缩成摘要,避免污染检索意图。

线上可能出现的问题:

  • 历史上下文过长:使用 context_snapshot 摘要。
  • 用户追问指代不清:结合上一轮问题做 query rewrite,必要时反问澄清。
  • 历史上下文误导检索:限制历史内容权重,当前问题优先。

第三步是意图识别。Agent 判断用户问题属于普通知识问答、需求查询、DTS 查询、代码辅助、故障排查、深度研究还是工具调用。不同意图使用不同检索参数和工具策略。

线上可能出现的问题:

  • 意图识别错误:保留兜底 RAG 检索,允许用户手动选择知识库或模式。
  • 问题过于复杂:转为深度研究异步任务,SSE/WebSocket 推送进度。
  • 问题缺少关键信息:先反问,比如产品线、版本、模块或时间范围。

第四步是 Query Rewrite。系统会做同义词扩展、缩写还原、业务术语扩展、错误码提取、DTS/需求编号识别、产品线和版本识别。对于中英文混合问题,会保留原始关键词,避免改写丢失精确标识。

线上可能出现的问题:

  • Query Rewrite 过度:保留 original query 和 rewritten query 双路召回。
  • 缩写歧义:结合产品线、用户部门、历史上下文选择解释,必要时多解释并行召回。
  • 编号类问题被语义化:DTS 编号、需求编号、接口名、错误码必须进入 BM25 精确召回。

第五步是权限和元数据 pre-filter。RAG 服务根据用户权限和问题条件生成过滤表达式,下推到 ES 和 Milvus。典型字段包括 knowledgeBaseId、projectId、sourceType、productLine、version、securityLevel、status、isDeprecated、updatedAt。

线上可能出现的问题:

  • 过滤过严导致无结果:逐级放宽业务条件,但不能放宽权限条件。
  • 过滤过宽导致噪声多:增加产品线、版本、sourceType、时间范围等约束。
  • 权限索引延迟:候选返回后做二次权限校验。

第六步是混合召回。ES BM25 负责关键词、编号、错误码、接口名、模块名等精确匹配;Milvus 向量检索负责自然语言语义召回。普通问答两路各取 TopK 30-50,复杂问题可提高到 80-100。

线上可能出现的问题:

  • ES 不可用:降级到 Milvus 向量检索。
  • Milvus 不可用:降级到 ES BM25。
  • 向量召回慢:减少 TopK、优化过滤字段、扩容 Milvus、缓存高频 query embedding。
  • 查询无结果:尝试放宽非权限过滤、使用原始 query、改写 query、多路召回,仍无结果则明确说明无证据。

第七步是结果融合。系统按 chunkId 去重,融合 BM25 分数、向量分数、来源类型、版本、更新时间、文档质量、是否 FAQ、是否废弃等特征。精确编号命中的结果可以加权,过期或废弃文档降权。

线上可能出现的问题:

  • BM25 和向量分数不可比:做归一化或使用 RRF 等 rank fusion 方法。
  • 同一文档多个 chunk 刷屏:同源去重或限制单文档进入 rerank 的 chunk 数。
  • 旧版本排在前面:版本匹配优先,过期文档降权。

第八步是 rerank 精排。融合后的候选进入 bge-reranker-v2-m3,Cross Encoder 对“用户问题 + chunk”成对打分。普通问答融合后大约 50-80 个候选进 rerank,最终取 TopN 5-8;深度研究可以取 10-20 并分步骤处理。

线上可能出现的问题:

  • rerank 不可用:降级到 BM25 分数 + 向量分数 + 元数据权重排序。
  • rerank 延迟高:减少候选数、批量推理、部署多实例、对高频问题缓存结果。
  • TopN 不理想:用 trace 回放定位召回、融合、rerank、chunk 或元数据问题,再针对性优化。

第九步是上下文组装。系统将 TopN chunk 组装为 Prompt 上下文,做引用编号、标题路径补充、同源合并、去重、截断、表格/代码块保护,并附带来源文档、版本、DTS 编号、需求编号、论坛链接等。

线上可能出现的问题:

  • 上下文超长:按 rerank 分数、来源多样性和引用必要性截断。
  • 证据不足:明确说明知识库没有足够证据,不强行编答案。
  • 证据冲突:提示存在版本或来源冲突,优先采用最新或权威来源。
  • 引用丢失:每个 chunk 必须携带 sourceId 和 citationId,生成后校验引用。

第十步是 LLM 生成。Agent Runtime 组装用户问题、会话摘要、检索证据、回答约束和引用要求,通过 ModelGateway 调用私有大模型,使用 SSE 流式返回。回答必须基于证据,不允许无依据扩展。

线上可能出现的问题:

  • LLM 超时或异常:切备用模型、缩短上下文,必要时返回检索摘要。
  • 模型幻觉:强制引用来源,无证据不回答,答案后处理检查引用覆盖。
  • 首 token 慢:SSE 流式输出,模型网关限流和并发控制。
  • 敏感信息输出:生成后做敏感信息扫描和脱敏。

第十一步是反馈和审计。系统记录 query、改写、召回结果、rerank 结果、TopN、Prompt 摘要、模型输出、引用、token 消耗、耗时、用户反馈和 traceId。用户点赞、点踩、标注错误会进入反馈闭环。

线上可能出现的问题:

  • 用户反馈错误:按 trace 回放定位是召回、排序、chunk、文档质量还是生成问题。
  • 高频失败问题:加入黄金问答集,回归评测后再调参。
  • 成本过高:FAQ Cache、小模型路由、token 配额、TopN 动态截断。

三、更新、删除和权限变更

新增文档走增量入库。内容变更先计算 hash,hash 不变则跳过 embedding;hash 变化则生成新版本,重新解析、切分、embedding 和索引。为了避免检索空窗,通常先构建新版本索引,成功后再把旧 chunk 标记为 INACTIVE 或 DEPRECATED。

删除文档时,不建议直接物理删除。先逻辑删除,将 MySQL、ES、Milvus 中的 status 改为 DELETED 或 INACTIVE,检索 filter 默认排除。物理删除由后台异步清理,保留一定审计窗口。

权限变更时,如果正文没变,不需要重新 embedding,只更新 MySQL 权威元数据,并同步 ES/Milvus 的过滤字段。权限变更优先级要高,防止越权窗口。

线上可能出现的问题:

  • 新旧版本同时被召回:检索 filter 只查 ACTIVE 最新版本,旧版本降权或排除。
  • 删除后仍被召回:ES/Milvus 状态字段未同步,触发一致性修复任务。
  • 权限变更未及时生效:权限事件高优先级处理,候选结果二次校验兜底。

四、缓存和性能优化

RAG 线上需要控制延迟和成本。常见优化包括:

  • FAQ Cache:高频问题直接返回已验证答案或减少召回范围。
  • Query embedding Cache:相似 query 复用 embedding。
  • 检索结果 Cache:对权限范围一致的高频 query 缓存 TopN。
  • 并行召回:BM25 和向量检索并行。
  • Rerank 候选控制:只对融合后的有限候选精排。
  • 动态 TopN:简单问题少取,复杂问题多取。
  • 长文档两级索引:摘要索引用于粗定位,原文索引用于细节回答。

线上可能出现的问题:

  • 缓存导致旧答案:缓存 key 包含知识库版本、权限版本、模型版本。
  • 热点 query 冲击模型:FAQ Cache 和限流。
  • 深度研究影响普通问答:异步队列隔离,普通问答优先。

五、降级策略

线上系统不能因为某个组件异常导致整体不可用,需要分层降级:

  • Milvus 不可用:降级到 Elasticsearch BM25。
  • Elasticsearch 不可用:降级到 Milvus 向量检索。
  • Rerank 不可用:使用融合排序。
  • LLM 不可用:切换备用模型,或只返回检索结果摘要。
  • Embedding 服务不可用:在线 query embedding 失败时返回稍后重试;离线任务进入队列等待恢复。
  • MinIO 不可用:不影响已有索引检索,但引用原文和新增入库受影响。
  • MySQL 或权限服务不可用:核心元数据和权限不可用时停止敏感检索,不放开权限。

六、监控和评测

线上需要监控完整链路指标:

  • 入库链路:同步延迟、解析失败数、embedding 队列积压、索引失败数、死信队列数量。
  • 检索链路:BM25 耗时、向量检索耗时、rerank 耗时、TopN 命中率、无结果率。
  • 生成链路:首 token 延迟、完整回答耗时、token 消耗、模型超时率。
  • 安全链路:权限过滤命中数、二次校验拦截数、敏感信息拦截数。
  • 质量指标:Recall@K、MRR、nDCG、答案忠实度、引用准确率、用户满意度。

评测上使用黄金问答集和 RAGAS。黄金问答集覆盖高频问题、DTS、需求、Wiki、故障复盘、接口说明和新人常问问题。每次调整 query rewrite、TopK、TopN、rerank、chunk 策略或 embedding 模型,都要做离线回归,再灰度上线。

七、面试总结

完整 RAG 不是“向量库查一下再问大模型”,而是一条生产链路:离线侧负责文档同步、解析、切分、embedding、索引、权限和版本管理;在线侧负责鉴权、query rewrite、元数据过滤、BM25 + 向量混合召回、融合、rerank、上下文组装、LLM 生成和引用追溯。

线上要重点考虑:权限不能越权、索引要可追踪、更新要无空窗、组件异常要可降级、TopN 质量要可评测、用户反馈要能回放定位。只有这些都闭环,RAG 才能从 Demo 变成企业级生产系统。

介绍一下向量数据库的索引?

向量数据库索引的核心作用是:在大量高维向量里快速找到和 query 向量最相似的 TopK 向量。如果不建索引,就要全量计算相似度,数据量大时延迟会很高。所以生产环境通常使用 ANN 近似最近邻检索,在召回率和查询性能之间做平衡。

向量检索要先明确两个概念:

  • 相似度度量:文本 embedding 常用 cosine 或 inner product,也可能用 L2 distance。
  • 索引结构:决定怎么组织向量、怎么减少扫描范围、怎么提升检索速度。

常见索引:

  • FLAT:暴力检索,最准确但最慢,适合小数据量、离线评测或作为基线。
  • IVF 系列:先把向量聚类成多个分区,查询时只扫部分分区;通过 nlist 和 nprobe 在速度和召回之间取舍。
  • HNSW:图结构索引,召回和延迟表现较好,适合在线检索,但内存占用和构建成本更高。
  • DiskANN / 磁盘型索引:适合更大规模、内存放不下的场景,需要结合 SSD 和缓存设计。

在 DevMate 场景里,预计 100 万到 300 万 chunk100GB 到 500GB 向量数据,不会用 FLAT 作为生产主索引。生产上会在 HNSW 和 IVF 系列 之间评测选择:

  • 如果内存充足、追求在线召回质量和低延迟,优先考虑 HNSW
  • 如果数据继续扩大、希望更可控的内存和查询成本,可以考虑 IVF
  • 无论哪种索引,都要配合 元数据 pre-filter,比如 knowledgeBaseId、projectId、productLine、version、securityLevel、status,先缩小候选集合。

索引参数也不是拍脑袋定的,需要用黄金问答集评估 Recall@K、MRR、nDCG、P95/P99 延迟、内存占用和构建耗时

面试总结:向量索引用来避免全量扫描,通过 ANN 在速度和召回率之间做平衡。常见索引有 FLAT、IVF、HNSW、DiskANN;DevMate 生产会根据百万级 chunk 规模在 HNSW 和 IVF 之间评测选择,并结合元数据 pre-filter、TopK 调参和黄金问答集验证效果。

实际使用场景中怎么区分图片该使用 OCR 还是模型提取摘要?

实际线上不会人工逐张判断,而是做 图片类型识别 + 策略路由。OCR 和图像摘要不是替代关系,而是互补关系:OCR 负责精确文字,图像摘要负责理解结构关系和视觉语义。

判断规则:

  • 文字密集型图片优先 OCR:报错截图、日志截图、配置截图、命令行截图,重点提取错误码、路径、IP、命令、配置项。
  • 表格截图用 OCR + 表格结构恢复:不仅要识别文字,还要恢复表头、行列关系和字段含义。
  • 架构图/流程图/时序图用 OCR + 图像摘要:OCR 提取节点文字,图像摘要描述模块关系、调用方向和流程含义。
  • 趋势图/监控图用 OCR + 图像摘要:OCR 提取指标名、坐标轴、时间、阈值;摘要描述趋势、峰值和异常点。
  • 产品界面截图用 OCR + 图像摘要:OCR 提取页面文字和字段,摘要描述页面状态、操作结果或异常提示。
  • logo、背景图、装饰图跳过解析:只保留原图到 MinIO,不生成检索 chunk。

工程流程:

1
2
3
4
5
6
提取图片
-> 轻量分类
-> 判断文字密度、表格线、箭头节点、图表坐标轴、截图特征
-> 路由到 OCR / OCR+表格恢复 / OCR+图像摘要 / 跳过
-> 生成 image_text 或 image_summary chunk
-> 绑定 sourceImageObjectKey、pageNo、imageType、ocrConfidence

默认策略可以这样落地:先做 OCR;如果 OCR 文本很少但图片面积较大,并且像架构图、流程图、监控图,就补充图像摘要;如果判断是装饰图,就跳过。

面试总结:文字密集看 OCR,关系图和趋势图看图像摘要,表格图做结构恢复,装饰图跳过。OCR 提取精确文本,图片摘要补充视觉语义,两者组合后再生成可检索的 image_text 或 image_summary chunk。

给我介绍一下 RRF

RRF 全称是 Reciprocal Rank Fusion,中文可以理解为 倒数排名融合。它常用于把多个检索器的结果合并排序,比如把 Elasticsearch BM25 的关键词检索结果和 Milvus 的向量检索结果融合在一起。

RRF 解决的核心问题是:不同检索系统的原始分数不可直接比较。例如 BM25 分数可能是 18.5,向量相似度可能是 0.82,这两个分数的量纲和分布完全不同,直接相加不合理。RRF 不看原始分数,而是看每个结果在各自检索结果里的排名。

RRF 的常见公式是:

1
RRF_score(d) = Σ 1 / (k + rank_i(d))

其中:

  • d 表示某个候选文档或 chunk。
  • rank_i(d) 表示这个 chunk 在第 i 个检索器里的排名。
  • k 是平滑参数,常用值是 60。
  • 如果某个 chunk 没出现在某一路检索结果里,那一路就不加分。

举个例子,假设有两个检索器:BM25 和向量检索。

chunkA 在 BM25 里排第 1,在向量检索里排第 5:

1
RRF(A) = 1 / (60 + 1) + 1 / (60 + 5)

chunkB 在 BM25 里排第 10,在向量检索里排第 2:

1
RRF(B) = 1 / (60 + 10) + 1 / (60 + 2)

如果一个 chunk 在 BM25 和向量检索里都排名靠前,它的 RRF 分数会更高;如果只在一路出现,也能得到分数,但通常不如两路都靠前的结果稳定。

RRF 的优点是:

  • 不需要归一化 BM25 分数和向量分数。
  • 对不同检索器的分数尺度不敏感。
  • 实现简单,线上稳定。
  • 适合混合检索结果融合。
  • 对“多路都认为相关”的结果更友好。

在 DevMate 的 RAG 里,RRF 可以用于 rerank 之前的候选融合,也可以作为 rerank 不可用时的降级排序方案

实际流程可以是:

1
2
3
4
5
6
7
BM25 TopK
+ Vector TopK
-> 按 chunkId 合并去重
-> 计算 RRF_score
-> 叠加元数据权重
-> 同文档去重
-> 取 TopN

元数据权重可以包括版本匹配、产品线匹配、文档是否最新、来源是否权威、是否高质量 FAQ 等。但权限不能作为权重,权限不满足必须直接过滤。

RRF 也有局限。它只利用排名,不利用原始相关性分数,所以如果某一路检索器的排序本身质量很差,也可能把噪声带进来。因此线上一般还会结合元数据过滤、同源去重、rerank 精排和黄金问答集评测。

面试总结:RRF 是一种基于排名的结果融合方法,不直接比较 BM25 分数和向量相似度,而是根据候选在不同检索器中的排名累加倒数分数。它特别适合混合检索,因为 BM25 和向量分数量纲不同,用 RRF 可以稳定地融合多路召回结果。

RAG 整个方案里会用到哪些模型,分别作用是什么?

RAG 方案里不是只用一个大模型,而是会在不同阶段使用不同类型的模型。整体可以分成 Embedding 模型、Rerank 模型、对话生成模型、Query Rewrite/意图识别模型、多模态/OCR 模型、安全与评测模型 几类。

1. Embedding 模型

方案里主选 bge-m3

作用是把文本转成向量,用于语义检索。它会用在两个地方:

  • 文档入库时:把 chunk 内容向量化,写入 Milvus。
  • 用户查询时:把用户 query 向量化,用于和文档 chunk 做相似度匹配。

Embedding 模型解决的是“语义相似召回”问题。比如用户问“模型网关挂了怎么降级”,即使文档里写的是“ModelGateway 不可用时切换备用模型”,向量检索也有机会召回。

2. Rerank 模型

方案里使用 bge-reranker-v2-m3

作用是在 BM25 和向量检索粗召回之后,对候选 chunk 做精排。它会把:

1
用户问题 + 候选 chunk

成对输入模型,输出相关性分数。

Embedding 适合快速粗召回,Rerank 更适合精细判断相关性。它可以过滤掉“关键词命中但语义不相关”的候选,提升最终 TopN 上下文质量。

3. 对话生成模型

方案里推荐私有化部署的 Qwen2.5-72B / DeepSeek-R1-Distill / 企业私有大模型,通过 vLLM 部署。

作用是基于检索到的证据生成最终答案。它不负责从全库找资料,而是基于 RAG 检索出来的上下文进行回答,并生成结构化结论、引用来源、排查步骤或方案对比。

在 DevMate 场景里,对话模型还承担:

  • 多轮问答。
  • 基于证据生成答案。
  • 总结 DTS、需求、故障复盘。
  • 生成深度研究报告。
  • 在证据不足时拒答或提示需要补充信息。

4. Query Rewrite 和意图识别模型

这部分可以用轻量模型、规则引擎,也可以由大模型承担。

作用是在检索前理解用户问题,并改写成更适合检索的形式。

包括:

  • 同义词扩展。
  • 缩写还原。
  • 产品线、版本、模块识别。
  • DTS 编号、需求编号、错误码提取。
  • 判断是知识问答、DTS 查询、需求查询、故障排查、代码辅助还是深度研究。

例如用户问“2.3 版本那个超时问题怎么处理”,Query Rewrite 可能会补充:

1
2
3
version = 2.3
keywords = 超时 / timeout / connection timeout
sourceType = DTS / Wiki / 故障复盘

5. OCR 模型

OCR 用于处理文档图片里的文字,比如报错截图、日志截图、配置截图、表格截图。

作用是把图片里的文字提取成可检索文本。例如:

1
2
3
error_code: DTS_504
connection timeout
node: storage-node-03

OCR 结果会作为 image_text chunk 入库,进入 ES 和 Milvus。

6. 图像理解 / 图片摘要模型

用于处理架构图、流程图、时序图、监控趋势图、产品界面截图。

作用不是简单读字,而是理解图里的结构关系和趋势。例如:

1
该架构图描述 Gateway 调用 Agent Runtime,Agent Runtime 分别调用 RAG、ToolHub 和 ModelGateway。

图片摘要会作为 image_summary chunk 入库。一般做法是 OCR + 图像摘要结合:OCR 提取精确文字,图像模型理解结构语义。

7. 代码摘要模型

代码 chunk 入库时,可以用规则或 LLM 生成代码语义摘要。

作用是增强自然语言召回。因为用户可能会问“Milvus 查询失败后怎么降级”,而代码里可能只有方法名 fallbackToEs。摘要可以把代码行为转成自然语言:

1
该方法在 Milvus 检索失败时降级调用 Elasticsearch BM25 检索,并记录 traceId。

代码摘要不是替代原代码,而是辅助 embedding 和检索。

8. 安全与脱敏模型/规则

这类不一定是大模型,也可以是规则引擎 + 分类模型。

作用是识别和拦截敏感信息:

  • 密钥、token、账号密码。
  • 内网地址。
  • 个人隐私。
  • 越权内容。
  • Prompt Injection。

在入库阶段和生成阶段都可以做安全检查。入库时避免把敏感内容直接进入索引;生成后避免把敏感信息输出给无权限用户。

9. 评测模型

评测阶段会用 RAGAS + 自研黄金问答集,必要时也可以用 LLM-as-judge。

作用是评估 RAG 效果,包括:

  • Recall@K。
  • MRR。
  • nDCG。
  • TopN 命中率。
  • 答案忠实度。
  • 引用准确率。
  • 答案相关性。

评测模型不参与线上回答主链路,主要用于离线评估、模型升级、参数调优和灰度验证。

10. 各模型在链路中的位置

可以总结为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
文档入库:
Parser/OCR/图像理解/代码摘要
-> bge-m3 embedding
-> 写入 ES + Milvus

用户查询:
意图识别 / Query Rewrite
-> bge-m3 query embedding
-> BM25 + 向量召回
-> bge-reranker-v2-m3 精排
-> 私有大模型生成答案
-> 安全脱敏和引用校验

离线评测:
黄金问答集 + RAGAS / LLM-as-judge

面试总结:RAG 里会用多类模型:bge-m3 做文档和 query embedding,bge-reranker-v2-m3 做候选精排,Qwen/DeepSeek 等私有大模型做最终答案生成,轻量模型或规则做 query rewrite 和意图识别,OCR 和图像理解模型处理图片,代码摘要模型增强代码检索,安全模型做脱敏和注入检测,评测模型用于离线质量验证。

文件存储为什么选择 MinIO?从技术选型的角度说一下

在 DevMate 的 RAG 方案里,MinIO 主要用于存储 原始文档、附件、图片、日志包、解析中间结果和历史版本文件。选择 MinIO 的核心原因是:它更适合企业私有化场景下的大文件和非结构化对象存储,而不是把这些内容放到 MySQL、Elasticsearch 或 Milvus 里。

从技术选型角度看,主要有几个考虑。

第一,MinIO 是对象存储,适合存原文和附件。RAG 入库时会遇到 PDF、Word、Excel、Markdown、图片、日志压缩包等文件,这些文件体积大、格式多、访问模式偏“上传、下载、归档、重解析”,不适合放在关系型数据库里。MySQL 更适合存元数据,比如 objectKey、hash、版本、权限、索引状态;MinIO 负责存文件本体。

第二,MinIO 支持私有化部署。DevMate 面向企业内部研发文档、DTS、需求记录、代码说明和故障复盘,数据安全要求高,不适合直接依赖公网对象存储。MinIO 可以部署在公司内网或 Kubernetes 集群里,数据不出内网,符合企业内部知识库和私有化大模型场景。

第三,MinIO 兼容 S3 协议。S3 API 是对象存储事实标准,Java 生态里 SDK 成熟,后续如果要迁移到其他对象存储,比如云厂商 OSS、OBS、S3,改造成本相对低。也就是说,业务代码只依赖对象存储接口,不强绑定某个厂商。

第四,MinIO 性能和运维复杂度比较适中。它部署简单,支持分布式模式、纠删码、桶策略、对象版本管理等能力。对于企业内部 RAG 项目来说,MinIO 比自研文件服务可靠,也比把文件塞进数据库更容易扩展和维护。

第五,它方便支持版本化和重建索引。RAG 的 chunk 策略、OCR 规则、embedding 模型后续都可能调整。如果原文保存在 MinIO,就可以根据 MySQL 里的 objectKey 重新读取原文,重新解析、切分和 embedding,而不需要再次依赖源系统拉取。对于源文档被删除、源系统不可用、历史答案追溯等场景,这一点很重要。

第六,它能和 RAG 分层存储架构清晰配合。我们的职责划分是:

1
2
3
4
5
MinIO:原文、附件、图片、解析中间结果
MySQL:文档元数据、chunk 元数据、权限、索引状态
Elasticsearch:关键词索引、精确检索
Milvus:向量索引、语义检索
Redis:缓存、会话快照、限流计数

这样每个系统只做自己擅长的事情,边界清晰,也方便排障。比如用户反馈某个答案错误,可以通过 chunkId 找到 documentId,再从 MySQL 找到 MinIO objectKey,回到原始文档检查到底是原文问题、解析问题、切分问题、召回问题还是生成问题。

当然,MinIO 也不是没有取舍。它需要我们自己部署、监控、备份和做容量规划;如果公司已经有统一对象存储平台,也可以复用内部对象存储。但在没有统一存储平台、又需要私有化和 S3 兼容能力的情况下,MinIO 是比较合适的工程选型。

面试总结:选择 MinIO 是因为 RAG 需要保存大量原始文档、附件、图片和历史版本,这类非结构化大对象不适合放 MySQL、ES 或 Milvus。MinIO 支持私有化部署、S3 协议、分布式扩展和版本留存,能支撑原文追溯、重新解析、索引重建和审计排障,是企业级 RAG 分层存储里比较合适的对象存储方案。

说一下你们生产环境上的服务器配置

我们的生产环境按 Kubernetes 集群 + 数据层集群 + AI 推理层集群 来规划,核心服务都做多副本和水平扩展,避免单点故障。整体目标是支撑约 1000 名内部用户、峰值 100-150 路并发会话、普通问答 QPS 20-40、知识检索 QPS 30-60。

业务服务层部署在 K8s 上,初始配置大概是:

组件 初始配置 说明
Gateway 2C4G x 2 统一入口、鉴权、限流、路由、审计
Conversation 4C8G x 3 会话管理、SSE 流式输出、上下文压缩
RAG 4C8G x 3 query rewrite、混合检索、rerank 前后处理、上下文组装
ToolHub 2C4G x 2 MCP 工具封装,连接 DTS、需求平台、W3 等
Research 4C8G x 2 深度研究异步任务、进度推送、报告生成
Indexer / Parser 4C8G x 2 文档同步、解析、chunk 切分、embedding 任务投递
Admin 2C4G x 1-2 知识库管理、权限配置、模型配置、评测管理

数据层会独立部署,避免和在线业务服务抢资源:

组件 初始配置 说明
MySQL 主从或 MGR 存用户、权限、文档元数据、chunk 元数据、审计日志
Redis Cluster 3 主 3 从或等价配置 会话快照、热点问答缓存、限流计数、分布式锁
Elasticsearch 8C32G x 3 关键词检索、过滤、聚合分析
Milvus 8C32G x 3 向量检索,支撑百万级 chunk 的语义召回
MinIO 分布式多节点 存原文、附件、图片、解析中间结果
RocketMQ 集群部署 文档解析、embedding、索引构建、深度研究任务削峰

AI 推理层单独规划 GPU 资源:

组件 初始配置 说明
vLLM 推理集群 GPU x 2 起 部署 Qwen / DeepSeek / 企业私有模型,按 token 吞吐扩容
Embedding 服务 CPU 或 GPU 多实例 bge-m3 文档和 query embedding
Rerank 服务 CPU 或 GPU 多实例 bge-reranker-v2-m3 候选精排

这里的配置不是固定不变的,会按指标扩容:

  • Gateway 按 QPS 和连接数扩容。
  • Conversation 按 SSE 并发连接数扩容。
  • RAG 按检索 QPS、rerank 耗时和 P95 延迟扩容。
  • Indexer / Parser 按文档入库高峰临时扩容。
  • ES 按索引规模、查询延迟和 shard 压力扩容。
  • Milvus 按向量规模、召回延迟、内存和索引构建压力扩容。
  • vLLM 按首 token 延迟、tokens/s、GPU 利用率和排队长度扩容。

生产上还会配套可观测能力:

  • Prometheus 采集 QPS、延迟、错误率、SSE 连接数。
  • Grafana 展示服务、检索、模型、索引构建大盘。
  • OpenTelemetry 串起 Gateway、Agent、RAG、ES、Milvus、ModelGateway 链路。
  • Loki / ELK 收集应用日志和审计日志。
  • 告警接入企业 IM,覆盖服务不可用、队列积压、索引失败、模型超时、权限校验异常等场景。

面试总结:生产环境不是单机部署,而是 K8s 多副本业务服务 + 独立数据集群 + GPU 推理集群。初始配置上,Gateway 2C4G x2,Conversation/RAG 4C8G x3,ToolHub 2C4G x2,Research 和 Indexer 4C8G x2,ES/Milvus 8C32G x3,vLLM GPU x2 起步;后续按并发会话、检索 QPS、chunk 规模和 token 吞吐水平扩容。

当前生产部署了多少服务器?支持多少数据?后续扩容成本怎么看?

如果从实际生产容量角度回答,可以按当前阶段 600 用户、70 万 chunk 的规模来说。我们不是按最终千人规模一次性拉满资源,而是先按当前使用量做一套中等规模部署,同时保留水平扩容空间。

当前可以按 约 10-12 台服务器 来描述,分成业务服务、数据检索、对象存储/消息队列、AI 推理几类:

类型 数量 主要承载
K8s 业务节点 3 台 Gateway、Conversation、RAG、ToolHub、Admin、Research、Indexer 等服务
ES 节点 3 台 关键词索引、BM25 检索、过滤查询
Milvus 节点 3 台 70 万 chunk 向量检索
MySQL / Redis / RocketMQ / MinIO 2-3 台或复用基础设施 元数据、缓存、消息队列、原文对象存储
GPU 推理节点 1-2 台 LLM 推理、embedding、rerank,可按公司 GPU 资源池复用

也可以更简洁地说:当前核心生产资源大约是 3 台业务节点、3 台 ES、3 台 Milvus、2-3 台基础组件节点,加 1-2 台 GPU 推理节点。如果公司已有统一 MySQL、Redis、MQ、对象存储或 GPU 资源池,那么实际独占机器数会更少。

当前这套配置主要支撑:

  • 注册/覆盖用户:约 600 人
  • 工作日活跃用户:约 300-400 人
  • 峰值在线用户:约 100-120 人
  • 峰值并发会话:约 60-80 路
  • 文档分片规模:约 70 万 chunk
  • 向量数据量:视 embedding 维度和索引类型,大致在 几十 GB 到百 GB 级
  • 普通知识问答:约 10-25 QPS
  • 知识检索请求:约 20-40 QPS
  • 深度研究任务:异步执行,通常按队列控制并发。

资源瓶颈通常不在一个地方,而是分层出现:

  1. 用户并发增长:优先扩 Gateway、Conversation、RAG 业务 Pod,一般加业务节点或提高副本数即可,成本相对可控。
  2. chunk 数增长:主要增加 Milvus 和 ES 的存储、内存、索引构建压力,成本增长比较明显。
  3. 问答量增长:会增加 rerank 和 LLM 推理压力,GPU 成本会变成大头。
  4. 文档入库高峰:会增加 Parser、Indexer、embedding 服务和 RocketMQ 消费压力,可以临时扩容消费节点。
  5. 深度研究任务增长:因为会多轮检索、多轮工具调用和长文本生成,要单独限制队列并发,避免影响普通问答。

后续扩容可以分阶段:

第一阶段:600 用户、70 万 chunk

当前配置即可支撑,重点是保证 ES/Milvus 三节点、业务服务多副本、GPU 推理有排队和限流能力。

第二阶段:1000 用户、100-150 万 chunk

主要扩:

  • 业务节点从 3 台扩到 4-5 台。
  • RAG、Conversation、ModelGateway 副本数增加。
  • ES 和 Milvus 继续保持至少 3 节点,但提高磁盘、内存,必要时增加到 5 节点。
  • GPU 推理从 1-2 台扩到 2-4 台,或接入统一模型服务池。

第三阶段:2000 用户、300 万 chunk 以上

需要更明确的集群化和成本治理:

  • ES/Milvus 按知识库、产品线或时间分片。
  • Milvus 增加 query node / data node,或者拆 collection。
  • 高频 FAQ、query embedding、检索结果缓存必须启用。
  • Rerank 做批量推理和限流。
  • LLM 做模型路由,简单问题走小模型,复杂问题走大模型。
  • 深度研究和普通问答资源隔离。

从成本角度看,后续扩容成本主要来自三块:

  • 向量和关键词索引成本:chunk 增长会带来 Milvus、ES 的存储、内存和索引构建成本。
  • 模型推理成本:用户并发和生成 token 增长后,GPU 是主要成本。
  • 异步入库成本:文档频繁更新时,embedding 重建、OCR、图片摘要和代码摘要会消耗计算资源。

控制成本的方式包括:

  • 内容 hash 去重,没变更不重新 embedding。
  • 权限变更只更新元数据,不重建向量。
  • 高频问题走 FAQ Cache。
  • 简单问题走小模型或只返回检索摘要。
  • 动态 TopK/TopN,减少 rerank 和 LLM 上下文 token。
  • 文档分级入库,高价值知识优先 embedding,低价值附件延迟处理。
  • 深度研究异步排队,限制并发。

面试总结:当前按 600 用户、70 万 chunk 规模,大约使用 10-12 台服务器,包括 3 台业务节点、3 台 ES、3 台 Milvus、2-3 台基础组件节点和 1-2 台 GPU 推理节点。当前能支撑 60-80 路并发会话、20-40 QPS 检索请求。后续扩容成本主要增长在 Milvus/ES 索引存储和 GPU 推理两块;用户增长先扩业务 Pod,chunk 增长扩 ES/Milvus,问答量增长扩 GPU,并通过缓存、动态 TopK/TopN、hash 去重和模型路由控制成本。

内容变更时,有没有保证最后都可以成功的机制?

严格来说,分布式系统里不能承诺任何情况下都 100% 最终成功。比如原文损坏、权限元数据缺失、ES/Milvus 长时间不可用、磁盘打满、embedding 模型一直失败,这些都不是单纯重试可以解决的。我们能保证的是:在可恢复故障下,通过 状态机、幂等、重试、补偿、死信队列、定时对账和人工介入,让任务最终达到一个确定状态,不会静默丢失。

核心机制如下。

第一,所有入库和更新任务都有状态机。比如:

1
PENDING -> PARSED -> CHUNKED -> EMBEDDING -> ES_INDEXED -> MILVUS_INDEXED -> ACTIVE

如果中间失败,会进入 FAILED 或 PARTIAL_INDEXED 状态,不会假装成功。线上检索只使用 ACTIVE 版本。

第二,任务通过 RocketMQ 异步执行,并支持失败重试。临时网络抖动、ES/Milvus 短暂不可用、embedding 服务超时,都可以通过重试恢复。重试要有最大次数和退避策略,避免故障时把下游打爆。

第三,写 ES 和 Milvus 必须幂等。chunkId、documentId、version、chunkIndex 要稳定,ES 的 documentId 和 Milvus 的 primary key 都使用同一套 chunkId。这样消息重复消费时只是覆盖同一条数据,不会产生重复 chunk。

第四,使用任务表或 outbox 表防止消息丢失。MySQL 事务里不仅更新文档状态,还记录 index_task。即使 MQ 消息丢了,后台定时扫描任务表也能重新投递,保证任务不会静默消失。

第五,失败多次进入死信队列。死信不是终点,而是告诉系统“这个任务自动处理不了了”。后台会展示失败原因,比如解析失败、embedding 失败、Milvus 写入失败、权限字段缺失等,支持人工修复后重新投递。

第六,定时一致性校验。后台 reconciliation job 会对账:

1
2
3
MySQL ACTIVE chunk 数量
ES ACTIVE doc 数量
Milvus ACTIVE vector 数量

发现 MySQL 有但 ES/Milvus 没有,就触发补写;ES/Milvus 有但 MySQL 已废弃,就触发删除或状态修正;版本不一致,以 MySQL activeVersion 为准。

第七,新版本构建成功前,旧版本继续可用。内容变更时不是覆盖旧索引,而是先构建新版本,等新版本 ES 和 Milvus 都成功后,再切换 activeVersion。这样即使新版本构建失败,也不会影响线上旧版本检索。

第八,最终状态必须可解释。一个任务最后只能处于几类明确状态:

  • ACTIVE:构建成功,可检索。
  • FAILED:自动处理失败,等待修复。
  • DEPRECATED:旧版本下线。
  • DELETED:已逻辑删除。

不能出现任务卡在中间状态没人知道。

所以面试时可以这样回答:我们不能承诺所有异常下都自动成功,但会保证任务不丢失、状态可追踪、失败可重试、异常可补偿。通过 RocketMQ 重试、幂等写入、outbox 任务表、死信队列和定时一致性校验,在可恢复故障下最终会成功;不可恢复故障会进入明确失败状态并告警,由人工修复后重新投递。

对于检索流程来说不理想,要怎么去分析?

检索效果不理想时,不能只看最终答案,也不能直接调大 TopK 或 TopN。正确做法是把一次 RAG 检索链路拆开,通过 trace 回放逐层定位问题。

一次完整检索可以拆成:

1
2
3
4
5
6
7
8
9
原始问题
-> Query Rewrite
-> 权限和元数据过滤
-> BM25 召回
-> 向量召回
-> 融合排序
-> Rerank
-> TopN 上下文
-> LLM 回答和引用

分析时先看 trace 里几个关键结果:

  • 原始 query 是什么。
  • 改写后的 query 是什么。
  • 权限 filter 和业务 filter 是什么。
  • BM25 TopK 召回了哪些 chunk。
  • 向量 TopK 召回了哪些 chunk。
  • 融合后排序如何。
  • rerank 前后排名是否变化合理。
  • 最终 TopN 是否包含正确证据。
  • 模型答案是否引用了正确 chunk。

然后按现象分类分析。

1. 正确文档完全没有召回

如果正确 chunk 没出现在 BM25 TopK,也没出现在向量 TopK,说明问题在粗召回阶段。

可能原因:

  • Query Rewrite 不充分,没有扩展业务同义词、缩写、错误码、模块名。
  • 文档没有入库或索引失败。
  • chunk 切分不合理,正确内容被切碎或混入大 chunk。
  • embedding 模型对该类问题表达不好。
  • 元数据过滤过严,把正确文档过滤掉。

处理方案:

  • 查看 gold chunk 是否存在于 MySQL、ES、Milvus。
  • 检查文档状态是否 ACTIVE。
  • 检查 ES doc 和 Milvus vector 是否都存在。
  • 优化 query rewrite,补充同义词、缩写、业务词表。
  • 适当扩大 BM25 / Vector TopK。
  • 调整 chunk 策略或重新 embedding。
  • 检查权限和元数据 filter 是否误杀。

2. 正确文档召回了,但排名很靠后

如果正确 chunk 在 TopK 里,但没有进入最终 TopN,说明问题在融合、rerank 或元数据权重。

可能原因:

  • BM25 和向量分数融合不合理。
  • RRF 或加权策略不适合当前问题类型。
  • rerank 对该类语料排序不好。
  • 版本、更新时间、来源权重设置不合理。
  • 同一文档多个无关 chunk 占据前排。

处理方案:

  • 对比 BM25 rank、Vector rank、融合 rank、rerank rank。
  • 对精确编号类问题提高 BM25 权重。
  • 对自然语言描述类问题提高向量权重。
  • 使用 RRF 融合,减少分数尺度差异影响。
  • 给版本匹配、产品线匹配、权威来源加权。
  • 对过期、废弃、低质量来源降权。
  • 做同文档去重或限制单文档 chunk 数。

3. TopN 里有正确证据,但答案仍然不理想

如果正确 chunk 已经进入 TopN,但模型回答错误,问题通常不在检索召回,而在上下文组装、Prompt 或生成阶段。

可能原因:

  • TopN 里噪声太多,干扰模型。
  • 正确证据被截断,关键信息没进入 Prompt。
  • 引用和 chunk 对不上。
  • Prompt 没约束“必须基于证据回答”。
  • 多个版本证据冲突,模型选错。

处理方案:

  • 检查最终 Prompt 里的上下文是否完整。
  • 减少低相关 chunk,提升 TopN 质量。
  • 对同一问题的多个版本做版本优先级。
  • 强制答案带引用,无证据不回答。
  • 对冲突证据要求模型说明冲突来源。

4. 无结果率变高

无结果率高说明用户问题经常找不到可用证据。

可能原因:

  • 知识库覆盖不足。
  • 文档未入库或更新延迟。
  • 权限过滤过严。
  • query rewrite 不稳定。
  • 用户问题过于口语化或缺少关键信息。

处理方案:

  • 统计无结果问题 TopN,补充知识源或 FAQ。
  • 检查入库任务和索引延迟。
  • 对非权限过滤条件做逐级放宽。
  • 对缺少产品线、版本、模块的问题先反问。
  • 对高频无结果问题加入黄金问答集。

5. 召回结果过期或版本不对

企业知识库里版本非常重要。检索到了相关文档,但版本不对,也会导致答案错误。

可能原因:

  • version 字段缺失或解析不准确。
  • 旧文档没有 DEPRECATED。
  • 排序没有给最新版本加权。
  • 用户问题里的版本没有被识别出来。

处理方案:

  • 在 query rewrite 阶段识别版本号。
  • 元数据中强制维护 version、status、updatedAt。
  • 检索 filter 优先查目标版本。
  • 旧版本降权或默认不召回。
  • 答案中明确标注引用版本。

6. 权限过滤导致异常

权限相关问题要特别谨慎。不能为了召回率放开权限。

可能原因:

  • ACL 同步延迟。
  • 用户项目权限计算错误。
  • ES/Milvus 里的权限过滤字段没更新。
  • MySQL 权威元数据和索引字段不一致。

处理方案:

  • 以 MySQL 权威权限为准。
  • 候选返回后做二次权限校验。
  • 权限变更任务优先处理。
  • 定时对账 ES/Milvus 权限字段。
  • 无权限时宁可不返回,也不能越权返回。

7. 如何量化分析

不能只靠人工感觉,需要看指标:

  • Recall@K:正确证据有没有被召回。
  • TopN 命中率:正确证据有没有进入最终上下文。
  • MRR:正确证据排得靠不靠前。
  • nDCG:整体排序质量。
  • 无结果率:知识覆盖和过滤是否异常。
  • rerank 前后命中变化:判断 rerank 是否带来提升。
  • 点踩率和人工纠错率:线上用户真实反馈。

其中线上最关键的是 TopN 命中率。因为 RAG 最终是让 LLM 基于 TopN 上下文回答,正确证据如果没有进入 TopN,模型就看不到。

8. 分析闭环

一次检索不理想,最终要形成闭环:

1
2
3
4
5
6
7
8
用户反馈 / 低分 case
-> trace 回放
-> 定位问题层级
-> 修复 query rewrite / filter / chunk / fusion / rerank / 数据
-> 加入黄金问答集
-> 离线回归评测
-> 灰度上线
-> 观察线上指标

面试总结:检索不理想时,先用 trace 把链路拆开,看正确证据是在召回前丢了、融合排序时掉了、rerank 后没进 TopN,还是进了 TopN 但生成没用好。不同位置对应不同优化手段:召回问题优化 query rewrite、TopK、chunk 和索引;排序问题优化 RRF、元数据权重和 rerank;上下文问题优化 TopN、截断和 Prompt;权限问题只能修权限同步和二次校验,不能放开权限。

检索不理想的前三类分析思路,举例说明

1. 正确文档完全没有召回

例子:用户问:

1
DTS_504 是什么原因?

标准答案在一篇 DTS 问题单里,里面写的是:

1
错误码 E_CONN_TIMEOUT,根因是 storage-node-03 连接超时。

但是 trace 里看到:

1
2
3
BM25 TopK:没有召回这篇 DTS
Vector TopK:也没有召回这篇 DTS
Final TopN:没有正确证据

这种情况说明正确证据在粗召回阶段就丢了,后面的 rerank 和 LLM 没机会修复。

可能原因:

  • 用户问的是 DTS_504,文档里写的是 E_CONN_TIMEOUT,query rewrite 没有做错误码映射。
  • DTS 文档没有成功入库,ES 或 Milvus 缺索引。
  • 文档被切得太碎,错误码、根因、解决方案分散在不同 chunk。
  • 权限或 sourceType filter 把 DTS 文档过滤掉。

分析动作:

1
2
3
4
5
6
1. 查 document_meta:这篇 DTS 是否存在,状态是否 ACTIVE
2. 查 document_chunk:是否生成 chunk
3. 查 ES:错误码、DTS 编号是否可搜到
4. 查 Milvus:对应 chunk 是否有 vectorId
5. 查 query rewrite:是否把 DTS_504 扩展为 E_CONN_TIMEOUT / timeout
6. 查 filter:sourceType、projectId、securityLevel 是否误杀

优化方案:

  • 补充错误码、模块名、缩写的业务词典。
  • 对 DTS 编号、错误码走 BM25 精确召回。
  • 修复缺失索引或重新入库。
  • 调整 chunk,把“问题现象 + 错误码 + 根因 + 解决方案”尽量放在同一语义块里。

2. 正确文档召回了,但没有进入最终 TopN

例子:用户问:

1
Milvus 不可用时系统怎么降级?

正确 chunk 内容是:

1
向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索,并记录告警。

trace 里看到:

1
2
3
4
5
Vector TopK:正确 chunk 排第 18
BM25 TopK:正确 chunk 排第 12
融合后:正确 chunk 排第 15
rerank 后:正确 chunk 排第 11
Final TopN:只取 Top8,所以没进上下文

这种情况说明不是没召回,而是排序阶段不理想,正确证据被挤出最终 TopN。

可能原因:

  • 融合排序时 BM25 和向量权重不合理。
  • reranker 对“降级 / fallback / 不可用”这类表达识别不好。
  • 一篇同源文档多个相似 chunk 占了前排。
  • 旧版本文档或低质量论坛帖权重过高。
  • 正确 chunk 标题路径缺失,rerank 输入语义不完整。

分析动作:

1
2
3
4
5
1. 对比 BM25 rank、Vector rank、Fusion rank、Rerank rank
2. 看排在前 8 的 chunk 为什么得分高
3. 检查是否同一文档多个 chunk 刷屏
4. 检查正确 chunk 的标题路径、版本、sourceType、updatedAt
5. 看 rerank 输入是否包含标题路径和关键上下文

优化方案:

  • 使用 RRF 融合,减少 BM25 和向量分数尺度差异。
  • 对同一文档做去重或限制单文档进入 TopN 的 chunk 数。
  • 给权威文档、最新版本、产品线匹配的 chunk 加权。
  • 给过期、废弃、低质量来源降权。
  • rerank 输入拼接标题路径、来源类型和版本。
  • 对复杂问题动态扩大 TopN,例如从 8 提到 12。

3. 正确证据进入 TopN,但答案仍然不理想

例子:用户问:

1
向量库不可用时怎么处理?会有什么影响?

Final TopN 里已经有正确 chunk:

1
2
3
向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索。
Rerank 不可用时,使用混合检索原始排序。
模型不可用时,切换备用模型或返回检索摘要。

但是模型回答成:

1
向量库不可用时,系统会自动重启 Milvus 集群,并清理缓存。

这种情况说明检索阶段基本没问题,问题在上下文组装、Prompt 约束或生成阶段。

可能原因:

  • TopN 里虽然有正确证据,但噪声太多,模型被其他 chunk 干扰。
  • 正确 chunk 被截断,只保留了“向量库不可用”,没保留“降级到 ES”。
  • Prompt 没要求“只能基于上下文回答,无证据不要编”。
  • 多个降级策略混在一起,模型把模型不可用、rerank 不可用、向量库不可用混淆了。
  • 引用校验缺失,模型引用了不支撑结论的 chunk。

分析动作:

1
2
3
4
5
1. 查看最终 Prompt,确认正确 chunk 是否完整进入上下文
2. 检查 TopN 中是否有冲突或高噪声 chunk
3. 看答案中的每个结论是否能在 citation 中找到支撑
4. 检查上下文截断策略是否截掉关键句
5. 检查 Prompt 是否约束基于证据回答

优化方案:

  • 对 TopN 做更严格的去噪和同源合并。
  • 上下文组装时保护关键句,不在“策略 + 影响”中间截断。
  • Prompt 中明确要求:只基于给定证据回答,证据不足时说明不足。
  • 对不同降级场景做结构化上下文,比如按“Milvus 不可用 / Rerank 不可用 / 模型不可用”分组。
  • 增加引用校验,答案中的关键结论必须能对应到引用 chunk。

面试总结:如果正确文档没召回,就是召回问题,要查 query rewrite、索引、chunk 和 filter;如果召回了但没进 TopN,就是排序问题,要查融合、RRF、rerank 和元数据权重;如果进了 TopN 但答案错,就是生成和上下文问题,要查 Prompt、截断、噪声和引用校验。

用 RAGAS + 自研脚本看答案忠实度、上下文相关性、答案完整性和引用准确性,举例说明

RAGAS 和自研脚本的作用是评估一次 RAG 回答到底好不好。它不是只看“答案像不像”,而是把一次回答拆成几个问题:

1
2
3
4
检索出来的上下文是否相关?
答案是否严格基于上下文?
答案是否覆盖了问题需要的关键点?
答案里的引用是否真的能支撑结论?

1. 示例数据

假设用户问题是:

1
Milvus 不可用时系统怎么降级?会有什么影响?

黄金答案是:

1
Milvus 不可用时,系统降级到 Elasticsearch BM25 关键词检索;影响是语义召回能力下降,但普通关键词检索仍可用,同时系统会记录告警和 trace。

标准证据 chunk 是:

1
2
chunk_001:
向量库不可用时,系统降级到 Elasticsearch 关键词检索;此时语义召回能力下降,但可以返回关键词检索摘要,并记录告警和 traceId。

本次 RAG 实际检索出的 TopN 上下文是:

1
2
3
4
5
6
7
8
chunk_001:
向量库不可用时,系统降级到 Elasticsearch 关键词检索;此时语义召回能力下降,但可以返回关键词检索摘要,并记录告警和 traceId。

chunk_002:
Rerank 服务不可用时,系统使用 BM25 分数、向量分数和元数据权重做融合排序。

chunk_003:
模型不可用时,系统切换备用模型或仅返回检索结果摘要。

模型实际回答是:

1
Milvus 不可用时,系统会降级到 Elasticsearch BM25 关键词检索,因此语义召回能力会下降,但关键词检索仍可用;系统会记录告警和 traceId。[引用1]

引用关系是:

1
引用1 -> chunk_001

2. RAGAS 怎么评估

RAGAS 一般需要输入 question、contexts、answer、ground_truth 等字段。

示例输入可以是:

1
2
3
4
5
6
7
8
9
10
{
"question": "Milvus 不可用时系统怎么降级?会有什么影响?",
"contexts": [
"向量库不可用时,系统降级到 Elasticsearch 关键词检索;此时语义召回能力下降,但可以返回关键词检索摘要,并记录告警和 traceId。",
"Rerank 服务不可用时,系统使用 BM25 分数、向量分数和元数据权重做融合排序。",
"模型不可用时,系统切换备用模型或仅返回检索结果摘要。"
],
"answer": "Milvus 不可用时,系统会降级到 Elasticsearch BM25 关键词检索,因此语义召回能力会下降,但关键词检索仍可用;系统会记录告警和 traceId。",
"ground_truth": "Milvus 不可用时,系统降级到 Elasticsearch BM25 关键词检索;影响是语义召回能力下降,但普通关键词检索仍可用,同时系统会记录告警和 trace。"
}

RAGAS 会评估几类通用指标。

3. 答案忠实度 Faithfulness

忠实度看的是:答案里的每个关键结论是否能被上下文支撑

在上面的例子中,答案包含几个结论:

1
2
3
4
1. Milvus 不可用时降级到 Elasticsearch BM25。
2. 语义召回能力下降。
3. 关键词检索仍可用。
4. 系统记录告警和 traceId。

这些都能在 chunk_001 中找到依据,所以忠实度高。

如果模型回答成:

1
Milvus 不可用时,系统会自动重启 Milvus 集群,并清理缓存。

上下文里没有“自动重启”和“清理缓存”,这就是不忠实,也就是幻觉。

评估方式:

  • RAGAS 会把答案拆成多个 statement。
  • 判断每个 statement 是否能被 contexts 支撑。
  • 支撑比例越高,faithfulness 越高。

可以理解成:

1
faithfulness = 被上下文支持的结论数 / 答案总关键结论数

4. 上下文相关性 Context Relevance

上下文相关性看的是:检索出来的 TopN chunk 是否和用户问题相关

本例中:

1
2
3
chunk_001:讲 Milvus/向量库不可用,相关性高。
chunk_002:讲 rerank 不可用,和问题不是一个组件,相关性中低。
chunk_003:讲模型不可用,和问题不是一个组件,相关性中低。

如果 TopN 里大部分都是 chunk_002、chunk_003 这种“同属降级策略但不是 Milvus 降级”的内容,上下文相关性就会被拉低。

评估方式:

  • RAGAS 会判断 context 是否有助于回答 question。
  • 自研脚本可以根据 gold_chunk_id 判断标准证据是否进入 TopN。
  • 也可以统计 TopN 中强相关、弱相关、无关 chunk 的比例。

例如人工标注:

1
2
3
chunk_001 = 强相关
chunk_002 = 弱相关
chunk_003 = 弱相关

那么结论是:有正确证据,但 TopN 噪声偏多,需要优化 rerank 或同类降级策略分组。

5. 答案完整性 Answer Completeness

完整性看的是:答案有没有覆盖问题要求的关键点

用户问的是:

1
怎么降级?会有什么影响?

所以完整答案至少应该包含:

1
2
3
4
1. 降级动作:降级到 ES BM25。
2. 影响:语义召回能力下降。
3. 可用能力:关键词检索仍可用或返回检索摘要。
4. 运维动作:记录告警和 trace。

如果模型回答:

1
Milvus 不可用时会降级到 Elasticsearch。

这句话是正确的,但不完整,因为没有回答“影响是什么”,也没有提到告警和 trace。

评估方式:

  • RAGAS 可以结合 ground_truth 看 answer correctness / answer similarity。
  • 自研脚本更适合检查关键点覆盖。

例如定义 expected_points:

1
2
3
4
5
6
7
8
{
"expected_points": [
"降级到 Elasticsearch/BM25/关键词检索",
"语义召回能力下降",
"关键词检索仍可用/返回检索摘要",
"记录告警/traceId"
]
}

脚本检查答案是否覆盖这些点:

1
2
3
覆盖 4/4:完整
覆盖 2/4:部分完整
覆盖 1/4:不完整

6. 引用准确性 Citation Accuracy

引用准确性看的是:答案中的引用是否真的支撑对应结论

正确情况:

1
2
答案:Milvus 不可用时降级到 ES BM25。[引用1]
引用1:chunk_001,内容确实写了向量库不可用时降级到 ES 关键词检索。

这是引用准确。

错误情况:

1
2
答案:Milvus 不可用时降级到 ES BM25。[引用2]
引用2:chunk_002,内容讲的是 rerank 不可用时使用融合排序。

这个引用不准确,因为 chunk_002 不能支撑 Milvus 降级到 ES 的结论。

自研脚本可以做几类检查:

1
2
3
4
1. citationId 是否存在于本次 TopN contexts。
2. citationId 是否有权限。
3. citationId 是否是 gold_chunk_id 或同文档同段落证据。
4. 答案关键句与引用 chunk 是否语义匹配。

引用准确率可以简单定义为:

1
citation_accuracy = 正确引用数 / 总引用数

7. 自研脚本补充检查什么

RAGAS 偏通用,但企业 RAG 还需要业务规则校验。自研脚本通常检查:

1
2
3
4
5
6
7
8
gold_chunk_id 是否进入 TopN
答案是否包含 expected_keywords
答案是否覆盖 expected_points
引用是否来自 TopN
引用是否有权限
引用是否能支撑答案关键句
是否输出敏感信息
是否引用废弃版本文档

比如本例可以配置:

1
2
3
4
5
6
7
8
9
10
{
"gold_chunk_id": "chunk_001",
"expected_keywords": ["Elasticsearch", "BM25", "语义召回", "trace"],
"expected_points": [
"降级到 Elasticsearch 关键词检索",
"语义召回能力下降",
"记录告警和 trace"
],
"forbidden_keywords": ["自动重启 Milvus", "清理缓存"]
}

脚本评估结果可能是:

1
2
3
4
5
6
TopN 命中:通过,chunk_001 在 TopN
关键词覆盖:通过,命中 Elasticsearch、BM25、语义召回、trace
完整性:通过,覆盖 3/3
引用准确性:通过,引用1 指向 chunk_001
敏感信息:通过,无敏感输出
禁用结论:通过,未出现自动重启 Milvus

8. 反例说明

如果模型回答:

1
Milvus 不可用时,系统会自动重启 Milvus,并清理缓存;如果仍失败,再降级到 ES。[引用1]

评估结果会是:

1
2
3
4
忠实度:低,因为“自动重启 Milvus、清理缓存”没有证据支撑
上下文相关性:中高,因为 chunk_001 相关,但可能混入噪声
完整性:部分通过,提到了 ES,但加入了错误处理步骤
引用准确性:低,因为引用1 不能支撑自动重启和清理缓存

这个 case 的优化方向不是召回,而是:

1
2
3
1. Prompt 强化:无证据不要编造处理步骤。
2. 答案后处理:关键结论必须能在引用 chunk 中找到支撑。
3. 引用校验:引用不支撑的句子要删除或改写。

9. 面试总结

面试可以这样回答:

我们会用 RAGAS 做通用质量评估,比如 faithfulness 判断答案有没有基于上下文,context relevance 判断检索上下文是否相关;同时用自研脚本做业务校验,比如 gold chunk 是否进入 TopN、答案是否覆盖关键点、引用是否来自本次上下文且有权限、引用内容是否支撑答案结论。

例如用户问“Milvus 不可用怎么降级”,标准证据是“降级到 ES BM25,语义召回下降并记录 trace”。如果答案完整覆盖这些点且引用指向该 chunk,则忠实度、完整性、引用准确性都高;如果答案编出“自动重启 Milvus”,即使语言流畅,也会被判定为忠实度低和引用不准确。

说明答案忠实度和答案相关性的定义,以及实际如何评估

答案忠实度和答案相关性是 RAG 评测里两个容易混淆但关注点不同的指标。

1. 答案忠实度 Faithfulness

答案忠实度关注的是:模型回答是否严格基于检索到的上下文,有没有编造上下文里不存在的信息

它回答的问题是:

1
答案里的结论,能不能被检索上下文支撑?

举个例子,用户问:

1
Milvus 不可用时系统怎么处理?

检索上下文是:

1
向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索,并记录告警和 traceId。

模型回答:

1
Milvus 不可用时,系统会降级到 Elasticsearch BM25,并记录告警和 traceId。

这个回答忠实度高,因为每个关键结论都能在上下文中找到依据。

但如果模型回答:

1
Milvus 不可用时,系统会自动重启 Milvus 集群,并清理缓存。

这个回答忠实度低,因为“自动重启 Milvus”和“清理缓存”在上下文里没有证据支撑,属于模型幻觉。

实际评估时,可以把答案拆成多个 atomic statements,也就是最小事实单元:

1
2
3
1. Milvus 不可用时会降级到 Elasticsearch BM25。
2. 系统会记录告警。
3. 系统会记录 traceId。

然后逐条判断每个 statement 是否被 contexts 支持:

1
supported / unsupported

一个简单计算方式是:

1
faithfulness = 被上下文支持的 statement 数 / 总 statement 数

实际落地可以用 RAGAS 来做 statement 拆解和 LLM 判断,也可以用自研脚本做补充校验。例如:

  • 关键结论是否能在引用 chunk 中找到语义支撑。
  • 答案是否出现 forbidden_keywords,比如“自动重启 Milvus”这类上下文没有的处理方式。
  • 答案中的引用是否能支撑对应句子。

所以忠实度重点看的是:有没有脱离证据编答案

2. 答案相关性 Answer Relevance

答案相关性关注的是:模型回答是否真正回答了用户的问题

它回答的问题是:

1
2
答案有没有围绕用户问题展开?
有没有答偏?

还是同一个问题:

1
Milvus 不可用时系统怎么处理?

如果模型回答:

1
Milvus 不可用时,系统会降级到 Elasticsearch BM25 关键词检索,并记录告警。

答案相关性高,因为它直接回答了“怎么处理”。

如果模型回答:

1
Milvus 是一个向量数据库,适合大规模向量检索,支持高性能相似度搜索。

这段话可能是事实正确的,但没有回答“不可用时怎么处理”,所以答案相关性低。

再比如用户问:

1
Milvus 不可用时有什么影响?

模型只回答:

1
系统会记录 traceId。

这虽然可能忠实于上下文,但没有回答主要问题“影响是什么”,所以相关性也不高。

实际评估时,RAGAS 通常会根据 question 和 answer 判断答案是否匹配用户问题。常见做法是:

  1. 从 answer 反向生成若干 possible questions。
  2. 看这些 possible questions 和原始 question 的语义相似度。
  3. 相似度越高,说明 answer 越相关。

也可以用 LLM-as-judge 直接打分:

1
2
3
4
请判断 answer 是否直接回答 question,分数 1-5。
1 = 完全无关
3 = 部分相关但没有完整回答
5 = 完全相关并直接回答问题

自研脚本可以结合问题类型做关键点校验。例如:

如果问题是:

1
Milvus 不可用时系统怎么降级?有什么影响?

那答案至少要覆盖:

1
2
降级动作
影响说明

如果只讲“Milvus 是什么”,相关性低;如果只讲“降级到 ES”但没讲影响,则相关性中等;如果两者都覆盖,则相关性高。

所以答案相关性重点看的是:有没有正面回答用户问题

3. 两者的区别

可以这样区分:

指标 关注点 典型问题
答案忠实度 答案是否被上下文支撑 有没有编造、有没有幻觉
答案相关性 答案是否回答用户问题 有没有答偏、有没有避重就轻

一个答案可能忠实但不相关:

1
2
用户问:Milvus 不可用时怎么降级?
答案:Milvus 是向量数据库,用于语义检索。

这句话可能来自上下文,所以忠实度不一定低,但它没有回答降级问题,所以相关性低。

一个答案也可能相关但不忠实:

1
2
用户问:Milvus 不可用时怎么降级?
答案:系统会自动重启 Milvus。

它看起来回答了问题,所以相关性可能不低,但上下文没有这个证据,所以忠实度低。

4. 实际评估流程

实际评估时,我们会对每条黄金问答样本保存:

1
2
3
4
5
6
7
8
{
"question": "Milvus 不可用时系统怎么降级?有什么影响?",
"contexts": [
"向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索;语义召回能力下降,并记录告警和 traceId。"
],
"answer": "Milvus 不可用时,系统会降级到 Elasticsearch BM25,语义召回能力会下降,并记录告警和 traceId。",
"ground_truth": "降级到 Elasticsearch BM25;影响是语义召回能力下降;记录告警和 traceId。"
}

然后分两步评估:

第一步,评估忠实度:

1
2
3
把 answer 拆成事实点
-> 判断每个事实点是否能被 contexts 支撑
-> 统计 supported 比例

第二步,评估相关性:

1
2
3
判断 answer 是否直接回答 question
-> 检查是否覆盖问题意图
-> 和 ground_truth / expected_points 对比

自研脚本会补充更业务化的规则:

1
2
3
4
expected_points 是否覆盖
forbidden_claims 是否出现
引用 chunk 是否支撑关键句
答案是否答到用户问的对象、版本、场景

5. 面试总结

面试可以这样回答:

答案忠实度看的是回答是否基于检索上下文,核心是防幻觉;我们会把答案拆成多个事实点,逐条判断是否能被上下文支撑。答案相关性看的是回答是否正面回应用户问题,核心是防答偏;我们会比较 question、answer 和标准答案/关键点,判断答案是否覆盖用户问题的核心意图。

比如用户问“Milvus 不可用时怎么降级”,回答“降级到 ES BM25”既相关又忠实;回答“Milvus 是向量数据库”可能忠实但不相关;回答“自动重启 Milvus”可能相关但不忠实。

说明 Recall@K 和 MRR 的定义,以及实际如何评估

Recall@K 和 MRR 都是检索阶段的核心指标,用来评估 RAG 检索结果质量。它们关注的不是模型最终回答,而是 正确证据有没有被检索出来,以及排得靠不靠前

1. Recall@K 的定义

Recall@K 表示:在前 K 个检索结果中,是否召回了正确证据

它回答的问题是:

1
正确文档或正确 chunk 有没有出现在 TopK 结果里?

比如用户问题是:

1
Milvus 不可用时怎么降级?

标准证据是:

1
gold_chunk_id = chunk_001

如果检索结果 Top5 是:

1
2
3
4
5
1. chunk_008
2. chunk_021
3. chunk_001
4. chunk_035
5. chunk_044

因为正确证据 chunk_001 出现在 Top5 里,所以:

1
Recall@5 = 命中

如果 Top5 没有 chunk_001,但 Top10 里有,那么:

1
2
Recall@5 = 未命中
Recall@10 = 命中

在多条评测样本上,Recall@K 的计算方式是:

1
Recall@K = TopK 命中的问题数 / 总问题数

例如有 100 个黄金问题,其中 85 个问题的标准证据进入了 Top10:

1
Recall@10 = 85 / 100 = 85%

2. Recall@K 在实际中怎么评估

实际评估时,我们会先构建黄金问答集,每条样本至少包含:

1
2
3
4
5
6
{
"question": "Milvus 不可用时怎么降级?",
"gold_doc_id": "doc_001",
"gold_chunk_id": "chunk_001",
"ground_truth": "降级到 Elasticsearch BM25 关键词检索。"
}

然后对每个 question 跑完整检索链路:

1
2
3
4
5
6
7
Query Rewrite
-> 权限过滤
-> BM25 召回
-> 向量召回
-> 融合排序
-> rerank
-> 输出 TopK

脚本判断:

1
gold_chunk_id 是否在 TopK 里

通常会分别统计:

1
2
3
4
Recall@5
Recall@10
Recall@20
Recall@50

这些指标可以分别看不同阶段:

  • BM25 Recall@K:关键词召回是否有效。
  • Vector Recall@K:向量召回是否有效。
  • Fusion Recall@K:融合后是否提升。
  • Rerank Recall@K / TopN 命中率:精排后正确证据是否保留下来。

如果 BM25 和 Vector 的 Recall@50 都低,说明粗召回有问题;如果 Fusion Recall@50 高但最终 TopN 命中低,说明排序或 rerank 有问题。

3. MRR 的定义

MRR 全称是 Mean Reciprocal Rank,中文可以理解为 平均倒数排名

它关注的是:第一个正确证据排在第几位

对于单个问题:

1
RR = 1 / 第一个正确结果的排名

如果正确 chunk 排第 1:

1
RR = 1 / 1 = 1

如果正确 chunk 排第 2:

1
RR = 1 / 2 = 0.5

如果正确 chunk 排第 5:

1
RR = 1 / 5 = 0.2

如果 TopK 里完全没有正确结果:

1
RR = 0

MRR 就是多个问题 RR 的平均值:

1
MRR = 所有问题 RR 之和 / 问题总数

4. MRR 举例

假设有 3 个问题:

1
2
3
问题1:正确 chunk 排第 1,RR = 1
问题2:正确 chunk 排第 2,RR = 0.5
问题3:正确 chunk 排第 5,RR = 0.2

那么:

1
MRR = (1 + 0.5 + 0.2) / 3 = 0.566

如果另一个系统的结果是:

1
2
3
问题1:正确 chunk 排第 3,RR = 0.333
问题2:正确 chunk 排第 8,RR = 0.125
问题3:没召回,RR = 0

那么 MRR 会明显更低。

所以 MRR 越高,说明正确证据越靠前。

5. MRR 在实际中怎么评估

实际落地时,也是在黄金问答集上跑检索链路,然后记录每个问题的正确 chunk 排名。

示例:

1
2
3
4
5
6
7
8
9
{
"question": "DTS_504 的根因是什么?",
"gold_chunk_id": "chunk_009",
"retrieved_chunks": [
"chunk_003",
"chunk_009",
"chunk_021"
]
}

这里 gold_chunk_id 排第 2:

1
RR = 1 / 2 = 0.5

脚本会对所有测试问题计算 RR,再求平均。

MRR 常用于评估排序质量。比如:

  • Recall@20 高,但 MRR 低:说明正确证据虽然被召回了,但排名靠后。
  • Recall@20 高,MRR 也高:说明不但召回了,而且排得靠前。
  • Recall@20 低,MRR 通常也低:说明粗召回就有问题。

6. Recall@K 和 MRR 的区别

可以这样理解:

指标 关注点 说明
Recall@K 有没有召回 正确证据是否出现在前 K 个结果中
MRR 排得靠不靠前 第一个正确证据排在第几位

举个例子:

系统 A:

1
2
3
正确 chunk 排第 1
Recall@10 = 命中
RR = 1

系统 B:

1
2
3
正确 chunk 排第 10
Recall@10 = 命中
RR = 0.1

两个系统 Recall@10 都命中,但系统 A 的 MRR 更高,说明 A 排序更好。

7. 在 RAG 中怎么看这两个指标

RAG 里只看 Recall@K 不够,因为正确证据即使在 Top50 里,如果最终 TopN 只取 8 个,它也可能进不了大模型上下文。

所以我们通常这样看:

1
2
3
Recall@K:判断粗召回能力
MRR:判断正确证据是否靠前
TopN 命中率:判断正确证据是否进入最终上下文

如果:

1
Recall@50 高,MRR 低

说明召回范围够,但排序差,要优化融合排序、RRF、rerank、元数据权重。

如果:

1
Recall@50 低

说明粗召回不行,要优化 query rewrite、embedding、BM25 字段、chunk 切分或索引完整性。

如果:

1
MRR 高,但答案仍然差

说明检索阶段可能没问题,要去看上下文组装、Prompt、引用和生成。

8. 面试总结

面试可以这样回答:

Recall@K 看正确证据有没有出现在前 K 个检索结果中,主要评估召回能力;MRR 看第一个正确证据排在第几位,主要评估排序质量。实际评估时,我们会基于黄金问答集记录 gold_chunk_id,跑完整 RAG 检索链路后检查 gold_chunk 是否进入 TopK,并计算它的排名倒数。Recall@K 低说明召回有问题,MRR 低说明召回到了但排序靠后,需要优化融合、rerank 或元数据权重。

怎么用 RAGAS 做评估?

RAGAS 的使用方式可以理解成:先准备一批标准问题,跑一遍自己的 RAG 系统,把 question、retrieved contexts、answer、ground truth 记录下来,再交给 RAGAS 计算指标。它不是替代线上 RAG,而是离线评测和回归验证工具。

1. 先准备黄金问答集

黄金问答集一般来自高频问题、DTS、需求平台、Wiki、故障复盘、人工标注 case。每条数据至少包含:

1
2
3
4
5
6
7
8
9
10
11
{
"question": "Milvus 不可用时系统怎么降级?会有什么影响?",
"ground_truth": "系统降级到 Elasticsearch BM25 关键词检索;语义召回能力下降;记录告警和 traceId。",
"gold_doc_id": "doc_001",
"gold_chunk_id": "chunk_001",
"expected_points": [
"降级到 Elasticsearch BM25",
"语义召回能力下降",
"记录告警和 traceId"
]
}

其中 ground_truthexpected_points 主要用于答案正确性、完整性和自研脚本校验;gold_chunk_id 主要用于 Recall@K、MRR、TopN 命中率。

2. 跑完整 RAG 链路,保存评估样本

对每个 question 调用自己的 RAG 服务,拿到:

1
2
3
4
5
6
7
8
9
10
11
{
"question": "Milvus 不可用时系统怎么降级?会有什么影响?",
"contexts": [
"向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索;语义召回能力下降,并记录告警和 traceId。",
"Rerank 不可用时,系统使用 BM25 分数、向量分数和元数据权重融合排序。"
],
"answer": "Milvus 不可用时,系统会降级到 Elasticsearch BM25,语义召回能力会下降,并记录告警和 traceId。",
"ground_truth": "系统降级到 Elasticsearch BM25 关键词检索;语义召回能力下降;记录告警和 traceId。",
"retrieved_chunk_ids": ["chunk_001", "chunk_009"],
"citations": ["chunk_001"]
}

RAGAS 主要消费的是:

1
2
3
4
question
contexts
answer / response
ground_truth / reference

字段名会随 RAGAS 版本有差异,工程里可以在评测脚本里做一层适配。

3. 选择 RAGAS 指标

常用指标可以分成两类。

检索侧:

1
2
context_precision:检索出来的上下文排序是否合理,相关内容是否靠前。
context_recall:标准答案需要的信息是否被 contexts 覆盖。

生成侧:

1
2
3
faithfulness:答案是否被 contexts 支撑,有没有幻觉。
response_relevancy / answer_relevancy:答案是否回答了用户问题。
answer_correctness:答案和 ground_truth 是否一致。

RAGAS 官方文档里也把这些作为 RAG 常用指标,包括 Context Precision、Context Recall、Response Relevancy、Faithfulness 等。

4. 示例评估代码

下面是一个简化版 Python 评估脚本,真实项目里会把数据从 CSV、JSONL 或评测数据库读取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
answer_correctness,
)

samples = [
{
"question": "Milvus 不可用时系统怎么降级?会有什么影响?",
"answer": "Milvus 不可用时,系统会降级到 Elasticsearch BM25,语义召回能力会下降,并记录告警和 traceId。",
"contexts": [
"向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索;语义召回能力下降,并记录告警和 traceId。",
"Rerank 不可用时,系统使用 BM25 分数、向量分数和元数据权重融合排序。"
],
"ground_truth": "系统降级到 Elasticsearch BM25 关键词检索;语义召回能力下降;记录告警和 traceId。"
}
]

dataset = Dataset.from_list(samples)

result = evaluate(
dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
answer_correctness,
],
)

print(result)
print(result.to_pandas())

如果公司内部不能使用外部模型,就要把 RAGAS 的 judge LLM 和 embedding 配成内网模型,比如内部 Qwen、DeepSeek 或企业模型服务。也就是说,评测模型也要私有化,不能把内部文档 contexts 发到外部 API。

5. RAGAS 输出怎么看

假设某条样本输出是:

1
2
3
4
5
faithfulness = 0.95
answer_relevancy = 0.90
context_precision = 0.72
context_recall = 0.88
answer_correctness = 0.86

可以这样解释:

  • faithfulness 高:答案基本基于上下文,没有明显幻觉。
  • answer_relevancy 高:答案基本回答了用户问题。
  • context_precision 一般:TopN 里有噪声,相关 chunk 排序还可以优化。
  • context_recall 较高:标准答案需要的信息大部分被检索上下文覆盖。
  • answer_correctness 较高:答案和标准答案基本一致。

如果出现:

1
faithfulness 高,answer_relevancy 低

说明答案可能基于上下文,但答偏了。

如果出现:

1
context_recall 低

说明检索上下文没有覆盖标准答案,需要查 query rewrite、召回、chunk、filter。

如果出现:

1
context_precision 低

说明 TopN 噪声多,需要优化融合排序、rerank 或元数据权重。

如果出现:

1
faithfulness 低

说明模型在编,需要加强 Prompt、引用约束和答案后校验。

6. 为什么还要自研脚本

RAGAS 是通用评估框架,但企业 RAG 还需要业务规则。比如:

1
2
3
4
5
6
7
8
gold_chunk_id 是否进入 TopN
gold_chunk_id 排名是多少
Recall@K / MRR / TopN 命中率
引用 citation 是否来自本次 contexts
引用是否有权限
答案是否包含 expected_points
是否引用废弃版本
是否输出敏感信息

这些可以用自研脚本做确定性校验。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def eval_custom(sample):
gold = sample["gold_chunk_id"]
retrieved = sample["retrieved_chunk_ids"]
answer = sample["answer"]
expected_points = sample["expected_points"]

topn_hit = gold in retrieved[:8]
rank = retrieved.index(gold) + 1 if gold in retrieved else None
point_coverage = sum(1 for p in expected_points if p in answer) / len(expected_points)

return {
"topn_hit": topn_hit,
"gold_rank": rank,
"point_coverage": point_coverage,
}

实际项目里更建议采用:

1
2
3
RAGAS 看通用质量
自研脚本看业务确定性指标
人工抽样看疑难 case

7. 实际落地流程

完整流程可以这样落地:

1
2
3
4
5
6
7
8
9
1. 准备黄金问答集
2. 跑当前 RAG 系统,记录 contexts、answer、citations、trace
3. 用 RAGAS 计算 faithfulness、answer relevancy、context precision、context recall
4. 用自研脚本计算 Recall@K、MRR、TopN 命中率、引用合法性、关键点覆盖
5. 输出评测报告
6. 对低分 case 做 trace 回放
7. 优化 query rewrite、chunk、TopK、rerank、Prompt
8. 再次回归评测
9. 通过后灰度上线

8. 面试总结

面试可以这样回答:

使用 RAGAS 时,我们先构建黄金问答集,然后批量调用自己的 RAG 服务,保存 question、contexts、answer、ground_truth。接着用 RAGAS 计算 faithfulness、answer relevancy、context precision、context recall 等通用指标,判断答案是否基于上下文、是否回答问题、检索上下文是否相关和完整。

但 RAGAS 不能完全替代业务评测,所以我们还会用自研脚本计算 Recall@K、MRR、TopN 命中率、引用准确性、权限合法性和关键点覆盖。最终通过 RAGAS + 自研脚本 + trace 回放形成评测闭环。

问题缓存怎么做?

RAG 的问题缓存不能简单理解成 “用户问题相同就直接返回上次答案”。企业知识库有权限、版本、文档更新、模型版本和引用来源等约束,如果缓存设计不当,可能出现越权返回、引用旧文档、答案过期等问题。

问题缓存一般分三层:

1
2
3
Query embedding 缓存
检索结果缓存
答案缓存 / FAQ Cache

1. Query embedding 缓存

用户问题经过 Query Rewrite 后,会生成 query embedding。对于高频相似问题,可以缓存 query embedding,减少 embedding 服务调用。

缓存 key 不能只用原始问题,通常使用:

1
hash(normalized_query + rewrite_version + embedding_model_version)

其中 normalized_query 会做大小写归一、空格清理、同义词规范化等处理。embedding 模型升级后,embedding_model_version 变化,旧缓存自然失效。

2. 检索结果缓存

检索结果缓存保存的是某个 query 在某个权限范围和知识库版本下的 TopK / TopN chunk 列表。

缓存内容可以包括:

1
2
3
4
5
6
7
query
rewritten_query
retrieved_chunk_ids
rerank_scores
citations
knowledge_base_version
permission_scope_hash

缓存 key 要包含权限和知识库版本,不能只按 query 缓存:

1
2
3
4
5
6
7
8
hash(
normalized_query
+ user_permission_scope_hash
+ knowledge_base_version
+ retrieval_param_version
+ embedding_model_version
+ rerank_model_version
)

这里最重要的是 user_permission_scope_hash。同一个问题,不同用户能看的文档范围不同,缓存必须隔离。比如 A 用户有项目权限,B 用户没有,如果只按问题文本缓存,就可能把 A 用户可见的文档返回给 B 用户。

3. 答案缓存 / FAQ Cache

答案缓存适合高频、稳定、权限范围明确的问题,比如:

1
2
3
4
某错误码含义
常见操作步骤
固定配置说明
新人 FAQ

答案缓存一般只对低风险问题启用,不适合缓存个性化强、权限敏感、实时性强、深度研究类问题。

答案缓存内容要包含:

1
2
3
4
5
6
7
answer
citation_ids
source_doc_versions
created_at
model_version
permission_scope_hash
quality_status

只有经过验证的答案才建议进入 FAQ Cache,比如用户点赞多、人工审核通过、来自黄金问答集的高可信答案。

4. 缓存失效策略

缓存失效是 RAG 缓存最关键的部分。常见失效条件包括:

  • 文档内容更新:knowledge_base_version 或 document_version 变化,相关缓存失效。
  • 权限变化:permission_scope_hash 变化,缓存失效。
  • embedding 模型升级:embedding_model_version 变化,query embedding 和检索缓存失效。
  • rerank 模型或参数变化:rerank_model_version / retrieval_param_version 变化,检索结果缓存失效。
  • Prompt 或生成模型变化:answer_prompt_version / llm_version 变化,答案缓存失效。
  • 文档被废弃或删除:包含该 citation 的缓存失效。

实际可以通过版本号 + TTL 双保险:

1
2
版本号控制强一致失效
TTL 控制兜底过期

例如:

1
2
3
query embedding cache:TTL 1-7 天
retrieval result cache:TTL 10-60 分钟
FAQ answer cache:TTL 1-7 天,人工审核后可更长

5. 缓存命中后的安全校验

即使命中缓存,也不能完全跳过权限校验。尤其是检索结果缓存和答案缓存,返回前要做轻量校验:

1
2
3
4
用户当前权限是否仍覆盖 citation
source_doc_version 是否仍为 ACTIVE
文档是否已删除或废弃
缓存 permission_scope_hash 是否匹配

如果不匹配,就丢弃缓存,重新走检索链路。

6. 相似问题缓存

对于自然语言问题,完全相同的问题不多,可以做相似问题缓存。但要谨慎。

流程可以是:

1
2
3
4
5
6
用户 query
-> 生成 query embedding
-> 在 FAQ Cache 或历史高频问题库中找相似问题
-> 相似度超过阈值
-> 权限和版本校验通过
-> 返回缓存答案或缓存检索结果

相似问题缓存阈值不能太低,否则容易答非所问。一般只对 FAQ、操作手册、错误码解释这类稳定问题启用。

7. 哪些问题不适合缓存?

以下问题不建议直接答案缓存:

  • 强权限敏感问题。
  • 涉及最新状态的问题,比如“这个 DTS 当前处理到哪一步了”。
  • 深度研究任务。
  • 多轮上下文依赖很强的问题。
  • 用户指定了时间范围、版本、项目且变化频繁的问题。
  • 需要调用工具实时查询的问题。

这类问题最多缓存 query embedding 或部分检索结果,不直接缓存最终答案。

8. 面试总结

面试可以这样回答:

问题缓存分为 query embedding 缓存、检索结果缓存和 FAQ 答案缓存。缓存 key 不能只用问题文本,必须包含权限范围、知识库版本、模型版本、检索参数版本等信息,避免越权和旧答案。文档更新、权限变化、模型升级、文档删除都会触发缓存失效。缓存命中后仍要做权限和引用版本校验。对于高频稳定问题可以使用 FAQ Cache,对于实时、权限敏感、深度研究类问题不直接缓存答案。

怎么处理 HTML、Markdown 和 PDF 文档?

这三类文档的处理重点不是“直接抽成纯文本”,而是先解析出文档结构,清洗掉噪声,再把内容整理成适合 chunk 切分的结构化 block。最终目标是让 chunk 既保留语义边界,又带上标题路径、页码、来源和权限元数据。

统一流程是:

1
2
3
4
5
6
原文入 MinIO
-> Parser 解析结构
-> 数据清洗和归一化
-> 生成结构化 block
-> 按标题 / 段落 / 表格 / 代码块切分 chunk
-> embedding + ES/Milvus 索引

1. HTML 怎么处理

HTML 通常来自 Wiki、W3 论坛、内部系统页面或接口文档。

解析方式:

使用 HTML Parser 解析 DOM 树,不直接正则抽文本。解析时识别 h1-h6 标题、正文段落、列表、表格、代码块和链接,并尽量定位正文区域,比如 articlemaincontent 容器。

数据清洗:

主要清洗页面噪声:

  • 去掉 scriptstyle、导航栏、页脚、侧边栏。
  • 去掉广告、按钮、目录重复项、面包屑等非正文内容。
  • 清理重复链接、空标签、无意义短文本。
  • 保留正文标题层级、表格、代码块和原始链接。

达到的效果:

清洗后 HTML 会被归一成类似 Markdown 的结构化 block,例如:

1
2
3
4
5
标题路径:一级标题 > 二级标题 > 三级标题
正文段落:...
表格:...
代码块:...
原始链接:...

这样切分时可以按标题确定主题边界,按段落生成文本 chunk,表格和代码块单独处理,避免把导航、页脚、无关按钮切进 chunk 里污染检索。

2. Markdown 怎么处理

Markdown 是结构最清晰的一类文档,因为它天然有标题、列表、代码块和表格。

解析方式:

使用 Markdown Parser 解析 AST,识别 heading、paragraph、list、table、code block、blockquote、link 等节点。对于 fenced code block,可以直接根据语言标记识别 Java、Python、SQL、YAML 等代码类型。

数据清洗:

主要做结构归一化:

  • 修正标题层级混乱的问题。
  • 清理空段落、重复分隔符、无意义模板文本。
  • 保留代码块语言、表格表头、引用块和链接。
  • 对未闭合代码块、格式不规范表格做兜底解析。

达到的效果:

Markdown 会被整理成带标题路径的结构化内容。切分时以标题作为边界,段落作为主要粒度:

  • 普通段落:短段合并,长段拆分。
  • FAQ:问题和答案成对保留。
  • 表格:保留表头,按行组切。
  • 代码块:小代码块整体保留,大代码块按函数、类、配置段切。

这样可以避免按固定长度切断语义,同时让每个 chunk 都知道自己属于哪个标题主题。

3. PDF 怎么处理

PDF 最复杂,因为它可能是文本型 PDF、扫描型 PDF,也可能包含图片、表格、多栏排版、页眉页脚和水印。

解析方式:

先判断 PDF 类型:

  • 文本型 PDF:用 PDF Parser 提取文本、页码、字体、坐标和排版信息。
  • 扫描型 PDF:走 OCR 提取图片中的文字。
  • 混合型 PDF:文本抽取 + 图片 OCR / 图片摘要结合处理。

对于 PDF 中的图片和表格,会单独抽取:

  • 报错截图、日志截图、配置截图:OCR。
  • 表格截图:OCR + 表格结构恢复。
  • 架构图、流程图、监控图:OCR + 图像摘要。
  • 装饰图:跳过,只保留原图。

数据清洗:

主要清洗 PDF 特有噪声:

  • 去掉页眉、页脚、页码、水印、重复目录。
  • 修正多栏排版导致的阅读顺序错乱。
  • 合并被换行打断的句子。
  • 对 OCR 低置信度内容打标,必要时降权。
  • 表格解析失败时保留原图引用,避免错误结构污染检索。

达到的效果:

PDF 会被还原成带页码和标题路径的 block:

1
2
3
4
5
6
pageNo:3
标题路径:故障处理 > 网络超时
正文段落:...
表格 block:...
图片 OCR block:...
图片摘要 block:...

这样切分时可以按标题、页码、段落和结构块来切。回答引用时也可以定位到具体页码和原文对象。

4. 统一切分效果

三类文档最终都会归一成统一结构:

1
2
3
4
5
6
contentType:text / table / code / image_ocr / image_summary
sourceType:html / markdown / pdf
titlePath:标题路径
pageNo:页码,可选
content:清洗后的正文
metadata:权限、版本、产品线、更新时间等

这样后续 chunk 切分就不再直接依赖原始文件格式,而是基于统一 block:

  • 文本按标题和段落切。
  • 表格保留表头按行组切。
  • 代码按函数、类、配置段切。
  • 图片 OCR 和图片摘要作为独立 chunk。
  • 每个 chunk 绑定标题路径、来源、页码、权限和版本。

面试总结:HTML 通过 DOM 解析提取正文并清理页面噪声;Markdown 通过 AST 保留天然结构并修正格式问题;PDF 先区分文本型和扫描型,文本型抽版面结构,扫描型走 OCR,同时处理表格、图片、页眉页脚和多栏顺序。清洗后的目标是把不同格式统一成结构化 block,再按标题、段落、表格、代码和图片语义切分 chunk。

对于文本型 PDF 怎么做数据清洗?

文本型 PDF 虽然可以直接抽取文字,但抽出来的内容通常不是干净正文,常见问题包括页眉页脚重复、页码混入正文、换行断句、多栏顺序错乱、水印干扰、目录重复、表格被拉平成文本等。所以文本型 PDF 的清洗重点是 恢复阅读顺序、去掉重复噪声、保留结构信息

1. 先抽取文本和版面信息

不会只抽纯文本,而是尽量抽取:

1
2
3
4
5
6
7
文本内容
页码 pageNo
坐标位置 bbox
字体大小 fontSize
字体样式 fontWeight
行间距
块级区域

这些版面信息后面用于判断标题、段落、页眉页脚、多栏顺序和表格区域。

2. 清理页眉、页脚和页码

PDF 里页眉页脚通常每页都重复,比如公司名、文档名、保密标识、页码。处理方式是统计跨页重复文本和固定位置文本:

1
2
3
同一位置重复出现
内容高度相似
出现在页面顶部或底部

满足这些特征就标记为 header/footer,从正文中移除。页码、版权声明、保密声明这类内容也会清理或只保留在元数据里。

3. 修复换行和断句

PDF 抽取经常会把一句话拆成多行,例如:

1
2
向量库不可用时,系统会降级到
Elasticsearch 关键词检索。

清洗时会根据标点、行宽、缩进、下一行首字符等规则合并:

1
向量库不可用时,系统会降级到 Elasticsearch 关键词检索。

但如果是列表、标题、表格行、代码块,就不能随意合并。

4. 处理多栏排版顺序

有些 PDF 是双栏或多栏。如果直接按抽取顺序,可能出现左栏第一行、右栏第一行交叉在一起。处理方式是根据文本块坐标做版面排序:

1
2
3
先按列分组
列内按 y 坐标排序
再按阅读顺序合并

这样可以尽量恢复人类阅读顺序。

5. 去掉目录、脚注和重复模板

目录页、脚注、免责声明、固定模板内容会影响检索。处理方式包括:

  • 目录页根据“目录”“第 x 页”“……”等特征识别。
  • 脚注根据小字体、页面底部位置识别。
  • 重复模板根据跨页重复模式识别。

这些内容一般不进入正文 chunk,或者降权处理。

6. 识别标题和段落

文本型 PDF 没有像 Markdown 那样天然的标题层级,需要根据版面特征推断:

1
2
3
4
5
字体更大
加粗
编号模式,如 1.2、3.1.4
独占一行
上下间距更大

识别出标题后,构建 titlePath。后续 chunk 切分时,标题路径会拼接到 chunk 中,增强语义。

7. 表格区域特殊处理

PDF 表格如果直接抽文本,会变成混乱的行列。清洗时需要识别表格区域:

1
2
3
4
线框
对齐的列坐标
重复表头
密集短文本单元格

小表格转成 Markdown 表格;大表格按“表头 + 行组”切分。表格解析失败时,保留原始页码和区域引用,避免错误表格污染正文。

8. 水印和噪声处理

水印通常有透明度、斜向、大字号、跨页重复等特征。文本抽取时如果把水印抽出来,会严重污染 chunk。处理时会根据位置、重复度、字体特征和文本模式过滤。

9. 清洗后的效果

清洗后希望得到的是结构化 block,而不是一整段纯文本:

1
2
3
4
pageNo: 5
titlePath: RAG 检索设计 > 降级策略
blockType: paragraph
content: 向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索。

或者:

1
2
3
4
pageNo: 6
titlePath: 性能指标 > 检索延迟
blockType: table
content: Markdown 表格或结构化行列

这样后续才能按标题、段落、表格、代码块做合理切分。

10. 面试总结

面试可以这样回答:

文本型 PDF 不是抽到文字就直接入库,而是先抽取文本和坐标、字体、页码等版面信息,再清洗页眉页脚、页码、水印、目录、脚注,修复换行断句和多栏阅读顺序,同时识别标题、段落和表格区域。清洗后的目标是把 PDF 还原成带 pageNo、titlePath、blockType 的结构化 block,再用于 chunk 切分和引用追溯。