项目常见问题总结

项目类

项目背景介绍一下

随着存储业务持续扩张,内部沉淀了海量私有技术资料:包含存储架构手册、接口文档、运维方案、故障案例、会议纪要、产品文档等,文档格式混杂 PDF、Word、Markdown等多种格式

原有知识管理存在以下痛点

文档分散、检索低效:资料散落在wiki、内部系统、组织群空间等多处,员工信息检索效率低

无法联动内部工具:查完文档还要手动登录 DTS、需求平台、调度系统查数据,信息割裂,重复操作多

新人上手成本极高:存储业务专业门槛高,新人遇到问题只能频繁请教老员工,资深工程师大量时间消耗在重复性答疑,挤占核心开发工作

平台上线后覆盖存储全业务线数百研发、运维人员,成为部门常态化知识查询、问题分析工具,有效释放资深工程师人力投入核心业务开发

你在项目中的角色/职责是什么

我主要负责企业知识助手 RAG 链路,包括文档解析、结构化切分、向量化入库、ES + Milvus 混合召回、RRF 融合、rerank 排序以及离线评测闭环。

多模态文档解析

表格怎么做切分

表格没有简单地按 Markdown 文本一刀切,而是按表格类型和表格规模做多粒度切分。

对于小表,比如几十行以内的接口参数表,我们会保留一个表级 chunk,里面包含文档名、章节、表名、表头和完整表格内容。这样用户问“这个接口有哪些请求参数”时,可以直接召回整张表。

对于大表,比如几百行的错误码表、配置项表,我们会拆成行级 chunk。但行级 chunk 不是只存一行数据,而是会把上下文补齐,比如:

1
文档:对象存储 OpenAPI 接口说明 章节:创建 Bucket 表格:请求参数 参数名:region 类型:string 是否必填:是 默认值:无 说明:Bucket 所在地域

同时 metadata 里会带上:

1
2
3
4
5
6
7
8
9
10
11
12
{
"chunk_type": "image",
"doc_id": "fault_doc_001",
"section_id": "disk_alarm",
"image_id": "img_006_01",
"page": 6,
"bbox": [100, 180, 820, 640],
"image_url": "minio://bucket/img_006_01.png",
"caption": "磁盘水位异常处理流程",
"parse_method": "ocr+summary",
"confidence_score": 0.91
}

这样用户问具体字段时,可以精准召回某一行;如果用户问“必填参数有哪些”或者“把请求参数表列出来”,我们会根据 table_id 回查同一张表的所有行,再按 row_index 聚合还原整表。

另外我们也会保留一份结构化 JSON 到 ES,比如 param_name、type、required、default_value、description。对于接口参数、错误码、配置项这种强结构表,优先走 BM25/ES 精确检索或字段过滤,再结合向量召回和 rerank。这样比单纯向量检索更稳定。

简历里说“Markdown 图片无法检索”,你具体怎么解决?

Markdown 图片无法检索的本质是:向量化时只看到了 markdown形式的图片链接,没有图片里的语义内容。

我们的解决方案是把图片当成一类独立但和上下文绑定的知识块处理:

  1. 解析 Markdown,提取图片链接、alt 文本、所在标题和前后段落。
  2. 下载或读取图片原文件,存储到 MinIO。
  3. 对图片做 OCR 或多模态模型理解,生成图片描述、关键文字、可能的业务语义。
  4. 把图片描述和上下文合并成可检索文本。
  5. 建立索引时保留 imageId、原文位置、文档路径,方便回答时返回图片来源。

例如一个 Markdown 里有系统架构图,用户问“订单服务调用库存服务链路是什么”,传统 RAG 召回不到;图片语义补充后,可以召回架构图对应 chunk,并在答案里引用图片来源。

图片怎么处理

我会先做图片分类,再决定处理策略。

  • 如果是代码截图、日志截图、配置截图,走 OCR,因为原文内容非常关键。
  • 如果是流程图、架构图、时序图,不能只 OCR,也不能只摘要,需要 OCR + 结构识别 + 摘要
  • 如果是普通产品截图或说明性图片,可以做图像摘要。
  • 如果图片清晰度低、文字密集、解析置信度低,会标记低置信度,必要时进入人工校验。

图片分类是怎么做的

第一层是规则和轻量特征

先做一次 OCR 预检测和图片基础特征分析,不一定完整 OCR,主要看:

  • 文本框数量、文本面积占比;
  • 是否有等宽字体、代码符号、错误码、IP、路径、堆栈关键字;
  • 是否存在大量矩形框、箭头、连线;
  • 图片所在章节标题、caption、前后文;
  • 图片尺寸、清晰度、颜色分布。

比如文字密度很高,并且包含 Exception、ERROR、timeout、/var/log 这类关键词,就倾向判断为日志或代码截图。

如果有明显节点、箭头、组件名,就倾向判断为流程图或架构图。

第二层是轻量分类模型或多模态大模型兜底

对于规则能高置信判断的图片,不调用大模型。

对于低置信度图片,才调用多模态大模型做意图识别。

第三层是输出统一路由结果

模型或规则最后输出统一结构:

1
2
3
4
5
6
7
{
"image_id": "img_006_01",
"image_type": "flowchart",
"route": "ocr+summary+structure",
"confidence": 0.87,
"reason": "包含多个文本节点和箭头关系,疑似故障处理流程图"
}

后续根据 route 决定处理方式:

  • ocr:日志、代码、配置截图;
  • summary:产品截图、普通说明图;
  • ocr+summary:告警截图、操作界面截图;
  • ocr+summary+structure:流程图、架构图、拓扑图。

低置信度人工处理

MinerU 私有化部署后,性能和稳定性上遇到过什么问题?怎么优化?

多模态解析最大的线上问题是解析慢、资源消耗高、失败重试成本大。

我们做了几类优化:

  • 异步化。文档上传后先写 MinIO 和 MySQL 元数据,再通过 RocketMQ 异步触发解析、切分、embedding、索引写入,避免用户请求阻塞。
  • 任务分级。普通文档走轻量解析,复杂 PDF 才走 MinerU;大文件拆分页面级任务,避免单任务过大。
  • 幂等和状态机。每个文档有解析状态,比如 uploaded、parsing、parsed、embedded、indexed、failed。每个阶段都可以重试,但要基于 documentId 和 version 做幂等。
  • 资源隔离。MinerU 这类重任务单独部署,和在线问答服务隔离,防止解析任务拖垮推理链路。
  • 失败兜底。如果 MinerU 失败,会降级到普通文本抽取或 OCR 结果,至少保证文档可检索一部分内容。

数据库

向量数据库的选型

我们生产环境用的是 Milvus,数据量级在百万条向量左右,每条是 1024 维,用 HNSW 索引,单次查询的延迟在 20 到 50 毫秒

选 Milvus 主要是因为它支持分布式部署和读写分离,适合数据量大、并发高的场景向量数据库的选型

说一下索引

我们采用的是 HNSW索引

  • HNSW索引:它构建的是一个多层图结构,查询时从最上层的稀疏图开始导航,逐层收窄范围,最终在底层找到最近邻
    • 优点是召回率高,查询速度快;缺点是建索引时内存消耗大,不适合内存极度受限的场景
  • IVF索引:对向量做聚类,把相似的向量分进同一个「桶」里,查询时只搜最相关的几个桶,而不是全量遍历
    • 优点是内存占用小、适合超大规模;缺点是精度比 HNSW 略低

说一下Milvus的核心概念

Collection(集合):类似关系数据库里的「表」,存储一类向量数据。我们的知识库就是一个 Collection,每条记录包含:文本 chunk 的 ID、向量(embedding)、原文内容、来源文档等 metadata。

Segment(段):是 Milvus 内部管理数据的基本单位。新写入的数据先进「增量段」,像一个临时的接收缓冲区;积累到一定量后触发合并,变成「封存段」。封存段会建好索引,查询时走索引检索速度很快。但合并这个动作本身会消耗 CPU 和磁盘,就像磁盘碎片整理,把一堆小文件合并成大文件,期间会抢资源。

数据库踩坑

大批量写入Milvus的时候,用户查询存在延迟

现象:知识库有增量更新,一次性写入几十万条新数据

原因:

Milvus 后台会触发 Segment 合并操作,把多个小的增量 Segment 合并成大的封存 Segment 并建好索引,这个过程很耗 CPU 和磁盘 IO,期间查询的 P99 延迟会有明显抖动,从正常的 60ms 涨到 300ms+

解决方案:

  1. 时间上错峰,把批量写入改到业务低峰期,避开查询高峰

  2. 把每批写入量控制在 500~1000 条以内,分成多批写,每批之间间隔几秒

内存不足,查询延迟飙升

现象:150w的chunk,1024维度,索引用 HNSW(M=16,ef_construction=128)的数据量,机器8G内存,查询延迟飙升

原因:

原始数据内存占用:150 万 × 1024 维 × 4 字节(float32)≈ 6GB,这是纯向量部分。实际跑起来需要大约10GB的内存,多出来的一般是HNSW 图结构本身、metadata等内存的占用

Milvus 把向量索引加载进内存之后,留给操作系统的空间已经很小了,稍微有点内存压力就开始频繁 swap(把内存数据换到磁盘),查询延迟直接从 20ms 飙到 2s+

解决方案:

开启 标量量化(SQ8)

把每个 float32 压缩成 int8(用 1 字节代替 4 字节)。直觉上理解:就像把精确到小数点后 7 位的数字保留到小数点后 2 位,大部分语义信息其实在高位,截掉低位精度损失极小,但数据量直接缩到 1/4。内存从 10GB 降到约 3GB,召回率基本无损(通常只下降 1 个百分点以内)

实测查询性能(单机 16 核 32G、本地千兆网、HNSW 在内存、ef=100):单次 top-5 查询 P50 延迟约 20ms,P99 约 60ms,并发 100 QPS 时延迟基本稳定。

RAG评估

为什么要做RAG评估

  1. RAG 链路比较长,涉及文档解析、切分、embedding、ES 关键词检索、Milvus 向量检索、RRF 融合、reranker 精排和大模型生成,如果只看最终答案,很难判断问题出在哪一层;
  2. 企业知识库内容持续变化,每次调整 chunk 策略、召回策略或 prompt,都需要一套可回归的评测集来判断优化是否真实有效。

所以我们的评测目标分三类:

离线用于策略选型,比如 BM25、向量检索、混合召回、rerank 的效果对比;

上线前用于回归验证,避免改动导致核心知识问答退化;

线上用于 bad case 回流,把用户低满意、追问、无答案的 case 补充进评测集。

评估数据集是怎么制作的

我们构建了 300+ 的 RAG 测试问题集,来源主要有三类:业务高频问题、历史文档中的典型知识点、以及基于文档内容生成的扩展问题。每条样本会尽量标注 query、标准答案、关联文档、证据 chunk、问题类型和是否需要多文档推理。

问题类型上,我们会覆盖事实查询、流程类问题、对比类问题、跨文档聚合问题、无答案问题,以及 PDF/Word/Markdown 中表格、图片说明相关的问题。因为DevMate 面向企业内部知识助手,真实用户不是只问标准 FAQ,所以评测集不能只覆盖简单事实问答,还要覆盖模糊表达、术语缩写、跨部门知识查询这些场景。

测试用例有哪些字段

  • query:用户问题
  • question_type:问题类型,例如事实型、流程型、多文档聚合、无答案问题
  • gold_answer:标准答案
  • gold_doc_id / gold_chunk_id:标准证据位置,用于判断检索是否命中
  • answerable:知识库是否应该能回答,用于评估拒答能力
  • expected_behavior:期望行为,例如正常回答、拒答、需要澄清
  • source:用例来源,例如人工构造、线上 bad case、LLM 合成
  • doc_version:文档版本,防止知识库更新后标准答案失效

以下是几个测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case_id: storage_rag_002
query: 分布式存储节点扩容前需要检查哪些条件?
question_type: 流程型
gold_answer: 需要检查当前集群健康状态、节点版本一致性、网络连通性、磁盘状态、容量水位、License/权限配置以及扩容窗口安排,确认满足扩容指导文档中的前置条件后再执行扩容。
gold_doc_id / gold_chunk_id: doc_distributed_storage_expansion_v2 / chunk_precheck_001
answerable: true
expected_behavior: 列出扩容前置检查项,不编造未在文档中的操作步骤
source: 人工构造
doc_version: v2.1
priority: 高

case_id: storage_rag_007
query: 我们下个月发布的新一代存储产品参数是多少?
question_type: 无答案问题
gold_answer: 当前知识库中未检索到该新一代存储产品的公开参数信息,无法基于现有文档回答。
gold_doc_id / gold_chunk_id: none
answerable: false
expected_behavior: 拒答,不编造未发布产品参数
source: 安全测试
doc_version: current
priority: 高

说明一下模型评估指标

离线评估指标:

  • Recall@10:正确答案相关文档有没有出现在前 10 个检索结果里
    • Recall@10 = 前 10 个结果中命中的相关文档数 / 所有应命中的相关文档数
    • 假设有3个相关chunk,召回了2个在Top10,那么 Recall@10 = 2 / 3
  • MRR:平均倒数排名,衡量的是第一个正确结果排得有多靠前
  • 忠实度:模型生成的答案是否忠实于检索到的上下文,有没有编造上下文中不存在的信息,关注的是“答案有没有幻觉”
  • 答案相关性:模型回答是否真正回答了用户的问题,关注的是“有没有答到点上”

线上评估指标:

  • P95/P99 延迟:
    • P95:95% 的问答请求耗时低于该值
    • P99 代表极端长尾请求耗时,平均延迟会掩盖少量慢请求,长尾延迟才代表真实最差用户体验
  • 无答案率:用户提问后 Agent 返回 “未检索到相关资料,无法解答” 的请求占总请求比例
  • 点赞点踩率:单次问答后用户手动点赞、点踩数量占有效问答的比例单次问答后用户手动点赞、点踩数量占有效问答的比例

好的RAG回答至少要满足什么标准

  1. 忠实于证据。答案里的事实必须能从 retrieved chunks 中找到依据,不能凭模型先验乱编。
  2. 回答相关且完整。它要正面解决用户问题,而不是只泛泛复述文档;如果问题有多个子问题,也要覆盖完整。
  3. 证据有效。召回给模型的文档必须是真正能回答问题的文档,且版本、时间、适用场景不能错。
  4. 可追溯。答案应该能引用来源,方便用户追溯。

召回率高但回答不准,怎么排查?

  1. 看召回结果是否“看似相关但业务无关”,比如语义相似但部门、版本、权限标签不匹配,所以我们引入了元数据 pre-filter。
  2. 看 RRF 融合后排序是否合理,BM25 和向量召回的结果权重是否需要调整。
  3. 看 bge-reranker-v2-m3 是否把真正证据排到前面,如果 rerank 后相关 chunk 仍然靠后,说明精排模型或输入粒度有问题。
  4. 看上下文拼接是否截断了关键证据,尤其是长 PDF、表格和多章节文档。
  5. 看生成模型是否忠实基于证据回答,有没有过度补全。

为什么比较关注Recall@K?而不关注Precision@K?

RAG 检索阶段通常更重视召回率,因为只要正确材料没有被召回,后面的重排和生成都无法补救

召回结果里混入一些噪声,还可以通过 rerank、阈值过滤和 TopK 截断来处理。

换句话说,召回率解决“有没有证据”的问题,准确率解决“证据池干不干净”的问题。

在多阶段 RAG 架构里,第一阶段通常追求高召回,第二阶段再用 reranker 提高准确性。

所以不是不关注准确率,而是召回率在前置检索阶段更关键。

模型类

大模型用的哪个

qwen3-vl-plus : 图片描述生成

bge-m3: 文档切片向量化、查询向量化,1024维

bge-reranker-v2-m3 : 检索结果重排

qwen-max-latest : 查询改写

qwen3.7-max : 拿到检索增强上下文后,流式生成最终回答

为什么会选择bge-m3作为embedding模型?对比过哪些模型?

我们基于自己的业务数据构造测试集,比较不同模型的Hit@5

综合看下来bge-m3模型在命中率、排序表现、延迟、成本都比较均衡

对比过的模型如下:

Qwen3-Embedding(理由如上)、text-embedding-3-small(英文场景模型)

为什么需要 BGE Reranker?如果已经有向量召回和 BM25 + RRF,rerank 解决什么问题?

向量召回和 BM25 主要解决候选召回问题,也就是从大规模知识库里快速找出可能相关的文档。它们更偏初筛:向量召回看语义相似度,BM25 看词项匹配。

但初召回容易有误排。比如向量召回可能召回语义相近但不能回答问题的 chunk;BM25 可能因为关键词匹配召回上下文不相关的 chunk。RRF 只是融合多路排序,并没有真正理解 query 和 chunk 是否能形成问答关系。

BGE Reranker 是 cross-encoder,会把 query 和候选 chunk 拼接输入模型,让模型对两者做深层交互判断。它能更准确地区分“看起来相关”和“真正能回答问题”。

BGE Reranker 和 embedding 双塔召回相比,计算复杂度、延迟、吞吐有什么差异?

embedding 双塔召回是 query 和 document 分别编码,文档向量可以提前离线计算,在线只需要计算 query 向量,再做 ANN 检索,所以延迟低、吞吐高,适合大规模召回。

BGE Reranker 这类 cross-encoder 需要把 query 和每个候选 chunk 拼接后在线推理,无法像向量召回那样提前计算文档表示,所以计算复杂度和延迟都更高,吞吐更低。

rerank 延迟影响 P95,但业务不能接受准确率明显下降,用什么方案优化?

先拆解链路耗时,确认 P95 主要耗在哪:是候选数量太多、模型推理慢、服务排队,还是上下游串行等待。然后分层优化。

模型侧,可以换轻量 reranker,或者用大 reranker 做 teacher、小 reranker 做 student 进行蒸馏;也可以做 FP16、INT8 量化,或者 ONNX/TensorRT 加速。

候选集侧,可以做动态 TopK。比如 RRF 头部结果分数差距明显时,只 rerank Top10;问题复杂或召回分散时再扩大到 Top30。还可以去重、合并同文档相邻 chunk,减少重复候选。

工程侧,可以把 reranker 独立服务化,做 GPU/CPU 资源隔离;对多个请求做 batch inference;并行执行 query rewrite、向量召回和 BM25;设置超时阈值,比如超过 300ms 就用已有 RRF 结果兜底。

缓存侧,可以缓存高频 query 的 rerank 结果,也可以缓存 query-chunk pair 的相关性分数。

体验侧,可以先流式返回检索状态,降低用户等待感。核心原则是优先优化主链路,降级只是兜底,不是常态方案。

实践类

说一个开发过程中的bad case

一个典型 badcase 是用户查询“SSDflash0012 故障处理建议”。早期我们主要依赖向量召回,向量库只根据 “故障、SSD” 语义匹配,结果系统召回了闪存故障的通用文档,但漏掉了 SSDflash0012 这个具体设备型号对应的运维手册。

我们定位后发现,问题不在生成阶段,而在召回阶段。因为设备编号、错误码、接口字段这类专有实体,在 embedding 空间里不一定能保持精确匹配,向量召回更容易召回语义相近的通用内容。

修复方案是增加 BM25 关键词召回,并在 ES 中对设备 ID、错误码、接口字段、命令参数建立倒排索引。融合时采用 RRF,把向量召回和 BM25 召回结果按排名融合,避免两路分数尺度不一致的问题。

修复后,专有编号类问题的 Recall@10 从 48% 提升到 90% 以上。同时我们把这类设备编号问题整理进专项评估集,后续每次修改召回策略都会做回归测试。

Query Rewrite是怎么做的

Query rewrite 的目标是提升召回质量。用户问题里经常有口语化表达、省略、代词、多意图问题。比如“这个报错怎么处理”需要结合上下文补全具体报错;“SSD 初始化失败和性能下降分别怎么排查”可以拆成两个子问题分别召回。

我们主要用大模型做改写,规则做辅助。改写包括消解指代、补全上下文、标准化术语、拆分多意图问题。但改写有风险,可能把用户原意改偏,所以原始 query 一定会保留,并和改写 query 一起参与召回。

防偏方面,我们会做实体保护,对设备编号、错误码、接口名、命令参数、版本号等关键实体要求改写前后必须保留;还会做语义相似度校验和实体覆盖率校验。如果改写 query 和原 query 相似度过低,或关键实体丢失,就拒绝改写。

多问题拆分后,会对子问题召回结果合并去重,再进入统一 rerank,避免每个子问题各自生成导致答案割裂。

chunk size和overlap是怎么指定的?

在RAG系统中,chunk_sizeoverlap的设置没有绝对的“黄金标准”,但是我认为核心思想应该是:从通用推荐值出发,根据文档类型和语言特性进行调整,并通过实验验证找到最适合你特定场景的参数

一般来说,如果是纯中文文档,可以设置500-1000左右的chunkSize,英文文档的话,可以设置2000-3000左右的chunkSize

overlap的话,可以根据chunksize来取一个10%-20%的比例

Word是怎么解析的

Word 也是通过 MinerU 进行处理的

早期我们采用 2.7.6 版本的 MinerU 的时候,不支持。

后续切换成 3.0.8 的版本

为什么需要混合检索?

传统的单一检索方式存在以下局限:

稠密检索/向量检索(如基于向量的语义相似度搜索):

  • 能捕捉语义信息,但可能在精确关键词匹配上表现不佳。
  • 对训练数据、包括文档的质量、切片和嵌入质量高度依赖,容易受嵌入偏差影响。

稀疏检索/关键字搜索(如 BM25):

  • 依赖文档切分出来的关键词进行匹配。
  • 无法理解语义相似但用词不同的查询与文档(例如“汽车” vs “轿车”)。