拓冰建站拓冰建站
首页 / 资讯中心 / 正文

RAG知识库问答系统从零到生产实践:文档处理·混合检索·Docker部署

1. 项目背景与目标定位1.1 为什么这一周选择做 RAG 知识库问答先说结论在这一周之前我花了一个多月的时间把大模型 API 调用、Prompt 工程、LangChain 基础流程、FastAPI 后端开发、React 前端这一套东西分别过了一遍但始终有个别扭的感觉——我会调模型、会写接口但做出来的东西没有真正私有的知识。换句话说模型懂全世界的常识却不懂我自己的文档、我团队的内部规范、我积累的几十份项目复盘。Week 5 我决定把 RAGRetrieval-Augmented Generation检索增强生成知识库问答系统从零到生产环境完整做一遍核心目的就两个一是打通文档上传 → 切片 → 向量化 → 检索 → 生成回答这条完整链路二是把之前零散学的全栈技能全部串起来做一个能真正被团队用起来的系统。这次项目的业务场景设定为团队内部知识库文档类型包括 Markdown 技术文档、PDF 需求说明书、TXT 会议纪要和几十条 FAQ 问答对。周报里给这个系统提了三个硬性指标回答准确率能达到 80% 以上、单次问答响应时间在 5 秒以内、支持同时上传多种格式文档。这三个指标直接决定了技术选型和架构设计的方向后面很多细节都是围绕它们展开的。1.2 这个项目适合谁来参考如果你正在学 AI 应用开发尤其是已经会调大模型 API、但没完整做过一个 RAG 项目的人这篇总结会对你有帮助。我会讲清楚每一步为什么这么设计、有哪些可以拿来就用的参数以及我在实际开发中踩过的坑和调整过程。同时如果你已经在做 RAG 但觉得效果不稳定、或者想从本地脚本升级为正式服务中间几节关于检索优化和部署实践的思路也值得一看。必须提前说明的是这个项目不是一个论文级的 RAG 框架研究而是一个以工程落地为目标的实践记录。所以我没有在算法层面做太多创新选用的都是目前比较成熟、社区资料丰富的组件重点在于把它们组合好、调优好、稳定跑起来。2. 整体架构设计与技术选型思路2.1 RAG 系统的基本链路回顾在做项目之前我先把 RAG 的核心链路在脑子里过了几遍因为它直接决定了后面的所有模块划分。一个典型的 RAG 问答系统包含两条流水线离线索引链路和在线问答链路。离线索引链路做的事情是把原始文档解析成纯文本 → 按一定策略切分成若干文本块chunk→ 对每个 chunk 调用 Embedding 模型生成向量 → 将向量和原始文本一起存入向量数据库。在线问答链路做的事情是接收用户问题 → 对问题做同样的 Embedding → 在向量数据库中检索最相似的 top-k 个文本块 → 将检索结果和用户问题组装成 Prompt → 调用大模型生成最终答案。这个流程看起来简单但每个环节都有很多魔鬼细节。比如分块大小怎么定、向量维度选多少、检索回来的文本块冗杂时怎么处理、用户问了意图不明确的问题怎么办这些都是在测试阶段才会暴露出来的问题。设计时我需要提前为这些环节预留调优的空间而不是搭完一个流水线就算完。2.2 技术栈选型与为什么这样选这次项目的技术栈选型原则是不追求最新最炫而追求稳定、资料多、自己能掌控。层次选型选型理由前端React Vite Ant Design熟悉度高开发效率快上传文件和管理知识库的界面组件成熟后端Python FastAPI异步支持好写 AI 应用生态无缝衔接自带 Swagger 文档便于调试Embedding 模型text2vec-large-chinese本地部署中文场景效果好本地运行无 API 费用隐私可控向量数据库MilvusDocker 部署支持百万级向量检索社区活跃有官方 Python SDKLLM国产大模型 API中文理解和生成能力强按量付费无需自己部署显卡编排框架LlamaIndex对文档加载和索引的封装比 LangChain 更顺手RAG 场景更专注任务队列Celery Redis处理文档解析和向量化这类耗时任务避免阻塞 API 请求这个选型里最值得说的是为什么没有用 LangChain 而选了 LlamaIndex。LangChain 确实名气更大、组件更全但它的抽象层级太多对于一个需要深度控制分块和检索细节的项目LlamaIndex 的文档加载器、节点解析器、检索器设计得更贴近 RAG 本身。举个例子LlamaIndex 里SimpleDirectoryReader可以直接读目录下所有文件RecursiveCharacterTextSplitter分块逻辑很清晰而我要自定义元数据比如把文档标题注入每个 chunk时LangChain 需要额外写很多胶水代码LlamaIndex 里通过配置NodeParser就能完成。当然这不是说 LangChain 不好如果你要做 Agent 类的复杂编排LangChain 的优势会明显一些。2.3 系统模块划分我将系统拆成四个独立模块每个模块可以单独测试和替换文档处理模块负责文件上传、格式解析、清洗、分块。这个模块是离线索引的前置依赖。向量索引模块负责调用 Embedding 模型、批量向量化、写入 Milvus 集合。需要对增量更新和删除有支持。检索问答模块负责问题向量化、检索、重排序、Prompt 组装、调用大模型。这是在线链路的核心。管理模块负责知识库的创建、文档列表管理、问答记录的查看。直接对接前端。模块划分的好处是当某个环节出问题时我可以快速定位是文档解析的问题、向量化的问题还是检索或生成的问题而不需要在一大坨代码里翻来翻去。实际开发中这让我省了很多排查的时间。3. 文档处理与向量索引的实现细节3.1 文件解析与清洗比想象中麻烦的环节文档解析是整个 RAG 系统里最容易被人忽视、但坑最多的一环。我一开始天真地以为 PDF 解析就是调用一下库直接读文本直到遇到扫描版 PDF、排版混乱的表格、以及带着大量无效页眉页脚的文档才发现这里需要做很多预处理工作。我最终的处理流程是文件上传后先用文件扩展名判断类型然后分流到不同的解析器。.md和.txt文件直接读文本.pdf文件用pypdf按页提取文本同时做 OCR 兜底如果某一页提取出的字符数少于 20判定可能是扫描件再用 PaddleOCR 识别一遍。文本清洗环节去掉多余的换行符、空格、不可见字符去掉页眉页脚中重复出现的公司名和页码对 Markdown 文件保留标题层级信息用特殊标记包裹标题方便后面切片时把这些结构信息注回 chunk。统一编码为 UTF-8防止后面 Embedding 时出现乱码。清洗这一步直接影响了检索质量。举个真实例子我处理一份 30 页的 PDF 需求说明书时原始解析出来的文本里每一页顶部都有XX 公司 内部资料和页脚页码如果不去掉检索时用户问部署需要什么环境很可能召回的是包含内部资料字样的噪声文本块。清洗后同样的检索准确率提升了大概 5 个百分点。3.2 分块策略的对比与最终参数选择分块是 RAG 里影响效果最敏感的参数之一我在实验中对比了固定长度分块、递归字符分块和基于标题结构分块三种方式。固定长度分块直接按 512 个字符切割实现简单但容易在句子中间截断导致语义不完整。递归字符分块按换行符、句号、逗号、空格这种优先级顺序递归切割能尽量保持语义边界。我用RecursiveCharacterTextSplitter实测下来比固定长度分块的效果好很多。基于标题结构分块先按 Markdown 或文档的大标题切分出章节章节内部再用递归字符分块细化。这个方案能保证每个 chunk 都带有明确的主题检索时命中率最高。最终我采用基于标题结构 递归字符分块的组合方案默认区块大小设为 400 个字符、重叠区 80 个字符。重叠区的目的是减少切分边界导致的语义断裂比如一个关键句子被从中间切开如果前后两个 chunk 有重叠检索时至少能命中其中一半。这个参数不是拍脑袋定的我在测试集上分别试了 300/400/500/600 四种区块大小400 的效果在回答准确率和上下文冗余度之间最平衡。区块大小太大会携带太多无关信息干扰模型回答太小则信息量不足检索到也答不全。分块之后我给每个 chunk 注入了元数据包括来源文档名、章节标题、文档类别技术文档/需求/FAQ、创建时间。这些元数据在后面的过滤检索和回答溯源时非常有用。3.3 Embedding 模型选型与本地部署Embedding 模型负责把文本变成向量它的选择直接决定了检索的上限。我对比了几种方案直接调用大模型厂商的 Embedding API效果好但需要网络调用批量索引 1000 个文档时耗时很长且费用不低。使用开源模型本地部署私密性好、无费用、速度快但需要推理资源。我最终选用text2vec-large-chinese一个中文效果不错且参数量适中的模型。部署方式也很简单先用sentence-transformers加载再把模型打包成一个独立的 Embedding 服务提供 HTTP 接口。实际测试中单个文本的向量化耗时为毫秒级批量处理时用 GPU 加速的话速度很快完全满足生产需要。关于向量维度text2vec-large-chinese输出 1024 维向量存入 Milvus 时直接按FloatVector字段声明维度即可。有一点需要注意线上和线下的 Embedding 模型版本必须保持完全一致否则会出现检索阶段无法命中的情况。我最初在本地测试时换过一次模型版本结果向量库里的向量分布完全不同了逼得我把整个索引重建了一遍。3.4 向量数据库的集合设计与写入优化Milvus 的集合Collection设计上我建了一个名为 knowledge_chunk 的集合字段包括chunk_id主键字符串类型doc_id文档 ID用于按文档删除数据text原始文本内容metadataJSON 字段存标题、文档名等信息vector1024 维浮点向量写入的时候我遇到了一个性能问题逐条 insert 1000 条向量需要几分钟对批量索引来说太慢了。后来我改成按批次写入每批 100 条用insert接口一次性提交整个索引时间从几分钟降到了十几秒。另外Milvus 的索引类型我选了 HNSW参数M 16、efConstruction 200检索时ef 64在 10 万条向量的测试集上查询延迟保持在 50ms 以内召回质量也不错。这里有个经验向量数据库不是越复杂越好如果你的数据量在几万到几十万这个量级用一套合理配置的 HNSW 就够了暂时不用考虑 IVF 或 ScaNN 这种更复杂的索引类型。4. 检索与生成链路让问答更准确的调试记录4.1 从纯向量检索到混合检索纯向量检索的问题在于它只做语义匹配对关键词一致性的把控不好。比如用户问RAG 是什么如果知识库里有一句话RAG 是一种检索增强生成技术向量检索能找到但如果文档里用的是英文缩写Retrieval-Augmented Generation而用户输入RAG向量检索就经常掉链子。反过来纯关键词检索又没法理解同义词和语义相近的表达。我最终采用了混合检索方案向量检索与 BM25 关键词检索并行执行各返回 top-20 结果然后合并去重取交集或综合打分排名靠前的 10 个作为候选。合并策略上我先为向量得分和 BM25 得分分别做 min-max 归一化然后按final_score 0.7 * 向量得分 0.3 * BM25 得分加权融合。这个权重经过实验调整语义匹配为主、关键词匹配为辅效果比单一检索方式有明显提升。在 50 条测试问题集上混合检索的 top-5 召回率比纯向量检索提高了约 8 个百分点。4.2 重排序环节用小模型做粗排后的精排混合检索召回 top-20 之后直接全部塞给大模型会有一个问题引入太多无关信息让大模型回答变得啰嗦甚至答非所问。这里我加入了一个**重排序Rerank**环节。Rerank 模型的思路是把用户问题和候选文本块做 cross-encoder 匹配输出一个相关性分数然后按分数重新排序只取 top-5 进入最终 Prompt。我选用的是一个中文 Rerank 模型部署方式和 Embedding 模型类似也是用 transformers 封装成服务。加入 Rerank 之后的效果非常明显回答的准确率从 68% 提升到了 79%尤其是在一些长文档、多主题混杂的场景下模型不再被无关信息误导。Rerank 也带来了一定的延迟开销单次 rerank 大概耗时 200ms但我可以通过只对 top-20 做精排来控制总耗时。4.3 Prompt 模板设计与上下文压缩当检索结果进入 Prompt 后怎么组织语言直接影响回答质量。我设计的 Prompt 模板包含以下几个部分系统提示说明你是团队知识库助手只能根据提供的资料回答资料中没有的信息要明确说知识库中没有相关内容不能编造。检索结果区用[1]、[2]等编号列出检索到的文本块每个文本块前标明来源文档名和章节。用户问题区单独标识用户的具体问题。输出要求要求回答时引用对应编号的资料来源如果用户问题与知识库无关也要礼貌提示。这个 Prompt 设计很重要的一点是限制模型自由发挥的空间。在大模型测试阶段我发现如果不加资料中没有的信息要明确说不知道这条约束模型会一本正经地编造答案这是 RAG 系统最忌讳的问题。加入约束后虽然回答会变得保守一些但准确率和可信度大幅提升。此外我还实现了检索结果的上下文压缩如果重排序后 top-5 的文本块总长度超过 3000 字我会通过再次提示对每个文本块做只保留与问题最相关的一句话或一段话的压缩处理避免超出模型的上下文窗口。当然大多数情况下不会触发这个逻辑但作为兜底策略是必要的。4.4 Agentic RAG 和 Graph RAG 的初步探索在做基础 RAG 稳定之后我花了一点时间调研了Agentic RAG和Graph RAG这两个方向。它们的思路是Agentic RAG不止做一次检索-生成而是让模型有自主判断能力比如判断是否需要多次检索、是否需要调用工具、是否需要追问用户澄清问题。我实验了用自定义 Agent 流程来处理多轮对话中用户没说完整实体的情况效果不错但工程复杂度明显上升。Graph RAG把文档中的实体和关系构建成知识图谱在处理谁和谁有什么关系这类问题时很有优势。但构建图谱的成本高、对结构化要求高我暂时只在小规模测试集上试了没有放到生产环境。结论是如果你的场景是文档问答标准 RAG 加混合检索、Rerank 已经足够稳定。Agentic RAG 适合需要复杂推理和工具调用的场景Graph RAG 适合强关系型知识的场景但都要根据自己的数据特点来判断不要盲目跟风。5. 生产级部署从本地脚本到可靠服务的改造5.1 离线索引与在线问答的职责拆分最初的代码是一个 Python 脚本导入文档就直接跑完整链路这在本地实验没问题但要部署成服务就有很多问题上传大文件会阻塞 API 请求、重复索引会导致数据混乱、没有失败重试机制等。生产级部署的第一步我把离线索引和在线问答彻底拆开。离线索引侧我用 Celery Redis 实现了异步任务队列。用户上传文档后API 接口只负责接收文件并创建一条待处理任务随后立即返回文档处理中的状态。后台 worker 任务依次执行文件解析、清洗、分块、向量化、写入 Milvus。这样即便有一个大文件处理很慢也不会卡住其他用户的访问。任务队列还天然支持失败重试例如网络抖动导致向量化失败时Celery 可以自动重试三次。在线问答侧我仍然使用 FastAPI 提供同步接口但内部做了并发控制。因为大模型 API 的并发有限我加了信号量限制同时进行的模型调用数防止调用超时或触发限流。5.2 异步改造与超时控制生产环境的另一个关键改造是超时控制。大模型 API 有时会因为网络问题迟迟不返回如果不加超时限制用户的请求会一直挂着前端只能干等。我做了这样几层超时设计调用大模型 API 时设置 30 秒超时超时后返回友好错误信息。整个问答接口的总超时时间设置为 45 秒包括检索时间和模型生成时间超过了就返回生成超时请重试。前端请求设置为 50 秒超时并加一个加载动画提示用户等待。这种逐层超时的机制非常实用避免了某个环节卡住拖垮整个服务的情况。5.3 Docker Compose 一键部署为了让系统可以在任何一台新服务器上快速跑起来我把所有依赖服务都用 Docker Compose 编排起来包括apiFastAPI 后端服务frontendNginx 托管前端静态文件同时做 API 反向代理milvusetcdminioMilvus 向量数据库及其依赖组件redisCelery 的 broker 和 result backendworkerCelery worker 进程embeddingrerank独立部署的模型推理服务Docker 化的过程中遇到的最大坑是 Milvus 依赖了 etcd 和 MinIO 两个组件三个容器的启动顺序和网络配置很讲究。我的做法是使用 docker-compose 的depends_on配置启动顺序同时在 API 服务里加了等待 Milvus 可用的启动健康检查避免服务启动就报错。5.4 数据库与文件存储的持久化方案生产环境里容器是无状态的如果容器重启Milvus 中的数据、上传的文件、问答记录都会丢失。这里我在 Docker Compose 的配置中为 Milvus 挂载了本地数据卷MinIO 的数据也做了持久化Redis 开启了 AOF 持久化。上传的原始文件存储到了服务器的指定目录并在结构化数据库中用字段记录文件路径。问答记录和文档元数据我存到了 PostgreSQL 中通过单独的schema.sql初始化表结构。这张表的设计包含doc_id、doc_name、doc_type、upload_time、chunk_count 等问答记录表包含 question、answer、sources、latency_ms、create_time 等字段。保存问答记录的目的不只是留痕更是为了后续做效果评估和问题分析。5.5 性能测试与容量规划部署完成后我做了一轮简单的性能压测。测试场景是 20 个并发用户同时提问每个问题都走检索 Rerank 大模型生成全链路。从压测数据来看单次问答的平均响应时间是 3.2 秒p95 是 5.8 秒符合周报里设定的 5 秒内基本达到的指标。容量规划上的建议是如果你的文档量在 10 万 chunk 以内单机部署完全够用如果超过这个量级Milvus 就要考虑分片和扩容了。Embedding 服务的 GPU 使用率在批量索引时一定要监控避免单任务把显存占满导致其他任务排队。6. 效果评估与调优指标怎么定、怎么测6.1 离线评估指标的选择很多初学者做完 RAG 系统最大的困惑是怎么知道它好不好用。我这次搭建了一个简单的离线评估流程用的指标是从 RAG 评估框架里提炼出来的几个核心指标指标含义我的评估方法Context Precision检索出的文本块中有多少是与答案真正相关的对每个测试问题标注相关文本块计算检索结果的精确率Context Recall所有相关的文本块中有多少被检索出来了标注相关文本块计算检索结果的召回率Faithfulness生成答案是否忠实于检索到的资料没有编造人工判断或 LLM 打分Answer Relevance生成答案和用户问题的相关程度人工判断或 LLM 打分我准备了一个包含 50 条问题的测试集覆盖了简单事实类步骤流程类对比类超纲类四类问题。测试时直接向评估脚本喂入问题自动跑完检索和生成链路输出各项指标。这个方法不需要复杂的框架用很少的代码就能实现但收益很大。6.2 人工评测与用户反馈闭环指标是冷冰冰的最终好不好用还是要看真实用户的感受。我在团队内部找了 5 名同事试用收集到的反馈中有几个典型问题有的问题问得太模糊比如怎么部署系统不知道用户是想问部署环境还是部署步骤回答会比较泛。有的问题包含两个子问题系统只回答了一半。有些文档内容本身相互矛盾系统没有指出矛盾而是直接选择了其中一个回答。针对这些问题我在应用层面做了几个改进对模糊问题增加一个澄清追问的交互先让用户明确意图再回答对多子问题Prompt 里加了将问题拆解为多个子问题并逐一回答的指令对文档矛盾Prompt 加了如果多个资料存在不一致请指出并说明。这些改进并不需要改动核心链路但对体验的提升非常明显。6.3 几轮调优后达到的效果经过分块参数调整、混合检索加入、Rerank 引入、Prompt 迭代这几轮调优后最终效果是简单事实类问题召回率和准确率已达到 85% 以上。步骤流程类问题整体可用但有时会漏掉流程中的前置条件。对比类问题需要用户把对比对象说清楚否则容易答偏。超纲类问题知识库中完全没有相关信息的识别准确率大幅提升系统基本能直接说没有找到相关资料不再胡编。整体上系统从能跑通进化到了在限定场景下具备生产可用性的程度。但距离什么都能答还有明显差距这也让我更清楚下一步优化的方向。7. 常见问题与排查技巧实录7.1 检索结果为空或相关性极差现象用户输入问题后检索返回的文本块与问题完全不相关。排查步骤第一步检查向量库中是否有数据。用 SQL 查询 collection 的实体数确认索引是否成功。检查用户问题的向量化结果是否正常。单独调用 Embedding 服务看返回的向量是否有值、维度是否正确。检查 Embedding 模型版本是否一致。这一点在前面提到过是我踩过最深的坑。检查查询语句的top_k参数是否设置过小比如只有 1就会导致召回很有限。7.2 回答中出现了知识库之外的信息现象生成答案连模型自带的常识都混进来了而不是完全基于知识库。原因大多是因为 Prompt 中约束不明确或者模型本身幻觉严重。解决办法在 Prompt 中强制加只能根据提供的资料回答禁止使用自身知识补充并在系统层面做一次答案检测如果模型回答中的关键实体在检索结果中找不到对应就把答案标记为低置信度。7.3 大文件上传后索引时间过长现象一份 50MB 的 PDF 上传后索引任务跑了十几分钟用户端一直在转圈。原因文件解析和向量化都是计算密集型操作在单 worker 下排队处理会很慢。解决办法将任务并发数调大同时把大文件切分后的每一个 chunk 的向量化任务也拆成子任务一条文档的索引任务被拆成多个小任务并行处理。实测 50MB 的文档索引时间从十几分钟降到了三分钟左右。7.4 服务重启后向量数据丢失现象Docker 容器重启后问答接口报错找不到集合。原因Milvus 的数据没有持久化容器删除后数据跟随消失。解决办法在 Docker Compose 中为 Milvus 挂载持久化数据卷并统一设置数据存储目录。同时定期对向量库做备份备份内容至少包含向量库数据、原始文件、结构化数据库数据三部分。7.5 并发升高后响应变慢现象压测阶段50 个并发请求时平均响应时间从 3 秒飙升到了 12 秒。原因大模型 API 的并发限制和 Embedding 服务的推理瓶颈同时出现。解决办法加请求队列控制同时调用大模型 API 的数量Embedding 服务改用 GPU 推理并开启动态批处理Milvus 查询改为连接池方式。8. 写在最后的几点经验做这个 Week 5 的项目最大的体会是RAG 系统的效果天花板不取决于模型而取决于数据质量和中间环节的精细度。同样的一个大模型配合清洗干净的数据、合理的分块、有效的重排序回答质量会有质的差别反之数据一团糟再强的模型也救不回来。另外有一个小技巧分享给大家日志一定要从第一天就开始打。我在项目初期偷懒很多环节没有日志结果出了问题只能靠猜。后来我在文档解析、向量化、检索、Rerank、模型调用每个环节都加了结构化日志记录耗时、输入输出摘要、错误信息调试效率提升了一倍不止。建议你也从项目开始就用好日志这一个基础设施。这个系统后续我会继续迭代的方向包括多模态文档的支持、基于反馈的自动重训机制、更强的关系型知识建模。不过这些都是后话了先把当前这套跑稳、用熟再一步步扩张。希望这篇实践总结能给你在搭建 RAG 知识库的路上提供一些实在的参考少踩几个我踩过的坑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门