当前位置: 首页 > news >正文

RAG 工程化实践:如何避免半成品文档进入在线召回

从“能检索”到“可生产”:一次 RAG 链路工程化优化实践

很多 RAG 项目刚开始做时,流程都差不多:

文档解析 ↓ 文本切分 ↓ 生成向量 ↓ 写入向量库 ↓ 用户提问时召回相关内容 ↓ 拼进 Prompt 交给大模型

从功能上看,只要问题能召回内容、模型能回答,RAG 就算跑通了。

但真正接到生产业务里以后,会发现“能跑”和“稳定可用”之间还有很长一段距离。

我参与的这条 RAG 链路主要服务于风险信息监控场景,知识源里既有 PDF,也有信用信息查询结果、处罚数据、企业风险汇总等内容。原来的实现已经区分了离线建索引和在线检索,但这种区分更多停留在代码逻辑上,工程上并没有完全拆开。

最典型的问题是:在线查询仍然可能触发文档扫描、切分、向量生成和索引刷新。一旦文档解析较慢,或者 embedding 服务发生异常,用户的一次普通查询就可能被拖住。

更麻烦的是,原来的链路没有可靠的文档状态控制。一个 PDF 如果只处理了一半,部分 chunk 已经写入 Milvus,在线检索仍然可能提前召回这些不完整数据。

所以这次优化的重点,不是简单调一下topK,也不是换一个 embedding 模型,而是先把整条 RAG 链路的工程边界重新梳理清楚。


一、原来的 RAG 链路是怎么运行的

原来的流程可以分成两部分。

离线阶段负责:

扫描知识源目录 读取 PDF 提取文本 切分 chunk 生成 embedding 写入 Milvus

在线阶段负责:

接收用户问题 生成 query embedding 从 Milvus 召回 topK 把命中的 chunk 拼进 Prompt 交给上层 Agent 使用

乍看之下,在线和离线已经分开了。

但实际查询时,系统会先调用一次类似ensure_ready()的逻辑。如果发现知识源目录发生变化,或者索引状态不完整,就会在当前请求中触发刷新。

这意味着一次在线查询,背后可能顺带执行:

目录扫描 PDF 解析 文本切分 embedding 调用 向量写入

一旦文档稍微多一些,查询耗时就会变得不可控。

所以原来的问题不是“没有离线阶段”,而是离线阶段没有真正从请求链路中拿出去。


二、最危险的问题:半成品文档也可能被召回

比查询慢更严重的是数据一致性问题。

假设一个 PDF 被切成 100 个 chunk,系统处理到第 40 个 chunk 时发生异常:

前 40 个 chunk 已经写入 Milvus 后 60 个 chunk 没有完成

此时从向量库的角度看,这个文档已经“存在”了。

如果在线查询正好命中了前 40 个 chunk,系统就会把这些内容当成正常知识使用。

这会带来几种情况:

召回内容不完整 上下文前后断裂 关键事实缺失 模型根据局部内容做出错误判断

风险监控场景对完整性要求比较高。

例如一条处罚信息,前半部分可能只有处罚对象和时间,真正的处罚原因、处罚金额、处理机关在后半部分。如果只召回了半成品 chunk,最终回答就可能产生明显偏差。

因此,这次优化里最重要的一个设计,就是引入文档状态机。


三、引入文档状态机,让半成品数据不可见

我把文档生命周期拆成了四个状态:

PROCESSING READY FAILED DELETED

它们分别表示:

PROCESSING:文档正在解析、切分或写入向量库 READY:文档已经完整处理,可以参与在线检索 FAILED:文档处理失败,不能参与召回 DELETED:源文件已经删除,历史向量不再使用

完整处理流程变成:

发现新文档 ↓ 状态更新为 PROCESSING ↓ 提取文本 ↓ 切分 chunk ↓ 生成 embedding ↓ 写入 Milvus ↓ 所有步骤成功 ↓ 状态更新为 READY

如果中间任何一步失败:

文本提取失败 chunk 切分失败 embedding 调用失败 Milvus 写入失败

文档都会被标记为FAILED

在线查询时增加一层门控:

只有 READY 状态的文档才允许参与召回

这样即使 Milvus 中已经写入了一部分向量,只要 PostgreSQL 里的文档状态还不是READY,这些半成品 chunk 就不会进入最终结果。

这个设计没有使用 PostgreSQL 和 Milvus 的分布式事务,而是采用:

最终一致性 + READY 状态门控

实现成本低,但能有效控制脏数据。


四、为什么状态放在 PostgreSQL,而不是 Milvus

Milvus 很适合保存向量和检索字段,但文档状态本质上属于事务型元数据。

除了状态本身,后面通常还需要记录:

文件名称 文件路径 文件摘要 处理时间 失败原因 重试次数 文件签名 更新时间

这类信息放在 PostgreSQL 更合适。

所以这次做了明确分工:

PostgreSQL:管理文档生命周期和处理状态 Milvus:负责 chunk 向量存储和相似度召回

这种拆分还有一个好处:以后需要做失败重试、任务审计、处理历史查询时,不需要从向量库里反推文档状态。


五、把索引构建真正移出在线请求

状态机解决了半成品数据问题,但如果索引构建还在用户请求中执行,查询延迟仍然不可控。

所以第二个重点改造是:

在线请求只负责发现变化和调度刷新,不负责同步建索引。

优化后的在线请求只做两件事:

判断知识源是否发生变化 如果发生变化,提交一个后台 ingestion 任务

真正的解析、切分、embedding 和向量写入都放到后台执行。

新的离线链路是:

扫描知识源 ↓ 识别新增、修改和删除文件 ↓ 提交后台 ingestion 任务 ↓ 更新文档状态 ↓ 文本提取 ↓ chunk 切分 ↓ 去重 ↓ 生成 embedding ↓ 批量写入 Milvus ↓ 状态切换为 READY

这样目录发生变化时,当前查询不会等待新文档完整建完索引。

它仍然可以使用上一版已经READY的数据完成检索,新文档则在后台完成更新。


六、为什么先用单线程后台执行器

索引构建移到后台后,可以选择的方案很多:

线程池 消息队列 定时任务平台 独立 ingestion 服务

当前知识源规模并不大,所以没有一开始就引入 MQ 或单独部署任务服务,而是先使用单线程后台执行器。

这样做主要是为了避免几个问题:

同一个目录被多个任务重复扫描 同一个文件被并发处理 文档状态被不同线程反复覆盖 Milvus 重复写入

单线程方案的优点是实现简单、状态清晰,也比较容易排查。

这不是最终形态,但符合当前规模。

如果后面文档量增加,可以再演进成:

任务表 ↓ 消息队列 ↓ 多个 ingestion worker ↓ 按文档 ID 做并发隔离

工程优化不一定要一步做到最复杂,先解决当前最真实的问题更重要。


七、知识源不能只支持 PDF

原来的 ingestion 主要围绕 PDF 设计,但风险监控业务的数据来源并不只有 PDF。

实际使用中常见的还有:

信用查询结果 JSON 处罚数据 JSON 企业风险 CSV 汇总说明 Markdown 普通 TXT 文档

如果系统只支持 PDF,会带来两个问题。

第一,很多真实业务数据必须先人工转换成 PDF,增加了使用成本。

第二,评测样本很难直接复用。很多风险查询结果本身就是结构化 JSON,如果必须转换后才能入库,测试和验证都会变得很麻烦。

所以这次把 ingestion 扩展成了统一入口,支持:

PDF MD TXT JSON CSV

不同格式分别解析,最后统一转成标准文本块,再进入后续的切分、向量化和入库流程。

这样做以后,信用中国、企查查、处罚汇总等真实样本可以直接进入知识库,不需要额外转换。


八、原来只有向量召回,效果还不够稳定

工程链路稳定以后,下一步才是检索质量。

原来的检索主要是:

query embedding ↓ Milvus 相似度搜索 ↓ 取 topK

这种方式能用,但有几个明显问题。

1. 缺少阈值控制

即使召回结果的相关度都比较低,只要设置了topK=5,系统仍然会返回 5 条结果。

这会把无关内容拼进 Prompt,反而干扰模型判断。

2. overlap 会造成重复内容

文本切分时通常会设置 overlap,相邻 chunk 之间会有一部分重复。

如果多个相邻 chunk 同时进入 topK,最终 Prompt 里会出现大量重复信息,浪费上下文窗口。

3. 单纯向量相似度不一定符合业务需求

向量召回擅长语义相似,但风险业务里还经常依赖:

企业名称 统一社会信用代码 处罚机关 法规名称 具体时间

这些关键词有时更适合词法匹配,而不是完全依赖向量距离。

因此,优化后的检索链路改成了多阶段处理。


九、优化后的检索流程

新的在线检索流程是:

用户 Query ↓ 生成 query embedding ↓ 从 Milvus 召回较大的候选集 ↓ 过滤非 READY 文档 ↓ 应用 metadata filter ↓ chunk 去重 ↓ 词法初排 ↓ 模型 rerank ↓ score threshold 截断 ↓ 返回最终 topK

这里不再直接把 Milvus 返回的前几条内容塞进 Prompt,而是先召回更多候选,再逐步收口。


十、metadata filter 解决什么问题

风险信息查询经常带有明确条件,例如:

只查某一个企业 只查某一种风险类型 只查指定来源 只查某个时间范围

如果完全依赖向量相似度,可能会召回语义接近、但来源错误的内容。

所以在候选召回后增加 metadata filter,例如:

source documentId fileName type publishDate authority

举个例子,用户明确问“某企业在信用中国的处罚记录”,系统就可以先限制来源为信用中国,再进行排序,而不是让企查查、内部报告和其他来源一起竞争。


十一、chunk 去重不能只按 ID

由于 overlap 的存在,相邻 chunk 可能内容高度相似。

如果只按 chunk ID 去重,它们仍然会被认为是不同结果。

因此可以综合使用:

文档 ID chunk 序号 文本摘要 内容哈希 相似度

来抑制重复内容。

比较简单的做法是先按文档聚合,再限制同一文档进入最终结果的 chunk 数量。

也可以对候选文本做归一化后计算内容哈希,过滤完全重复或高度重复的片段。

目标不是完全消除相邻内容,而是避免最终 Prompt 被同一段话重复占满。


十二、为什么增加词法初排

向量召回负责语义相似,词法初排负责保留关键字匹配优势。

例如用户问题里出现了统一社会信用代码,这种内容具有非常强的精确匹配特征。

如果某个 chunk 精确包含这个代码,即使向量分数不是最高,也应该提高排序。

所以可以给候选结果增加一部分词法得分,例如关注:

企业名称命中 信用代码命中 处罚机关命中 法规关键词命中 问题关键词覆盖率

最终先基于:

向量分 + 词法分

完成一次初排,再把较小的候选集交给模型重排。

这样既控制了模型调用成本,也提高了精确字段的权重。


十三、为什么暂时使用 LLM 做 rerank

重排通常有几种方案:

规则重排 专用 reranker 模型 大模型重排

当前项目已经有稳定的大模型客户端,因此为了快速验证效果,先选择了 LLM JSON 重排。

做法是把用户问题和候选 chunk 一起交给模型,让模型为每个候选结果给出相关度评分,再根据评分重新排序。

这种方案的优点是:

接入速度快 对复杂语义判断效果较好 不需要额外部署模型

缺点也比较明显:

调用成本更高 延迟高于本地 reranker JSON 输出可能不稳定

因此它更适合当前阶段做效果验证,不一定是最终方案。

后面数据量和调用量上来以后,可以替换成专用 reranker 模型,把大模型重排作为兜底或者离线评测工具。


十四、重排失败时不能拖垮整个查询

RAG 的目标是提升回答质量,但不能因为重排服务失败,导致整个检索不可用。

所以这里做了降级:

LLM rerank 成功 ↓ 使用模型重排结果 LLM rerank 失败 ↓ 退回向量分 + 词法分排序

重排只是增强能力,不应该成为单点依赖。

同样的思路也适用于其他非核心环节:

统计失败不影响查询 监控写入失败不影响召回 后台刷新失败不影响已有 READY 数据

先保证主链路可用,再尽可能提升质量。


十五、score threshold 比固定 topK 更重要

固定topK容易产生一个问题:无论有没有相关内容,都必须返回固定数量的结果。

更合理的方式是:

先按相关度排序 再过滤低于 threshold 的结果 最后从剩余结果里取 topK

这样可能出现:

返回 5 条 返回 2 条 甚至一条也不返回

一条都不返回并不一定是坏事。

与其把明显不相关的内容塞给大模型,不如明确告诉上层:

当前知识库没有找到足够相关的信息

这对降低幻觉反而更有帮助。


十六、没有评测,优化就只能靠感觉

RAG 调优很容易陷入一种状态:

改了 chunk size,感觉好了一点 加了 rerank,感觉更准了 调了 topK,好像回答更完整了

但如果没有固定评测集,这些结论都不够可靠。

所以这次补了一套风险场景基线评测集,围绕真实业务问题构造:

用户问题 期望命中的文档 期望命中的来源 期望答案关键词

并统计:

full_hit_rate answer_hit_rate source_hit_rate recall_at_1 recall_at_3 mrr_at_3

其中:

  • Recall@1表示正确结果是否排在第一位

  • Recall@3表示正确结果是否出现在前三位

  • MRR@3会同时考虑正确结果有没有命中,以及命中位置是否靠前

有了这些指标以后,就可以对比:

纯向量召回 向量 + 词法 向量 + 词法 + rerank 不同 threshold 不同 chunk size

RAG 优化才从“凭感觉”变成“有数据验证”。


十七、可观测性也要跟上

除了离线评测,运行期也需要有基本观测数据。

目前主要记录:

ingestion 当前状态 ingestion 失败原因 query 总数 query 命中数 hit rate rerank 调用次数 rerank 成功次数

ingestion 状态可以包括:

queued running idle failed

这样排查问题时,可以快速判断:

是文档还没处理完 是 ingestion 失败 是查询没有召回 还是 rerank 失败后发生了降级

如果没有这些信息,RAG 很容易变成黑盒。

用户只知道“这次没回答出来”,但开发无法判断问题发生在哪个环节。


十八、优化后的完整链路

最终整条链路可以概括为:

知识源发生变化 ↓ 后台调度 ingestion ↓ 文档状态置为 PROCESSING ↓ 解析 PDF / JSON / CSV / MD / TXT ↓ 切分和去重 ↓ 生成 embedding ↓ 写入 Milvus ↓ 全部成功后状态置为 READY

在线查询则是:

用户提问 ↓ 生成 query embedding ↓ 召回候选 chunk ↓ 过滤非 READY 文档 ↓ metadata filter ↓ chunk 去重 ↓ 词法初排 ↓ LLM rerank ↓ 失败则降级为规则排序 ↓ score threshold 截断 ↓ 返回最终 topK ↓ 注入 Prompt

这时的 RAG 已经不再只是“向量库查询”,而是一条完整的检索工程链路。


十九、这次优化真正解决了什么

回头看,这次改造并不是单纯为了让召回分数更高,而是解决了几个更基础的问题。

1. 在线和离线真正解耦

文档解析和索引构建不再阻塞用户查询,在线请求耗时更加稳定。

2. 半成品数据不可见

通过PROCESSING / READY / FAILED / DELETED状态控制,只有完整处理完成的文档才会参与检索。

3. 检索结果更可控

从单纯向量 topK,升级为:

候选召回 metadata 过滤 去重 词法初排 模型重排 阈值截断

4. 更贴近真实风险场景

知识源从只支持 PDF,扩展到 JSON、CSV、Markdown 和 TXT,可以直接使用更多真实业务数据。

5. 优化效果可以量化

通过 Recall、MRR、命中率和运行期统计,后续调整不再依赖主观感受。


二十、总结

这次 RAG 优化给我最大的体会是,RAG 的问题不一定首先出在模型或向量库。

很多时候,真正影响生产效果的是:

在线和离线没有解耦 文档状态不可控 半成品数据提前可见 异常没有降级 召回结果没有过滤 优化效果无法评测

如果这些基础问题没有解决,即使换了更好的 embedding 模型,或者把topK调得更大,整体效果也不一定稳定。

所以这次优化的顺序是:

先把工程边界立住 再保证数据状态可控 然后提升检索质量 最后建立评测闭环

当 RAG 具备了后台 ingestion、状态门控、检索重排、异常降级和评测指标之后,它才真正从一个“能演示的功能”,变成了一条可以继续演进的生产链路。

http://www.jsqmd.com/news/1125715/

相关文章:

  • 用运筹学与强化学习构建个人发展量化分析模型
  • 杭州萧然医院环境怎么样
  • yolov26改进 | 融合改进篇 | 利用尺度统一检测头DynamicHead融合P2增加小目标检测层(让小目标无所遁形)
  • Boss-Key终极指南:3秒实现Windows窗口隐身术,保护你的数字隐私空间
  • 基于13DOF传感器的高精度定位导航系统设计与实现
  • 图像和视频处理的核心概念(在图像上画直线)
  • C++协程用法总结
  • 如何在5分钟内免费下载网络视频:VideoDownloadHelper终极指南
  • AI工具推荐 第一期:WorkBuddy对标codex,适合职场人的AI工具
  • 2026年6月最新安徽大健康行业GEO优化机构盘点:服务趋势观察
  • 【Qwt 7.0 系列】多坐标轴与多绘图布局 —— 寄生绘图与 QwtFigure 容器
  • 入门级降噪耳机怎么选:从通勤、会议和续航看 5 款值得关注的产品
  • 嵌入式八股文 第一期
  • Perplexity vs 秘塔AI vs Google SGE:三大AI搜索引擎横评
  • 四类芯片对比(一)
  • UNY Finance生态航母再扩容,UNY Bet(UNY预测)即将上线!
  • 通产美伦MB8010能量平台运维质控实操方案分享
  • 【极简监控·番外篇】被逼无奈的“降维打击”:Java Remote Debug 救火指南
  • MongoDB 大数据备份,新手教程
  • Git脏树(Dirty Tree)介绍(指工作目录中存在未提交修改的状态)已修改、未跟踪、git status、线上线下不一致问题
  • Gateway API:Ingress 的下一代替代方案
  • UE4 SceneCaptureComponent2D 实战:3步实现UI内3D模型360°预览(附蓝图)
  • 教育学论文降AI工具免费推荐:2026年教育学毕业论文AIGC超标4.8元亲测99.26%知网完整方案
  • CodaYun 一站式浏览器工作台:开发者 设计师专属效率解决方案
  • C++中的String的常用函数用法
  • 【算法从零到千】【32-41】位运算(详细讲解+题目运用)
  • Allegro 生产文件导出:Gerber 274X 与钻孔文件 5 步标准化检查清单
  • 羽球联盟 HarmonyOS NEXT 实战系列 (03/20):四Tab首页容器与资讯首屏搭建
  • Agentic AI:换个角度,从问题拆解到交付验证
  • 史上最简单!sirpdboy固件一键搞定软路由刷机、调试、扩容,彻底告别麻烦!