
AI和LLM工程实战到了RAG这一块最容易被高估的不是模型能力而是“把文档塞进知识库之后能不能稳定拿到答案”。这个GenAI、RAG完整指南的第三部分我的理解就是把重点从“跑通Demo”挪到“处理真实文档”文档加载、切分、向量检索、Prompt拼接、答案校验以及最后怎么用指标判断知识库到底好不好用。如果你已经知道什么是LLM也见过几个RAG示例现在想自己搭一套能够处理PDF、Word、Markdown、HTML网页内容的问答系统这篇内容会比较合适。我不打算只贴一张架构图而是按实际落地顺序把链路拆开先看环境选型再看文档解析然后讲检索质量指标最后给一套批量任务和生产化的参考流程。你可以把这篇当作一个“从能跑到能用”的经验记录。1. 先确认第三部分要解决的核心问题RAG离可用还差在哪1.1 RAG不是“向量库ChatGPT”这么简单很多人在第一次搭RAG时会把注意力放在用什么模型、用哪个向量库以为只要“LLM Embedding 向量数据库”三个组件连起来知识库问答就能用了。实际跑下来你会发现真正影响结果的往往是那些不起眼的环节文档加载完不完整、切分粒度是否合理、检索TopK设置多大、Prompt里怎么把检索结果塞进去、最终答案要不要做校验。RAG解决的问题是“让LLM具备外部知识”模型不记得你公司内部文档也不记得某份协议的具体条款所以需要先检索出相关片段再让模型基于这些片段生成回答。这句话说起来容易但“检索出相关片段”本身就可能出问题片段切太碎上下文语义断掉切太大向量编码时内容太杂检索精度下降检索出来的片段排在前面的未必是真正能回答问题的内容。第三部分真正该练的是把这个链路每一层都验证一遍。先能用再稳定最后再谈批量。1.2 适合谁看需要哪些前置基础这篇内容更适合已经具备以下条件的人知道LLM的基本概念能调用大模型接口或本地部署模型。写过至少一个最简单的RAG Demo哪怕只是用框架把文档灌进向量库。手头有真实文档需要做问答例如产品手册、设计方案、协议条款、论文、课程资料。如果完全没有接触过LLM建议先把“什么是Token”“什么是Embedding”“什么是上下文窗口”这几个概念补上再来读RAG实战。否则很多参数调整会找不到方向。还有一点容易被忽略RAG不是一门纯算法课它很依赖工程能力。不会调试代码没关系但至少要有耐心看日志、逐段打印中间结果、手动检查解析出来的文本。很多时候问题不是模型不行而是文档解析后的文本本身就是乱的。1.3 一套完整RAG链路有哪些环节在进入具体操作前先把链路画清楚。一个常规的RAG系统至少包含以下几个环节文档加载从PDF、Word、Markdown、HTML、TXT等来源读取内容。文档解析与清洗去页码、去页眉页脚、修正表格、处理扫描件OCR。文本切分把长文档切成适合向量化的Chunk设置合适的重叠度。Embedding将每个Chunk转成向量并写入向量库。用户查询处理必要时对问题做改写、扩展比如把口语化问题转成检索友好的描述。相似度检索从向量库召回TopK个相关片段。上下文组装把片段和对话历史拼进Prompt控制Token长度。生成与验证LLM生成答案必要时检测答案是否引用了检索内容避免编造。很多教程把第1步到第4步叫作“知识库构建”把第5步到第8步叫作“问答链路”。实际上这两段是连在一起的任何一段出现问题最终答案都会偏差。后面所有排查也都是围绕这个链条逐层找问题。我比较建议在刚开始搭建时就把这8个步骤分别做成函数或独立脚本。这样每跑一步都能检查输出。否则所有代码混在一起出了错很难快速定位是哪一层的问题。2. 环境与选型LLM、Embedding、向量库怎么搭不踩坑2.1 本地还是API先回答三个问题先不要急着装组件先确认自己的运行条件。环境选择其实只需要回答三个问题数据是不是敏感、有没有GPU、你是不是需要长期跑批量任务。如果数据敏感比如公司内部协议、未公开文档建议优先考虑本地模型。本地方案通常用量化后的开源模型在显存8GB到16GB的机器上可以跑。如果只是学习用API方案最省事不用研究显卡驱动、CUDA版本、推理框架这些杂事。我的习惯是学习阶段用API模型因为会把复杂问题暴露在“检索质量”上而不是“部署环境”上。等流程稳定后再根据数据要求把模型切成本地部署。这里还有一个判断标准如果你的知识库只有几十篇文档用户量也不大API方案完全可以支撑。但如果是生产系统需要每天处理大量请求独立部署或自建推理服务会更可控成本也更可预测。2.2 向量库选型对比向量库的选型取决于数据量和部署方式。可以先按下面这个思路判断方案适合场景优点注意事项FAISS单机、百万级以内轻量、Python集成方便不提供服务适合脚本和内部工具Chroma学习、小批量、本地安装简单默认持久化高并发和分布式能力较弱Milvus生产、大规模、高并发功能全、支持分布式部署较重需要单独维护pgvector已有PostgreSQL系统复用数据库管理统一需要额外装插件性能在超大向量量级有限如果只是搭一个知识库先跑通FAISS或Chroma足够。等数据到了几十万条以上检索速度明显变慢再迁移到Milvus或pgvector。不用一开始就把架构做重。这里特别说明一下向量库不是RAG的“主角”它只是帮你在海量片段里快速找候选。我见过很多人花大量时间研究向量库性能结果原始文档解析和切分都没做好。向量库选一个你熟悉的、能持久化的就好把时间留给更需要关注的上游环节。2.3 一个最小可运行的RAG链路配置示例下面给一个最小链路示例。注意这是示例不是固定标准版本和依赖要以当前环境为准。重点是你能在配置里看到“文档切分、Embedding、向量库、检索、生成”这几个部分分别在哪里。# 最小RAG链路示例伪代码 from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain.chains import RetrievalQA from langchain_openai import ChatOpenAI # 1. 加载文档 loader TextLoader(handbook.txt, encodingutf-8) docs loader.load() # 2. 切分 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50 ) chunks splitter.split_documents(docs) # 3. 向量化入库 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore FAISS.from_documents(chunks, embeddings) # 4. 检索 生成 llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) qa RetrievalQA.from_chain_type( llmllm, retrievervectorstore.as_retriever(search_kwargs{k: 4}) ) answer qa.run(公司的年假规定是什么) print(answer)这段代码里最值得关注的参数是chunk_size、chunk_overlap和k。它们不是越大越好也不是越小越好。下面会专门讲。这里的代码只是一个示例具体模型名、API地址、依赖版本必须结合你自己环境里的实际组件。现在很多教程喜欢直接复制一段“能跑”的代码但如果你用的Embedding模型或向量库不同参数和调用方式都会有差异。建议先跑通再优化。2.4 Embedding模型选择别只盯着大模型RAG里LLM负责生成答案Embedding模型负责判断“文本之间像不像”。很多同学会花大力气选ChatGPT或开源LLM却随手用一个默认Embedding模型。实际上Embedding模型的质量直接影响检索命中率。选择Embedding模型时我一般会看三个维度是否支持你文档的语言、是否有足够大的向量维度、在专业领域上的表现。比如处理中文技术文档就用中文效果更好的Embedding模型处理英文协议就用英文为主的模型如果领域很专业比如协议、专利、3GPP规范最好拿你的一小批真实文档做个对比测试看看哪个模型能把同类内容聚得更近。不要根据模型参数数量去猜效果。最快的验证方法是准备10个检索问题分别用不同Embedding模型构建索引看Top5召回率。这一步花不了多少时间但能避免后续返工。3. 文档加载与切分知识库准不准很多问题出在这一步3.1 不同文档格式的加载方式RAG效果不好的原因有一大半不在模型而在文档加载阶段。因为现实里的文档太乱PDF里可能有扫描图片、Word里有表格、Markdown里有代码块、HTML里有广告导航。你需要先判断每个文件该用哪种解析器。以PDF为例文本型PDF可以直接提取文字扫描型PDF需要先做OCR否则检索出来的片段全是空的。Word文档里的表格如果直接提取成纯文本行列关系会丢失导致检索时语义不完整。HTML网页需要过滤掉导航、页脚、脚本标签再提取正文。我自己处理文档时会按这个顺序做一遍先抽样打开几个文件确认格式。用不同的加载器分别处理比如PDF用 PDFPlumber 或 PyMuPDFHTML用 BeautifulSoup纯文本直接用编码读取。输出清洗后的文本文件先肉眼检查前几段。确认没有乱码、截断、重复页眉再进入切分。这一步不能省。如果你直接把几百个文件一股脑灌进去之后检索结果乱掉时很难判断是哪个文件出了问题。3.2 切分策略固定长度、递归切分、语义切分怎么选文本切分决定了检索的最小单位。切得太小一个完整知识点被拆成两半检索到前半段却找不到后半段切得太大一个Chunk里包含多个主题向量化后的语义过于分散相似度检索会不准。常见的切分策略有三种固定长度切分按Token或字符数切逻辑简单但容易切断句子。递归切分按段落、句子、单词逐级切整体效果更接近自然语言。LLM框架里常用这个方法。语义切分根据句子向量相似度自动判断断点效果更好但计算成本更高。在还不清楚自己文档结构时先用递归切分chunk_size从500到1000字符之间试chunk_overlap设为chunk_size的10%到20%。这个范围是常见起点不是绝对标准。判断切分是否合适不需要看理论直接看输出。把一段真实文档切完逐一查看Chunk内容重点看三个点有没有句子被拦腰切断。每个Chunk是否包含一个相对完整的主题。相邻Chunk之间是否有足够重叠保证跨Chunk的信息不丢。如果文档本身有清晰的标题结构比如Markdown的二级标题、协议里的条款编号最好先按标题或章节边界切再对超过长度限制的块做二次切分。这种方式比纯靠字符数切更稳定。3.3 Prompt拼接和上下文长度控制切分完成之后检索回来的多个片段要拼进Prompt。这里常犯的错误是把所有TopK片段一次性塞进去不做长度控制。LLM上下文窗口有上限。如果文档片段太长拼接后可能把用户问题和指令挤出上下文模型就开始乱编。我的做法是给每个片段设置一个最大字符数在拼入Prompt前先截断保证总长度不超过模型上下文窗口的三分之二留出输出空间。Prompt里要明确告诉模型“只能根据提供的片段回答如果片段中没有答案就说不知道。”这不是废话。LLM有很强的补充倾向不加约束时它会把检索到的片段和自己记忆里的内容混在一起生成一个看起来更完整但不可靠的答案。Prompt的写法可以很简单但一定要包含三层指令角色定位、引用要求、拒答要求。例如你是一个知识库问答助手。 请根据下面提供的资料片段回答用户问题。 如果资料片段中没有足够信息请明确回答“资料中未找到相关内容”不要自行补充。 回答时尽量引用片段原意不要编造细节。这段Prompt看着普通但在实际运行中能显著降低幻觉。越到生产环境越要重视Prompt的稳定性而不是频繁换模型。4. 检索质量怎么看召回率、命中率、相关性指标是一套判断体系4.1 不能只看“答没答对”很多人测试RAG时只问几个问题看到答案大致对就说“行”。实际做知识库尤其要批量处理文档时这种验证方式远远不够。原因很简单问答是一个多环节链路最终答案正确可能只是因为某个环节碰巧对了。如果检索阶段没召回关键片段但模型凭自己的知识把答案猜出来了你反而会误以为RAG没问题。一旦换个专业问题模型答不上来你才发现是检索问题。所以要建立一套可复用的评估方式。对RAG进行质量评估不能只看“一句话好不好”至少要看“有没有召回到正确答案对应的片段”和“模型有没有使用这些片段生成答案”。4.2 关键指标召回率、命中率、MRR、上下文相关性在RAG评估中常用这几个指标它们分别回答不同的问题指标回答的问题怎么理解命中率正确片段是否出现在检索结果里只要TopK结果包含一个正确答案就算命中召回率正确片段在TopK结果中的覆盖程度适合一个答案对应多个片段的场景MRR正确答案在结果中排得靠不靠前更关注排序质量排第一和排第五意义不同上下文相关性检索出的片段是否与问题相关用来检查是不是返回了大量无关内容具体计算时会有一个测试集预先整理一批问题并为每个问题标注哪些文档片段是正确答案。然后运行RAG检查检索结果和最终答案。这个测试集不用很大二三十个覆盖不同章节的问题就足以暴露大部分问题。制作测试集时有一个小技巧不要只从文档标题里生成问题那样太容易命中。最好参考真实用户可能怎么问多写一些口语化、带同义词的问题。比如文档里写“法定节假日安排”用户可能问“过年能休几天”。这种问题能让检索压力更大也更容易暴露问题。4.3 指标不好时先调检索不要急着换模型一个常见的误区是RAG效果不好就直接换一个更大的LLM。实际上如果检索回来的片段就是错的换再大的语言模型也补不回来。模型再强也只能基于错误信息生成错误答案。正确顺序是先调检索检查召回率和命中率。检查TopK数量。检查Chunk大小和重叠度。检查Embedding模型是不是和文档领域匹配。最后才考虑调整生成模型或Prompt。这套顺序不用每次都完整跑一遍但在指标明显不好时按这个顺序排查最省时间。如果命中率低重点查Embedding模型和切分方式。如果MRR低重点查排序和查询改写。如果上下文相关性低说明检索结果里混入了大量无关片段需要提高相似度阈值或者减少TopK。这里说的“阈值”不是一个固定值你需要通过一批样本观察相似度分数的分布再决定取多少。5. 批量任务与生产化一套可复用的RAG处理流程5.1 单条跑通之后怎么设计批量导入RAG从“能跑”到“能用”的关键是批量文档怎么稳定处理。很多人本地跑单个文件没问题一旦导入几十个文件就会出现部分文件解析失败、某些Chunk为空、向量库中混入重复内容、日志里没有任何错误。我的建议是不要直接用一个大脚本把全部文件灌进去。先做一个“单文件导入”的脚本先把一个文件完整跑通确认输出没有问题。然后把文件列表改成参数逐文件循环执行每处理一个文件就打印文件名和Chunk数量。批量导入时要单独处理这几个问题文件编码不同TXT、CSV文件的编码可能不同统一转成UTF-8再处理。重复导入同一份文档如果重复导入会出现多个重复片段。建议先计算文件哈希建立已导入文件表。失败跳过单个文件失败不能中断整个批量任务要记录失败原因最后统一查看。输出命名向量库索引文件或结果文件建议按批次命名避免覆盖上一次结果。一个简单的批量循环可以写成这样import os from pathlib import Path input_dir Path(./docs) failed_files [] for file_path in input_dir.glob(*.pdf): try: chunks process_single_file(file_path) add_to_vectorstore(chunks, sourcefile_path.name) print(fOK: {file_path.name}, chunks{len(chunks)}) except Exception as e: failed_files.append((file_path.name, str(e))) print(fFAIL: {file_path.name}, error{e}) print(失败文件, failed_files)这里使用try...except捕获单个文件异常目的是让批量任务在执行过程中不中断。失败文件先记录下来统一排查比直接崩掉整个脚本更实际。5.2 缓存、失败重试和日志对于生产化RAG还要求三个能力缓存、重试、日志。缓存是为了避免同样的问题反复调用LLM。常见的做法是用问题哈希值做键把问题和答案存入数据库或缓存中间件。命中缓存时直接返回结果不再调用模型既省成本也加快响应。但要注意知识库更新后缓存要能失效否则用户会得到旧答案。失败重试主要针对接口调用时可能出现的限流、超时、临时网络错误。重试不是无限重试一般建议设定最大次数比如3次每次间隔递增。超过重试次数后要把失败记录写入日志方便人工排查。日志是很多人容易忽略的。生产环境里RAG跑得好不好不是靠“看起来对不对”而是靠日志判断。每次问答至少记录用户问题、检索TopK片段ID、最终答案、模型消耗Token数、耗时、是否命中缓存。这样出问题时才能快速定位是检索问题、生成问题还是业务逻辑问题。我见过不少RAG项目上线后一问三不知但日志里没有任何记录连“检索到了什么片段”都看不到。这种系统没办法排查。日志不用一开始就做得很复杂但至少要能回放一次问答的完整过程。5.3 接口化和Agentic RAG的升级方向批量任务稳定之后可以把RAG封装成HTTP服务让其它系统调用。接口设计不需要复杂但要把参数暴露清楚输入问题、可选的历史记录、检索TopK、是否开启缓存。返回结果最好包含答案和引用片段这样前端可以展示引用来源用户也更容易判断答案是否可信。再往后就是Agentic RAG。传统RAG是“一次检索、一次生成”Agentic RAG会根据问题做多轮检索先判断需要哪些知识再决定用不用外部工具甚至能调用多个知识库。这个方向适合复杂问题但工程复杂度也更高。如果基础RAG还没稳定不建议直接上Agent容易把排查难度放大。现在很多框架比如LangChain、LlamaIndex、Dify、Spring AI都在封装这些能力。框架封装度高能加快开发但同时也容易让人忽略底层链路。我的建议是先用框架跑通再手动把每一层拆出来看一遍。这样出问题时你至少知道该去查哪个环节。特别是“查询改写”这个功能在框架里经常会被默认开启但很多人不知道它存在。比如框架可能会自动把长问题拆成子问题或者把口语问题转成关键词。如果你发现检索结果和预期不一致先确认框架默认做了什么再决定要不要关掉或调整。6. 常见问题排查清单回答不准、内容不完整、检索不到怎么办6.1 回答不准先看检索到的片段再看Prompt如果模型回答内容“看起来像但细节不对”先不要怀疑模型参数。最优先的做法是打印出当前问题检索到的TopK片段人眼判断这些片段是否真的包含正确答案。可能的情况有三种片段里根本没有正确答案问题出在检索环节去查切分、Embedding、文档解析。片段里有正确答案但模型没用问题出在Prompt没有明确约束模型必须基于片段回答。片段顺序不对正确答案排在TopK后面被其它相似片段干扰可以调整TopK数量或相似度阈值。在代码里加一行打印把检索结果输出到日志或控制台比反复改模型参数有效得多。有时候你只要看一次实际检索结果就能发现文档里很多段落根本没被正确解析。6.2 内容不完整切分粒度、TopK、上下文长度如果答案只覆盖了问题的一部分比如问“请假流程和需要提交的材料”模型只回答了流程没提材料通常有三个原因答案需要的信息分散在两个Chunk里但只召回了其中一个。这时可以增大TopK或调大Chunk Overlap。相关片段被切分时拆散了。检查切分结果如果同一个标题下的内容被拆到多个Chunk考虑改用按标题切分。Prompt里片段总长度超出模型上下文限制被截断。控制每个片段的长度或者减少TopK数量留出空间。答案不完整和答案错误是两类问题不要用同一种方式排查。错误重点看检索是否找到不完整重点看需要的片段是否都被找到、是否都进入Prompt。专门说一个常见场景协议、专利、3GPP规范这类专业文档经常用“第X条”“第Y节”来组织内容答案可能分散在多个章节。如果切分时完全按字符数来很容易把条款内容切断。处理这类文档时建议先按条款编号或章节标题做结构化切分给每个Chunk保留来源信息比如“文档名章节号”。这样即使答案分散你也能看出还需要补查哪个片段。6.3 检索不到先查文档是否真被正确入库检索不到时有一种情况很隐蔽文档确实加载了但加载出来的全是乱码或空白。比如扫描版PDF没有OCR表格被转成无意义字符HTML解析只拿到了导航链接。此时向量库里存的就是错误内容怎么检索都找不到正确答案。排查顺序应该是用独立的脚本查看某篇文档解析后的前500字。查看该文档生成了多少个Chunk有没有空Chunk。直接对单个Chunk做相似度检索看能否召回。如果召回内容不是预期检查Embedding模型是否与文档语言、专业领域匹配。如果文档来自特殊领域比如协议、专利、3GPP规范建议先做术语归一化和段落结构拆分再入库。还有一个经常被忽略的地方文档路径和权限。如果你的程序运行在Linux服务器上文件目录名带中文、空格或特殊符号可能导致加载失败。不要只看“文件存在”要确认当前用户有读取权限进程工作目录是否正确。最后再说一个偏经验的问题有些知识库问题不是“检索不到”而是“用户问法跟文档原文差距太大”。这时候不要急着改向量库可以先做一个查询改写模块把用户问题转换成更接近文档用语的表达。比如用户问“过年能休几天”文档里写的是“法定节假日安排”如果直接原句检索可能匹配度不够改写后才更容易命中正确的片段。这部分在很多RAG课程里被归为“Agentic RAG”或“查询理解”其实在基础RAG阶段就可以做成本不高效果明显。最简单的做法是先用LLM把用户问题改写成一个或两个检索问句再拿改写后的句子去向量库检索。如果按照上面的顺序把一个知识库从零搭完你会发现RAG真正花时间的部分不在“调模型”而在“让文档变成可检索的干净内容”、“让检索结果可解释”、“让批量任务可重跑”。这个第三部分给我的最大感受是先解决数据问题再谈模型能力。踩过几次之后我也更明白很多问题不是工具不够而是前置环境和输入材料没有处理干净。