RAG入门到RAG优化

根据YellowDuck的上两篇博客LangChian和LangGraph的学习,YellowDuck已经学会了如何自制一个简易的Agent,但是YellowDuck发现,为什么自制的Agent的能力总是泛泛而谈,似乎更多的是横向的扩展,并没有某一方面的特化,于是YellowDuck开始思考...
惊人的发现,原来很多软件的内置Agent并不仅仅是接一个API大模型那么简单,原来他们有一个很关键的东西,那就是他们各自企业的庞大的知识库,YellowDuck开始思考...如何构建一个根据自己的知识库领域垂直特化的Agent呢?
YellowDuck大吃一惊,原来RAG可以解决这个问题,YellowDuck开始学习...

吉伊卡哇引人入胜版(后续有正经流程图,如果觉得整个看起来复杂)
ChatGPT Image 2026年5月10日 17_09_03

RAG

YellowDuck Tips:RAG,顾名思义,Retrieval-Augmented Generation,检索增强生成,从字面理解,就是根据检索出来的信息,去增强我们模型生成的答案,说白了,让AI说的话有迹可循,形象的比喻,一个学者有很强的学习能力,但是如果我给他专门看一个领域的书籍,他把所有知识全部学会,装进脑子里,说话的时候,一提到相关的,他就会自动蹦出相关文献。根据这个比喻,RAG的过程也正如这个过程。

RAG简版手搓

准备工作,收集书籍(学者过程)
准备工作,收集资料(可为任意类型,word,pdf,excel...)(RAG过程)
 
准备工作,书籍整理(学者过程)
准备工作,资料整理(包含切分,向量化)(RAG过程)
 
构建知识大脑,书籍入脑(学者过程)
构建知识仓库,资料入库(持久化chormaDB...)(RAG过程) #准备过程结束


接收对话,对话翻译(学者过程)
接收对话,对话向量化(RAG过程)
 
对话思考,头脑风暴(脑中寻找相关知识ing)(学者过程)
对话思考,向量库风暴(向量库中寻找相关知识ing)(RAG过程)
 
想到相关知识,整合资料,思考如何表述(学者过程)
找到相关片段,整合片段,思考如何输出(RAG过程)

至此YellowDuck发现,这不跟YellowDuck的思考模式完全一样吗?真是太棒了!YellowDuck继续学习,如何写代码呢?

#常用头文件
import os
import pickle
import jieba
from dotenv import load_dotenv

from langchain_community.document_loaders import PyPDFLoader
from langchain_community.retrievers import BM25Retriever

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

问题一:读取

下表绿色为已经写出读取方法

文件 / 数据类型 读取器 用途
.txt - [YES] TextLoader 读取普通文本
.pdf - [YES] PyPDFLoader 读取 PDF
.csv - [YES] CSVLoader 读取表格 CSV
.docx - [YES] Docx2txtLoader 读取 Word
.json - [NO] JSONLoader 读取 JSON
文件夹 - [YES] DirectoryLoader 批量读取一个文件夹
网页 - [YES] WebBaseLoader 读取网页内容
Markdown - [NO] TextLoader / UnstructuredMarkdownLoader 读取 .md 文件
HTML - [NO] UnstructuredHTMLLoader 读取 HTML 文件
任意常见文件 - [NO] UnstructuredFileLoader 自动尝试解析多种文件

这里为各种读取方式生成表格 可以按需查看 图中有些YellowDuck认为比较复杂 故标出常用的讲解 便于后续阅读 未讲解用法的后续博客会详细说明

(1)普通文本读取

Q: 适合什么?
A: .txt

from langchain_community.document_loaders import TextLoader

loader = TextLoader("data/example.txt", encoding="utf-8")
raw_text = loader.load()

(2)PDF读取

Q: 适合什么?
A: 你读过PDF吗?你个 ... 当然是适合PDF啦~

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("data/example.pdf")
raw_text = loader.load()

(3)Word文档读取

Q: 适合什么?
A: 你读过word吗?你个 ... 当然是适合word啦~(没开二度) 一般是.docx

from langchain_community.document_loaders import Docx2txtLoader

loader = Docx2txtLoader("data/example.docx")
raw_text = loader.load()

(4)CSV读取

Q: 适合什么?
A: .csv

from langchain_community.document_loaders import CSVLoader

loader = CSVLoader("data/example.csv", encoding="utf-8")
raw_text = loader.load()

(5)网页读取

Q: 适合什么?
A: 官网文档 博客文章 帮助中心 在线知识库 产品说明页

from langchain_community.document_loaders import WebBaseLoader

loader = WebBaseLoader("https://example.com")
raw_text = loader.load()

(6)文件夹读取

from langchain_community.document_loaders import DirectoryLoader

loader = DirectoryLoader("data/")
raw_text = loader.load()

历经千辛万苦,YellowDuck发现终于学会最简单的如何读取文件,虽然还有很多类文件不太明白...YellowDuck觉得够用了,对于读取文件已经小有感触,赶紧进行下一步吧...

问题二:切分

(1)使用LangChain切分器

这里使用最为方便和简洁的LangChain切分

#实际最简单写法
from langchain_text_splitters import RecursiveCharacterTextSplitter

def split(chunk_size,chunk_overlap,raw_text):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=[
        "\n\n",
        "\n",
        "。",
        "!",
        "?",
        ";",
        ",",
        "、",
        " ",
        ""
    ],
    is_separator_regex=False
    #这些 separators 只是普通字符串,不是正则表达式。

    )

    chunks = splitter.split_documents(raw_text)

    return chunks

问题三:放入向量库

这里使用chormaDB,可以直接在你电脑本地跑,不用一开始就搞服务器、集群、云数据库。官方文档也说它可以在本机运行,并且内置了开始所需的东西。

(1)加载向量模型

from langchain_openai import OpenAIEmbeddings

embedding = OpenAIEmbeddings(
  model="..."
  api_key="..."
  base_url="..."
  
  dimensions="..."
  chunk_size="..."
  
  check_embedding_ctx_length=False
  #这行很重要,不开的话这里又会进行自动切分,导致传递参数问题
)

(2)加载向量库

from langchain_chroma import Chroma

vector_store = Chroma(
    collection_name="my_rag_collection",
    #普通数据库里面是多种表 向量数据库里面是多个collection集合

    embedding_function=embeddings,
    #向量化模型选择 可自定义

    persist_directory="./chroma_db",
    #就是把向量库保存到当前项目下的 chroma_db 文件夹。
    #如果这个文件夹不存在,它会创建。
    #如果这个文件夹已经有数据,它会加载之前的数据。
)

(3)存入向量库

#docs = [...]

ids = [f"text_chunk{i}" for i in range(len(docs)]

vector_store.add_documents(
    documents=docs,
    ids=ids
)

print(f"已经存入{len(chunks)}个chunk")

问题四:接收和处理输入

def question_answer(user_in):
    docs = vector_store.similarity_search(user_in, k=3)
    #去查找相关片段 取topK=3
    #返回的是Document列表类型    

    if not docs:
        return "知识库里没有检索到相关资料,我不知道。"

    context_list = []
    #用于整理返回内容
    sources = []
    #保存来源信息

    for i, doc in enumerate(docs, start=1):
    #遍历每一个doc,i从1开始
        metadata = doc.metadata or {}
        #取出每一一个document类的metadata

        source = metadata.get("source", "未知来源")
        page = metadata.get("page", None)
        #取出元数据中重要的来源和页数        

        if page is not None:
            page_show = page + 1
        else:
            page_show = "未知页码"
        #处理一下页数
        
        context_list.append(
f"""
【资料{i}】
来源:{source}
页码:第 {page_show} 页
内容:
{doc.page_content}
"""
#整理一下结构 分出资料1.2.3.4 来源 页码 内容 (给模型看)
         )

        sources.append({
            "source": source,
            "page": page_show
        })
#处理一下资源列表,后续方便展示 (给读者看
)
    context = "\n\n".join(context_list)
 #处理一下每一个资料,换行链接,合成字符串

    prompt = ChatPromptTemplate.from_template(""" 
你是一个专业的健康顾问,但不是医生,你只能根据参考文档回答。

要求:
1. 只根据参考文档回答。
2. 如果参考文档里没有答案,直接说:文档资料没有提到,我不知道。
3. 不要编造参考文档里没有的内容。
4. 你只是提供健康建议,不能代替医生诊断。
5. 回答要通俗易懂,说人话。
6. 不要在正文里标注来源。

参考文档:
{context}

用户问题:
{user_in}
""")
#构建提示词

    chain = prompt | llm | StrOutputParser()
#langchain写法ICEL链式编排

    answer = chain.invoke({
        "context": context,
        "user_in": user_in
    })
#向prompt注入所需数据

    source_text = "\n".join([
        f"{i + 1}. {item['source']},第 {item['page']} 页"
        for i, item in enumerate(sources)
    ])
#结构化输出来源信息 方便查看

    return f"{answer}\n\n参考文档:\n{source_text}"
#返回字符串
#结构为
#{answer}
#
#
#参考文档:{source_text}

完整代码及返回结果:

点击查看代码
import os
from dotenv import load_dotenv

from langchain_community.document_loaders import PyPDFLoader,TextLoader ,Docx2txtLoader, CSVLoader,WebBaseLoader,DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings,ChatOpenAI

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

def load_pdf(path):
    loadder = PyPDFLoader(path)
    raw_my_docs = loadder.load()

    return raw_my_docs

def split(chunk_size,chunk_overlap,raw_my_docs):

    splitters = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=[
        "\n\n",
        "\n",
        "。",
        "!",
        "?",
        ";",
        ",",
        "、",
        " ",
        ""
        ],
        is_separator_regex=False,
    )

    chunks = splitters.split_documents(raw_my_docs)

    print(f"切分出{len(chunks)}个chunk")
    return chunks

def vector_in(chunks):

    ids = [f"text_new_chunk{i}" for i in range(len(chunks))]

    vector_store.add_documents(
        documents=chunks,
        ids=ids
    )

    print(f"成功入库{len(chunks)}个chunk")

def question_answer(user_in):

    docs = vector_store.similarity_search(user_in,k=3)

    context_list = []
    source_list = []

    for i,doc in enumerate(docs,start=1):
        metadata = doc.metadata

        source = metadata.get("source","未知来源")
        page = metadata.get("page",None)

        if page:
            page_show = page + 1
        else:
            page_show = "未知页码"

        context_list.append(
f"""
[资料{i}]
来源:{source}
页码:第 {page_show} 页
内容:
{doc.page_content}
"""
        )

        source_list.append({
            "source": source,
            "page": page_show
        })

    context = "\n\n".join(context_list)

    prompt = ChatPromptTemplate.from_template(
"""
你是一个专业的健康顾问,但不是医生,你只能根据参考文档回答。

要求:
1. 只根据参考文档回答。
2. 如果参考文档里没有答案,直接说:文档资料没有提到,我不知道。
3. 不要编造参考文档里没有的内容。
4. 你只是提供健康建议,不能代替医生诊断。
5. 回答要通俗易懂,说人话。
6. 不要在正文里标注来源。

参考文档:
{context}

用户问题:
{user_in}
"""
    )

    chain = prompt | llm | StrOutputParser()

    answer = chain.invoke({
        "context": context,
        "user_in": user_in
    })

    source_text = "\n".join([
        f"{i + 1}. {item['source']},第 {item['page']} 页"
        for i, item in enumerate(source_list)
    ])

    return f"{answer}\n\n参考文档:\n{source_text}"
        

if __name__ == '__main__':
    path = r"...儿童青少年生长迟缓食养指南 2023版.pdf"

    load_dotenv()

    # raw_my_docs = load_pdf(path)
    # chunks = split(500,100,raw_my_docs)

    embedding = OpenAIEmbeddings(
        model=os.getenv("DASHSCOPE_EMBEDDING_MODEL"),
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        base_url=os.getenv("DASHSCOPE_BASE_URL"),

        dimensions=1024,
        chunk_size=10,

        check_embedding_ctx_length=False,
    )

    vector_store = Chroma(
        collection_name="my_rag_collection",
        embedding_function=embedding,
        persist_directory="./chroma_db",
    )

    # vector_in(chunks)

    llm = ChatOpenAI(
        model=os.getenv("DASHSCOPE_CHAT_MODEL"),
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        base_url=os.getenv("DASHSCOPE_BASE_URL"),
    )

    user_in = input("you question:").strip()
    answer = question_answer(user_in)

    print(answer)

返回结果:
you question:青少年生长缓慢怎么办
青少年生长缓慢可能和长期营养不均衡、饮食不合理或不良饮食习惯有关。首先要注意优化饮食,保证食物多样化,选择营养密度高的食物,确保摄入足够的蛋白质、维生素和矿物质。

如果存在挑食、偏食、食欲差等情况,要逐步调整饮食行为,规律进餐,避免过度节食。同时,可以考虑在专业人士指导下合理补充必要的营养素。

如果生长发育长期不理想,改善效果不明显,或者怀疑有疾病因素,应及时去医院就诊,明确原因,不能只靠饮食调节。

总之,重点是吃好三餐,营养均衡,必要时寻求专业指导,严重情况务必就医。

参考文档:
1. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 12 页
2. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 3 页
3. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 4 页

到这里YellowDuck发现已经可以很轻松的读懂代码,同时也可以自己手搓出一个最简单的RAG,不需要依靠AI的帮助了,只需要拿出需要的头文件,就可以手搓出一套。是的在AI时代YellowDuck依然坚持手搓,以上代码基本均为手搓,有少数提示词模板借用AI的帮助,但是对于YellowDuck来说,总感觉差点东西,也就是我们的RAG,现在是否过于简陋,YellowDuck开始思考...

RAG优化

本流程图由AI生成,暂时只包含三个优化chunk优化,补充BM25关键词的混合检索优化,以及rerank优化,便于理解,后续继续更新...

ChatGPT Image 2026年5月10日 16_58_33

 
Q1:普通RAG会有什么问题?

A1:
1.问得明明在 PDF 里,结果查不到
2.查出来的 chunk 不相关
3.查出来太多废内容
4.模型根据不完整资料乱答
5.回答太慢
6.同一份资料重复入库
7.中文 PDF 切分效果不好
8.表格、标题、章节信息丢失

Q2:为什么需要RAG优化?

A2:
1.让检索查的更准,如果资料拿错,后续如何优化都是徒劳,所以RAG优化首先优化的是找资料的能力
2.为了减少大模型的胡说,大模型可能会根据自己的知识回答,但我的目标是根据制定PDF回答,而不是让他自由发挥

Q3:RAG优化的具体方向?

A3:
1.文档加载优化
2.chunk切分优化
3.embedding模型优化
4.检索策略优化
5.rerank重排优化
6.prompt优化
7.数据库优化

Q4:RAG什么情况下需要优化?什么情况不用?

AI提供
A4:
需要优化RAG
1.基于 PDF / Word / TXT 的知识问答
2.企业内部知识库
3.医疗健康知识助手
4.法律 / 合同问答
5.客服机器人
6.论文 / 学术助手
7.代码文档助手
8.多文档综合分析

不需要优化RAG
1.普通聊天
2.写一篇作文
3.翻译一句话
4.解释一个通用概念
5.写简单 Python 代码
6.做数学计算

Tips:RAG优化的概念也就是 文档 → 切分 → 向量化 → 向量数据库 → 检索 → 拼接上下文 → LLM回答 对于这一条流水线,每个其实都有各自不同的优化策略,其中由于一个个展开讲,过于复杂,所以这里首先主要优化检索部分

RAG优化

这里由于是初学 仅仅暂时做最常见和简单的优化的解释和实现

(1)chunk优化

首先chunk切的不好,RAG会很笨,这里建议手动实验对比,以下附上YellowDuck实验效果

问题 后果
chunk 太小 上下文不够,回答断章取义
chunk 太大 找回来的内容太杂,模型抓不住重点
overlap 太小 上下文容易断
overlap 太大 重复内容太多,浪费 token
1.
# raw_my_docs = load_pdf(path)
# chunks = split(300,50,raw_my_docs)    
# vector_in(chunks)

切分出101个chunk
成功入库101个chunk

you question:青少年生长缓慢怎么办
青少年生长缓慢,首先要从饮食和生活习惯入手。关键是要保证营养均衡,多吃富含优质蛋白质的食物,比如瘦肉、鱼、禽类、蛋和大豆制品。每天要喝奶或吃奶制品来补钙,同时注意补充维生素D,必要时可在专业人士指导下适当补充。

还要多吃新鲜蔬菜和水果,尤其是深色蔬菜,同时可以吃一些动物肝脏或动物血来预防缺铁性贫血,必要时也可在指导下补充铁剂。

避免偏食,主食不要只吃一种,可以用杂粮、薯类替换部分米饭或面条,肉类也要换着吃,比如猪肉换鸡肉、鱼虾等,让食物种类更丰富。

如果长期生长发育不理想,改善后效果不明显,或者怀疑有疾病原因,建议及时去医院检查,找专业医生评估。

记住,这些只是健康建议,具体问题还得由医生判断。

参考文档:
1. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 12 页
2. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 7 页
3. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 3 页
2.
# raw_my_docs = load_pdf(path)
# chunks = split(500,100,raw_my_docs)    
# vector_in(chunks)

切分出64个chunk
成功入库64个chunk

you question:青少年生长缓慢怎么办
青少年生长缓慢,首先要注意是否因为长期饮食不均衡、营养摄入不足导致的。这种情况会影响身高发育,也可能和挑食、偏食、食欲不好、吃得太少或家长喂养方式不当有关。

建议从以下几个方面改善:

1. **合理搭配饮食**:保证每天吃的食物种类丰富,包括谷类、蔬菜水果、优质蛋白(如鱼、禽、蛋、奶、豆制品)等,提高食物的营养密度,也就是在同样分量下获取更多营养。

2. **规律进餐**:一日三餐定时定量,避免暴饮暴食或长时间不吃,尤其不能跳过早餐。

3. **关注微量营养素**:如果孩子有缺铁、缺锌、缺维生素D等情况,可能影响生长发育。必要时可在专业人员指导下适当补充营养素。

4. **中医角度调理**:有些孩子胃口差、容易积食、消化不良,中医认为是“脾常不足”,脾胃功能弱,影响营养吸收。可以通过调整饮食习惯和生活作息来改善。

5. **及时就医**:如果长期个子长得慢,尝试调整饮食后效果也不明显,或者怀疑有疾病因素,一定要去医院检查,排除病理原因。

总之,先从日常饮食和生活习惯入手,同时密切观察生长情况,有问题及时找专业医生评估,不要自行判断或拖延。

参考文档:
1. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 12 页
2. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 3 页
3. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 4 页
3.
# raw_my_docs = load_pdf(path)
# chunks = split(800,150,raw_my_docs)    
# vector_in(chunks)

you question:青少年生长缓慢怎么办
青少年生长缓慢可能和长期饮食不合理、营养摄入不足有关,比如蛋白质、能量、维生素或矿物质吃得不够。这种情况属于长期营养不良的一种表现。

首先,要从饮食入手,保证每天吃的食物种类丰富,营养均衡。重点多吃富含优质蛋白的食物(如鱼、肉、蛋、奶、豆制品),同时搭配新鲜蔬菜水果,主食也要粗细搭配。提高食物的营养密度,也就是在适量的食物里获取更多营养。

如果孩子有挑食、偏食、厌食或者胃口不好等问题,需要慢慢调整饮食习惯,规律三餐,避免零食代替正餐。

要是已经长期吃得不错但身高增长还是不明显,或者怀疑有缺乏某些营养素的情况,建议在医生或营养指导人员的帮助下评估是否需要补充一些营养素,比如钙、铁、锌或维生素D等。

特别提醒:如果生长迟缓改善效果不明显,或者怀疑是疾病引起的(比如内分泌问题、慢性病等),一定要及时去医院检查,不能只靠调整饮食解决。

总之,先从科学饮食做起,必要时寻求专业人员帮助,别自己乱补或拖延就医。

参考文档:
1. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 12 页
2. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 4 页
3. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 3 页

最直观来看在仅仅改变了chunk_size和chunk_overlap的情况下,明显第2种更规范,具有前面的序号,以及markdown格式的加粗标记,所以可以直观理解但是并不能确定为这个参数,只是说在我的pdf中选择500,100的参数可以获得更好的效果

(2)BM25关键检索

它像“传统搜索引擎”,看用户问的问题里有哪些关键词,然后找包含这些关键词的资料

检索方式 擅长
向量检索 用户换个说法也能找
BM25 用户问了明确关键词时很准
混合检索 两个都用,更稳
#BM25实现
import pickle
import jieba

from langchain_community.retrievers import BM25Retriever

def vector_in(...):
    ...
    with open(BM25_DOCS_PATH, "wb") as f:
    pickle.dump(chunks, f)
    #按照路径大概或者创建文件f,进行存入dump
    ...

def chinese_tokenizer(text):
    """
    给 BM25 用的中文分词函数。
    说人话:把一句中文切成一个个词。
    """
    return list(jieba.cut(text))

def load_bm25_retriever(k=5):
    """
    加载 BM25 检索器。
    注意:必须先入库一次,生成 bm25_docs.pkl。
    """

    if not os.path.exists(BM25_DOCS_PATH):
        raise FileNotFoundError(
            f"找不到 {BM25_DOCS_PATH},请先选择入库模式,生成 BM25 文档。"
        )

    with open(BM25_DOCS_PATH, "rb") as f:
        docs = pickle.load(f)
    #按路径打开文件f进行读取

    bm25_retriever = BM25Retriever.from_documents(
        docs,
        #Document类型

        preprocess_func=chinese_tokenizer,
        #儿童生长迟缓怎么吃
        #分词可能出现多种可能:
        #A 儿童 / 生长 / 迟缓 / 怎么 / 吃
        #B 儿童 / 青少年 / 生长 / 迟缓 / 食养 / 指南
        #这些词都对上了,于是认为这段资料相关。如果不分词,中文可能会被当成一长串字,BM25 效果会很差
        
        k=k,
        #返回Topk
    )
    #实例化检索器,

    return bm25_retriever

-----------------------------------------------------------------------------------------
#混合检索实现
def hybrid_search(user_in, bm25_k=5, vector_k=5, final_k=5):
    """
    混合检索:
    BM25 找关键词匹配的资料。
    Chroma 找语义相似的资料。
    最后合并去重。
    """

    bm25_docs = bm25_retriever.invoke(user_in)
    vector_docs = vector_store.similarity_search(user_in, k=vector_k)
    #使用BM25和向量数据库分别检索

    docs = []
    seen = set()

    for doc in bm25_docs + vector_docs:
        source = doc.metadata.get("source", "")
        page = doc.metadata.get("page", "")
        content_head = doc.page_content[:80]
        #获取键所需参数        

        key = (source, page, content_head)
        #用来源,页码,前80个字符,作为key,去重        

        if key not in seen:
            docs.append(doc)
            seen.add(key)
        #更新

    return docs[:final_k]
    #返回前final_k个

附上结构整理,BM25优化,chunk优化完整代码,以及附上同样问题的回复效果

点击查看代码
import os
import pickle
import jieba
from dotenv import load_dotenv

from langchain_community.document_loaders import PyPDFLoader
from langchain_community.retrievers import BM25Retriever

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


# =========================
# 1. 全局配置
# =========================

PDF_PATH = r"C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf"

BM25_DOCS_PATH = "./bm25_docs.pkl"
COLLECTION_NAME = "my_rag_collection"
DB_PATH = "./chroma_db"

CHUNK_SIZE = 500
CHUNK_OVERLAP = 100

BM25_K = 5
VECTOR_K = 5
FINAL_K = 5


# =========================
# 2. 文档加载与切分
# =========================

def load_pdf(path):
    """
    读取 PDF。
    返回 LangChain 的 Document 列表。
    """
    loader = PyPDFLoader(path)
    return loader.load()


def split_documents(raw_docs, chunk_size=500, chunk_overlap=100):
    """
    把大文档切成小块。
    chunk 说人话:就是一小段资料。
    """
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=[
            "\n\n",
            "\n",
            "。",
            "!",
            "?",
            ";",
            ",",
            "、",
            " ",
            "",
        ],
        is_separator_regex=False,
    )

    chunks = splitter.split_documents(raw_docs)

    print(f"切分出 {len(chunks)} 个 chunk")
    return chunks


# =========================
# 3. 初始化模型和数据库
# =========================

def create_embedding_model():
    """
    创建 embedding 模型。
    embedding 说人话:把文字变成数字,方便机器比较相似度。
    """
    return OpenAIEmbeddings(
        model=os.getenv("DASHSCOPE_EMBEDDING_MODEL"),
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        base_url=os.getenv("DASHSCOPE_BASE_URL"),
        dimensions=1024,
        chunk_size=10,
        check_embedding_ctx_length=False,
    )


def create_vector_store(embedding):
    """
    创建 Chroma 向量数据库。
    Chroma 说人话:本地知识库。
    """
    return Chroma(
        collection_name=COLLECTION_NAME,
        embedding_function=embedding,
        persist_directory=DB_PATH,
    )


def create_llm():
    """
    创建大模型。
    """
    return ChatOpenAI(
        model=os.getenv("DASHSCOPE_CHAT_MODEL"),
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        base_url=os.getenv("DASHSCOPE_BASE_URL"),
    )


# =========================
# 4. BM25 相关
# =========================

def chinese_tokenizer(text):
    """
    给 BM25 用的中文分词函数。
    分词说人话:把一句中文切成一个个词。
    """
    return [word.strip() for word in jieba.cut(text) if word.strip()]


def save_bm25_docs(chunks):
    """
    保存 chunks,给 BM25 使用。
    """
    with open(BM25_DOCS_PATH, "wb") as f:
        pickle.dump(chunks, f)

    print(f"BM25 文档已保存到 {BM25_DOCS_PATH}")


def load_bm25_retriever(k=5):
    """
    加载 BM25 检索器。
    BM25 说人话:按关键词找资料。
    """
    if not os.path.exists(BM25_DOCS_PATH):
        raise FileNotFoundError(
            f"找不到 {BM25_DOCS_PATH},请先选择入库模式。"
        )

    with open(BM25_DOCS_PATH, "rb") as f:
        docs = pickle.load(f)

    retriever = BM25Retriever.from_documents(
        docs,
        preprocess_func=chinese_tokenizer,
    )

    retriever.k = k

    return retriever


# =========================
# 5. 入库、删除
# =========================

def ingest_documents(vector_store, chunks):
    """
    入库:
    1. 存进 Chroma,给向量检索用。
    2. 存成 pkl,给 BM25 用。
    """
    ids = [f"text_new_chunk_{i}" for i in range(len(chunks))]

    vector_store.add_documents(
        documents=chunks,
        ids=ids,
    )

    save_bm25_docs(chunks)

    print(f"成功入库 {len(chunks)} 个 chunk")


def delete_collection(vector_store):
    """
    删除 Chroma collection 和 BM25 文件。
    collection 说人话:知识库里的一张表。
    """
    vector_store.delete_collection()
    print(f"{COLLECTION_NAME} 已删除")

    if os.path.exists(BM25_DOCS_PATH):
        os.remove(BM25_DOCS_PATH)
        print(f"{BM25_DOCS_PATH} 已删除")


# =========================
# 6. 混合检索
# =========================

def hybrid_search(
    user_question,
    bm25_retriever,
    vector_store,
    bm25_k=5,
    vector_k=5,
    final_k=5,
):
    """
    混合检索:
    1. BM25 按关键词找
    2. Chroma 按意思找
    3. 合并
    4. 去重
    5. 返回前 final_k 条
    """

    # 设置 BM25 返回数量
    bm25_retriever.k = bm25_k

    # 关键词检索
    keyword_docs = bm25_retriever.invoke(user_question)

    # 语义检索
    semantic_docs = vector_store.similarity_search(
        user_question,
        k=vector_k,
    )

    final_docs = []
    seen_keys = set()

    for doc in keyword_docs + semantic_docs:
        unique_key = (
            doc.metadata.get("source", ""),
            doc.metadata.get("page", ""),
            doc.page_content[:80],
        )

        if unique_key not in seen_keys:
            final_docs.append(doc)
            seen_keys.add(unique_key)

    return final_docs[:final_k]


# =========================
# 7. 构造上下文
# =========================

def build_context(docs):
    """
    把检索到的资料整理成 prompt 里的参考文档。
    """
    context_list = []
    source_list = []

    for i, doc in enumerate(docs, start=1):
        metadata = doc.metadata

        source = metadata.get("source", "未知来源")
        page = metadata.get("page", None)

        if page is not None:
            page_show = page + 1
        else:
            page_show = "未知页码"

        context_list.append(
            f"""
[资料{i}]
来源:{source}
页码:第 {page_show} 页
内容:
{doc.page_content}
"""
        )

        source_list.append({
            "source": source,
            "page": page_show,
        })

    context = "\n\n".join(context_list)

    return context, source_list


# =========================
# 8. 问答
# =========================

def answer_question(
    user_question,
    llm,
    bm25_retriever,
    vector_store,
):
    """
    完整问答流程:
    1. 混合检索
    2. 拼接上下文
    3. 调用大模型
    4. 返回答案和来源
    """
    docs = hybrid_search(
        user_question=user_question,
        bm25_retriever=bm25_retriever,
        vector_store=vector_store,
        bm25_k=BM25_K,
        vector_k=VECTOR_K,
        final_k=FINAL_K,
    )

    context, source_list = build_context(docs)

    prompt = ChatPromptTemplate.from_template(
        """
你是一个专业的健康顾问,但不是医生,你只能根据参考文档回答。

要求:
1. 只根据参考文档回答。
2. 如果参考文档里没有答案,直接说:文档资料没有提到,我不知道。
3. 不要编造参考文档里没有的内容。
4. 你只是提供健康建议,不能代替医生诊断。
5. 回答要通俗易懂,说人话。
6. 不要在正文里标注来源。

参考文档:
{context}

用户问题:
{user_question}
"""
    )

    chain = prompt | llm | StrOutputParser()

    answer = chain.invoke({
        "context": context,
        "user_question": user_question,
    })

    source_text = "\n".join([
        f"{i + 1}. {item['source']},第 {item['page']} 页"
        for i, item in enumerate(source_list)
    ])

    return f"{answer}\n\n参考文档:\n{source_text}"


# =========================
# 9. 菜单功能
# =========================

def run_ingest(vector_store):
    """
    入库模式。
    """
    raw_docs = load_pdf(PDF_PATH)
    chunks = split_documents(
        raw_docs,
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP,
    )
    ingest_documents(vector_store, chunks)


def run_qa(llm, vector_store):
    """
    提问模式。
    """
    bm25_retriever = load_bm25_retriever(k=BM25_K)

    while True:
        user_question = input("your question,输入 q 退出:").strip()

        if user_question.lower() == "q":
            print("已退出提问模式")
            break

        if not user_question:
            print("问题不能为空")
            continue

        answer = answer_question(
            user_question=user_question,
            llm=llm,
            bm25_retriever=bm25_retriever,
            vector_store=vector_store,
        )

        print("\n" + answer + "\n")


def run_delete(vector_store):
    """
    删除模式。
    """
    confirm = input(f"确认删除 {COLLECTION_NAME} 吗?输入 yes:").strip()

    if confirm == "yes":
        delete_collection(vector_store)
    else:
        print("已取消删除")


# =========================
# 10. 主函数
# =========================

def main():
    """
    主函数只做三件事:
    1. 初始化环境
    2. 初始化模型和数据库
    3. 根据用户选择调用不同功能
    """
    load_dotenv()

    embedding = create_embedding_model()
    vector_store = create_vector_store(embedding)
    llm = create_llm()

    while True:
        print("\n请选择模式:")
        print("1 = 入库")
        print("2 = 提问")
        print("3 = 删除 collection")
        print("q = 退出")

        mode = input("请输入:").strip()

        if mode == "1":
            run_ingest(vector_store)

        elif mode == "2":
            run_qa(llm, vector_store)

        elif mode == "3":
            run_delete(vector_store)

        elif mode.lower() == "q":
            print("程序已退出")
            break

        else:
            print("请输入 1、2、3 或 q")


if __name__ == "__main__":
    main()

返回结果:
your question,输入 q 退出:青少年生长缓慢怎么办

青少年生长缓慢,首先要考虑是不是因为营养跟不上,比如蛋白质、能量、维生素或矿物质吃得不够,导致身高增长不达标。这种情况属于长期营养不良的一种。

你可以这样做:

1. **先做评估**:定期检查孩子的身高、体重,最好能连续观察6个月到1年,看看生长速度有没有明显变慢。同时要了解孩子平时的饮食、运动、睡眠和心理状态,综合判断。

2. **调整饮食**:保证每天吃够富含优质蛋白的食物,比如鸡蛋、鱼、瘦肉、奶类、豆制品;主食粗细搭配,多吃新鲜蔬菜水果,确保营养全面均衡。

3. **养成好习惯**:避免挑食、厌食、节食等不良饮食行为,规律三餐,尤其要吃好早餐。少吃高油、高糖、加工零食。

4. **充足睡眠和运动**:保证每天有足够的睡眠,多进行跳绳、篮球、游泳等有助于长高的运动。

5. **中医调理参考**:如果孩子长期食欲不好,中医认为可能和脾胃功能有关,可以通过饮食调养来改善。

重要的是,这个指南主要是针对营养问题引起的生长缓慢,不是用来处理疾病原因(如生长激素缺乏、慢性病等)的。如果情况比较严重,建议及时找医生或专业营养师,在指导下制定适合孩子的个性化方案。

总之,别着急,先从吃好、睡好、动好做起,持续观察变化。

参考文档:
1. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 3 页
2. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 2 页
3. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 4 页
4. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 11 页
5. C:\Users\向梒宇\Desktop\health_konwlege\通用\儿童青少年生长迟缓食养指南 2023版.pdf,第 3 页

这里因篇幅原因暂时不继续讲解,后续补上 RAG优化策略续

Tips:本篇同样为新手YellowDuck初学过程,理解不到位或者有细小错误务必告知YellowDuck,一定修改

posted @ 2026-05-10 11:06  LiYellowDuck  阅读(40)  评论(0)    收藏  举报