☰
一个RAG系统是怎样运行的:从文档分块到大模型回答
2026/10/7 8:31:29 网站建设 项目流程

本文主要结合我在MediGraph项目中的实践,介绍一个RAG系统从文档进入知识库,到最终生成可追溯回答的完整流程。

一、为什么需要RAG

直接使用大模型回答业务问题时,经常会遇到三个问题:

  1. 大模型不了解企业内部或垂直领域的数据;
  2. 模型训练数据存在时间限制,无法获取最新内容;
  3. 模型可能生成听起来合理、实际上没有依据的答案。

例如,在医疗问答场景中,如果直接询问模型某种药物的禁忌证,模型可能依据训练数据作答,但我们无法确认答案来自哪份指南或药品说明书。

RAG(Retrieval-Augmented Generation,检索增强生成)的思路是:

先从知识库中检索与问题相关的资料,再让大模型基于这些资料生成答案。

它不是让模型“记住”文档,而是在每次回答前为模型寻找参考资料。

二、RAG的完整流程

一个基本的RAG系统可以分成两条链路。

1. 知识库构建链路

原始文档 → 文档解析 → 文本清洗 → 文档分块 → Embedding向量化 → 写入向量数据库

2. 问答链路

用户问题 → 问题向量化 → 相似度检索 → 获取Top-K相关片段 → 组装Prompt → 大模型生成答案 → 返回答案和来源

知识库构建通常是离线执行的,而问题检索与答案生成发生在用户提问时。


三、第一步:解析和清洗文档

知识库的原始数据可能来自多种文件:

  • PDF;
  • Word;
  • Markdown;
  • TXT;
  • 网页;
  • 数据库记录。

文档进入系统后,需要先提取正文,并清理页眉、页脚、重复换行和无意义字符。

以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 = 50

Overlap的作用是保留相邻片段之间的上下文。

例如:

Chunk 1:……华法林可能增加出血风险,需要监测INR。 Chunk 2:需要监测INR。与阿司匹林联用时应特别注意……

如果完全没有重叠,“监测INR”和“联合用药风险”可能被切到两个互不关联的片段中。

需要注意的是,512 + 50并不是适用于所有项目的固定答案。分块大小需要根据文档类型、模型上下文长度和实际检索效果调整。


五、第三步:Embedding到底做了什么

Embedding模型会把一段文字转换成一组向量。

例如:

“华法林与阿司匹林能否同时使用?”

经过Embedding模型后,会变成类似下面的数值:

[0.021, -0.137, 0.452, ...]

语义越接近的文本,其向量在空间中的距离通常越近。

在MediGraph中,我使用BGE-M3生成文本向量。选择它的主要原因是:

  • 支持中文语义检索;
  • 适合处理中英文混合的医学内容;
  • 可以在本地运行;
  • 能够减少医疗文本发送到外部服务的风险。

需要明确的是,Embedding不是让模型理解并回答问题,它只负责把文本转换成便于计算相似度的向量。


六、第四步:向量数据库保存什么

Embedding生成后,需要将向量、原始文本和元数据一起写入向量数据库。

在项目中,我使用ChromaDB,并按照数据类型划分Collection:

medical_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_results=5 )

这里的Top-5并不代表五条内容一定正确,只表示它们在当前向量空间中最相似。

实际开发中,我遇到过一个典型问题:

系统确实返回了五条内容,但其中没有真正支持答案的证据。

原因可能包括:

  • 知识库本身没有相关内容;
  • 文档分块破坏了完整语义;
  • 查询中的药品名称没有正确识别;
  • 向量检索只找到了语义相似但实体不同的内容;
  • Top-K设置不合理。

所以,“检索到了五条内容”不等于“检索正确”。


八、第六步:把检索结果交给大模型

获取相关片段后,需要将它们组织成Prompt。

一个简化版本如下:

你是一名医疗辅助问答助手。 请仅根据以下参考资料回答问题。 如果资料不足,请明确说明未找到足够依据。 不要编造药品信息、剂量或指南内容。 参考资料: [来源1:抗凝治疗指南] …… [来源2:阿司匹林药品说明书] …… 用户问题: 华法林和阿司匹林能否同时使用?

项目中还将生成温度设置得较低:

temperature = 0.2

低温度可以减少模型在相同上下文下的随机发挥,但它不能从根本上消除幻觉。

真正重要的是:

  • Prompt要求模型只能依据证据回答;
  • 检索片段带有来源信息;
  • 没有有效证据时允许模型拒答;
  • 患者数值等事实由数据库工具提供,而不是交给模型猜测。

九、第七步:为什么答案必须携带来源

如果系统只返回一段答案,用户无法判断内容是否可靠。

因此,每个检索片段都应保留:

  • doc_id;
  • collection;
  • 文档标题;
  • 来源类型;
  • 原始文本。

最终结果可以同时返回答案和来源:

{ "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 Hit@K:正确证据是否进入前K名;
  • MRR:正确证据排得是否靠前;
  • 关键事实正确率;
  • 引用支持率;
  • 无依据回答率;
  • 正确拒答率。

这些指标将在后续的RAG评测文章中详细说明。


十一、一个最小化的RAG伪代码

将整个流程压缩后,大致如下:

def build_knowledge_base(documents): for document in documents: text = parse_document(document) chunks = split_text( text, chunk_size=512, overlap=50 ) for chunk in chunks: vector = embedding_model.embed(chunk.text) vector_db.add( document=chunk.text, embedding=vector, metadata=chunk.metadata ) def answer_question(question): query_vector = embedding_model.embed(question) contexts = vector_db.search( query_vector, top_k=5 ) if not contexts: return { "answer": "当前知识库未找到足够依据", "sources": [] } prompt = build_prompt( question=question, contexts=contexts ) answer = llm.generate( prompt, temperature=0.2 ) return { "answer": answer, "sources": extract_sources(contexts) }

实际项目还需要处理鉴权、异常、重试、缓存、审计、流式输出和数据权限,但RAG的核心链路基本如此。


十二、总结

一个完整的RAG系统并不是简单调用一次向量数据库,而是一条连续的数据链路:

文档质量 → 分块质量 → Embedding效果 → 检索效果 → 上下文组织 → 生成约束 → 来源追溯 → 效果评测

其中任何一个环节存在问题,都可能导致最终答案不准确。

我在MediGraph项目中的一个重要认识是:

RAG效果不好时,不应该首先修改Prompt,而应该先检查知识库是否包含答案、文档是否正确分块、检索是否命中证据,以及模型最终是否严格依据证据回答。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询