介绍一款集成Llama 3模型的开源生成式搜索引擎,实现了本地文件的智能语义搜索。
本文分享一个开源项目——一款创新的生成式搜索引擎,能够实现用户与本地文件的智能互动。此项目在微软Copilot等现有工具的基础上,推出了一种开放源代码的替代方案,旨在推动技术共享与创新。
为构建本地生成式搜索引擎或助手,需要几个组件:
组件之间的交互方式如下所示:
系统设计和架构。使用Qdrant作为向量存储,Streamlit用于用户界面。Llama 3可以通过Nvidia NIM API(700B版本)使用,也可以通过HuggingFace下载(80B版本)。文档分块使用Langchain完成。
构建本地生成式搜索引擎的第一步是创建索引,用以存储和检索本地文件内容。当用户提出问题,系统会通过这个索引快速定位到最相关的文档。随后,选定的文档内容被送入高级语言模型,该模型不仅生成答案,还提供对引用文档的明确标注。最终,用户界面将这些信息以清晰、易于理解的方式展示给用户。
语义索引旨在通过分析文件内容与查询之间的相似度,提供最相关的文档匹配。索引的构建采用了Qdrant作为其向量存储解决方案。Qdrant客户端库的便利之处在于,它不需要完整的服务器端安装,便能在工作内存中直接进行文档相似性比较,极大地简化了部署流程,仅需通过pip命令安装Qdrant客户端即可。
Qdrant初始化时,需要预先设定所使用的向量化方法和度量标准(注意,hf参数稍后定义)。向量化和度量的具体配置应在客户端初始化阶段完成。以下是Qdrant初始化的一个示例:
fromqdrant_clientimportQdrantClient
fromqdrant_client.modelsimportDistance,VectorParams
client=QdrantClient(path="qdrant/")
collection_name="MyCollection"
ifclient.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的内置分块工具来完成:
fromlangchain_text_splittersimportTokenTextSplitter
text_splitter=TokenTextSplitter(chunk_size=500,chunk_overlap=50)
texts=text_splitter.split_text(file_content)
metadata=[]
foriinrange(0,len(texts)):
metadata.append({"path":file})
qdrant.add_texts(texts,metadatas=metadata)
在编写的代码中,把文本切割成500个token的段落,并设置了50个token的重叠区域,这样做是为了在段落的首尾保持上下文的连贯性。接着,为每个段落创建了包含文档存储路径的元数据,并将其与文本段落一并索引。
在将文件内容索引之前,必须先读取这些文件。而在读取之前,需要先确定哪些文件需要被索引。本项目简化了这一流程,允许用户指定他们希望索引的文件夹。索引器将递归地搜索该文件夹及其子文件夹中的所有文件,并索引那些支持的文件类型,如PDF、Word、PPT和TXT格式。
以下是检索给定文件夹及其子文件夹内所有文件的递归方法:
defget_files(dir):
file_list=[]
forfinlistdir(dir):
ifisfile(join(dir,f)):
file_list.append(join(dir,f))
elifisdir(join(dir,f)):
file_list=file_list+get_files(join(dir,f))
returnfile_list
完成文件检索后,接下来便是读取这些文件中的文本内容。目前,工具支持的文件格式包括MS Word文档(.docx)、PDF文档、MS PowerPoint演示文稿(.pptx)以及纯文本文件(.txt)。
对于MS Word文档的读取,采用docx-python库来实现。以下是将文档内容读取到字符串变量中的函数示例:
importdocx
defgetTextFromWord(filename):
doc=docx.Document(filename)
fullText=[]
forparaindoc.paragraphs:
fullText.append(para.text)
return'\n'.join(fullText)
对于MS PowerPoint文件的处理,采取相似的方法。为此,需要下载并安装pptx-python库,并编写如下函数:
frompptximportPresentation
defgetTextFromPPTX(filename):
prs=Presentation(filename)
fullText=[]
forslideinprs.slides:
forshapeinslide.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)
foriinrange(0,len(reader.pages)):
file_content=file_content+""+reader.pages[i].extract_text()
最后,整个索引函数是这样:
file_content=""
forfileinonlyfiles:
file_content=""
iffile.endswith(".pdf"):
print("indexing"+file)
reader=PyPDF2.PdfReader(file)
foriinrange(0,len(reader.pages)):
file_content=file_content+""+reader.pages[i].extract_text()
eliffile.endswith(".txt"):
print("indexing"+file)
f=open(file,'r')
file_content=f.read()
f.close()
eliffile.endswith(".docx"):
print("indexing"+file)
file_content=getTextFromWord(file)
eliffile.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=[]
foriinrange(0,len(texts)):
metadata.append({"path":file})
qdrant.add_texts(texts,metadatas=metadata)
print(onlyfiles)
print("Finishedindexing!")
如前所述,这里采用了LangChain的TokenTextSplitter工具,将文本划分为500个token的段落,并在段落间保留了50个token的重叠,确保了内容的连续性。在此基础上,已经成功建立了索引。接下来,将开发一个Web服务,它不仅能够查询索引,还能根据查询结果智能生成答案。
这里通过FastAPI框架搭建Web服务,用于承载生成式搜索引擎。这个API将连接到之前建立的Qdrant客户端索引,通过向量相似性搜索算法深入挖掘,再借助Llama 3模型对筛选出的最相关块生成精准答案,并将这些答案反馈给用户。
为了配置并引入生成式搜索的关键组件,以下是相应的代码示例:
fromfastapiimportFastAPI
fromlangchain_community.embeddingsimportHuggingFaceEmbeddings
fromlangchain_qdrantimportQdrant
fromqdrant_clientimportQdrantClient
frompydanticimportBaseModel
importtorch
fromtransformersimportAutoTokenizer,AutoModelForCausalLM
importenvironment_var
importos
fromopenaiimportOpenAI
classItem(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
ifenvironment_var.nvidia_key!="":
client_ai=OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=environment_var.nvidia_key
)
use_nvidia_api=True
elifuse_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("/")
asyncdefroot():
return{"message":"HelloWorld"}
这个函数会返回格式为 {"message":"Hello World"} 的 JSON。
为了确保API能够正常工作,将设计两个功能:第一个功能专门进行语义搜索;第二个功能则在搜索的基础上,选取最相关的前10个文本块作为上下文,进一步生成答案,并对使用的文档进行引用。
@app.post("/search")
defsearch(Item:Item):
query=Item.query
search_result=qdrant.similarity_search(
query=query,k=10
)
i=0
list_res=[]
forresinsearch_result:
list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content})
returnlist_res
@app.post("/ask_localai")
asyncdefask_localai(Item:Item):
query=Item.query
search_result=qdrant.similarity_search(
query=query,k=10
)
i=0
list_res=[]
context=""
mappings={}
i=0
forresinsearch_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":"Answeruser'squestionusingdocumentsgiveninthecontext.Inthecontextaredocumentsthatshouldcontainananswer.Pleasealwaysreferencedocumentid(insquerebrackets,forexample[0],[1])ofthedocumentthatwasusedtomakeaclaim.Useasmanycitationsanddocumentsasitisnecessarytoanswerquestion."}
messages=[
rolemsg,
{"role":"user","content":"Documents:\n"+context+"\n\nQuestion:"+query},
]
ifuse_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 的第一个参数对应文件名):
importuvicorn
if__name__=="__main__":
uvicorn.run("api:app",host='0.0.0.0',port=8000,reload=False,workers=3)
本地生成式搜索引擎的用户界面是其最后一块拼图,采用Streamlit构建,界面简洁,包含查询输入框、搜索按钮、结果展示区以及可交互的文档列表。关键代码不足45行:
importre
importstreamlitasst
importrequests
importjson
st.title('_:blue[LocalGenAISearch]_:sunglasses:')
question=st.text_input("Askaquestionbasedonyourlocalfiles","")
ifst.button("Askaquestion"):
st.write("Thecurrentquestionis\"",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("
OST",url,headers=headers,data=payload)
answer=json.loads(response.text)["answer"]
rege=re.compile("\[Document\[0-9]+\]|\[[0-9]+\]")
m=rege.findall(answer)
num=[]
forninm:
num=num+[int(s)forsinre.findall(r'\b\d+\b',n)]
st.markdown(answer)
documents=json.loads(response.text)['context']
show_docs=[]
forninnum:
fordocindocuments:
ifint(doc['id'])==n:
show_docs.append(doc)
a=1244
fordocinshow_docs:
withst.expander(str(doc['id'])+"-"+doc['path']):
st.write(doc['content'])
withopen(doc['path'],'rb')asf:
st.download_button("Downlaodfile",f,file_name=doc['path'].split('/')[-1],key=a
)
a=a+1
最终:
本文阐述了如何融合Qdrant的语义搜索技术与生成式人工智能,构建了针对本地文件的检索增强生成(RAG)流程,这一流程能够对文档中的声明进行引用说明。代码总计约300行,用户可选择三种不同参数规模的Llama 3模型以满足不同场景的需求。在本用例中,无论是8亿还是70亿参数的模型,均能稳定运行并提供出色的性能。
本书共包括7章,涵盖了从基础理论到实际应用的全方位内容。第1章深入探讨了大模型的基础理论。第2章和第3章专注于Llama 2大模型的部署和微调,提供了一系列实用的代码示例、案例分析和最佳实践。第4章介绍了多轮对话难题,这是许多大模型开发者和研究人员面临的一大挑战。第5章探讨了如何基于Llama 2定制行业大模型,以满足特定业务需求。第6章介绍了如何利用Llama 2和LangChain构建高效的文档问答模型。第7章展示了多语言大模型的技术细节和应用场景。本书既适合刚入门的学生和研究人员阅读,也适合有多年研究经验的专家和工程师阅读。通过阅读本书,读者不仅能掌握Llama 2大模型的核心概念和技术,还能学会如何将这些知识应用于实际问题,从而在这一快速发展的领域中取得优势。
| 欢迎光临 链载Ai (https://www.lianzai.com/) | Powered by Discuz! X3.5 |