本文分享一个开源项目——一款创新的生成式搜索引擎,能够实现用户与本地文件的智能互动。此项目在微软Copilot等现有工具的基础上,推出了一种开放源代码的替代方案,旨在推动技术共享与创新。
1 系统设计
为构建本地生成式搜索引擎或助手,需要几个组件:
-
内容索引系统:负责存储本地文件内容,并配备信息检索引擎,以便高效地搜索与用户查询或问题最相关的文档。
-
语言模型:用于分析选定的本地文档内容,并据此生成精炼的总结性答案。
-
用户界面:为用户提供直观的操作界面,以便轻松地进行查询和获取信息。
组件之间的交互方式如下所示:
系统设计和架构。使用Qdrant作为向量存储,Streamlit用于用户界面。Llama 3可以通过Nvidia NIM API(700B版本)使用,也可以通过HuggingFace下载(80B版本)。文档分块使用Langchain完成。
构建本地生成式搜索引擎的第一步是创建索引,用以存储和检索本地文件内容。当用户提出问题,系统会通过这个索引快速定位到最相关的文档。随后,选定的文档内容被送入高级语言模型,该模型不仅生成答案,还提供对引用文档的明确标注。最终,用户界面将这些信息以清晰、易于理解的方式展示给用户。
2 语义索引
语义索引旨在通过分析文件内容与查询之间的相似度,提供最相关的文档匹配。索引的构建采用了Qdrant作为其向量存储解决方案。Qdrant客户端库的便利之处在于,它不需要完整的服务器端安装,便能在工作内存中直接进行文档相似性比较,极大地简化了部署流程,仅需通过pip命令安装Qdrant客户端即可。
Qdrant初始化时,需要预先设定所使用的向量化方法和度量标准(注意,hf参数稍后定义)。向量化和度量的具体配置应在客户端初始化阶段完成。以下是Qdrant初始化的一个示例:
from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams client = QdrantClient(path="qdrant/") collection_name = "MyCollection" if client.collection_exists(collection_name): client.delete_collection(collection_name) client.create_collection(collection_name,vectors_config=VectorParams(size=768, distance=Distance.DOT)) qdrant = Qdrant(client, collection_name, hf)
为构建向量索引,必须对硬盘中的文档进行嵌入处理。需要选择合适的嵌入方法和向量比较度量标准,不同的段落、句子或词嵌入技术将产生不同的结果。在文档向量搜索中,主要挑战之一是非对称搜索问题,这在信息检索领域极为常见,尤其是在处理短查询与长文档匹配时。传统的单词或句子嵌入技术通常针对相似长度的文档进行优化,如果文档长度与查询长度差异过大,就可能导致信息检索效果不佳。
然而,有一种嵌入方法能够有效应对非对称搜索问题。以MSMARCO数据集为例,该数据集基于Bing的搜索查询和文档,由Microsoft发布,并针对此类问题进行了优化。MSMARCO数据集的模型经过微调,能够提供出色的搜索效果,非常适合解决当前面临的问题。
在本次实现中,选用了针对MSMARCO数据集进行过微调的模型,名为:
sentence-transformers/msmarco-bert-base-dot-v5
这个模型基于BERT架构,并针对点积相似性度量进行了特别优化。在初始化Qdrant客户端时,已明确采用点积作为衡量相似性的方法(注意此模型的维度为768):
client.create_collection(collection_name,vectors_config=VectorParams(size=768, distance=Distance.DOT))
在选择相似性度量标准时,虽然余弦相似性是一个可行选择,但鉴于模型已针对点积优化,采用点积能够实现更优的性能表现。点积的优势在于它不仅关注向量间的角度差异,还包括了向量的大小因素,这在评估向量整体相似度时尤为重要。通过归一化处理,可以在特定条件下使两种度量标准达到相同效果。然而,当向量的大小成为一个关键考量时,点积显然是更为合适的度量手段。
模型初始化建议利用GPU以提升计算效率,具体代码实现如下:
model_name = "sentence-transformers/msmarco-bert-base-dot-v5" model_kwargs = {'device': 'cpu'} encode_kwargs = {'normalize_embeddings': True} hf = HuggingFaceEmbeddings( model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs )
BERT类模型受限于其内存消耗的二次方增长特性,只能处理有限长度的上下文,通常不超过512个token。面对这一局限,有两种应对策略:一是仅利用文档前512个token生成答案,舍弃之后的内容;二是将文档切分为多个小块,每块作为一个独立单元存储于索引之中。为了保留完整的信息,我们选择了后者。文档的分块工作,计划利用LangChain的内置分块工具来完成:
from langchain_text_splitters import TokenTextSplitter text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50) texts = text_splitter.split_text(file_content) metadata = [] for i in range(0,len(texts)): metadata.append({"path":file}) qdrant.add_texts(texts,metadatas=metadata)
在编写的代码中,把文本切割成500个token的段落,并设置了50个token的重叠区域,这样做是为了在段落的首尾保持上下文的连贯性。接着,为每个段落创建了包含文档存储路径的元数据,并将其与文本段落一并索引。
在将文件内容索引之前,必须先读取这些文件。而在读取之前,需要先确定哪些文件需要被索引。本项目简化了这一流程,允许用户指定他们希望索引的文件夹。索引器将递归地搜索该文件夹及其子文件夹中的所有文件,并索引那些支持的文件类型,如PDF、Word、PPT和TXT格式。
以下是检索给定文件夹及其子文件夹内所有文件的递归方法:
def get_files(dir): file_list = [] for f in listdir(dir): if isfile(join(dir,f)): file_list.append(join(dir,f)) elif isdir(join(dir,f)): file_list= file_list + get_files(join(dir,f)) return file_list
完成文件检索后,接下来便是读取这些文件中的文本内容。目前,工具支持的文件格式包括MS Word文档(.docx)、PDF文档、MS PowerPoint演示文稿(.pptx)以及纯文本文件(.txt)。
对于MS Word文档的读取,采用docx-python库来实现。以下是将文档内容读取到字符串变量中的函数示例:
import docx def getTextFromWord(filename): doc = docx.Document(filename) fullText = [] for para in doc.paragraphs: fullText.append(para.text) return '\n'.join(fullText)
对于MS PowerPoint文件的处理,采取相似的方法。为此,需要下载并安装pptx-python库,并编写如下函数:
from pptx import Presentation def getTextFromPPTX(filename): prs = Presentation(filename) fullText = [] for slide in prs.slides: for shape in slide.shapes: fullText.append(shape.text) return '\n'.join(fullText)
读取文本文件:
f = open(file,'r') file_content = f.read() f.close()
对于PDF文件,使用 PyPDF2 库:
reader = PyPDF2.PdfReader(file) for i in range(0,len(reader.pages)): file_content = file_content + " "+reader.pages[i].extract_text()
最后,整个索引函数是这样:
file_content = "" for file in onlyfiles: file_content = "" if file.endswith(".pdf"): print("indexing "+file) reader = PyPDF2.PdfReader(file) for i in range(0,len(reader.pages)): file_content = file_content + " "+reader.pages[i].extract_text() elif file.endswith(".txt"): print("indexing " + file) f = open(file,'r') file_content = f.read() f.close() elif file.endswith(".docx"): print("indexing " + file) file_content = getTextFromWord(file) elif file.endswith(".pptx"): print("indexing " + file) file_content = getTextFromPPTX(file) else: continue text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50) texts = text_splitter.split_text(file_content) metadata = [] for i in range(0,len(texts)): metadata.append({"path":file}) qdrant.add_texts(texts,metadatas=metadata) print(onlyfiles) print("Finished indexing!")
如前所述,这里采用了LangChain的TokenTextSplitter工具,将文本划分为500个token的段落,并在段落间保留了50个token的重叠,确保了内容的连续性。在此基础上,已经成功建立了索引。接下来,将开发一个Web服务,它不仅能够查询索引,还能根据查询结果智能生成答案。
3 生成式搜索API
这里通过FastAPI框架搭建Web服务,用于承载生成式搜索引擎。这个API将连接到之前建立的Qdrant客户端索引,通过向量相似性搜索算法深入挖掘,再借助Llama 3模型对筛选出的最相关块生成精准答案,并将这些答案反馈给用户。
为了配置并引入生成式搜索的关键组件,以下是相应的代码示例:
from fastapi import FastAPI from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_qdrant import Qdrant from qdrant_client import QdrantClient from pydantic import BaseModel import torch from transformers import AutoTokenizer, AutoModelForCausalLM import environment_var import os from openai import OpenAI class Item(BaseModel): query: str def __init__(self, query: str) -> None: super().__init__(query=query)
FastAPI 框架用来创建 API 接口,以实现数据的高效交互。通过 qdrant_client 库,能够访问之前建立的索引数据,而 langchain_qdrant 库则增强了其功能。在处理模型嵌入和本地化部署 Llama 3 模型时,分别采用了 PyTorch 和 Transformers 这两个业界领先的库。此外,项目还通过 OpenAI 库与 NVIDIA NIM API 进行了集成,相关的 API 密钥被安全地存储在预设的 environment_var 文件中,确保了与 Nvidia 和 HuggingFace 的无缝对接。
为了更高效地处理请求参数,开发了一个名为 Item 的类,它基于 Pydantic 的 BaseModel 进行扩展,并且包含了一个关键字段:query,该字段专用于捕获和传递用户的查询指令。
紧接着,项目将启动机器学习模型的初始化过程:
model_name = "sentence-transformers/msmarco-bert-base-dot-v5" model_kwargs = {'device': 'cpu'} encode_kwargs = {'normalize_embeddings': True} hf = HuggingFaceEmbeddings( model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs ) os.environ["HF_TOKEN"] = environment_var.hf_token use_nvidia_api = False use_quantized = True if environment_var.nvidia_key !="": client_ai = OpenAI( base_url="https://integrate.api.nvidia.com/v1", api_key=environment_var.nvidia_key ) use_nvidia_api = True elif use_quantized: model_id = "Kameshr/LLAMA-3-Quantized" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.float16, device_map="auto", ) else: model_id = "meta-llama/Meta-Llama-3-8B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.float16, device_map="auto", )
系统已完成对基于MSMARCO数据集优化的BERT模型的加载,该模型用于文档索引工作。
若存在nvidia_key,系统会调用NVIDIA NIM API,启用具有70亿参数的Llama 3 instruct模型。若无nvidia_key,鉴于本地部署限制,将加载或量化处理后的8亿参数Llama 3模型,使其在减少内存占用的同时,保持模型性能。
接下来,启动Qdrant客户端的初始化过程,以便进行高效的数据索引和检索:
client = QdrantClient(path="qdrant/") collection_name = "MyCollection" qdrant = Qdrant(client, collection_name, hf)
同时,使用 FastAPI 创建第一个模拟 GET 函数:
app = FastAPI() @app.get("/") async def root(): return {"message": "Hello World"}
这个函数会返回格式为 {“message”:“Hello World”} 的 JSON。
为了确保API能够正常工作,将设计两个功能:第一个功能专门进行语义搜索;第二个功能则在搜索的基础上,选取最相关的前10个文本块作为上下文,进一步生成答案,并对使用的文档进行引用。
@app.post("/search") def search(Item:Item): query = Item.query search_result = qdrant.similarity_search( query=query, k=10 ) i = 0 list_res = [] for res in search_result: list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content}) return list_res @app.post("/ask_localai") async def ask_localai(Item:Item): query = Item.query search_result = qdrant.similarity_search( query=query, k=10 ) i = 0 list_res = [] context = "" mappings = {} i = 0 for res in search_result: context = context + str(i)+"\n"+res.page_content+"\n\n" mappings[i] = res.metadata.get("path") list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content}) i = i +1 rolemsg = {"role": "system", "content": "Answer user's question using documents given in the context. In the context are documents that should contain an answer. Please always reference document id (in squere brackets, for example [0],[1]) of the document that was used to make a claim. Use as many citations and documents as it is necessary to answer question."} messages = [ rolemsg, {"role": "user", "content": "Documents:\n"+context+"\n\nQuestion: "+query}, ] if use_nvidia_api: completion = client_ai.chat.completions.create( model="meta/llama3-70b-instruct", messages=messages, temperature=0.5, top_p=1, max_tokens=1024, stream=False ) response = completion.choices[0].message.content else: input_ids = tokenizer.apply_chat_template( messages, add_generation_prompt=True, return_tensors="pt" ).to(model.device) terminators = [ tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids("<|eot_id|>") ] outputs = model.generate( input_ids, max_new_tokens=256, eos_token_id=terminators, do_sample=True, temperature=0.2, top_p=0.9, ) response = tokenizer.decode(outputs[0][input_ids.shape[-1]:]) return {"context":list_res,"answer":response}
这两个函数均采用POST方法,并通过JSON格式利用Item类传递查询参数。第一个函数负责返回10个最相似的文档片段,同时提供每个片段的路径,并赋予其从0至9的文档ID。该函数主要执行基础的语义搜索,使用点积作为相似性度量标准,这一点在Qdrant索引创建期间已设定——即在定义中包含了distance=Distance.DOT的参数。
第二个名为ask_localai的函数则更为复杂,它在第一个函数的搜索机制基础上进行了扩展,增加了生成答案的功能。该函数为Llama 3模型构建了一个包含系统提示消息的提示模板,指示模型如何生成答案:
请使用上下文中给出的文档回答用户的提问。上下文中的文档应当包含问题的答案。在陈述时,请始终引用用来提出主张的文档的ID(用方括号表示,例如[0]、[1])。根据回答问题的需要,尽可能多地引用文献和文档。
用户的消息包含了一个文档列表,列表中的每个文档都按ID(0-9)编号,并在下一行显示文档内容。为了保持ID与文档路径之间的映射关系,我们创建了一个名为list_res的列表,其中包含了ID、路径和内容。用户提示以“Question”一词结束,随后是用户的查询内容。
响应包含上下文和生成的答案。然而,答案再次由 Llama 3 70B 模型(使用 NVIDIA NIM API)、本地 Llama 3 8B 或本地量化的 Llama 3 8B 生成,具体取决于传递的参数。
API 可以从包含以下代码的单独文件启动(假设生成组件在名为 api.py 的文件中,Uvicorn 的第一个参数对应文件名):
import uvicorn if __name__=="__main__": uvicorn.run("api:app",host='0.0.0.0', port=8000, reload=False, workers=3)
4 简单的用户界面
本地生成式搜索引擎的用户界面是其最后一块拼图,采用Streamlit构建,界面简洁,包含查询输入框、搜索按钮、结果展示区以及可交互的文档列表。关键代码不足45行:
import re import streamlit as st import requests import json st.title('_:blue[Local GenAI Search]_ :sunglasses:') question = st.text_input("Ask a question based on your local files", "") if st.button("Ask a question"): st.write("The current question is \"", question+"\"") url = "http://127.0.0.1:8000/ask_localai" payload = json.dumps({ "query": question }) headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) answer = json.loads(response.text)["answer"] rege = re.compile("\[Document\ [0-9]+\]|\[[0-9]+\]") m = rege.findall(answer) num = [] for n in m: num = num + [int(s) for s in re.findall(r'\b\d+\b', n)] st.markdown(answer) documents = json.loads(response.text)['context'] show_docs = [] for n in num: for doc in documents: if int(doc['id']) == n: show_docs.append(doc) a = 1244 for doc in show_docs: with st.expander(str(doc['id'])+" - "+doc['path']): st.write(doc['content']) with open(doc['path'], 'rb') as f: st.download_button("Downlaod file", f, file_name=doc['path'].split('/')[-1],key=a ) a = a + 1
最终:
5 结语
本文阐述了如何融合Qdrant的语义搜索技术与生成式人工智能,构建了针对本地文件的检索增强生成(RAG)流程,这一流程能够对文档中的声明进行引用说明。代码总计约300行,用户可选择三种不同参数规模的Llama 3模型以满足不同场景的需求。在本用例中,无论是8亿还是70亿参数的模型,均能稳定运行并提供出色的性能。
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。