RAG 文档摄入全链路,从原理到生产落地
去年有个朋友找我帮忙,他公司积累了几百份技术文档和员工培训资料,每次新员工入职,都要人工翻找、逐一阅读,效率极低。他的诉求很简单:能不能做一个内部知识库,直接问问题就能得到答案? 我说可以,用 RAG。 他问:RAG 是什么? 我说:让大模型读你的文档。 他说:大模型不是本来就能回答问题吗?这个问题很典型。很多人以为大模型「什么都知道」,但实际上,大模型只知道它训练数据里有的东西。你公司的内部文档、你手头的候选人简历、你积累的技术规范 —— 这些它从来没见过,直接问,要么说不知道,要么编一个听起来像样但根本不准确的答案。RAG(Retrieval-Augmented Generation,检索增强生成)就是为了解决这个问题而存在的。它的思路说起来很简单: 用户提问 → 从知识库里找到最相关的内容 → 把这些内容连同问题一起送给大模型 → 大模型基于真实内容生成回答。
整套系统要解决什么?
在没有 RAG 知识库之前,用大模型做私有文档问答,面对的是这些问题:RAG 知识库系统要做的事,是解决这四个问题的数据基础:把私有文档变成可被精准检索的结构化知识。智能切块(chunk_size + overlap + 句末回溯)每一步都不是可以省略的,任何一步做不好,最终的回答质量都会受影响。 下面逐步拆解。
第一步:向量数据库 —— 为什么不用 MySQL?
很多人第一反应是:把文档内容存到 MySQL 里,查询的时候用 LIKE 搜索,不就行了?第一,关键字搜索找不到语义相关的内容。 「候选人会 Python 后端开发」和「应聘者具备 Python 服务端编程技能」,意思完全一样,但关键字不同,LIKE 搜索找不到它们之间的关联。第二,向量相似度搜索用 MySQL 会慢到无法使用。 向量搜索的本质是「找最相似的那几个」,在 MySQL 里只能暴力遍历 ——10 万条记录就算 10 万次,100 万条就算 100 万次,延迟高得根本用不了。我们用的是 Qdrant,开源、性能好、本地 Docker 一条命令跑起来:docker run -d --name qdrant -p 6333:6333 \-v %USERPROFILE%\qdrant_storage:/qdrant/storage qdrant/qdrantQdrant 用 HNSW(分层可导航小世界图) 算法做向量检索,原理类似分层地图:先在稀疏层找大方向,再逐步缩小范围定位目标,不需要遍历全量数据,查询速度比暴力搜索快几十倍甚至几百倍,精度损失极小。Collection:相当于一张表,我们建了 knowledge_chunks,存所有文档的向量块Point:一条记录,对应一个文本块,包含 id(唯一标识)、vector(向量数值)、payload(原文 + 文档类型等元信息)Payload 过滤:查询时先按文档类型、用户 ID 等字段缩小范围,再做向量相似度排名距离度量选余弦相似度 —— 它只看向量的方向,忽略长度。为什么这个选择重要?因为「这个人会 Python」和「候选人具备扎实的 Python 开发技能」,向量方向接近,但长度可能差很多。如果用欧氏距离(看两点之间的直线距离),长短不同的句子会被判定为不相似,误判率很高。
第二步:Embedding 模型 —— 文字怎么变成数字?
向量数据库解决的是「怎么存、怎么查」,Embedding 模型解决的是「文字怎么变成向量」。原理不复杂:把一段文字,通过神经网络压缩成一个固定维度的浮点数数组。核心特性是:语义越相近的内容,向量方向越接近。这就是语义检索的根基。「候选人会 Python 后端开发」和「应聘者具备 Python 服务端编程技能」,在向量空间里的方向非常接近,即使用词完全不同,检索时也能互相命中。这是关键字搜索永远做不到的。模型选型上,我们用阿里云百炼 text-embedding-v3,输出 1024 维稠密向量(每个维度都有值)。选它的原因很务实:中文效果好,针对中文优化,我们的场景以中文文档为主零部署成本,API 调用即用,不用配 GPU 环境调用方式很简单,但有一个坑要注意:单次最多 10 条,超过需要分批,并且要按 text_index 对结果重排,保证文本和向量一一对应,顺序不错乱。response = TextEmbedding.call(model="text-embedding-v4",最多 10 条
按 text_index 重排,保证顺序和输入一致
response.output["embeddings"],key=lambda x: x["text_index"]
第三步:文档解析 —— 把文件变成干净的文字
文档上传进来,第一步是把文件里的文字提取出来。不同格式处理方式不同。PDF(PyPDF2):按页遍历提取文字,适合数字原生 PDF(软件直接生成的)。有一个大坑:扫描版 PDF 存的是图片,不是文字,PyPDF2 读不到任何内容。 需要检测并单独处理(走 OCR 或跳过提示用户)。Word(python-docx):分两步提取,先取段落,再取表格。这里有个细节很多人会忽略 —— 简历里经常把基本信息放在表格里,如果只提取段落,姓名、联系方式、教育背景这些核心信息会全部丢失。去掉零宽字符(肉眼看不见,但会把「Python」变成两个不同的词)
text = re.sub(r'[\u200b\u200c\u200d\ufeff]', '', text)全角空格替换成普通空格
text = text.replace('\u3000', ' ')连续空行压缩成一个
text = re.sub(r'\n{3,}', '\n\n', text)零宽字符这个坑,第一次踩的时候会很懵 —— 明明文字看起来正常,但向量化效果很差,调试半天才发现文字里藏着肉眼看不见的字符。
第四步:切块策略 —— 这是整套系统最关键的环节
很多人第一次做 RAG 知识库,会忽略切块的重要性,以为把文档存进去就行了。实际上,RAG 效果的上限,90% 取决于切块质量。为什么不能整篇文档直接向量化? 两个根本原因: 一,Embedding 模型有输入长度限制。 text-embedding-v3 单次最多 8192 个 token,几万字的文档直接塞不进去。 二,整篇文档向量化,语义会被严重稀释。 一份 3000 字简历涵盖工作经历、技能、教育背景…… 整篇变成一个向量,所有信息混合在一起。用户问「这个人会不会 Kubernetes」,这个词只占整篇的一句话,被稀释在大向量里,检索命中率极低。切块之后,每个块只聚焦一小段内容,语义纯粹,检索精度大幅提升。① chunk_size + overlap 每块最多 500 个字符(简历),相邻块之间保留 50 个字符的重叠(约 10%)。 重叠的意义在于:同一句话会同时出现在相邻两个块里,不会因为切割位置不巧而从知识库里「消失」。块 B:第 451~950 字 ← 前 50 字和块 A 重叠块 C:第 901~1400 字 ← 前 50 字和块 B 重叠② 句末标点回溯切割 到了切割点之后,往前最多找 100 个字符,找到句末标点(。!?\n)就在那里断开。 这解决了固定长度切块最大的问题:切断句子。「候选人熟悉 Kube」和「rnetes 容器化技术」分在两个块里,两个块都残缺,检索时都找不到。for i in range(end, max(start + overlap, end - 100), -1):if text[i] in ('。', '!', '?', '\n', '.', '!', '?'):③ MD5 哈希去重 每个块的文字内容算 MD5,哈希值相同的块只保留第一个。 解决重复内容问题 ——PDF 每页都有页眉,提取后重复出现,如果不去重,会产生大量完全一样的向量,浪费存储,也干扰检索排名。父块(1000 字):上下文充足,命中子块后返回对应父块给大模型这解决了一个根本矛盾:小块检索精准但上下文不够,大块上下文够但检索不准。 父子块两全其美 —— 用小块找到,用大块回答。对有明显结构的文档,按原生结构切比按字符数切好得多:对高价值、格式复杂的文档,用 LLM 直接判断语义边界来切块,效果最好,但成本高,适合文档少但质量要求极高的场景。
第五步:文档分类 —— 让检索更精准
切块之前先给文档分类,分类信息写入 Qdrant 的 payload。有了分类,检索时可以定向召回 —— 用户问简历相关的问题,只在简历里找,不会把技术文档的内容混进来。分类结果同步写入 MySQL(支持业务层按类型筛选文档列表)和 Qdrant payload(支持向量检索时用 Payload Filter 过滤)。
第六步:工程化重构 —— 从能用到可上线
把以上所有逻辑写通了之后,如果你是按单文件写的,会发现一个问题:所有代码耦合在一起,改一处可能影响全部,没有异常处理,一个文件解析失败可能卡死整个流程,临时文件不清理会占满磁盘。纯处理逻辑,六步标准化管道:文本解析清洗 → 文档分类 → 智能切块 → 批量向量化 → 向量入库 → 统一封装parse_fn = parsers.get(file_ext)入库时用 upsert(存在就更新,不存在就插入),保证幂等性 —— 同一份文档重复入库,不会产生重复数据。搭载 user_id 等元数据,实现多用户数据隔离。上层 rag\[_service.py](_service.py)内存字节流 → 写临时文件 → core 处理 → 清理临时文件MinIO 下载 → 重命名(补全扩展名)→ 释放连接 → core 处理 → 清理临时文件两种场景的差异完全封装在 rag\[_service.py](_service.py) 里,[core.py](core.py) 完全不感知来源差异。新增一种文件来源,只改业务层,核心层一行不动。
整套系统的完整数据流
① 文字提取 + 清洗(去零宽字符、全角空格、多余空行)② 文档分类(三层漏斗,写入 MySQL + Qdrant payload)③ 切块去重(chunk_size=500 + overlap=50 + 句末回溯 + MD5)④ 批量向量化(百炼 API,1024 维,分批 25 条,结果重排)⑤ 写入 Qdrant(余弦相似度,upsert 幂等,带 user_id 隔离)⑥ 更新 MySQL knowledge_docs 表状态(success / failed + chunk_count)
最后说几句
做完这套系统之后,那个朋友的知识库跑起来了。他们的新员工现在直接问问题,系统会检索相关文档片段,交给大模型生成回答,还会标注「来自哪份文档的第几页」,用户可以点击跳转验证。老实说:每个单独的技术点都不难 ——Docker 跑 Qdrant、调 Embedding API、用 PyPDF2 提取文字,单独拿出来都是几行代码的事。难的是把它们全部串起来,同时把每个环节都做到够好:切块参数调对了,文档分类做准了,工程化做稳了,这些加在一起,才是一套真正能用的 RAG 系统。
本文内容来自 「Harness & Hermes」多智能体开发特训营。RAG 知识库全链路:文档摄入、向量检索、生成问答、引用溯源前后端完整闭环:不只是后端逻辑,包括用户交互的完整实现感兴趣的话,可以了解一下「Harness & Hermes」多智能体开发特训营。💡 课程直达:https://class.imooc.com/sale/hagent