一个RAG系统是怎样运行的:从文档分块到大模型回答

本文主要结合我在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,而应该先检查知识库是否包含答案、文档是否正确分块、检索是否命中证据,以及模型最终是否严格依据证据回答。

相关推荐
Lucas_coding1 小时前
【LLM】多平台思考模式的开启、关闭与响应判断
语言模型·ai编程
RockHopper20251 小时前
AI时代模型生命周期与软件资产重构:概要版
人工智能·系统架构·软件工程·ai编程·世界模型
ZzT2 小时前
同一个 skill 存了三份:多 agent 环境下的 skill 管理与两款开源工具
ai编程·claude
熊猫钓鱼>_>2 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
开发语言·华为·kotlin·ai编程·inject·鸿蒙·openharmony
VIP_CQCRE2 小时前
在 Visual Studio 里接入 Ace Data Cloud:用 LMLocal 直接调用 OpenAI 兼容模型
openai·ai编程·visual studio·ace data cloud·lmlocal
AINative软件工程3 小时前
LLM 上下文满了别直接报错:Context Eviction 工程实践,5 种淘汰策略的生产对比
后端·llm·ai编程
SuperHeroWu73 小时前
TraeCode 国内版接入 DevEco CLI:用官方知识开发鸿蒙应用
ai编程·harmonyos·知识库·trae·aicoding·skills·deveco cli
自律懒人3 小时前
TileLang 从零上手:5 步用 Python 写出一个矩阵乘法算子
ai编程
熊猫钓鱼>_>14 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 KMPNotifier 实现 OpenHarmony 本地通知引擎
kotlin·ai编程·harmonyos·鸿蒙·openharmony·适配·kmp