一个RAG系统是怎样运行的:从文档分块到大模型回答
本文主要结合我在MediGraph项目中的实践介绍一个RAG系统从文档进入知识库到最终生成可追溯回答的完整流程。一、为什么需要RAG直接使用大模型回答业务问题时经常会遇到三个问题大模型不了解企业内部或垂直领域的数据模型训练数据存在时间限制无法获取最新内容模型可能生成听起来合理、实际上没有依据的答案。例如在医疗问答场景中如果直接询问模型某种药物的禁忌证模型可能依据训练数据作答但我们无法确认答案来自哪份指南或药品说明书。RAGRetrieval-Augmented Generation检索增强生成的思路是先从知识库中检索与问题相关的资料再让大模型基于这些资料生成答案。它不是让模型“记住”文档而是在每次回答前为模型寻找参考资料。二、RAG的完整流程一个基本的RAG系统可以分成两条链路。1. 知识库构建链路原始文档 → 文档解析 → 文本清洗 → 文档分块 → Embedding向量化 → 写入向量数据库2. 问答链路用户问题 → 问题向量化 → 相似度检索 → 获取Top-K相关片段 → 组装Prompt → 大模型生成答案 → 返回答案和来源知识库构建通常是离线执行的而问题检索与答案生成发生在用户提问时。三、第一步解析和清洗文档知识库的原始数据可能来自多种文件PDFWordMarkdownTXT网页数据库记录。文档进入系统后需要先提取正文并清理页眉、页脚、重复换行和无意义字符。以MediGraph为例知识库中的数据主要包括医疗指南药品说明书临床案例疾病百科和医学实体关系。不同来源的可信度并不相同。医疗指南和正式说明书可以作为高置信证据而百科类资料更适合用于补充知识覆盖。因此写入知识库时不能只保存正文还需要保存来源信息metadata { doc_id: guideline-001, title: 高血压临床诊疗指南, source_type: medical_guideline, year: 2024, department: 心血管内科, }这些元数据会在后续的来源展示、过滤检索和答案审计中发挥作用。四、第二步为什么必须进行文档分块大模型和Embedding模型都不能无限接收长文档所以需要把文档切分为较小的Chunk。最简单的方式是按照固定字符数切分每500个字符切一段但这种方式容易破坏语义结构。例如一段药品说明书可能包含适应证 → 推荐剂量 → 禁忌证 → 不良反应 → 注意事项如果正好在“禁忌证”的中间切断检索到的片段就可能缺少完整语义。在MediGraph中我采用了递进式分块策略优先按照空行划分段落 → 超长段落按照句子切分 → 将短段落合并到512字符左右 → 超长内容保留50字符重叠 → 使用MD5对重复片段去重其中CHUNK_SIZE 512 CHUNK_OVERLAP 50Overlap的作用是保留相邻片段之间的上下文。例如Chunk 1……华法林可能增加出血风险需要监测INR。 Chunk 2需要监测INR。与阿司匹林联用时应特别注意……如果完全没有重叠“监测INR”和“联合用药风险”可能被切到两个互不关联的片段中。需要注意的是512 50并不是适用于所有项目的固定答案。分块大小需要根据文档类型、模型上下文长度和实际检索效果调整。五、第三步Embedding到底做了什么Embedding模型会把一段文字转换成一组向量。例如“华法林与阿司匹林能否同时使用”经过Embedding模型后会变成类似下面的数值[0.021, -0.137, 0.452, ...]语义越接近的文本其向量在空间中的距离通常越近。在MediGraph中我使用BGE-M3生成文本向量。选择它的主要原因是支持中文语义检索适合处理中英文混合的医学内容可以在本地运行能够减少医疗文本发送到外部服务的风险。需要明确的是Embedding不是让模型理解并回答问题它只负责把文本转换成便于计算相似度的向量。六、第四步向量数据库保存什么Embedding生成后需要将向量、原始文本和元数据一起写入向量数据库。在项目中我使用ChromaDB并按照数据类型划分Collectionmedical_guidelines drug_labels clinical_cases一条向量记录大致包含{ id: guideline-001-chunk-03, document: 与抗凝药物联合使用时应评估出血风险……, embedding: [0.021, -0.137, 0.452], metadata: { doc_id: guideline-001, title: 抗凝治疗指南, source_type: medical_guideline, chunk_index: 3 } }向量数据库保存的并不只是向量。如果只保存向量而不保存正文和来源后续即使检索到了相关结果也无法把原始证据交给大模型更无法向用户展示答案来源。七、第五步用户提问时如何检索用户提出问题后系统会使用相同的Embedding模型对问题进行向量化query_vector embedding_model.embed( 华法林和阿司匹林能否同时使用 )然后在ChromaDB中计算问题向量与文档向量的相似度并取回最相关的Top-K片段。results collection.query( query_embeddings[query_vector], n_results5 )这里的Top-5并不代表五条内容一定正确只表示它们在当前向量空间中最相似。实际开发中我遇到过一个典型问题系统确实返回了五条内容但其中没有真正支持答案的证据。原因可能包括知识库本身没有相关内容文档分块破坏了完整语义查询中的药品名称没有正确识别向量检索只找到了语义相似但实体不同的内容Top-K设置不合理。所以“检索到了五条内容”不等于“检索正确”。八、第六步把检索结果交给大模型获取相关片段后需要将它们组织成Prompt。一个简化版本如下你是一名医疗辅助问答助手。 请仅根据以下参考资料回答问题。 如果资料不足请明确说明未找到足够依据。 不要编造药品信息、剂量或指南内容。 参考资料 [来源1抗凝治疗指南] …… [来源2阿司匹林药品说明书] …… 用户问题 华法林和阿司匹林能否同时使用项目中还将生成温度设置得较低temperature 0.2低温度可以减少模型在相同上下文下的随机发挥但它不能从根本上消除幻觉。真正重要的是Prompt要求模型只能依据证据回答检索片段带有来源信息没有有效证据时允许模型拒答患者数值等事实由数据库工具提供而不是交给模型猜测。九、第七步为什么答案必须携带来源如果系统只返回一段答案用户无法判断内容是否可靠。因此每个检索片段都应保留doc_idcollection文档标题来源类型原始文本。最终结果可以同时返回答案和来源{ answer: 两种药物联用可能增加出血风险需要评估患者情况并监测相关指标。, sources: [ { doc_id: guideline-001, collection: medical_guidelines, title: 抗凝治疗指南 } ] }前端再通过doc_id collection查询原始片段。这样RAG系统提供的就不只是一段“看起来正确”的文字而是一条能够回到原始资料的证据链。十、实际开发中遇到的四个问题1. 文档数量增加但检索效果没有明显提升原因通常不是文档越多越好而是新增内容存在重复、来源不明或者分类不清。我的处理方式是使用MD5对重复Chunk去重对不同知识来源分级保存年份、科室和来源类型避免低置信百科内容覆盖正式指南。2. 向量检索找到了相似内容却找错了实体医学问题中药品名称、疾病名称和相互作用关系非常重要。单纯向量检索可能找到“语义相似”的资料却不一定命中正确实体。因此后续我又加入了Neo4j图谱检索并使用RRF融合两路结果。这部分将在下一篇文章中单独介绍。3. 检索为空时模型仍然尝试回答如果没有对空结果进行判断大模型可能依靠自身知识继续生成答案。项目中增加了空结果检查检索结果 chunks [] → 标记结果不完整 → 尝试调整实体或切换工具 → 达到最大轮次后说明证据不足4. 无法判断优化是否真正有效不能只看生成答案是否“读起来不错”。RAG至少需要分别评估Evidence HitK正确证据是否进入前K名MRR正确证据排得是否靠前关键事实正确率引用支持率无依据回答率正确拒答率。这些指标将在后续的RAG评测文章中详细说明。十一、一个最小化的RAG伪代码将整个流程压缩后大致如下def build_knowledge_base(documents): for document in documents: text parse_document(document) chunks split_text( text, chunk_size512, overlap50 ) for chunk in chunks: vector embedding_model.embed(chunk.text) vector_db.add( documentchunk.text, embeddingvector, metadatachunk.metadata ) def answer_question(question): query_vector embedding_model.embed(question) contexts vector_db.search( query_vector, top_k5 ) if not contexts: return { answer: 当前知识库未找到足够依据, sources: [] } prompt build_prompt( questionquestion, contextscontexts ) answer llm.generate( prompt, temperature0.2 ) return { answer: answer, sources: extract_sources(contexts) }实际项目还需要处理鉴权、异常、重试、缓存、审计、流式输出和数据权限但RAG的核心链路基本如此。十二、总结一个完整的RAG系统并不是简单调用一次向量数据库而是一条连续的数据链路文档质量 → 分块质量 → Embedding效果 → 检索效果 → 上下文组织 → 生成约束 → 来源追溯 → 效果评测其中任何一个环节存在问题都可能导致最终答案不准确。我在MediGraph项目中的一个重要认识是RAG效果不好时不应该首先修改Prompt而应该先检查知识库是否包含答案、文档是否正确分块、检索是否命中证据以及模型最终是否严格依据证据回答。