学习目标:
1. RAG 技术概述
2. RAG WorkFlow 及 RAG 工程化
3. 基于 LlamaIndex 快速构建 RAG 项目(不使用 HuggingFace加载大模型,使用Ollama部署的本地大模型
4. 使用 LlamaIndex 存储和读取 Embedding 向量
5. 追踪哪些文档片段被用于检索增强生成
6. 深度剖析 RAG 检索底层实现细节
7. 自定义 RAG Prompt Template
8. RAG 项目企业级生产部署最佳实践

一、 RAG 技术概述

1.1 大模型目前固有的局限性

大语言模型(LLM)是概率生成系统
知识时效性 :模型知识截止于训练数据时间点( 联网搜索
推理局限性 :本质是概率预测而非逻辑运算,复杂数学推理易出错( DeepSeek-R1的架构有所不同
专业领域盲区 :缺乏垂直领域知识
幻觉现象 :可能生成看似合理但实际错误的内容

1.2 什么是 RAG?

RAG(Retrieval Augmented Generation)顾名思义,通过 检索 的方法来增强 生成模型 的能力。
一个典型的 RAG 工作流程可以分为三个主要阶段: 检索 ->  增强 ->  生成
阶段一:检索
  1. 知识库准备

    • 收集所有可能用到的文档(PDF, Word, 网页,数据库等)。

    • 将这些文档切分成更小的、语义完整的文本块。这是因为模型有上下文token长度限制,且小块信息更易于精准检索。【这里注意最好是结构化

    • 使用嵌入embedding 模型将这些文本块转换为向量。这个过程被称为“向量化”。向量可以理解为一段文本在高维空间中的数学表示,语义相似的文本其向量在空间中的距离也更近。

    • 将所有文本块的向量及其对应的原始文本存储到向量数据库中。

  2. 实时检索

    • 当用户提出一个问题时,系统使用同一个嵌入模型将这个问题也转换为一个向量。

    • 在向量数据库中进行相似性搜索,找出与问题向量最相似的几个文本块向量。这些被找出来的文本块就是与问题最相关的“参考资料”。

阶段二:增强
  • 将用户的原始问题 和 检索到的相关文本块 组合在一起,构建成一个新的、富含上下文的提示。

  • 这个提示通常会遵循一个精心设计的模板,例如:

    请根据以下提供的背景信息来回答问题。
    
    【背景信息】
    {这里插入检索到的相关文本块1}
    {这里插入检索到的相关文本块2}
    ...
    
    【问题】
    {用户的原始问题}
    
    【要求】
    如果背景信息足以回答问题,请根据背景信息生成答案。如果背景信息不足以回答,请说明你无法根据提供的信息回答问题。
  • 这个过程就是“增强”,它用外部知识增强了原始提示。

阶段三:生成
  • 将这个增强后的提示发送给大语言模型。

  • 大语言模型基于这个包含背景信息的提示,生成一个精准、相关且可靠的答案。

  • 最终,系统将答案和(可选的)引用的源文档返回给用户。

二、RAG 工程化

2.1 RAG系统的基本搭建流程

搭建过程:
1. 文档加载,并按一定条件 切割 成片段 -> 向量化
2. 将切割的文本片段灌入 检索引擎
3. 封装 检索接口
4. 构建 调用流程 :Query -> 检索 -> Prompt -> LLM -> 回复

三、项目环境配置

3.1 使用 conda 创建项目环境

安装anaconda,打开你的命令行,或者

# 创建环境
conda create -n tcm-ai-rag python=3.10
# 激活环境
conda activate tcm-ai-rag

3.2 安装项目所需依赖库

# 安装 LlamaIndex 相关包
pip install llama-index
pip install llama-index-embeddings-huggingface
pip install llama-index-llms-huggingface
# 安装 CUDA 版本 Pytorch
pip install torch==2.6.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cu118

四、模型下载

安装Ollama,官网:Ollama

然后直接在Ollama就可以搜索选择模型下载,后面的8b就是模型参数量大小,越大模型效果越好,但也吃显存,选择一个合适的即可。

当然,也可以去魔搭的模型库直接搜索下载:模型库首页 · 魔搭社区

为什么使用Ollama

  1. 性能考虑:Ollama 通常比直接加载 HuggingFace 模型更节省资源

  2. 懒人方便直接下载部署大模型。

    方式 优点 缺点
    HuggingFace直接加载 完全控制,无需网络 占用显存大,加载慢
    Ollama API 资源占用小,启动快 需要服务运行,网络依赖

推荐使用 Ollama 方式,特别是对于大模型,它能更好地管理资源并提供稳定的服务。

如果你数据也特别大、就需要把数据和模型都加载到显存里。

4.1 下载 Embedding 模型权重

embedding 模型比较小,直接用使用 modelscope 提供的 sdk 进行模型下载 

使用BAAI开源的中文bge模型作为embedding模型,使用modlescope提供的SDK将模型权重下载到本地(这里下载的模型放在D盘的 AIProject / modelscope):

  • BAAI/bge-large-zh: 智源出的中文Embedding模型,在中文任务上表现非常出色。

from modelscope import snapshot_download
# model_id 模型的id
# cache_dir 缓存到本地的路径
model_dir = snapshot_download(model_id="BAAI/bge-base-zh-v1.5", cache_dir="D:/AIProject/modelscope")

Ollama也可以找一个300MB大小的embedding模型,自己选择【后面都要注意自己模型放的位置】

五、构建中医临床诊疗术语证候问答

5.1 语料准备

本应用使用的文档是由国家卫生健康委员和国家中医药管理局发布的中医临床诊疗术语:
《中医临床诊疗术语第1部分:疾病》(修订版).docx
《中医临床诊疗术语第2部分:证候》(修订版).docx
《中医临床诊疗术语第3部分:治法》(修订版).docx
下载途径,有三个 国家标准全文公开系统、国家中医药管理局官网、登录中国政府网
        我用的第一个系统
  • 国家标准全文公开系统访问路径进入国家标准化管理委员会主办的 “国家标准全文公开系统”(https://openstd.samr.gov.cn/bzgk/gb),在搜索栏输入标准号或名称即可查询。

    • 输入 “GB/T 16751.1-2023” 可直达《中医临床诊疗术语 第 1 部分:疾病》现行版本
    • 同理,输入 “GB/T 16751.2-2021” 和 “GB/T 16751.3-2023” 可分别获取证候和治法部分的全文。

  • 国家中医药管理局官网http://www.natcm.gov.cn),点击导航栏中的 “政策文件” 栏目,在搜索框输入 “中医临床诊疗术语” 即可找到相关通知及附件。
  • 登录中国政府网www.gov.cn),在 “政策” 栏目下选择 “国务院部门文件”,搜索 “中医临床诊疗术语” 即可找到两部门联合发布的通知及附件。

也可直接从文章顶部资源绑定下载

类似于这种结构化的比较好

5.2 基于 LlamaIndex 来快速构建知识库

5.2.1 导入所需的包

import logging
import sys
import torch
from llama_index.llms.ollama import Ollama
from llama_index.core import PromptTemplate, Settings, SimpleDirectoryReader, VectorStoreIndex, load_index_from_storage, StorageContext, QueryBundle
from llama_index.core.schema import MetadataMode
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.huggingface import HuggingFaceLLM
from llama_index.core.node_parser import SentenceSplitter

5.2.2 定义日志配置

# 定义日志配置
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

5.2.3 使用 llama_index_llms_Ollama 调用本地大模型

# 使用 Ollama 部署的 deepseek-r1 模型
llm = Ollama(
    model="deepseek-r1:8b", # 模型名称
    base_url="http://localhost:11434",# Ollama服务地址
    system_prompt="你是一个有帮助的AI助手,请用中文回答",
    request_timeout=1200.0,# 请求超时时间
    context_window=8192,  # 上下文窗口大小,根据模型能力调整
    max_new_tokens=2048,# 生成的最大新 token 数
    temperature=0.1,# 温度 [0-1] ,模型的随机性(是不是固定的回答【如果有知识库的时候】){创意生成、文案写作的可以往上调}
    # 其他可选参数
    top_p=0.9,# 核采样 [0-1],控制词汇多样性
)

Settings.llm = llm

Ollama提供了丰富的参数来配置模型行为、有些模型支持、有的模型不支持、但大部分都支持。

下面是一些参数,你可以选择调整的。

llm = Ollama(
    # 1. 基本模型参数
    model="deepseek-r1:8b",           # 模型名称
    base_url="http://localhost:11434", # Ollama服务地址
    request_timeout=120.0,            # 请求超时时间    

    # 2. 生成参数
    temperature=0.7,                  # 温度 [0-1],控制随机性
    top_p=0.9,                       # 核采样 [0-1],控制词汇多样性
    top_k=40,                        # 候选词数量
    num_predict=2048,                # 最大生成token数
    repeat_penalty=1.1,              # 重复惩罚 [1.0-2.0]
    presence_penalty=0.0,            # 存在惩罚
    frequency_penalty=0.0,           # 频率惩罚 
    
    # 3. 上下文和系统设置    
    context_window=8192,             # 上下文窗口大小
    system_prompt="你是一个有帮助的AI助手",  # 系统提示词
    num_ctx=4096,                    # 上下文长度
    num_thread=4,

    # 3. 高级配置
    seed=42,                         # 随机种子,确保可重复性
    mirostat=0,                      # Mirostat采样 [0,1,2]
    mirostat_tau=5.0,                # Mirostat tau参数
    mirostat_eta=0.1,                # Mirostat eta参数
    tfs_z=1.0,                       # 尾部自由采样
    stop=["\n", "。", "!"],         # 停止序列
​​​​​​​)

5.2.4 使用 llama_index_embeddings_huggingface 调用本地 embedding 模型

刚刚下载的embedding模型就在这里调用了

# 调用本地 embedding 模型
Settings.embed_model = HuggingFaceEmbedding(
 model_name="D:/AIProject/modelscope/BAAI/bge-base-zh-v1___5"
)

5.2.5 读取文档

SimpleDirectoryReader是llamaindex加载文档的函数

# 读取文档 required_exts指定加载文档的类型,最好是txt,因为自然语言的格式不乱,embedding效果更好
# 在你当前文件夹下新建一个documents文件夹,把数据换成txt格式放到documents文件夹下

# 读取文档 required_exts指定加载文档的类型,最好是txt,因为自然语言的格式不乱,embedding效果更好
# 在你当前文件夹下新建一个documents文件夹,把数据换成txt格式放到documents文件夹下
documents = SimpleDirectoryReader("./documents", required_exts=[".txt"]).load_data()

5.2.6 对文档进行处理【重点】

  1. 对原始文档进行切分
  2. 将切分后的文本片段转换为embedding向量表示
  3. 基于向量化结果构建高效索引
# 对文档进行切分,将切分后的片段转化为embedding向量,构建向量索引
index = VectorStoreIndex.from_documents(documents, transformations=[SentenceSplitter(chunk_size=256)])

代码解释:

        VectorStoreIndex:这是一个索引类(常见于 LlamaIndex 等框架),用于管理文档的向量表示。它的本质是将文本转换为数值向量(通过嵌入模型)并组织存储,方便后续通过 “语义相似性” 快速检索相关内容。

        from_documents:这是VectorStoreIndex的类方法,用于从原始文档直接创建索引。它简化了 “文档处理→向量生成→索引构建” 的流程。

        documents:传入的参数,是一个文档集合(通常是列表形式)刚刚加载的txt文件。这些文档可以是从文本文件、PDF、网页等提取的原始文本数据(例如Document对象列表)。

transformations=[SentenceSplitter(chunk_size=256)]:文档预处理的转换步骤,这里使用了

        SentenceSplitter(句子分割器):

        SentenceSplitter的作用是将长文档切割成较小的文本块(“chunk”)。

        chunk_size=256表示每个文本块的长度限制(通常以字符或词为单位,具体取决于实现),即每个块大约包含 256 个字符。

你可以对SentenceSplitter 参数详细设置(不详细设置的话就用默认的就行):
预设会以 1024 个 token 为界切割片段, 每个片段的开头重叠上一个片段的 200 个 token 的内容。
chunk_size=1024, # 切片 token 数限制
chunk_overlap=200, # 切片开头与前一片段尾端的重复 token
paragraph_separator='\n\n\n', # 段落的分界
secondary_chunking_regex='[^,.; 。?! ]+[,.; 。?! ]?' # 单一句子的样式
separator=' ', # 最小切割的分界字元

5.2.7 将 embedding 向量和向量索引存储到文件中

#将 embedding 向量和向量索引存储到文件中
index.storage_context.persist(persist_dir='doc_emb')

在当前目录下新建一个doc_emb文件夹,储存这些向量,用storage_context.persist方法给它储存下来,方便下次调用。

这时候有一个问题、为啥要用向量索引,向量索引又是什么:

向量索引(Vector Index)是专门为高维向量(如 embedding 向量)设计的 “快速检索数据结构”,核心作用是解决 “当向量数量极大时(如百万、亿级),如何快速找到与查询向量相似的向量” 的问题。

为什么需要向量索引?

假设你有 100 万个文档,每个文档对应一个 768 维的 embedding 向量。当用户输入一个查询(比如 “如何学习 Python”),系统会先将查询转为向量,然后需要从 100 万个向量中找到 “最相似” 的 Top N 个(这是 RAG 等语义检索的核心步骤)。

如果没有索引,只能用 “暴力搜索”:逐个计算查询向量与 100 万个向量的相似度(如余弦相似度),然后排序取 Top N。但这种方式有两个致命问题:

  • 速度太慢:每个相似度计算需要 O (d) 时间(d 是向量维度,如 768),100 万向量就是 O (10⁶×d),实际中可能需要几秒甚至几十秒,无法满足实时需求。
  • 资源消耗大:大量计算会占用极高的 CPU/GPU 资源,成本不可接受。

向量索引的作用就是:通过预先构建特殊的数据结构,将原本 “全量比对” 的过程优化为 “定向快速筛选”,大幅降低计算量,让相似向量检索从 “不可用” 变得 “实时可用”

向量索引的核心原理

向量索引本质是一种 “空间组织策略”—— 它不存储原始向量的全部信息,而是通过对向量的空间分布进行 “预处理”,构建一个 “导航结构”,让查询时能快速缩小范围,只对少量候选向量计算相似度。

常见的向量索引类型:精确索引近似索引

1. 精确索引(Exact Index)
  • 原理:保证能找到所有真正相似的向量,无遗漏。
  • 典型代表:KD 树(KD-Tree)、球树(Ball Tree)。
  • 局限性:仅适合低维向量(如 d<20),当维度超过 50 时,查询速度会急剧下降(“维度灾难”),无法处理百万级以上向量。
2. 近似索引(Approximate Index,主流选择)
  • 原理:牺牲极少量召回率(比如从 100% 降到 95%),换取 10-100 倍的速度提升,在实际场景中(如搜索、推荐)用户几乎感知不到差异。
  • 典型代表
    • IVF(Inverted File Index,倒排文件索引):先对所有向量做聚类(比如聚成 1000 个簇),每个簇有一个 “中心向量”。查询时,先找到与查询向量最接近的几个簇(比如 Top 10),只在这些簇内的向量中计算相似度。核心:通过 “聚类缩小范围”,把 100 万向量的比对变成 10 个簇(假设每个簇 1000 个向量)共 1 万向量的比对,速度提升 100 倍。

    • HNSW(Hierarchical Navigable Small World,分层导航小世界):构建多层 “图结构”:底层是所有向量的连接图(每个向量连接几个近邻),上层是精简的 “导航图”(保留更重要的连接)。查询时从顶层开始,通过导航图快速定位到大致区域,再逐层下探到具体向量。优势:速度极快,是目前工业界最常用的索引之一(Milvus、FAISS 等库均支持)。

    • Annoy(Approximate Nearest Neighbors Oh Yeah):构建多棵随机二叉树,每棵树通过随机超平面将向量空间二分。查询时,在每棵树中快速找到候选向量,最后合并结果。适合中小规模向量集。

向量索引的关键指标

  • 召回率(Recall):索引返回的 “相似向量” 中,真正属于 “最相似 Top N” 的比例(近似索引会低于 100%)。
  • 查询延迟(Latency):单条查询从输入到返回结果的时间(目标是毫秒级)。
  • 构建时间(Indexing Time):构建索引所需的时间(数据量越大,时间越长)。
  • 存储空间(Memory/Disk Usage):索引本身占用的存储(通常远小于原始向量,但因结构不同差异较大)。

5.2.8 构建查询引擎

基于已构建的向量索引(index),创建一个用于处理用户查询的 “查询引擎” 对象,并指定每次查询仅返回与查询语义最相似的前 5 个文本片段

query_engine = index.as_query_engine(similarity_top_k=5)

query_engine
这是创建后的查询引擎对象(用as_query_engine方法),是后续处理用户查询的核心工具。它封装了 “查询向量化→相似文本检索→结果整理(或结合大模型生成回答)” 的完整逻辑,只需调用它的query()方法输入问题,就能直接得到结果。

index
即之前通过VectorStoreIndex.from_documents()创建的向量存储索引。它内部存储了切割后的文本块及其对应的 embedding 向量,还有对应索引,是查询引擎的 “数据来源”。

as_query_engine()
这是VectorStoreIndex类的核心方法,作用是将 “静态的向量索引” 转换为 “可交互的查询引擎”。它相当于给向量索引套了一层 “交互接口”,让索引从 “仅存储向量的数据结构” 变成 “能理解用户查询、返回相关结果的工具”。

similarity_top_k=5
这是查询引擎的关键参数,控制检索结果的数量:
similarity:表示检索逻辑基于 “语义相似度”(通过向量相似度计算)。
top_k=5:表示每次查询时,仅从向量索引中返回与查询向量语义最相似的前 5 个文本块(即 Top 5 相似片段)。

查询引擎的工作流程:

        当你用query_engine.query("你的问题")发起查询时query_engine会按以下步骤执行:

查询向量化 -> 相似文本检索(返回前五个相似度最高的) -> 结果处理与返回

查询引擎会将检索到的 5 个文本块整理成结构化结果,具体返回形式取决于配置:

  • 默认基础模式:直接返回这 5 个文本块的内容及相似度分数。
  • 结合大模型模式(RAG 核心):将这 5 个文本块作为 “上下文”,传递给大模型(如 GPT、Llama),让模型基于这些精准上下文生成自然、连贯的回答,而非依赖模型自身的训练数据。

5.2.9 最后一步,调用生成

response = query_engine.query("疲乏无力,消瘦,,可伴见腹胀如水状,大便或黑,皮肤燥痒可能是哪些病症?")
print(response)

至此已经完成了RAG的检索 -> 增强 -> 生成

六、使用LlamaIndex存储和读取embedding向量

在对文档进行切分,将切分后的片段转化为embedding向量,构建向量索引时,会花费大量的时间
,所以如果你保存过这些向量,可以直接给它加载进来继续用。
已经有了就不需要在构建向量了,以下是详细说明:
  • default_vector_store.json:用于存储embedding向量
  • docstore.json:用于存储文档切分出来的片段
  • graph_store.json:用于存储知识图数据
  • image__vector_store.json:用于存储图像数据
  • index_store.json:用于存储向量索引

存储

# 将embedding向量和向量索引存储到文件中
# ./doc_emb 是存储路径
index.storage_context.persist(persist_dir='./doc_emb')
# 很方便的集成目前主流的向量数据集 chroma

上面用这个persist方法存储,如何读取呢?先从存储文件中加载,再直接构建到内存中

# 从存储文件中读取embedding向量和向量索引
storage_context = StorageContext.from_defaults(persist_dir="doc_emb")
# 根据存储的embedding向量和向量索引重新构建检索索引
index = load_index_from_storage(storage_context)

把这三步可以直接换成上面的就可以了

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐