欢迎光临
我们一直在努力

RAG搭建本地知识库

目录

一、数据下载

二、技术栈选择

三、程序逻辑

Step1:文档预处理

Step2:知识库构建

Step3:问答查询

概念区分:


        今天我们要基于DeepSeek和Faiss搭建一个本地知识库检索。我们前面也做过一个基于 LangChain 的多文件RAG应用 。可以看看区别。

一、数据下载

从百度文库里面下载这样一个PDF(或者其他的内容也行)。使用向量相似度搜索从PDF文档中检索相关内容(检索Retrieval), 将检索到的文档片段作为上下文(增强Augmentation) ,基于上下文和用户问题生成答案(生成Generation)。

二、技术栈选择

        • 向量数据库:Faiss作为高效的向量检索

        • 嵌入模型:阿里云DashScope的text-embedding-v1

        • 大语言模型:deepseek-v4

        • 文档处理:PyPDF2用于PDF文本提取(复杂的PDF解析可以用MinerU PDF解析工具-MinerU)

三、程序逻辑

Step1:文档预处理

1) PDF文本提取

from PyPDF2 import PdfReader

…..

PDF_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf')
# 读取PDF文件
pdf_reader = PdfReader(PDF_PATH)

# 返回内容
# text: 提取的文本内容
# char_page_mapping: 每个字符对应的页码列表
text, char_page_mapping = extract_text_with_page_numbers(pdf_reader)

2)提取文本和页码信息

def extract_text_with_page_numbers(pdf) -> Tuple[str, List[Tuple[str, int]]]:
text = ""
char_page_mapping = []

for page_number, page in enumerate(pdf.pages, start=1):
extracted_text = page.extract_text()
if extracted_text:
text += extracted_text
# 为当前页面的每个字符记录页码
char_page_mapping.extend([page_number] * len(extracted_text))
else:
print(f"No text found on page {page_number}.")

return text, char_page_mapping

Step2:知识库构建

1) 文本分割并创建知识库knowledgeBase

# 创建文本分割器,用于将长文本分割成小块
text_splitter = RecursiveCharacterTextSplitter(
separators=["\\n\\n", "\\n", ".", " ", ""],
chunk_size=1000, #1000个token
chunk_overlap=200,
length_function=len,
)

# 分割文本
chunks = text_splitter.split_text(text)
print(f"文本被分割成 {len(chunks)} 个块。")

# 创建嵌入模型
embeddings = DashScopeEmbeddings(
model="text-embedding-v1",
dashscope_api_key=DASHSCOPE_API_KEY,
)

# 从文本块创建知识库
knowledgeBase = FAISS.from_texts(chunks, embeddings)
print("已从文本块创建知识库。")

RecursiveCharacterTextSplitter 是 LangChain 提供的文本切分工具,切分过程本身不会消耗 Token。

  • chunk_size:代表单个文本块允许的最大 Token 数量。
  • separators:配置切分的分隔符。 该工具不会粗暴地把单词、句子从中间截断;达到最大长度限制时,会向前寻找分隔符完成切割。因此切分出来的文本块实际大小不一定恰好等于 chunk_size,只是保证不超过该上限。

chunk_size 需要根据业务问题的颗粒度来调整: 如果业务问题偏向细节查询,chunk_size 就要设置得更小;如果设置得过大,文本向量化压缩的时候容易丢失大量细节信息。

chunk_overlap 代表文本块之间的重叠 Token 数量。 例如设置 chunk_overlap=200,就是前一个 chunk 的末尾 200 个 Token,会和下一个 chunk 的开头 200 个 Token 内容完全重复。它的核心作用是制造内容冗余: 防止完整的语义、关键句子刚好被切割在两个 chunk 的边界上,避免因为切分,把一整段完整语义拆开到两个不同文本块,导致检索的时候丢失上下文信息。

大模型也可以通过提示词的方式做文本切分的,但是成本会比较高(消耗token)。

3)页码映射处理

遍历切分的chunks,基于字符位置计算每个chunks的页码 ,如果chunks跨页了,使用众数统计确定文本块的主要来源页码,建立文本块与页码的映射关系。然后保存向量库

page_info = {}
current_pos = 0

for chunk in chunks:
chunk_start = current_pos
chunk_end = current_pos + len(chunk)

# 找到这个文本块中字符对应的页码
chunk_pages = char_page_mapping[chunk_start:chunk_end]

# 取页码的众数(出现最多的页码)作为该块的页码
if chunk_pages:
# 统计每个页码出现的次数
page_counts = {}
for page in chunk_pages:
page_counts[page] = page_counts.get(page, 0) + 1

# 找到出现次数最多的页码
most_common_page = max(page_counts, key=page_counts.get)
page_info[chunk] = most_common_page
else:
page_info[chunk] = 1 # 默认页码

current_pos = chunk_end

knowledgeBase.page_info = page_info
print(f'页码映射完成,共 {len(page_info)} 个文本块')

# 如果提供了保存路径,则保存向量数据库和页码信息
if save_path:
# 确保目录存在
os.makedirs(save_path, exist_ok=True)

# 保存FAISS向量数据库
knowledgeBase.save_local(save_path)
print(f"向量数据库已保存到: {save_path}")

# 保存页码信息到同一目录
with open(os.path.join(save_path, "page_info.pkl"), "wb") as f:
pickle.dump(page_info, f)
print(f"页码信息已保存到: {os.path.join(save_path, 'page_info.pkl')}")

保存下来的文件如下:

• Faiss索引文件(.faiss) • 元数据信息(.pkl) • 页码映射关系(page_info.pkl)

Step3:问答查询

相似度检索,在Faiss中搜索最相似的文档块,similarity_search返回Top-K相关文档,构建提示词prompt,调用LLM(llm.invoke(prompt))获得结果

from langchain_community.llms import Tongyi
llm = Tongyi(model_name="deepseek-v4", dashscope_api_key=DASHSCOPE_API_KEY) # qwen-turbo

# 设置查询问题
query = "客户经理被投诉了,投诉一次扣多少分"
#query = "客户经理每年评聘申报时间是怎样的?"
if query:
# 执行相似度搜索,找到与查询相关的文档
docs = knowledgeBase.similarity_search(query,k=10)

# 构建上下文
context = "\\n\\n".join([doc.page_content for doc in docs])

# 构建提示
prompt = f"""根据以下上下文回答问题:

{context}

问题: {query}"""

# 直接调用 LLM
response = llm.invoke(prompt)
print(response)
print("来源:")

输出结果:

以上代码可以替换为下面的代码:

# 替换前
docs = knowledgeBase.similarity_search(query, k=10)
context = "\\n\\n".join([doc.page_content for doc in docs]) # 手动拼
prompt = f"""根据以下上下文回答问题:\\n\\n{context}\\n\\n问题: {query}""" # 手动拼 prompt
response = llm.invoke(prompt) # 手动调
#———————————————————————-
# 替换后
from langchain_classic.chains.question_answering import load_qa_chain
docs = knowledgeBase.similarity_search(query, k=10)
qa_chain = load_qa_chain(llm, chain_type="stuff")
response = qa_chain.invoke({"input_documents": docs, "question": query})["output_text"]

chain_type有如下四种(一般默认第一种就好了):

本篇内容和基于 LangChain 的多文件RAG应用 逻辑一致,代码略有不同。大家可以对比看一下

概念区分:

Jieba:句子里面加空格(分词),word2vec训练Embedding时用:Embedding和Word2Vec

Chunk:知识的最小颗粒度,用Embedding模型转成向量后放入向量数据库中FAISS.from_texts(chunks, embeddings)

Embedding模型和向量数据库介绍: Embedding模型和向量数据库的选择

Tokenizer:LLM如何理解文字的,文字和token的map映射

Q:如果LLM可以处理无限上下文了,RAG还有意义吗?

效率与成本:LLM处理长上下文时计算资源消耗大,响应时间增加。RAG通过检索相关片段,减少输入长度。

知识更新:LLM的知识截止于训练数据,无法实时更新。RAG可以连接外部知识库,增强时效性。

可解释性:RAG的检索过程透明,用户可查看来源,增强信任。LLM的生成过程则较难追溯。

定制化:RAG可针对特定领域定制检索系统,提供更精准的结果,而LLM的通用性可能无法满足特定需求。

数据隐私:RAG允许在本地或私有数据源上检索,避免敏感数据上传云端,适合隐私要求高的场景。

=> 结合LLM的生成能力和RAG的检索能力,可以提升整体性能,提供更全面、准确的回答。

赞(0)
未经允许不得转载:171主机测评 » RAG搭建本地知识库
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址