RAG Q&A
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 重排。
完整流程可以按七步讲:
- Query Rewrite:做同义词扩展、缩写还原、业务术语扩展、关键词提取。
- 权限过滤:根据用户部门、项目、角色、文档 ACL、密级生成过滤条件。
- 并行召回:ES 走 BM25,适合编号、接口名、错误码;Milvus 走向量检索,适合语义描述。
- 候选融合:两路 TopK 合并、去重,并结合文档类型、版本、更新时间、是否废弃等元数据初步调权。
- Rerank 精排:用 bge-reranker-v2-m3 判断问题和候选 chunk 的真实相关性。
- 上下文组装:取 TopN,做引用编号、同源合并、去重、截断、保留表格和代码块。
- 生成答案:大模型基于上下文生成答案,并展示引用来源。
关键点是: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 切割不是固定长度硬切,而是 结构感知 + 语义边界 + 少量重叠 + 元数据继承。
流程如下:
- 文档解析清洗:解析 Wiki、DTS、需求文档、Markdown、Word、PDF、Excel 等,清理页眉页脚、重复导航和无意义模板。
- 按结构切分:优先按标题层级、章节、接口、问答对、步骤来切,保持一个 chunk 在同一主题下。
- 按语义补切:章节过长时,按自然段、列表项、步骤、问题答案对继续切,不在句子、表格、代码块中间硬截断。
- 控制长度:普通文本目标约 500-800 中文字 或 300-500 token,最大一般不超过 800-1000 token。
- 保留 overlap:相邻 chunk 保留约 50-100 token,避免边界信息丢失。
- 特殊结构保护:表格保留表头,代码按函数/类/配置段切,FAQ 保持“问题 + 答案”成对。
- 元数据继承:每个 chunk 带上来源、标题路径、版本、更新时间、密级、ACL、是否废弃等。
面试总结:先解析清洗,再按标题章节切,再按语义段落补切,对表格和代码块做结构保护,最后绑定权限、版本、来源等元数据。
你们用这种方案切割出来的 chunk 不会太大吗?
不会,因为我们不是只按章节切,而是有 二次语义补切和长度上限。
需要澄清一点:“按章节切”不是把完整章节直接作为一个 chunk,而是把章节标题当作 主题边界和上下文路径。真正生成 chunk 时,还是会在章节内部按段落、列表、步骤、问答对继续切。
常见策略是:
- 章节用于确定主题边界。
- 段落是主要切分粒度。
- 句子用于过长段落的兜底拆分。
- 多个过短句子会合并,避免切得太碎。
- 相邻 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 管原文。
那你们检索的时候,怎么用元数据过滤去提高向量数据库的检索效率呢?
核心思路是 过滤前置、过滤下推、应用层二次校验。
流程如下:
- 用户登录后,权限服务提供用户的部门、项目、角色、知识库范围、密级范围等。
- RAG 服务结合问题意图,识别产品线、模块、版本、文档类型、时间范围等条件。
- 把高选择性字段下推到 Milvus 和 ES,例如 knowledgeBaseId、projectId、sourceType、productLine、version、securityLevel、status、isDeprecated。
- Milvus 只在过滤后的向量子集里做 ANN 检索,减少扫描范围。
- ES 也带同样的 filter 做 BM25 召回,减少无关候选。
- 候选返回后,再基于 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 放在 混合召回之后、上下文组装之前,目标是从“可能相关”的候选里选出“最值得给大模型看的证据”。
流程:
- ES BM25 和 Milvus 向量检索分别召回 TopK。
- 按 chunkId 合并去重,保留原始分数、文档类型、版本、更新时间、密级、是否废弃等元数据。
- 过滤无权限、废弃、状态异常的 chunk,对过期文档降权。
- 用 bge-reranker-v2-m3 做 Cross Encoder 精排,把“用户问题 + 候选 chunk”成对输入,得到相关性分数。
- 结合业务规则做最终排序,例如版本是否匹配、文档是否最新、是否同产品线、是否高质量 FAQ。
- 普通问答取 TopN 5-8 个进入上下文,复杂问题取 10-20 个并分步骤处理。
- 上下文组装时做引用编号、同源合并、去重截断、表格/代码块保护。
为什么要 rerank:第一阶段召回追求覆盖率,可能会带入噪声;LLM 上下文有限,不能把所有候选都塞进去。rerank 的核心价值是提升最终证据片段的相关性,减少无关内容干扰大模型。
降级策略:如果 rerank 服务不可用,会临时退回到 BM25 分数 + 向量分数 + 元数据权重 的融合排序,并记录告警和 trace,保证问答链路可用。
后续有新增文档的时候,要怎么做?
新增文档走 增量入库流程,不会手工直接写向量库。
整体链路:
数据源同步 -> 原文入 MinIO -> 元数据入 MySQL -> 解析清洗 -> chunk 切分 -> RocketMQ 投递 embedding 任务 -> 写入 Milvus 和 Elasticsearch -> 更新索引状态
关键步骤:
- 发现新增文档:从 Wiki、W3、DTS、需求平台、代码仓库、运维平台或人工上传入口同步,方式可以是 Webhook、消息订阅、定时扫描。
- 保存原文和元数据:原文写 MinIO,MySQL 记录 documentId、sourceId、objectKey、hash、版本、作者、部门、密级、ACL 等。
- 解析和清洗:不同格式走不同 Parser,保留标题、段落、表格、代码块、列表等结构。
- chunk 切分和元数据增强:每个 chunk 绑定 documentId、标题路径、sourceType、产品线、版本、密级、ACL、状态等。
- 异步 embedding 和索引构建:RocketMQ 投递任务,bge-m3 生成向量写 Milvus,文本和关键词写 ES,映射关系写 MySQL。
- 状态和失败处理:记录 PARSED、CHUNKED、EMBEDDED、INDEXED、FAILED 等状态,失败重试,多次失败进死信队列。
特别要强调权限:新增文档入库时必须同步 ACL、密级、项目、部门等权限元数据,不能出现先全员可见、后补权限的窗口。
面试总结:新增文档先保存原文和元数据,再解析清洗、结构化切分、异步 embedding,最后分别写入 MySQL、ES、Milvus 和 MinIO;全流程有状态记录、失败重试、死信队列和权限同步。
内容变更怎么做?
内容变更采用 hash 判断 + 版本化重建 + 索引无空窗切换。
流程:
- 发现变更:通过 Webhook、源系统事件、定时扫描或人工上传发现文档更新。
- hash 对比:和 MySQL 中上一次 hash 比较;hash 不变就跳过 embedding,只同步状态或时间戳。
- 生成新版本:hash 变化时生成 documentVersion / chunkVersion,新原文写入 MinIO。
- 重新解析切分:中小文档默认整篇重建;超大文档后续可以做 chunk 级 diff,只处理变化段落。
- 先建新索引:新版本 ES 文档和 Milvus 向量都写成功后,再把旧 chunk 标记为 INACTIVE / DEPRECATED。
- 后台清理旧数据:物理删除延迟异步做,保留审计窗口和回滚能力。
如果只是权限、密级、项目归属变化,正文没变,就 不需要重新 embedding。只更新 MySQL 权威元数据,并同步 ES/Milvus 的过滤字段,让权限尽快生效。
面试总结:内容变更先做 hash 判断,没变就跳过 embedding;变了就生成新版本并重建索引。为了避免检索空窗,通常先建新索引再下线旧 chunk;权限变更只更新元数据和过滤字段,不重新生成向量。
如果检索时的 TopN 不理想,你有什么方案吗?
TopN 不理想不能只靠把 TopN 调大解决。正确做法是先用 trace 回放定位问题,再针对性优化。
排查顺序:
- 正确文档没被召回:是 query rewrite、BM25、向量召回、TopK 或过滤条件问题。
- 召回了但排得靠后:是融合排序、元数据权重或 rerank 问题。
- 进入 TopN 但答案错:是 Prompt、上下文组装或模型生成问题。
- 版本不对或文档过期:是版本字段、更新时间、废弃状态和排序权重问题。
- chunk 不完整或噪声多:是切分策略、overlap、标题路径拼接问题。
- 权限过滤误伤:是 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 | 数据源接入 |
第一步是数据源接入。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 | 用户提问 |
第一步是鉴权、限流和审计。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 万 chunk、100GB 到 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 | 提取图片 |
默认策略可以这样落地:先做 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 | BM25 TopK |
元数据权重可以包括版本匹配、产品线匹配、文档是否最新、来源是否权威、是否高质量 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 | version = 2.3 |
5. OCR 模型
OCR 用于处理文档图片里的文字,比如报错截图、日志截图、配置截图、表格截图。
作用是把图片里的文字提取成可检索文本。例如:
1 | error_code: DTS_504 |
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 | 文档入库: |
面试总结: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 | MinIO:原文、附件、图片、解析中间结果 |
这样每个系统只做自己擅长的事情,边界清晰,也方便排障。比如用户反馈某个答案错误,可以通过 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。
- 深度研究任务:异步执行,通常按队列控制并发。
资源瓶颈通常不在一个地方,而是分层出现:
- 用户并发增长:优先扩 Gateway、Conversation、RAG 业务 Pod,一般加业务节点或提高副本数即可,成本相对可控。
- chunk 数增长:主要增加 Milvus 和 ES 的存储、内存、索引构建压力,成本增长比较明显。
- 问答量增长:会增加 rerank 和 LLM 推理压力,GPU 成本会变成大头。
- 文档入库高峰:会增加 Parser、Indexer、embedding 服务和 RocketMQ 消费压力,可以临时扩容消费节点。
- 深度研究任务增长:因为会多轮检索、多轮工具调用和长文本生成,要单独限制队列并发,避免影响普通问答。
后续扩容可以分阶段:
第一阶段: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 | MySQL ACTIVE chunk 数量 |
发现 MySQL 有但 ES/Milvus 没有,就触发补写;ES/Milvus 有但 MySQL 已废弃,就触发删除或状态修正;版本不一致,以 MySQL activeVersion 为准。
第七,新版本构建成功前,旧版本继续可用。内容变更时不是覆盖旧索引,而是先构建新版本,等新版本 ES 和 Milvus 都成功后,再切换 activeVersion。这样即使新版本构建失败,也不会影响线上旧版本检索。
第八,最终状态必须可解释。一个任务最后只能处于几类明确状态:
- ACTIVE:构建成功,可检索。
- FAILED:自动处理失败,等待修复。
- DEPRECATED:旧版本下线。
- DELETED:已逻辑删除。
不能出现任务卡在中间状态没人知道。
所以面试时可以这样回答:我们不能承诺所有异常下都自动成功,但会保证任务不丢失、状态可追踪、失败可重试、异常可补偿。通过 RocketMQ 重试、幂等写入、outbox 任务表、死信队列和定时一致性校验,在可恢复故障下最终会成功;不可恢复故障会进入明确失败状态并告警,由人工修复后重新投递。
对于检索流程来说不理想,要怎么去分析?
检索效果不理想时,不能只看最终答案,也不能直接调大 TopK 或 TopN。正确做法是把一次 RAG 检索链路拆开,通过 trace 回放逐层定位问题。
一次完整检索可以拆成:
1 | 原始问题 |
分析时先看 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 | 用户反馈 / 低分 case |
面试总结:检索不理想时,先用 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 | BM25 TopK:没有召回这篇 DTS |
这种情况说明正确证据在粗召回阶段就丢了,后面的 rerank 和 LLM 没机会修复。
可能原因:
- 用户问的是
DTS_504,文档里写的是E_CONN_TIMEOUT,query rewrite 没有做错误码映射。 - DTS 文档没有成功入库,ES 或 Milvus 缺索引。
- 文档被切得太碎,错误码、根因、解决方案分散在不同 chunk。
- 权限或 sourceType filter 把 DTS 文档过滤掉。
分析动作:
1 | 1. 查 document_meta:这篇 DTS 是否存在,状态是否 ACTIVE |
优化方案:
- 补充错误码、模块名、缩写的业务词典。
- 对 DTS 编号、错误码走 BM25 精确召回。
- 修复缺失索引或重新入库。
- 调整 chunk,把“问题现象 + 错误码 + 根因 + 解决方案”尽量放在同一语义块里。
2. 正确文档召回了,但没有进入最终 TopN
例子:用户问:
1 | Milvus 不可用时系统怎么降级? |
正确 chunk 内容是:
1 | 向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索,并记录告警。 |
trace 里看到:
1 | Vector TopK:正确 chunk 排第 18 |
这种情况说明不是没召回,而是排序阶段不理想,正确证据被挤出最终 TopN。
可能原因:
- 融合排序时 BM25 和向量权重不合理。
- reranker 对“降级 / fallback / 不可用”这类表达识别不好。
- 一篇同源文档多个相似 chunk 占了前排。
- 旧版本文档或低质量论坛帖权重过高。
- 正确 chunk 标题路径缺失,rerank 输入语义不完整。
分析动作:
1 | 1. 对比 BM25 rank、Vector rank、Fusion rank、Rerank rank |
优化方案:
- 使用 RRF 融合,减少 BM25 和向量分数尺度差异。
- 对同一文档做去重或限制单文档进入 TopN 的 chunk 数。
- 给权威文档、最新版本、产品线匹配的 chunk 加权。
- 给过期、废弃、低质量来源降权。
- rerank 输入拼接标题路径、来源类型和版本。
- 对复杂问题动态扩大 TopN,例如从 8 提到 12。
3. 正确证据进入 TopN,但答案仍然不理想
例子:用户问:
1 | 向量库不可用时怎么处理?会有什么影响? |
Final TopN 里已经有正确 chunk:
1 | 向量库不可用时,系统降级到 Elasticsearch BM25 关键词检索。 |
但是模型回答成:
1 | 向量库不可用时,系统会自动重启 Milvus 集群,并清理缓存。 |
这种情况说明检索阶段基本没问题,问题在上下文组装、Prompt 约束或生成阶段。
可能原因:
- TopN 里虽然有正确证据,但噪声太多,模型被其他 chunk 干扰。
- 正确 chunk 被截断,只保留了“向量库不可用”,没保留“降级到 ES”。
- Prompt 没要求“只能基于上下文回答,无证据不要编”。
- 多个降级策略混在一起,模型把模型不可用、rerank 不可用、向量库不可用混淆了。
- 引用校验缺失,模型引用了不支撑结论的 chunk。
分析动作:
1 | 1. 查看最终 Prompt,确认正确 chunk 是否完整进入上下文 |
优化方案:
- 对 TopN 做更严格的去噪和同源合并。
- 上下文组装时保护关键句,不在“策略 + 影响”中间截断。
- Prompt 中明确要求:只基于给定证据回答,证据不足时说明不足。
- 对不同降级场景做结构化上下文,比如按“Milvus 不可用 / Rerank 不可用 / 模型不可用”分组。
- 增加引用校验,答案中的关键结论必须能对应到引用 chunk。
面试总结:如果正确文档没召回,就是召回问题,要查 query rewrite、索引、chunk 和 filter;如果召回了但没进 TopN,就是排序问题,要查融合、RRF、rerank 和元数据权重;如果进了 TopN 但答案错,就是生成和上下文问题,要查 Prompt、截断、噪声和引用校验。
用 RAGAS + 自研脚本看答案忠实度、上下文相关性、答案完整性和引用准确性,举例说明
RAGAS 和自研脚本的作用是评估一次 RAG 回答到底好不好。它不是只看“答案像不像”,而是把一次回答拆成几个问题:
1 | 检索出来的上下文是否相关? |
1. 示例数据
假设用户问题是:
1 | Milvus 不可用时系统怎么降级?会有什么影响? |
黄金答案是:
1 | Milvus 不可用时,系统降级到 Elasticsearch BM25 关键词检索;影响是语义召回能力下降,但普通关键词检索仍可用,同时系统会记录告警和 trace。 |
标准证据 chunk 是:
1 | chunk_001: |
本次 RAG 实际检索出的 TopN 上下文是:
1 | chunk_001: |
模型实际回答是:
1 | Milvus 不可用时,系统会降级到 Elasticsearch BM25 关键词检索,因此语义召回能力会下降,但关键词检索仍可用;系统会记录告警和 traceId。[引用1] |
引用关系是:
1 | 引用1 -> chunk_001 |
2. RAGAS 怎么评估
RAGAS 一般需要输入 question、contexts、answer、ground_truth 等字段。
示例输入可以是:
1 | { |
RAGAS 会评估几类通用指标。
3. 答案忠实度 Faithfulness
忠实度看的是:答案里的每个关键结论是否能被上下文支撑。
在上面的例子中,答案包含几个结论:
1 | 1. Milvus 不可用时降级到 Elasticsearch BM25。 |
这些都能在 chunk_001 中找到依据,所以忠实度高。
如果模型回答成:
1 | Milvus 不可用时,系统会自动重启 Milvus 集群,并清理缓存。 |
上下文里没有“自动重启”和“清理缓存”,这就是不忠实,也就是幻觉。
评估方式:
- RAGAS 会把答案拆成多个 statement。
- 判断每个 statement 是否能被 contexts 支撑。
- 支撑比例越高,faithfulness 越高。
可以理解成:
1 | faithfulness = 被上下文支持的结论数 / 答案总关键结论数 |
4. 上下文相关性 Context Relevance
上下文相关性看的是:检索出来的 TopN chunk 是否和用户问题相关。
本例中:
1 | chunk_001:讲 Milvus/向量库不可用,相关性高。 |
如果 TopN 里大部分都是 chunk_002、chunk_003 这种“同属降级策略但不是 Milvus 降级”的内容,上下文相关性就会被拉低。
评估方式:
- RAGAS 会判断 context 是否有助于回答 question。
- 自研脚本可以根据 gold_chunk_id 判断标准证据是否进入 TopN。
- 也可以统计 TopN 中强相关、弱相关、无关 chunk 的比例。
例如人工标注:
1 | chunk_001 = 强相关 |
那么结论是:有正确证据,但 TopN 噪声偏多,需要优化 rerank 或同类降级策略分组。
5. 答案完整性 Answer Completeness
完整性看的是:答案有没有覆盖问题要求的关键点。
用户问的是:
1 | 怎么降级?会有什么影响? |
所以完整答案至少应该包含:
1 | 1. 降级动作:降级到 ES BM25。 |
如果模型回答:
1 | Milvus 不可用时会降级到 Elasticsearch。 |
这句话是正确的,但不完整,因为没有回答“影响是什么”,也没有提到告警和 trace。
评估方式:
- RAGAS 可以结合 ground_truth 看 answer correctness / answer similarity。
- 自研脚本更适合检查关键点覆盖。
例如定义 expected_points:
1 | { |
脚本检查答案是否覆盖这些点:
1 | 覆盖 4/4:完整 |
6. 引用准确性 Citation Accuracy
引用准确性看的是:答案中的引用是否真的支撑对应结论。
正确情况:
1 | 答案:Milvus 不可用时降级到 ES BM25。[引用1] |
这是引用准确。
错误情况:
1 | 答案:Milvus 不可用时降级到 ES BM25。[引用2] |
这个引用不准确,因为 chunk_002 不能支撑 Milvus 降级到 ES 的结论。
自研脚本可以做几类检查:
1 | 1. citationId 是否存在于本次 TopN contexts。 |
引用准确率可以简单定义为:
1 | citation_accuracy = 正确引用数 / 总引用数 |
7. 自研脚本补充检查什么
RAGAS 偏通用,但企业 RAG 还需要业务规则校验。自研脚本通常检查:
1 | gold_chunk_id 是否进入 TopN |
比如本例可以配置:
1 | { |
脚本评估结果可能是:
1 | TopN 命中:通过,chunk_001 在 TopN |
8. 反例说明
如果模型回答:
1 | Milvus 不可用时,系统会自动重启 Milvus,并清理缓存;如果仍失败,再降级到 ES。[引用1] |
评估结果会是:
1 | 忠实度:低,因为“自动重启 Milvus、清理缓存”没有证据支撑 |
这个 case 的优化方向不是召回,而是:
1 | 1. Prompt 强化:无证据不要编造处理步骤。 |
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 | 1. Milvus 不可用时会降级到 Elasticsearch BM25。 |
然后逐条判断每个 statement 是否被 contexts 支持:
1 | supported / unsupported |
一个简单计算方式是:
1 | faithfulness = 被上下文支持的 statement 数 / 总 statement 数 |
实际落地可以用 RAGAS 来做 statement 拆解和 LLM 判断,也可以用自研脚本做补充校验。例如:
- 关键结论是否能在引用 chunk 中找到语义支撑。
- 答案是否出现 forbidden_keywords,比如“自动重启 Milvus”这类上下文没有的处理方式。
- 答案中的引用是否能支撑对应句子。
所以忠实度重点看的是:有没有脱离证据编答案。
2. 答案相关性 Answer Relevance
答案相关性关注的是:模型回答是否真正回答了用户的问题。
它回答的问题是:
1 | 答案有没有围绕用户问题展开? |
还是同一个问题:
1 | Milvus 不可用时系统怎么处理? |
如果模型回答:
1 | Milvus 不可用时,系统会降级到 Elasticsearch BM25 关键词检索,并记录告警。 |
答案相关性高,因为它直接回答了“怎么处理”。
如果模型回答:
1 | Milvus 是一个向量数据库,适合大规模向量检索,支持高性能相似度搜索。 |
这段话可能是事实正确的,但没有回答“不可用时怎么处理”,所以答案相关性低。
再比如用户问:
1 | Milvus 不可用时有什么影响? |
模型只回答:
1 | 系统会记录 traceId。 |
这虽然可能忠实于上下文,但没有回答主要问题“影响是什么”,所以相关性也不高。
实际评估时,RAGAS 通常会根据 question 和 answer 判断答案是否匹配用户问题。常见做法是:
- 从 answer 反向生成若干 possible questions。
- 看这些 possible questions 和原始 question 的语义相似度。
- 相似度越高,说明 answer 越相关。
也可以用 LLM-as-judge 直接打分:
1 | 请判断 answer 是否直接回答 question,分数 1-5。 |
自研脚本可以结合问题类型做关键点校验。例如:
如果问题是:
1 | Milvus 不可用时系统怎么降级?有什么影响? |
那答案至少要覆盖:
1 | 降级动作 |
如果只讲“Milvus 是什么”,相关性低;如果只讲“降级到 ES”但没讲影响,则相关性中等;如果两者都覆盖,则相关性高。
所以答案相关性重点看的是:有没有正面回答用户问题。
3. 两者的区别
可以这样区分:
| 指标 | 关注点 | 典型问题 |
|---|---|---|
| 答案忠实度 | 答案是否被上下文支撑 | 有没有编造、有没有幻觉 |
| 答案相关性 | 答案是否回答用户问题 | 有没有答偏、有没有避重就轻 |
一个答案可能忠实但不相关:
1 | 用户问:Milvus 不可用时怎么降级? |
这句话可能来自上下文,所以忠实度不一定低,但它没有回答降级问题,所以相关性低。
一个答案也可能相关但不忠实:
1 | 用户问:Milvus 不可用时怎么降级? |
它看起来回答了问题,所以相关性可能不低,但上下文没有这个证据,所以忠实度低。
4. 实际评估流程
实际评估时,我们会对每条黄金问答样本保存:
1 | { |
然后分两步评估:
第一步,评估忠实度:
1 | 把 answer 拆成事实点 |
第二步,评估相关性:
1 | 判断 answer 是否直接回答 question |
自研脚本会补充更业务化的规则:
1 | expected_points 是否覆盖 |
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 | 1. chunk_008 |
因为正确证据 chunk_001 出现在 Top5 里,所以:
1 | Recall@5 = 命中 |
如果 Top5 没有 chunk_001,但 Top10 里有,那么:
1 | Recall@5 = 未命中 |
在多条评测样本上,Recall@K 的计算方式是:
1 | Recall@K = TopK 命中的问题数 / 总问题数 |
例如有 100 个黄金问题,其中 85 个问题的标准证据进入了 Top10:
1 | Recall@10 = 85 / 100 = 85% |
2. Recall@K 在实际中怎么评估
实际评估时,我们会先构建黄金问答集,每条样本至少包含:
1 | { |
然后对每个 question 跑完整检索链路:
1 | Query Rewrite |
脚本判断:
1 | gold_chunk_id 是否在 TopK 里 |
通常会分别统计:
1 | Recall@5 |
这些指标可以分别看不同阶段:
- 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 | 问题1:正确 chunk 排第 1,RR = 1 |
那么:
1 | MRR = (1 + 0.5 + 0.2) / 3 = 0.566 |
如果另一个系统的结果是:
1 | 问题1:正确 chunk 排第 3,RR = 0.333 |
那么 MRR 会明显更低。
所以 MRR 越高,说明正确证据越靠前。
5. MRR 在实际中怎么评估
实际落地时,也是在黄金问答集上跑检索链路,然后记录每个问题的正确 chunk 排名。
示例:
1 | { |
这里 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 | 正确 chunk 排第 1 |
系统 B:
1 | 正确 chunk 排第 10 |
两个系统 Recall@10 都命中,但系统 A 的 MRR 更高,说明 A 排序更好。
7. 在 RAG 中怎么看这两个指标
RAG 里只看 Recall@K 不够,因为正确证据即使在 Top50 里,如果最终 TopN 只取 8 个,它也可能进不了大模型上下文。
所以我们通常这样看:
1 | Recall@K:判断粗召回能力 |
如果:
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 | { |
其中 ground_truth 和 expected_points 主要用于答案正确性、完整性和自研脚本校验;gold_chunk_id 主要用于 Recall@K、MRR、TopN 命中率。
2. 跑完整 RAG 链路,保存评估样本
对每个 question 调用自己的 RAG 服务,拿到:
1 | { |
RAGAS 主要消费的是:
1 | question |
字段名会随 RAGAS 版本有差异,工程里可以在评测脚本里做一层适配。
3. 选择 RAGAS 指标
常用指标可以分成两类。
检索侧:
1 | context_precision:检索出来的上下文排序是否合理,相关内容是否靠前。 |
生成侧:
1 | faithfulness:答案是否被 contexts 支撑,有没有幻觉。 |
RAGAS 官方文档里也把这些作为 RAG 常用指标,包括 Context Precision、Context Recall、Response Relevancy、Faithfulness 等。
4. 示例评估代码
下面是一个简化版 Python 评估脚本,真实项目里会把数据从 CSV、JSONL 或评测数据库读取出来。
1 | from datasets import Dataset |
如果公司内部不能使用外部模型,就要把 RAGAS 的 judge LLM 和 embedding 配成内网模型,比如内部 Qwen、DeepSeek 或企业模型服务。也就是说,评测模型也要私有化,不能把内部文档 contexts 发到外部 API。
5. RAGAS 输出怎么看
假设某条样本输出是:
1 | faithfulness = 0.95 |
可以这样解释:
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 | gold_chunk_id 是否进入 TopN |
这些可以用自研脚本做确定性校验。
例如:
1 | def eval_custom(sample): |
实际项目里更建议采用:
1 | RAGAS 看通用质量 |
7. 实际落地流程
完整流程可以这样落地:
1 | 1. 准备黄金问答集 |
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 | Query embedding 缓存 |
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 | query |
缓存 key 要包含权限和知识库版本,不能只按 query 缓存:
1 | hash( |
这里最重要的是 user_permission_scope_hash。同一个问题,不同用户能看的文档范围不同,缓存必须隔离。比如 A 用户有项目权限,B 用户没有,如果只按问题文本缓存,就可能把 A 用户可见的文档返回给 B 用户。
3. 答案缓存 / FAQ Cache
答案缓存适合高频、稳定、权限范围明确的问题,比如:
1 | 某错误码含义 |
答案缓存一般只对低风险问题启用,不适合缓存个性化强、权限敏感、实时性强、深度研究类问题。
答案缓存内容要包含:
1 | answer |
只有经过验证的答案才建议进入 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 | 版本号控制强一致失效 |
例如:
1 | query embedding cache:TTL 1-7 天 |
5. 缓存命中后的安全校验
即使命中缓存,也不能完全跳过权限校验。尤其是检索结果缓存和答案缓存,返回前要做轻量校验:
1 | 用户当前权限是否仍覆盖 citation |
如果不匹配,就丢弃缓存,重新走检索链路。
6. 相似问题缓存
对于自然语言问题,完全相同的问题不多,可以做相似问题缓存。但要谨慎。
流程可以是:
1 | 用户 query |
相似问题缓存阈值不能太低,否则容易答非所问。一般只对 FAQ、操作手册、错误码解释这类稳定问题启用。
7. 哪些问题不适合缓存?
以下问题不建议直接答案缓存:
- 强权限敏感问题。
- 涉及最新状态的问题,比如“这个 DTS 当前处理到哪一步了”。
- 深度研究任务。
- 多轮上下文依赖很强的问题。
- 用户指定了时间范围、版本、项目且变化频繁的问题。
- 需要调用工具实时查询的问题。
这类问题最多缓存 query embedding 或部分检索结果,不直接缓存最终答案。
8. 面试总结
面试可以这样回答:
问题缓存分为 query embedding 缓存、检索结果缓存和 FAQ 答案缓存。缓存 key 不能只用问题文本,必须包含权限范围、知识库版本、模型版本、检索参数版本等信息,避免越权和旧答案。文档更新、权限变化、模型升级、文档删除都会触发缓存失效。缓存命中后仍要做权限和引用版本校验。对于高频稳定问题可以使用 FAQ Cache,对于实时、权限敏感、深度研究类问题不直接缓存答案。
怎么处理 HTML、Markdown 和 PDF 文档?
这三类文档的处理重点不是“直接抽成纯文本”,而是先解析出文档结构,清洗掉噪声,再把内容整理成适合 chunk 切分的结构化 block。最终目标是让 chunk 既保留语义边界,又带上标题路径、页码、来源和权限元数据。
统一流程是:
1 | 原文入 MinIO |
1. HTML 怎么处理
HTML 通常来自 Wiki、W3 论坛、内部系统页面或接口文档。
解析方式:
使用 HTML Parser 解析 DOM 树,不直接正则抽文本。解析时识别 h1-h6 标题、正文段落、列表、表格、代码块和链接,并尽量定位正文区域,比如 article、main、content 容器。
数据清洗:
主要清洗页面噪声:
- 去掉
script、style、导航栏、页脚、侧边栏。 - 去掉广告、按钮、目录重复项、面包屑等非正文内容。
- 清理重复链接、空标签、无意义短文本。
- 保留正文标题层级、表格、代码块和原始链接。
达到的效果:
清洗后 HTML 会被归一成类似 Markdown 的结构化 block,例如:
1 | 标题路径:一级标题 > 二级标题 > 三级标题 |
这样切分时可以按标题确定主题边界,按段落生成文本 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 | pageNo:3 |
这样切分时可以按标题、页码、段落和结构块来切。回答引用时也可以定位到具体页码和原文对象。
4. 统一切分效果
三类文档最终都会归一成统一结构:
1 | contentType:text / table / code / image_ocr / image_summary |
这样后续 chunk 切分就不再直接依赖原始文件格式,而是基于统一 block:
- 文本按标题和段落切。
- 表格保留表头按行组切。
- 代码按函数、类、配置段切。
- 图片 OCR 和图片摘要作为独立 chunk。
- 每个 chunk 绑定标题路径、来源、页码、权限和版本。
面试总结:HTML 通过 DOM 解析提取正文并清理页面噪声;Markdown 通过 AST 保留天然结构并修正格式问题;PDF 先区分文本型和扫描型,文本型抽版面结构,扫描型走 OCR,同时处理表格、图片、页眉页脚和多栏顺序。清洗后的目标是把不同格式统一成结构化 block,再按标题、段落、表格、代码和图片语义切分 chunk。
对于文本型 PDF 怎么做数据清洗?
文本型 PDF 虽然可以直接抽取文字,但抽出来的内容通常不是干净正文,常见问题包括页眉页脚重复、页码混入正文、换行断句、多栏顺序错乱、水印干扰、目录重复、表格被拉平成文本等。所以文本型 PDF 的清洗重点是 恢复阅读顺序、去掉重复噪声、保留结构信息。
1. 先抽取文本和版面信息
不会只抽纯文本,而是尽量抽取:
1 | 文本内容 |
这些版面信息后面用于判断标题、段落、页眉页脚、多栏顺序和表格区域。
2. 清理页眉、页脚和页码
PDF 里页眉页脚通常每页都重复,比如公司名、文档名、保密标识、页码。处理方式是统计跨页重复文本和固定位置文本:
1 | 同一位置重复出现 |
满足这些特征就标记为 header/footer,从正文中移除。页码、版权声明、保密声明这类内容也会清理或只保留在元数据里。
3. 修复换行和断句
PDF 抽取经常会把一句话拆成多行,例如:
1 | 向量库不可用时,系统会降级到 |
清洗时会根据标点、行宽、缩进、下一行首字符等规则合并:
1 | 向量库不可用时,系统会降级到 Elasticsearch 关键词检索。 |
但如果是列表、标题、表格行、代码块,就不能随意合并。
4. 处理多栏排版顺序
有些 PDF 是双栏或多栏。如果直接按抽取顺序,可能出现左栏第一行、右栏第一行交叉在一起。处理方式是根据文本块坐标做版面排序:
1 | 先按列分组 |
这样可以尽量恢复人类阅读顺序。
5. 去掉目录、脚注和重复模板
目录页、脚注、免责声明、固定模板内容会影响检索。处理方式包括:
- 目录页根据“目录”“第 x 页”“……”等特征识别。
- 脚注根据小字体、页面底部位置识别。
- 重复模板根据跨页重复模式识别。
这些内容一般不进入正文 chunk,或者降权处理。
6. 识别标题和段落
文本型 PDF 没有像 Markdown 那样天然的标题层级,需要根据版面特征推断:
1 | 字体更大 |
识别出标题后,构建 titlePath。后续 chunk 切分时,标题路径会拼接到 chunk 中,增强语义。
7. 表格区域特殊处理
PDF 表格如果直接抽文本,会变成混乱的行列。清洗时需要识别表格区域:
1 | 线框 |
小表格转成 Markdown 表格;大表格按“表头 + 行组”切分。表格解析失败时,保留原始页码和区域引用,避免错误表格污染正文。
8. 水印和噪声处理
水印通常有透明度、斜向、大字号、跨页重复等特征。文本抽取时如果把水印抽出来,会严重污染 chunk。处理时会根据位置、重复度、字体特征和文本模式过滤。
9. 清洗后的效果
清洗后希望得到的是结构化 block,而不是一整段纯文本:
1 | pageNo: 5 |
或者:
1 | pageNo: 6 |
这样后续才能按标题、段落、表格、代码块做合理切分。
10. 面试总结
面试可以这样回答:
文本型 PDF 不是抽到文字就直接入库,而是先抽取文本和坐标、字体、页码等版面信息,再清洗页眉页脚、页码、水印、目录、脚注,修复换行断句和多栏阅读顺序,同时识别标题、段落和表格区域。清洗后的目标是把 PDF 还原成带 pageNo、titlePath、blockType 的结构化 block,再用于 chunk 切分和引用追溯。





