去年在做一个RAG知识库项目时,我连续踩了一个星期的坑。最终定位到的核心问题不在大模型,也不在向量数据库,而在PDF解析——表格里的内容总被切碎,检索出来的上下文和问题完全对不上。把解析层重写之后,效果立刻好转。这件事给我的体感很直接:RAG的难点从来不是“怎么把大模型接进去”,而是整个数据链路能不能扛住真实场景的复杂度。
最近系统梳理Udemy那门《AI & LLM Engineering Mastery: GenAI, RAG Complete Guide》Part 2的学习笔记时,这个感受又被反复印证。Part 2的重点不是基础概念,而是从文档解析到评估指标、再到Agent化的完整工程链路。很多教程喜欢把RAG讲成一个“三步走”方案:加载文档、向量化、交给大模型。但真实项目里,这三步每一步都能裂变成一堆细节。
这篇文章不打算复述课程目录,而是结合项目实战经验,把RAG链路按“解析→分块→索引→评估→精度→Agent化”的顺序拆一遍。适合那些已经跑通过demo、正准备把RAG放进真实业务的读者。
1. RAG入门之后,真正的分水岭在工程化
1.1 为什么demo能跑,生产却问题不断
很多人第一次接触RAG,用的都是现成例子:一个PDF放进去,按固定长度切块,embedding之后存到向量库,再用LangChain或LlamaIndex把检索结果拼进Prompt,最后让模型生成答案。这一套在演示环境里通常很丝滑,因为样例文档干净、问题经过筛选、没有并发压力、也不会有人问知识库边界之外的事情。
一旦换成真实业务文档,问题立刻涌现。常见的有:
- PDF里的表格跨页,切块后语义断裂。
- 扫描件没有OCR层,检索质量急剧下降。
- Word文档带批注、文本框、页眉页脚,解析出来全是噪音。
- 不同来源的文档元数据不统一,过滤时无从下手。
- 用户问法和知识库原文的表述差异太大,直接向量检索召不回内容。
这些问题的共性是:它们都不在模型层,而在数据工程层。RAG系统的上限,首先由数据管道的质量决定,其次才是模型能力。Part 2花了大量篇幅讲“文档加载解析详细全流程”,原因就在这里——它不是最显眼的部分,但它是决定成败的部分。
这里先给一个核心判断:任何RAG项目,都应该把大部分精力花在“让检索之前的一切变得可靠”上,而不是反复调Prompt和换模型。理由很直接——检索喂进去的内容本身就是错乱的,再强的生成能力也只是建立在错误上下文之上。
1.2 RAG完整链路,从文档到生成
既然要谈工程化,就要先把链路拆开。一个完整的RAG系统,通常包含下面几个环节:
| 环节 | 主要任务 | 常见工具/方法 |
|---|---|---|
| 文档加载 | 读取不同格式的文件 | PDF、Word、Markdown、HTML解析器 |
| 内容清理 | 去页眉页脚、去噪音、保留表格结构 | 文本清洗、OCR、版面分析 |
| 分块 | 把长文本切成适合向量化的片段 | 固定长度、递归分割、语义分块 |
| 向量化 | 将文本块转成向量 | Embedding模型 |
| 索引 | 建向量索引、元数据索引 | 向量数据库(开源或云端) |
| 检索 | 根据问题召回相关片段 | 向量检索、混合检索、重排序 |
| 生成 | 把检索结果组装成Prompt交给LLM | Prompt模板、模型推理 |
| 评估 | 验证检索和生成的质量 | 评估集、自动化指标 |
这个链路里,每一个环节都有“能做”和“能做好”的差距。比如文档加载,能读到文字和能保留版面结构是两回事;分块能切出片段和能切出语义完整的片段是两回事;检索能返回相似内容与能返回当前问题真正需要的上下文,也是两回事。
Part 2的重点就在这些“两回事”上。它不是教你怎么调通一个RAG,而是教你怎么把RAG调到可以交付给业务方使用。这个认知转变,是从“会用工具”走向“能做工程”的分水岭。
1.3 该先掌握框架,还是先理解数据链路
现在市面上RAG相关框架很多,有LangChain、LlamaIndex这类开发框架,也有Dify这类低代码平台,还有Spring AI这种把大模型能力整合进Java生态的尝试。工具选型很容易让人陷入纠结,但对初学者来说,框架本身不是关键。
我的建议是,先手写一个最小链路,不用任何重量级框架。用最朴素的代码,完成从文档解析到向量化再到检索生成的过程。这个过程中,你会被迫理解每一步在干什么:解析器返回了什么结构,分块之后文本变成了什么形态,embedding之后的向量如何比较相似度,检索到的片段又如何拼进Prompt。
理解完这些底层机制,再上手LangChain或Dify,你会像看地图一样清晰:框架只不过是把这些环节封装成了配置项和调用链。框架会更新、会变化,但数据链路的底层问题一直都在。
如果你已经完全理解链路,直接用Dify这类平台做业务原型也完全合理。它的价值在于把重复劳动简化,尤其适合快速验证。但要注意,平台化工具降低了上手门槛,并不意味着工程问题会自动消失。数据质量、评估闭环、权限管理这些事,平台不会替你做。
2. 文档加载与解析,是RAG最容易被低估的环节
2.1 PDF、Word、扫描件,三类格式各有各的坑
RAG项目中,文档解析是第一个隐藏瓶颈。很多教程的代码只有三五行:把PDF文件传给某个解析器,得到文本,完事。但真实场景里,文本的版式、层级、表格信息一旦丢失,后面的检索和生成都跟着失真。
先说最常见的PDF。PDF本身不是一种“文档”格式,而是一种“版面描述”格式。它记录的是一行行文字该放在哪里,而不是“标题是什么”“表格有几行几列”。所以直接抽取文本时,经常碰到:
- 标题层级丢失,标题和正文混在一起。
- 表格被按行读取,单元格之间的关系被拆散。
- 多栏排版的文档,文字顺序错乱。
- 页眉页脚和正文混入同一文本块。
这会导致什么后果?举个例子,一个产品规格表里有“型号A:支持10W快充”和“型号B:支持50W快充”,切块后可能变成两个互不关联的片段。用户问“哪款支持50W快充”,模型检索到的内容里缺少“型号B”和“50W”的关联上下文,答案自然不准。
Word文档的问题也不小。它本身有结构,但很多解析器没有把段落、表格、批注、文本框区分开。扫描件则是另一类问题——没有OCR层,图片里的文字完全无法被检索到。现实业务中,大量历史合同和票据恰恰是扫描件。
2.2 解析流程的通用拆解
从工程经验看,一个稳定的解析流程通常包含四步:
- 格式识别:按扩展名或文件头判断文档类型。
- 内容提取:调用对应解析器,拿到粗文本。
- 结构还原:尽量还原标题、段落、表格、列表等结构。
- 清洗过滤:去掉页眉页脚、重复空行、乱码字符、无关批注。
这里给一个通用示例结构,表示文本解析脚本的主干。具体解析库需要结合你的环境选择,代码只是骨架。
def extract_document(file_path): file_type = detect_file_type(file_path) # pdf / docx / md / html if file_type == "pdf": content = extract_pdf_with_layout(file_path) # 尽量保留表格和层级 elif file_type == "docx": content = extract_docx_with_structure(file_path) else: content = extract_markdown_or_html(file_path) cleaned_content = remove_noise(content) return cleaned_content注意,这只是一个示例结构,不是可以直接复制使用的代码。落地前要结合自己的文档库样本,确认解析器对表格、多栏和字体大小的处理效果。不同解析库在版面还原能力上差异很大,遇到复杂文档时,宁可多花时间选型,也不要盲目相信默认输出。
2.3 小样本验证法
文档解析这块,我见过最多的错误是“没有验证直接批量跑”。结果几千份文档喂进去,事后才知道一半都解析错了,清洗成本和返工成本非常高。
更稳妥的做法是五步:
- 从真实文档库里随机挑30份,覆盖PDF、Word、扫描件等不同来源。
- 把这30份全部解析一遍,人工检查输出质量。
- 统计失败率,定位最常见的解析问题。
- 针对问题调整解析器配置或增加预处理步骤。
- 30份确认稳定之后,再扩大到几百份做灰度验证。
判断标准也很简单:一份文档解析完,标题层级是否保留、表格结构是否完整、正文顺序是否正确、噪音是否除净。这些表面上看起来和响应速度无关,但决定了后续检索质量的基线。
注意:不要用样例文档验证解析流程,一定要用真实业务文档。真实文档里的版式混乱程度,通常远超样例数据。
2.4 解析常见的排查顺序
如果解析结果不理想,不要急着换工具。先按下面的顺序排查:
- 看原始文件本身是否正常:编码、缺损、加密、扫描清晰度。
- 看解析器输出:是完全是空,还是文本错乱,还是结构丢失。
- 看是否涉及特殊版面:表格、多栏、文本框、公式、图片。
- 看清洗步骤是否过度:有些清洗规则会把表格分隔符或者关键标点一起删掉。
- 看文件规模和并发:超过解析器单文件限制时,结果可能被截断。
这个排查顺序的核心思想,是从“文件不可用”到“解析器不行”再到“清洗规则误伤”,逐层缩小范围。如果不加区分,很容易把解析问题误判成embedding或模型问题。
3. 分块、向量化与索引,决定召回质量的三块拼图
3.1 分块策略怎么定
文档解析完,下一步是分块。分块直接影响检索质量,因为它决定了向量化之后,每个片段是否“语义完整”。
常见的分块方式大致有几种:
| 分块方式 | 基本思路 | 适用场景 |
|---|---|---|
| 固定长度分块 | 按字符数或token数切分 | 快速验证、通用场景 |
| 递归字符分割 | 先按段落,再按句子逐级切分 | 大部分文本型文档 |
| 语义分块 | 利用embedding或模型判断语义边界 | 内容复杂、主题切换频繁的文档 |
| 结构分块 | 按标题、表格、章节等结构切分 | 手册、合同、法规等强结构文档 |
固定长度分块是很多教程默认推荐的,因为它简单。但它的缺点是经常把一句话、一个表格、一组对照关系切成两半。递归字符分割会好一些,因为它会根据段落和句子来切。实践中,更建议分层处理:
- 有明确结构的文档,优先按结构分块。
- 没有结构的长文本,用递归分割,再设置合理的块大小和重叠。
- 块大小要根据embedding模型的上下文长度来确定。
- 对同一文档,可以保留多种块策略,检索时按元数据选择。
这部分没有标准答案,因为分块是否合理,最终要用检索效果来验证。重要的是先建立“分块效果可以观察”的意识,而不是套一个固定参数就完事。
3.2 向量化模型选型和使用
分块之后是向量化,也就是用embedding模型把文本转成向量。这一步有四个关键点:
- 模型上下文长度。如果embedding模型支持的最大输入长度是512个token,那你的块就不要无脑设成1000个token。
- 文本规范化。同一批文档的大小写、标点、换行格式最好统一,避免无意义的向量漂移。
- 批量向量化。大规模场景不要一条条调接口,要批量提交,能明显降低耗时和成本。
- 向量化结果的缓存。同一份文档如果只改了后续版本,可以复用未变化部分的向量,减少重复计算。
很多项目的第一步和第二步都会出问题。比如分块大小和模型上下文不匹配,文本被截断,导致语义信息丢失;比如同一份文档在不同版本里格式不一致,向量检索时出现不可解释的偏差。
embedding模型选型时,还要注意语言的匹配度。如果你的知识库以中文为主,就要优先选择中文效果好的模型,并针对自己的业务术语做小规模检索测试。不要只看公开榜单分数,要拿自己的问题去测。
3.3 元数据过滤与索引设计
RAG项目做到后面,单纯靠向量相似度往往不够。一个常见的改善手段是“元数据过滤”。比如:
- 只从某一类文档里检索。
- 只检索最近一段时间内的内容。
- 只检索某个部门或某个产品线的知识。
- 根据文档类型过滤掉FAQ或公告。
这些都需要在向量化时保留元数据,并在检索时把元数据条件作为过滤器。常见的做法是把元数据和向量放在同一行记录里,例如:
{ "id": "doc_001_chunk_003", "text": "型号B支持50W快充……", "embedding": "[...]", "metadata": { "source": "product_spec_2025.pdf", "doc_type": "spec", "category": "charger", "updated_at": "2025-01-15" } }有了这样的结构,检索时就可以先按metadata过滤,再算向量相似度,也可以在向量相似度基础上加一层重排序。很多RAG项目召回质量差,不是embedding模型不够强,而是没有利用好元数据这个“先验条件”。
3.4 为什么调了embedding模型,效果还是不稳定
这是RAG项目中非常普遍的现象:换了更强的embedding模型,检索结果却没有明显变好。原因通常不在embedding模型本身,而在前面的步骤。
最常见的情况是分块方式没有跟着变。比如之前是固定512字,换了embedding模型后,模型能处理的上下文长度变了,块大小却没有同步调整,结果长尾句被截断,语义信息照旧丢失。另一种情况是文档解析质量不稳定。同样一份文档,解析器不同,得到的文本结构不同,向量化后的空间分布也就不同。这种情况下,embedding模型的差异被解析噪声掩盖了。
所以,遇到检索效果不好,优先排查的是“输入到embedding模型的那段文本是不是干净的、语义完整的”,而不是立刻怀疑模型选型。这条经验在多个项目里都验证过。
4. RAG评估指标,不能靠“看着不错”
4.1 检索层指标
RAG效果好不好,不能只看“模型回答得顺不顺”。因为即使检索到的内容完全无关,大模型也能编出一段看起来很流畅的答案。所以要建立一套系统化的评估指标。
检索层关注的是“召回的片段是否真的对”。常用指标有:
- Recall@K:前K个结果里包含多少个相关文档。
- Precision@K:前K个结果里真正相关的比例。
- MRR:第一个正确结果排在第几位。
- NDCG:考虑排序位置的相关性折损。
实践里,我建议先盯Recall@K。因为RAG生成答案的前提是相关上下文被召回。如果相关片段根本没进前几名,后面的生成质量基本无从谈起。
4.2 生成层指标
生成层的指标,目前常用的有:
| 指标 | 关注点 | 通俗解释 |
|---|---|---|
| Faithfulness / Groundedness | 答案是否忠于检索到的上下文 | 有没有在知识库之外瞎编 |
| Answer Relevance | 答案是否对应用户的问题 | 答非所问会拉低分数 |
| Context Relevance | 检索到的上下文是否和问题相关 | 上下文不相关但答案流畅,也是问题 |
这三个指标可以组成一个固定的评估模板。也有的团队用RAGAS这类开源评估框架来批量计算。不过如果用自动评估框架,建议先在小样本上人工核对分数是否合理。自动评估模型本身也有误差,不要完全交给机器。
4.3 构建自己的评估集
评估指标不是随便定的,它需要一组“已知正确”的问题和上下文来跑。我把评估集建设的思路总结为三步:
- 从真实用户问题里挑50到100条,覆盖常见问题、边角问题和反例问题。
- 对每个问题,人工标注出知识库中对应的正确文档片段。
- 用这组数据分别跑检索和生成,记录指标分数。
评估集不必一开始做得很大,但一定要覆盖业务真实场景。否则指标好看,上线照样翻车。
这里额外提醒一点:评估集要持续积累。每出现一个线上失败的Case,就把它补进评估集。时间长了,评估集本身会变成团队最宝贵的资产。它能防止同一个问题反复发生,也能让模型迭代有据可依。
4.4 评估分数低时,怎么定位是哪个环节出了问题
评估分数不理想时,不要笼统地说“效果不好”。要分环节定位。
先看检索。把用户问题丢进检索层,拉出前5个片段,人工判断这些片段是否和问题真正相关。如果不相关,问题在检索之前的链路:解析、分块、向量化、索引、元数据过滤,逐个排查。
再看生成。如果检索到的片段是相关的,但最终答案不对,问题就在生成侧:Prompt构造是否清晰、上下文是否被截断、模型是否遵循指令、是否引入了无关信息。
一个可复用的小技巧是:先不看最终答案,只看检索出来的片段。如果片段质量本身很高,再排查生成环节。这一步能快速把问题范围缩小一半。
注意:评估分数的意义是“发现问题”,不是“证明上线”。如果评估集只包含简单问题,分数再高也不代表生产环境没问题。
5. LLM精度细节:FP16、FP32、BF16,工程化绕不开的底层配置
5.1 三种精度格式的核心差异
RAG项目做到工程化,会碰到一个绕不开的底层话题:LLM的精度格式。这跟算法本身无关,但它直接影响显存占用、推理速度和结果稳定性。FP32、FP16、BF16是现在最常碰到的三种格式。
| 格式 | 全称 | 指数位 | 尾数位 | 特点 |
|---|---|---|---|---|
| FP32 | 单精度浮点数 | 8位 | 23位 | 精度高,占用大 |
| FP16 | 半精度浮点数 | 5位 | 10位 | 占用减半,但表示范围窄 |
| BF16 | 脑浮点数 | 8位 | 7位 | 和FP32指数范围一致,但尾数更少 |
简单理解,FP16的优点是省显存、算得快,缺点是数值范围小,容易出现上溢或下溢。BF16的指数范围和FP32一样,所以数值范围更稳,但尾数精度低。深度学习场景对尾数精度不那么敏感,所以BF16在训练和推理中都很常用。
很多模型在训练时用的是BF16或FP16混合精度。到了推理阶段,为了省显存也会切到半精度。这里的风险在于:如果训练时使用的精度和推理时不一致,某些数值敏感的任务可能会表现出微妙但不可忽视的结果差异。
5.2 不同阶段的精度选择
不同阶段应该有不同策略。一个保守的实践方案是:
- 评估阶段:先用FP32或原始权重推理,得到“答案基线”。
- 开发阶段:切到BF16,看结果和基线是否有明显差异。
- 生产阶段:再根据显存和延迟需求决定是否启用FP16或量化。
这个顺序的出发点,是先保证“结果正确”,再优化“资源占用”。如果一上来就为了省显存切到FP16,遇到不稳定的输出,很难判断是模型能力问题还是精度问题,排查成本会明显增加。
还有一个细节是,如果项目使用了量化工具,需要在日志里记录量化前后同一批问题的输出对比。很多团队在模型量化的第一版会出现明显的效果回退,但没有留下对比数据,导致后面很难判断是量化还是Prompt改动引起的。
5.3 精度不一致带来的问题
精度不一致最常见的问题是数值敏感型任务结果抖动。比如一些对数字计算、逻辑推理、格式输出很敏感的任务,在FP32下正常,到了FP16下可能偶尔出现小数点偏差或格式错乱。
另一个问题是日志和复现。如果团队A用FP32推理,团队B用BF16推理,两边对同一个问题的结果可能不一样。这不是模型版本不一致,而是精度配置不一致。这种问题很难查,尤其是当大家都觉得“半精度应该差不多”的时候。
我的建议是:把精度格式写进模型配置里,和模型版本一起记录。复现问题或做评估时,先确认精度格式一致。这样看似多余,但在跨团队协作时能省下大量排查时间。
5.4 精度和显存之外的工程细节
精度配置经常和显存占用、部署方案一起讨论,但工程化还要关注另外几个点:
- 推理服务并发和超时。同一个模型在批量并发时,显存占用会上升,如果不做限制,可能出现OOM。
- 输入长度动态变化。不同问题输入长度差异很大,固定分配显存会导致浪费,但动态分配又需要估算峰值。
- 结果缓存。对高频相似问题做缓存,能明显降低模型调用压力。
- 日志结构化。记录每次请求的输入长度、精度格式、模型版本、检索到的文档ID,便于回溯。
这些点看起来零碎,却是RAG系统能否稳定运行的关键。很多项目在初期只关注“模型能不能答对”,但上线后真正考验的是“系统在负载下还能不能稳定地答对”。
6. 从传统RAG到Agentic RAG,再回到稳定可控
6.1 传统RAG与Agentic RAG的差别
传统RAG是单向流程:用户问题→检索→生成→返回。Agentic RAG则会引入一个Agent来动态规划流程。比如Agent先判断用户问法是否需要去查外部工具,再决定是直接生成还是先走检索;或者先拆解问题,再分多次检索。
这个变化意味着什么?传统RAG适合“用户问题直接检索知识库”的多数场景;Agentic RAG适合“问题边界模糊、信息分散、需要多步决策”的场景。
Part 2把Agentic RAG作为一个进阶方向来讲,也是这个逻辑:先掌握稳定的传统RAG,再引入Agent来增强。如果基础RAG的质量都不稳定,引入Agent只会放大问题。
6.2 何时适合引入Agent
我的判断是,以下信号出现时,可以考虑引入Agent:
- 用户问题经常需要分两步以上才能回答。
- 同一个问题需要检索多个不同来源,才能拼出完整答案。
- 用户问题经常依赖上下文才能确定意图,直接检索容易偏。
- 需要调用外部工具来补充信息,而不只是从向量库取内容。
但如果知识库结构简单、问题单一,传统RAG就够了。引入Agent会带来额外的延迟、成本和不确定性,维护上也更复杂。
需要说清楚的一点是:Agent不是“更聪明的RAG”,而是一种流程控制机制。它让系统在面对复杂问题时有能力拆分步骤、选择工具、验证结果。但这同时也意味着,系统的行为不再是完全确定性的,调试和回归的难度会变大。
6.3 工程化落地时的风险控制
引入Agent之后,最需要注意的是“控制边界”。常见方法:
- 给Agent限定可调用的工具范围,不开放无限制规划。
- 设置最大迭代次数,避免死循环。
- 对关键步骤做日志埋点,方便回溯。
- 保留“降级路径”,Agent计划失败时直接回到传统RAG。
一句话,Agent可以补充灵活性,但不能让系统失去可解释性。真正重要的不是功能多花哨,而是出问题的时候,能不能快速定位是哪一层出了问题。
如果你用的是比较重的平台或框架,还要注意Agent相关配置是不是容易失控。比如工具调用权限、外部API的鉴权、Prompt注入防护等。这些话题展开会很长,但核心原则是一致的:引入能力的同时,要同步引入约束。
6.4 RAG、微调与Agent,怎么组合
很多人会问,RAG和模型微调到底怎么选。我的理解是,它们解决的是两类不同问题。
RAG解决的是“知识更新和外部资料引用”的问题。它适合业务知识频繁变化、需要引用具体来源的场景。微调解决的是“模型行为模式不对”的问题。它适合模型格式不稳定、语气不对、工具调用规范不符合预期的场景。
Agent则解决的是“复杂任务需要动态规划”的问题。它坐在RAG和微调之上,负责调度。
所以,技术选型不是二选一。常见的组合方式是:用RAG管理知识库,用微调固定输出格式,用Agent处理多步决策。但这套组合对团队和运维的要求都很高,建议一次只做一项改造,每项上线前都跑一遍评估集。
回到开头那个RAG知识库项目。我最后没有用什么复杂架构,就是把解析做扎实了,把评估集建起来了,把精度格式记录清楚了,再用最简单的检索流程上线。这个经验放到今天依然成立:RAG的工程化不是拼模型强弱,而是把数据、索引、评估和可观测性这些“琐碎但关键”的部分做成一套稳定流程。
如果你的RAG项目卡住了,别急着换大模型。先把文档解析、分块和评估重新看一遍。看起来慢,实际是最快的一条路。