【Langchain快速本地知识库】

该文章已生成可运行项目,

一、什么是RAG

RAG(Retrieval-Augmented Generation)

  • 检索增强生成,是一种结合信息检索(Retrieval)和文本生成(Genertaion)的技术。

  • RAG技术通过实时检索相关文档或信息,并将其作为上下文输入到生成模型中,提高生成结果的时效性和准确性。
    在这里插入图片描述

二、RAG的优势

  • 减少模型幻觉:通过引入外部的知识,RAG可以减少模型的生成的内容的不可性(一本正经的胡说八道)

  • 提高专业领域回答的质量:结合专业知识库,生成更专业的回答

  • 解决知识时效性问题:基座模型的数据基本都是静态数据,无法涵盖最新信息,通过RAG工具可以通过外挂知识库实时更新信息

三、RAG的核心流程

在这里插入图片描述

 1. 数据预处理

 构建知识库:收集本整理文档、PDF、网页等数据源,构建知识库

 文档分块:知识库中的文档,将文档分为合适大小的片段

 文本量化:使用嵌入模型将文本转化为向量,并存储到向量数据库中

2. 检索阶段

查询处理:

将用户输入的问题转换为向量,并在向量数据库中进行相似度检索,找到最相关的文本片段

重排序:

对检索结果进行相关性排序,选择最相关的片段作为生成阶段的输入

3. 生成阶段

构建上下文:将从知识库中检索到的相关文档片段与用户问题结合,形成增强的上下文输入

生成回答:基座大模型基于构建的上下文生成最终回答

四、Langchain快速搭建本地知识库检索

1、本地环境准备

本地安装好conda环境

配置好api key (如使用阿里百炼平台)

2、搭建检索流程

加载本地文档,并将文档分割为片段

将分割后的文档,使用嵌入模型量化,并灌入向量数据库

加载本地向量数据库,封装检索方法

构建查询流程:Query ->检索 -> Prompt -> LLM -> 回答

3、导入依赖包

from typing import Tuple, List
from PyPDF2 import PdfReader  # 引入PDF阅读器
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import  ChatOpenAI # 引入openai的嵌入模型(可以不需要写)
from langchain_community.embeddings import DashScopeEmbeddings  # 引入阿里百练平台提供的嵌入模型
from langchain.text_splitter import RecursiveCharacterTextSplitter  # 引入解析器
from langchain_community.vectorstores import FAISS      # 引入向量数据库
from dotenv import load_dotenv
from langchain.prompts import PromptTemplate

#加载环境变量
load_dotenv()

API_KEY = os.getenv("DASHSCOPE_API_KEY")

4、读取PDF文件内容

def from_pdf_extract_text_page_number(pdf_name) -> Tuple[str, List[int]]:
    """
    从传入的PDF文件中获取文本和对应的页码数,并返回
    """
    __text = ""
    page_numbers = []

    for __page_number,page in enumerate(pdf_name.pages, start=1):  # 用于在遍历可迭代对象时同时获取元素的索引和值
        extract_text = page.extract_text()
        if extract_text:
            __text += extract_text
            page_numbers.extend([__page_number] * len(extract_text.split("\n")))
        else:
            logging.warning(f"No text extract on page {__page_number}")
    return __text, page_numbers

5、分割文本,并量化存储到本地

def splitter_and_embedding(text_info:str,text_page_number:List[int],save_path:str = None) -> FAISS :
    """
    将文本分割,调用嵌入模型,量化
    :return:
    """
    # 创建文本分割器,将文本内容分割
    text_splitter = RecursiveCharacterTextSplitter(
        separators= ["\n\n","\n",".","。"," "],
        chunk_size= 512,
        chunk_overlap= 128,
        length_function= len  # 也可以使用tokenizer方式
    )

    # 分割文本
    text_chunks = text_splitter.split_text(text_info)
    logging.debug(f"text_chunks: {text_chunks}, len(text_chunks): {len(text_chunks)}")
    print(f"文本被分为 {len(text_chunks)} 个chunk")

    # 使用阿里百炼平台提供的embedding模型
    embeddings = DashScopeEmbeddings(model="text-embedding-v2",dashscope_api_key=API_KEY)

    # 加载已有的FAISS索引
    if save_path and os.path.exists(os.path.join(save_path, "index.faiss")):
        # 加载已有的FAISS索引
        local_knowledge = FAISS.load_local(folder_path=save_path,embeddings=embeddings,allow_dangerous_deserialization=True)
        logging.debug(f"加载本地已经存在的FAISS数据库,从{save_path}路径")
    else:
        # 如果没有则创建一个新的索引
        local_knowledge = FAISS.from_texts(text_chunks,embeddings)
        logging.debug(f"已经创建一个新的索引,该索引的长度为{len(text_chunks)}")
    # 添加新的文本块到FAISS索引
    local_knowledge.add_texts(text_chunks)
    # 合并页码信息
    page_info_path = os.path.join(save_path, "page_info.pkl")
    existing_page_info = {}
    if os.path.exists(page_info_path) and os.path.getsize(page_info_path) > 0:
        try:
            with open(page_info_path, "rb") as f:
                existing_page_info = pickle.load(f)
        except (EOFError, pickle.UnpicklingError) as e:
            print(f"警告: page_info.pkl 文件损坏或为空,将重新初始化。错误: {e}")
            existing_page_info = {}

    # 生成新页码信息
    new_page_info = {chunk: text_page_number[i] for i, chunk in enumerate(text_chunks)}
    existing_page_info.update(new_page_info)
    local_knowledge.page_info = existing_page_info

    # 保存更新后的 FAISS 索引和页码信息
    if save_path:
        # 确保存在目录
        os.makedirs(save_path, exist_ok=True)

        # 保存 FAISS 索引
        local_knowledge.save_local(folder_path=save_path)
        print(f"FAISS 索引已更新并保存到: {save_path}")

        # 保存更新后的页码信息
        with open(page_info_path, "wb") as f:
            pickle.dump(existing_page_info, f) # type ignore
        print(f"页码信息已更新并保存到: {page_info_path}")

    return local_knowledge

6、加载本地向量数据库信息

def load_local_knowledge(local_path:str,embeddings=None) -> FAISS :
    """
    从磁盘加载向量数据库和页码信息
    """
    # 判断是否传入embedding,如果没有则填入一个
    local_knowledge_load = None
    if embeddings is None:
        embeddings = DashScopeEmbeddings(model="text-embedding-v2",dashscope_api_key=API_KEY)

    # 加载向量数据库
    local_knowledge_load = FAISS.load_local(embeddings=embeddings,allow_dangerous_deserialization=True,folder_path=local_path)
    logging.debug(f"向量数据已经从{local_path}加载")

    # 加载页码信息
    page_info_path = os.path.join(local_path,"page_info.pkl")
    if os.path.exists(page_info_path):
        with open(page_info_path, "rb") as f:
            page_info = pickle.load(f)
        local_knowledge_load.page_info = page_info
        print(f"页码信息已加载:{page_info_path}")
    else:
        print("未找到页码信息")

    return local_knowledge_load

7、定义用户的查询函数,通过知识库进行问题回答

def user_query(query:str, knowledge_bass):

    if query is None:
        print("请输入需要查询的问题")
        return None
    else:
        # 执行相似度搜索,找到与查询相关的文档
        docs = knowledge_bass.similarity_search(query)
        # 初始化对话大模型
        chat_llm = ChatOpenAI(
            api_key=API_KEY,
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
            model="deepseek-v3",
            streaming=True,
            verbose=True
        )

        #将文档内容合并为上下文
        context = "\n".join(doc.page_content for doc in docs)
        # 构建提示词模板
        template = f"""
        以下为与问提相关的上下文信息:{context}
        请根据上述信息回答以下问题,如果有额外问题回答不知道即可:
        问题:{query}
        回答: 
        """
        # 创建提示词模板
        prompt = PromptTemplate.from_template(template)
        # 创建llm_chain
        llm_chain = (
            {"context": lambda x:x["context"],"query": RunnablePassthrough()}
            | prompt
            | chat_llm
        )
        # 构建输入数据
        input_data = {"context":context,"query":query}

        # 回答流式输出
        answer_text = ""

        for chunk in llm_chain.stream({"query": query, "context": context}):
            if hasattr(chunk, "content"):
                content = chunk.content
            else:
                content = str(chunk)
                answer_text += content
            print(content, end="", flush=True)

        # # 记录唯一的页码值
        unique_pages = set()

        # 显示每个文档块的页码来源
        unique_pages = [doc.page_content for doc in docs]
        #print("\n\n【来源】")
        for page_index,source_info in enumerate(unique_pages,1):
            pass
            #print(f"<UNK>{page_index}<UNK>,{source_info[:-1]}")
        return {
            "info": answer_text,
            "sources": unique_pages

        }

8、函数入口

if __name__ == '__main__':

    path = os.path.dirname(__file__)
    pdf_path = os.path.join(path,"第2章_RAG技术与应用\\PDF")
    print(pdf_path)
    save_path = os.path.join(path,"vector_db01")
    print(save_path)
    directory_name_list = os.listdir(pdf_path)
    #遍历目录中所有的pdf文件
    for filename in directory_name_list:
        if filename.endswith(".pdf"):
            file_path = os.path.join(pdf_path,filename)
            # 执行pdf读取方法
            pdf = PdfReader(file_path)
            # 提取PDF的文本和页码信息
            text,page_number = from_pdf_extract_text_page_number(pdf)
            # 量化文本信息,并同步保存到磁盘
            splitter_and_embedding(text,page_number,save_path=save_path)  # 不需要其返回的local_knowledge对象
    # 加载本地向量数据库
    local_knowledge = load_local_knowledge(local_path=save_path)
    # 用户提问
    query = "客户经理的职责是啥?"
    user_query(query, local_knowledge)
    query = "flask的框架是啥?"
    user_query(query, local_knowledge)

9、本地环境变量

OPENAI_API_KEY="xxxxxxxxxxxxxxxxxxxxx"
OPENAI_BASE_URL="https://api.deepseek.com"
DASHSCOPE_API_KEY="xxxxxxxxxxxxxxxxxxxxxx"
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值