赞
踩
大家好,我是程序锅。
github上的代码封装程度高,不利于小白学习入门。
常规的大模型RAG框架有langchain等,但是langchain等框架源码理解困难,debug源码上手难度大。
因此,我写了一个人人都能看懂、人人都能修改的大模型RAG框架代码。
整体项目结构如下图所示:手把手教你大模型RAG框架架构
整个小项目分为10个章节,和github高度封装的RAG代码不同,我们将从0到1搭建大模型RAG问答系统,所有代码评论区回复rag免费获取!
前序文章:
3.项目环境依赖按照
本篇文章将介绍4.知识库构建,知识库构建算是大模型RAG领域最核心的部分了(见图1),主要由知识存储、知识处理、知识向量化、知识检索和知识比较四大部分构成。不过,我们也可以从数据流转角度出发,涉及文件预处理、文件切分、向量化、构建索引等部分。
输入:专业知识文件(pdf、word、txt等)
输出:结构化的json文件
在构建一个高效的RAG系统时,首要步骤是准备知识文档。现实场景中,我们面对的知识源可能包括多种格式,如Word文档、TXT文件、CSV数据表、Excel表格,甚至是PDF文件、图片和视频等。因此,第一步需要使用专门的文档加载器(例如PDF提取器)或多模态模型(如OCR技术),将这些丰富的知识源转换为大语言模型可理解的纯文本数据。
例如,处理PDF文件时,可以利用PDF提取器抽取文本内容;对于图片和视频,OCR技术能够识别并转换其中的文字信息。
文件预处理如果考虑全面,会涉及很多很多工作。比如你的pdf怎么解析、excel表格如何解析、如何去清洗*%@等垃圾字符等等。文件预处理模块很大程度会决定知识库的丰富程度。
在这个小项目中,为了每个人都能看懂并且能够修改。只涉及pdf、md、txt文件的纯文本解析。
import PyPDF2 import markdown import html2text import json from tqdm import tqdm import tiktoken from bs4 import BeautifulSoup import re import os def read_pdf(cls, file_path: str): # 读取PDF文件 with open(file_path, 'rb') as file: reader = PyPDF2.PdfReader(file) text = "" for page_num in range(len(reader.pages)): text += reader.pages[page_num].extract_text() return text def read_markdown(cls, file_path: str): # 读取Markdown文件 with open(file_path, 'r', encoding='utf-8') as file: md_text = file.read() html_text = markdown.markdown(md_text) # 使用BeautifulSoup从HTML中提取纯文本 soup = BeautifulSoup(html_text, 'html.parser') plain_text = soup.get_text() # 使用正则表达式移除网址链接 text = re.sub(r'http\S+', '', plain_text) return text def read_text(cls, file_path: str): # 读取文本文件 with open(file_path, 'r', encoding='utf-8') as file: return file.read()
文件预处理后,我们还需执行一项关键步骤:文档切片。我们需要将文档分割成多个文本块,以便更高效地处理和检索信息。
首先为什么分割成多个文本块呢?
由于embedding模型对输入token有限制,因此只能分割成多个文本块,才能满足embedding模型的需求。
有人就会说:如果不考虑embedding模型,分块是越大越好吗?
不是。在分块的时候,也需要尽可能减少嵌入内容中的噪声,从而更有效地找到与用户查询最相关的文档部分。如果分块太大,可能包含太多不相关的信息,从而降低了检索的准确性。
因此,文件切分后的段落大小,必须有一个最大token长度max_token_len
和最小token长度min_token_len
。其中max_token_len
是为了满足embedding模型对输入token的限制,也需减少嵌入内容中的噪声;min_token_len
是为了要求切分后的段落不能太小,切分后的段落太小,可能会丢失必要的上下文信息,导致生成的回应缺乏连贯性或深度。
下面直接上代码:
def get_chunk2(cls, text: str, min_token_len: int = 600, max_token_len: int = 1600, cover_content: int = 150): chunk_text = [] chunk_lenth = [] curr_len = 0 curr_chunk = '' lines = text.split('\n') # 假设以换行符分割文本为行 for line in lines: line = line.replace(' ', '') line_len = len(enc.encode(line)) if curr_len > max_token_len: while (curr_len > max_token_len): split_a = enc.encode(curr_chunk)[:max_token_len] split_b = enc.encode(curr_chunk)[max_token_len:] curr_chunk = enc.decode(split_a) chunk_text.append(curr_chunk) chunk_lenth.append(max_token_len) curr_chunk = curr_chunk[-cover_content:] + enc.decode(split_b) curr_len = cover_content + curr_len - max_token_len else: if (curr_len <= min_token_len): curr_chunk += line curr_chunk += '\n' curr_len += line_len curr_len += 1 else: chunk_text.append(curr_chunk) chunk_lenth.append(curr_len) curr_chunk = curr_chunk[-cover_content:] + line curr_len = line_len + cover_content if curr_chunk: chunk_text.append(curr_chunk) chunk_lenth.append(curr_len) return chunk_text
什么是向量化?
举个例子,设计4个句子,其中3个与大模型相关,1个与java相关。段落通过向量化后,使得与大模型相关的段落会在向量空间中距离比较近。java和大模型没那么相关,因此在向量空间中离得较远。
回到主题上来。长文本呢切分后需要给文本块做向量化。这里需要明确一点,知识库存的是向量化后的文本块。如下图示例:
由上面这一个示意图可以看出,知识库本质上就是一个列表,其中包含多个维度相同的向量。比如我们这个小项目采用智谱的embedding模型
,模型编码为:embedding-2
,向量化后的维度为1024。
代码如下:
import zhipu class ZhipuEmbedding(BaseEmbeddings): """ class for Zhipu embeddings """ def __init__(self, path: str = '', is_api: bool = True, embedding_dim = 1024) -> None: super().__init__(path, is_api) if self.is_api: from zhipuai import ZhipuAI self.client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY")) self.embedding_dim = embedding_dim def get_embedding(self, text: str) -> List[float]: response = self.client.embeddings.create( model="embedding-2", input=text, ) return response.data[0].embedding
好了现在我们已经得到了知识库,我们需要思考一个问题。
问题向量与知识库匹配,是不是很慢?
答案肯定是的,如果不构建任何索引,非常简单粗暴的方法就是写一个for循环,挨个比较。
为了演示简单,暂时没有构建索引。后续文章会介绍如何采用Milvus 、Faiss等向量知识库技术改进构建索引部分,加快向量搜索。
所有代码评论区回复rag免费获取!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。