RAG 文档摄入全链路,从原理到生产落地

去年有个朋友找我帮忙,他公司积累了几百份技术文档和员工培训资料,每次新员工入职,都要人工翻找、逐一阅读,效率极低。他的诉求很简单:能不能做一个内部知识库,直接问问题就能得到答案? 我说可以,用 RAG。 他问:RAG 是什么? 我说:让大模型读你的文档。 他说:大模型不是本来就能回答问题吗?

这个问题很典型。很多人以为大模型「什么都知道」,但实际上,大模型只知道它训练数据里有的东西。你公司的内部文档、你手头的候选人简历、你积累的技术规范 ------ 这些它从来没见过,直接问,要么说不知道,要么编一个听起来像样但根本不准确的答案。

RAG(Retrieval-Augmented Generation,检索增强生成)就是为了解决这个问题而存在的。

它的思路说起来很简单: 用户提问 → 从知识库里找到最相关的内容 → 把这些内容连同问题一起送给大模型 → 大模型基于真实内容生成回答。

把这套东西真正做出来,是这篇文章要讲的事。


整套系统要解决什么?

在没有 RAG 知识库之前,用大模型做私有文档问答,面对的是这些问题:

  • 大模型不认识你的文档,无法回答私有知识问题

  • 就算你把文档粘贴进去,超长内容超出上下文限制

  • 大模型容易「幻觉」,编造看起来合理但不准确的答案

  • 无法追溯答案来源,用户不知道该不该信

RAG 知识库系统要做的事,是解决这四个问题的数据基础:把私有文档变成可被精准检索的结构化知识。

完整的流程是这样的:

Plain 复制代码
用户上传文档(PDF / Word / TXT)
      ↓
自动解析 + 文字提取 + 清洗
      ↓
文档分类(简历 / 学习资料 / 通用文档)
      ↓
智能切块(chunk_size + overlap + 句末回溯)
      ↓
向量化(Embedding 模型,1024 维)
      ↓
写入向量数据库(Qdrant)
      ↓
用户提问时精准召回 → 大模型生成有依据的回答

每一步都不是可以省略的,任何一步做不好,最终的回答质量都会受影响。 下面逐步拆解。


第一步:向量数据库 ------ 为什么不用 MySQL?

很多人第一反应是:把文档内容存到 MySQL 里,查询的时候用 LIKE 搜索,不就行了?

不行,原因有两个。

第一,关键字搜索找不到语义相关的内容。 「候选人会 Python 后端开发」和「应聘者具备 Python 服务端编程技能」,意思完全一样,但关键字不同,LIKE 搜索找不到它们之间的关联。

第二,向量相似度搜索用 MySQL 会慢到无法使用。 向量搜索的本质是「找最相似的那几个」,在 MySQL 里只能暴力遍历 ------10 万条记录就算 10 万次,100 万条就算 100 万次,延迟高得根本用不了。

所以需要向量数据库。

我们用的是 Qdrant,开源、性能好、本地 Docker 一条命令跑起来:

bash 复制代码
docker run -d --name qdrant -p 6333:6333 \
  -v %USERPROFILE%\qdrant_storage:/qdrant/storage qdrant/qdrant

Qdrant 用 HNSW(分层可导航小世界图) 算法做向量检索,原理类似分层地图:先在稀疏层找大方向,再逐步缩小范围定位目标,不需要遍历全量数据,查询速度比暴力搜索快几十倍甚至几百倍,精度损失极小。

在 Qdrant 里,数据组织分三层:

  • Collection:相当于一张表,我们建了 knowledge_chunks,存所有文档的向量块

  • Point:一条记录,对应一个文本块,包含 id(唯一标识)、vector(向量数值)、payload(原文 + 文档类型等元信息)

  • Payload 过滤:查询时先按文档类型、用户 ID 等字段缩小范围,再做向量相似度排名

距离度量选余弦相似度 ------ 它只看向量的方向,忽略长度。为什么这个选择重要?因为「这个人会 Python」和「候选人具备扎实的 Python 开发技能」,向量方向接近,但长度可能差很多。如果用欧氏距离(看两点之间的直线距离),长短不同的句子会被判定为不相似,误判率很高。


第二步:Embedding 模型 ------ 文字怎么变成数字?

向量数据库解决的是「怎么存、怎么查」,Embedding 模型解决的是「文字怎么变成向量」。

原理不复杂:把一段文字,通过神经网络压缩成一个固定维度的浮点数数组。核心特性是:语义越相近的内容,向量方向越接近。

这就是语义检索的根基。「候选人会 Python 后端开发」和「应聘者具备 Python 服务端编程技能」,在向量空间里的方向非常接近,即使用词完全不同,检索时也能互相命中。这是关键字搜索永远做不到的。

模型选型上,我们用阿里云百炼 text-embedding-v3,输出 1024 维稠密向量(每个维度都有值)。选它的原因很务实:

  • 中文效果好,针对中文优化,我们的场景以中文文档为主

  • 零部署成本,API 调用即用,不用配 GPU 环境

  • 数据不出境,企业场景合规友好

  • 价格极低,处理几百份文档通常不超过几毛钱

调用方式很简单,但有一个坑要注意:单次最多 10 条,超过需要分批,并且要按 text_index 对结果重排,保证文本和向量一一对应,顺序不错乱。

python 复制代码
response = TextEmbedding.call(
    model="text-embedding-v4",
    input=texts,          # 最多 10 条
    dimension=1024,
    output_type="dense",
)
# 按 text_index 重排,保证顺序和输入一致
embeddings = sorted(
    response.output["embeddings"],
    key=lambda x: x["text_index"]
)

第三步:文档解析 ------ 把文件变成干净的文字

文档上传进来,第一步是把文件里的文字提取出来。不同格式处理方式不同。

PDF(PyPDF2):按页遍历提取文字,适合数字原生 PDF(软件直接生成的)。有一个大坑:扫描版 PDF 存的是图片,不是文字,PyPDF2 读不到任何内容。 需要检测并单独处理(走 OCR 或跳过提示用户)。

Word(python-docx):分两步提取,先取段落,再取表格。这里有个细节很多人会忽略 ------ 简历里经常把基本信息放在表格里,如果只提取段落,姓名、联系方式、教育背景这些核心信息会全部丢失。

提取完之后要清洗,不然噪声会影响向量质量:

python 复制代码
# 去掉零宽字符(肉眼看不见,但会把「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%)。 重叠的意义在于:同一句话会同时出现在相邻两个块里,不会因为切割位置不巧而从知识库里「消失」。

Plain 复制代码
块 A:第 1~500 字
块 B:第 451~950 字  ← 前 50 字和块 A 重叠
块 C:第 901~1400 字 ← 前 50 字和块 B 重叠

② 句末标点回溯切割 到了切割点之后,往前最多找 100 个字符,找到句末标点(。!?\n)就在那里断开。 这解决了固定长度切块最大的问题:切断句子。「候选人熟悉 Kube」和「rnetes 容器化技术」分在两个块里,两个块都残缺,检索时都找不到。

python 复制代码
if end < len(text):
    for i in range(end, max(start + overlap, end - 100), -1):
        if text[i] in ('。', '!', '?', '\n', '.', '!', '?'):
            end = i + 1
            break

③ MD5 哈希去重 每个块的文字内容算 MD5,哈希值相同的块只保留第一个。 解决重复内容问题 ------PDF 每页都有页眉,提取后重复出现,如果不去重,会产生大量完全一样的向量,浪费存储,也干扰检索排名。

进阶切块策略

基础方案适合大多数场景,复杂场景需要专项处理:

父子块模式(最常用的进阶方案)

同一段内容同时生成两种粒度的块:

  • 子块(200 字):语义精准,用于检索命中

  • 父块(1000 字):上下文充足,命中子块后返回对应父块给大模型

这解决了一个根本矛盾:小块检索精准但上下文不够,大块上下文够但检索不准。 父子块两全其美 ------ 用小块找到,用大块回答。

结构化切块

对有明显结构的文档,按原生结构切比按字符数切好得多:

  • PDF → 按页切

  • 聊天记录 → 按对话轮次切

  • 日志文件 → 按时间戳切

大模型智能切块

对高价值、格式复杂的文档,用 LLM 直接判断语义边界来切块,效果最好,但成本高,适合文档少但质量要求极高的场景。


第五步:文档分类 ------ 让检索更精准

切块之前先给文档分类,分类信息写入 Qdrant 的 payload。

有了分类,检索时可以定向召回 ------ 用户问简历相关的问题,只在简历里找,不会把技术文档的内容混进来。

我们用三层漏斗来判断分类:

Plain 复制代码
第一层:前端用户手动选择的分类(最可信,直接用)
    ↓ 没有手动传值时
第二层:用正文关键词计分自动判断
    命中「工作经历、技能、求职意向」→ 简历
    命中「章节、课程、知识点」→ 学习资料
    ↓ 都没命中时
第三层:兜底为通用文档

分类结果同步写入 MySQL(支持业务层按类型筛选文档列表)和 Qdrant payload(支持向量检索时用 Payload Filter 过滤)。


第六步:工程化重构 ------ 从能用到可上线

把以上所有逻辑写通了之后,如果你是按单文件写的,会发现一个问题:所有代码耦合在一起,改一处可能影响全部,没有异常处理,一个文件解析失败可能卡死整个流程,临时文件不清理会占满磁盘。

这在本地测试看不出来,上了线就是定时炸弹。

工程化重构的核心是分层解耦,我们把代码拆成两层:

底层 core.py(core.py)

纯处理逻辑,六步标准化管道: 文本解析清洗 → 文档分类 → 智能切块 → 批量向量化 → 向量入库 → 统一封装

文件格式用字典策略模式适配:

python 复制代码
parsers = {
    ".pdf": parse_pdf,
    ".docx": parse_docx,
    ".txt": parse_txt,
}
parse_fn = parsers.get(file_ext)

新增格式只需加一个键值对,不改任何现有逻辑。

入库时用 upsert(存在就更新,不存在就插入),保证幂等性 ------ 同一份文档重复入库,不会产生重复数据。搭载 user_id 等元数据,实现多用户数据隔离。

上层 rag\_service.py(_service.py)

业务适配层,处理两种真实场景:

场景一:用户实时上传

Plain 复制代码
内存字节流 → 写临时文件 → core 处理 → 清理临时文件

场景二:MinIO 历史文件补录入库

Plain 复制代码
MinIO 下载 → 重命名(补全扩展名)→ 释放连接 → core 处理 → 清理临时文件

两种场景的差异完全封装在 rag\_service.py(_service.py) 里,core.py(core.py) 完全不感知来源差异。新增一种文件来源,只改业务层,核心层一行不动。


整套系统的完整数据流

把六步串在一起,看清楚整条链路:

Plain 复制代码
用户上传文档(PDF / Word / TXT)
        ↓ 前端格式校验 + 强制选择文档分类
rag_service.py 接收
        ↓ 写临时文件 or 从 MinIO 下载
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 系统。

如果你也在做类似的事,希望这篇文章能帮到你。


想系统学习多智能体和 RAG 开发?

本文内容来自 「Harness & Hermes」多智能体开发特训营。

这门课从零开始,覆盖多智能体系统的完整开发链路:

  • RAG 知识库全链路:文档摄入、向量检索、生成问答、引用溯源

  • 多智能体架构:智能体协作、任务编排、工具调用

  • 工程化落地:从原型到生产级代码的完整重构路径

  • 前后端完整闭环:不只是后端逻辑,包括用户交互的完整实现

感兴趣的话,可以了解一下慕课网的 「Harness & Hermes」多智能体开发特训营。

相关推荐
小和尚同志1 小时前
AI 自动化测试探索(一):Playwright MCP
前端·人工智能·aigc
硅谷秋水2 小时前
面向长上下文自动驾驶的规划对齐Token压缩
人工智能·深度学习·机器学习·计算机视觉·自动驾驶
JaydenAI2 小时前
[对比学习LangChain和MAF-07]如何引入人机交互的审批流程
python·ai·langchain·c#·agent·hitl·maf
郭泽斌之心2 小时前
MQL5 EA 怎么和外部程序通信?文件三件套协议:参数热更新不重启、状态心跳、远程触发
人工智能·经验分享·深度学习·ea·fay数字人·easydeal
mit6.8242 小时前
“泄露了windows12“
人工智能
syc78901232 小时前
中文语境下AI编码工具实战对比:从迭代体验看日常开发选择
linux·人工智能·ubuntu
dualven_in_csdn2 小时前
用户点击“一键起飞“
人工智能
米核AI易山2 小时前
扣子工作流变量体系深度解析:从踩坑到精通
人工智能·coze·扣子工作流·米核ai易山
nap-joker2 小时前
用于转录组信息精确肿瘤学和药物机制分析的多模态可解释深度学习
人工智能·深度学习·药物敏感性·多层级生物网络·细胞异质性·可解释性多模态