RAG流水线全流程实现
RAG流水线全流程实现
面试官您好,我结合之前落地企业级内部知识库的项目经验,从工程实现角度拆解 RAG 完整流水线。
RAG 核心是检索外部知识库 + 大模型增强生成,解决大模型幻觉、知识时效性差、私有领域知识缺失的问题,全流程分为「离线数据处理」和「在线检索生成」两大阶段,具体实现如下:
离线数据处理流水线(数据生产侧)🔧
这是 RAG 效果的底座,数据质量直接决定最终回答上限。
1.多源数据采集与清洗
- 核心动作:接入 PDF/Word/Markdown、业务数据库、内部 Wiki 等多源数据,做格式归一与脏数据过滤
- Java 技术栈:用 Apache Tika 解析多格式文档,Jsoup 处理网页数据,POI 处理 Office 文件
- 关键点:去重、去除页眉页脚 / 水印 / 乱码,避免无效数据进入向量库
2.文档智能分块(Chunking)
- 核心逻辑:把长文档切分为适配模型上下文的小块,平衡检索粒度与上下文完整性
- 常用策略:固定长度分块(512-1024token)、按标题层级分块、递归字符分块;块间保留 20% 左右重叠,避免语义断裂
- Java 实现:项目中用 LangChain4j 的
RecursiveCharacterTextSplitter,按段落→句子层级递归拆分
3.向量嵌入(Embedding)
- 核心动作:将文本块映射为高维语义向量
- 选型建议:通用场景用 BGE/M3E 开源模型,闭源用通义 / OpenAI Embedding 接口
- 踩坑提醒:入库和检索必须使用完全相同的 Embedding 模型,否则向量空间不一致,召回率会暴跌;我之前踩过这个坑,统一模型后召回率从 28% 提升到 82%
4.向量存储与索引构建
- 核心动作:向量 + 原文元数据存入向量库,构建检索索引
- Java 生态选型:轻量场景用 Milvus(配套官方 Java SDK),存量 ES 环境直接用 ES 8.x 原生向量能力
- 元数据设计:存储文档来源、分块序号、业务标签,支持后续按权限、业务域过滤检索
在线检索生成流水线(服务调用侧)🚀
用户请求时的实时链路,核心是快、准。
1.Query 预处理与改写
- 核心动作:用户输入纠错、短 query 扩写、意图识别与路由
- 优化价值:比如把 “工资怎么算” 扩写为 “薪资核算规则与个税计算方式”,大幅提升模糊问题的召回准确率
- 实现方式:用轻量小模型做意图分类,或者基于 Prompt 让大模型改写
2.混合检索召回
- 基础版:将 Query 向量化后,用余弦相似度召回 Top10 候选块
- 进阶版:向量检索 + BM25 关键词检索混合召回,解决专有名词、数字类内容向量召回不准的问题;项目中用 ES 实现混合检索,专有名词召回率提升 27%
- Java 实现:Milvus
search接口、ES Knn 查询 + Match 查询结果融合
3.Rerank 重排序精筛
- 核心动作:对初召的 10-20 条结果做精细化语义排序,筛选出 Top3-5 最相关片段
- 选型:BGE-Reranker、ColBERT 等重排模型
- 划重点:这是 RAG 优化里性价比最高的一步,成本很低,准确率能提升 20%-30%,强烈建议不要省
4.Prompt 上下文组装
- 核心动作:按相似度从高到低,将召回的知识片段、系统指令、用户问题拼接为最终 Prompt
- 指令设计:明确约束 “仅基于下方参考资料回答,无法回答时直接说明,不要编造内容”,从源头降低幻觉
- Java 实现:用 LangChain4j 的
PromptTemplate,或者 Thymeleaf 模板引擎做变量替换
5.大模型生成与流式返回
- 核心动作:将组装好的 Prompt 传入大模型,生成最终回答
- Java 技术栈:Spring AI、对应厂商官方 Java SDK,通过 SSE 实现流式响应,提升用户体验
- 工程兜底:配置超时熔断,大模型不可用时直接返回检索到的原文片段,保证服务可用
后置处理与效果优化✨
1.结果溯源与校验
- 给答案标注引用来源,支持用户跳转原文核对;同时可做事实一致性校验,降低幻觉风险
2.效果迭代闭环
- 核心指标:召回率、答案准确率、幻觉率、用户满意度
- 运营机制:收集 Bad Case 回流,反向优化分块策略、Embedding 模型、Prompt 模板
RAG 核心优化项收益对比表 📊
| 优化环节 | 优化手段 | 效果提升 | 实现成本 |
|---|---|---|---|
| 召回层 | 新增 Rerank 重排序 | 准确率提升 20%-30% | 低 |
| 召回层 | 向量 + 关键词混合检索 | 专有名词召回率提升 20%+ | 低 |
| 数据层 | 语义分块替代固定分块 | 上下文连贯性显著提升 | 中 |
| 生成层 | Prompt 强约束 + 引用溯源 | 幻觉率降低 40% 以上 | 低 |
| Query 层 | Query 语义改写扩写 | 模糊问题召回率大幅提升 | 中 |
真实面试模拟
真实面试模拟
🧑💼 面试官
“我看到你简历里写了 AI 应用落地这块,那正好聊聊。现在业务有个典型需求:公司内部积累了几万篇技术文档,要做一个智能问答助手,而且不能瞎编,必须基于文档回答。你打算怎么设计这个系统?帮我捋一下完整的 RAG 流水线吧,最好能结合你熟悉的 Java 技术栈来说。”
👨💻 候选人
“好的面试官,我的思路是把整个 RAG 流水线拆成离线索引和在线查询两个阶段。先让知识入库,再让模型现查现答。我画个简图方便理解:
整体上,离线阶段把文档变成可以检索的向量,在线阶段把用户问题转成向量去库里找最相关的上下文,喂给大模型生成答案。下面我具体说下每个环节怎么用 Java 生态落地。”
🧑💼 面试官
“清楚。那先聊离线阶段吧,从拿到一份 PDF 文档开始,你怎么把它变成一条条可检索的知识片段?”
👨💻 候选人
“第一步是文档解析。Java 里最常用的就是 Apache Tika,它能自动识别 PDF、Word、Markdown 这些格式,直接抽取出纯文本,还能保留部分标题、表格等元数据。这块很重要,因为后面分块策略会用到这些结构信息。”
🧑💼 面试官
“嗯,Tika 我知道。那第二步你提到的智能分块,不能简单按字数切吧,具体怎么做?”
👨💻 候选人
“对,硬切很容易把一句完整的话劈开。我们一般用递归字符分割:设定优先级分隔符,比如先按段落 \n\n,再按句号、分号,保证 chunk 语义完整。参数上通常取 chunk_size=512 tokens,overlap=50,让相邻块有一点重叠,避免边界信息丢失。
Java 里可以直接用 LangChain4j 的 DocumentSplitter,它内置了这种策略。如果遇到表格,我会先用专门的解析器把它转成结构化 JSON,这样检索时更精准,而不是拆散成碎片。”
🧑💼 面试官
“提到 overlap,我倒想追问一个细节:你觉得 overlap 设置过大或过小,在实际效果上会有什么影响?”
👨💻 候选人
“这是个很好的问题。overlap 太小,比如 10,会导致 chunk 边界附近的上下文被割裂。如果用户问的答案刚好跨在两个块之间,可能一个块都检索不到完整信息。overlap 太大的话,比如 200,又会让相邻块重复内容太多,既浪费向量存储空间,检索时也容易把大量同质化内容召回,反而稀释了有效信息,还会增加 prompt 长度。所以一般在 10%~20% 的 chunk 长度里做折中,像 512 的块,重叠 50~100 是比较常见的经验值。”
🧑💼 面试官
“不错,经验值说得很实在。那分完块之后,向量化这块你用什么方案?Java 里怎么调用?”
👨💻 候选人
“我们团队一般走两条路:如果要求数据安全、低延迟,就在本地部署 BGE-large-zh 或 text2vec-large-chinese 模型,通过 ONNX Runtime 做推理,Java 有对应的 ONNX Runtime 绑定,速度很快。如果项目允许调云服务,就用 OpenAI 的 text-embedding-ada-002 或者阿里云的 DashScope,直接发 HTTP 请求就好。
代码层面,用 LangChain4j 的 EmbeddingModel 接口统一抽象,切换模型不影响业务逻辑。比如:
EmbeddingModel embeddingModel = new BgeEmbeddingModel();
List<TextSegment> segments = ...;
List<Embedding> embeddings = embeddingModel.embedAll(segments);每条 chunk 就变成 768 或 1024 维的浮点向量了。”
🧑💼 面试官
“向量存哪里?你对 Java 栈下的选型有什么偏好?”
👨💻 候选人
“主力是 Elasticsearch 8.x,因为 Java 生态集成太成熟了,它的 dense_vector 字段原生支持 kNN 检索,同时还能用 BM25 做关键词匹配,天然就能实现我们后面要说的混合检索。当然,如果是超大规模、对召回速度要求极高,可以考虑 Milvus 或 Qdrant,不过 ES 对大多数企业级文档量已经够用了。
存的时候不光存向量,还会把原始文本、所在文档标题、章节、chunk 序号作为元数据一起写进去,方便最后答案溯源。”
🧑💼 面试官
“离线阶段清楚了。那用户一问问题,在线这条线怎么跑?特别是怎么保证召回的上下文确实管用?”
👨💻 候选人
“用户问题先用同一个 embedding 模型转成向量,去 ES 里跑 knn 查询,初步召回 Top-K,比如 10 条。但光靠向量相似度容易漏掉精确关键词匹配,所以我一定会加上 混合检索:同时发一个 BM25 查询,把两路结果用 RRF(倒数排名融合) 加权合并,召回率能提升不少。
但这还不够,10 条里肯定有不相关的。我会再引入一个重排序模型,比如 bge-reranker-large,它是一个 Cross-Encoder,把 query 和每个 chunk 拼起来打分,重新排序后只取 Top-3 送进大模型。这个 rerank 服务一般用 Python 部署成微服务,Java 这边直接 HTTP 调。整个流程就是:
粗排 10 篇 → 精排取前 3 → 灌入 Prompt🧑💼 面试官
“Prompt 怎么拼?生成结果怎么返回给前端?”
👨💻 候选人
“我会用结构化模板,严格限定模型的角色和依据:
你是一名技术助手。请严格根据以下参考资料回答问题,如果无法回答请说“不知道”。
参考资料:
{chunk1}
{chunk2}
...
问题:{user_query}
回答:这样能最大化抑制幻觉。生成这块,为了用户体验,肯定要用流式输出。后端 Spring Boot 用 WebFlux 或者直接 StreamingResponseBody,把大模型返回的 SSE 流透传给前端,用户就能看到字一个一个蹦出来,不用干等。整个链路延迟控制在 2 秒内比较理想。”
🧑💼 面试官
“整体方案很落地,Java 生态里的组件你基本都点到了,尤其混合检索和 rerank 是工业界非常看重的加分项。我最后问一个场景题:如果文档更新了,你怎么保证问答系统能及时知道,而不是用过时的知识回答?”
👨💻 候选人
“这个可以通过增量索引解决。我们在文档管理后台挂一个事件通知,文档一更新就发消息到 Kafka,消费者这边做:重新解析 → 分块 → 向量化 → 更新 ES 中对应文档的所有 chunk(先删旧块再插新块)。如果只是局部修改,甚至可以只更新变动的块,避免全量重建。这样整个系统知识的时效性就能做到准实时了。”
🧑💼 面试官
“很好 👍 整个链路从前到后,包括工程细节和优化点,你都有实际思考。这个题你答得很扎实,我没啥补充了。咱们继续下一个问题……”
