项目概述
本周我参加了Nvidia的2024 summer bootcamp 学习构建RAG 应用。
在这个无论什么行业都把要把大模型写在自己的宣传材料中的时代,真神有且只有一个,多年前,还在餐厅刷盘子的黄先生与合伙人探讨要做一家什么样的公司时,他们最后的结论是“一家让人嫉妒的眼睛发绿的公司”,所以他们选择了一只绿色的眼睛作为自己公司的logo,并给自己的公司起名 Nvidia,拉丁语中,这个单词是嫉妒的意思。
多年后的今天,人类进入了一个全新的时代,上个世纪八十年代曾经一度喧嚣尘上又归于冷寂的一个领域,人工智能,在Nvidia 的GPU的推动之下重新散发光芒,并展现出了巨大的应用前景,Nvidia 在 AI 领域的影响力称之为“统治”完全不为过,他们是智慧时代真正的王。
基于以上背景,要想在AI时代获得更好的技术、经验、洞见,一个简单朴素的想法就是学习他们的论文,但是,这样的学习无疑要消耗大量的时间与精力。所以我们是不是可以让大模型来帮我们学,然后教给我们呢?
说干就干,走你。
技术方案
想法有了,如何证道?我仔细回忆了最近三天所学的皮毛,决定构建一个基于文档向量化+rag 的机器人,为了优化使用体验,我给他配备了语音/文本双模态交互。
整体看,这个机器人包括数据收集+向量化模块,RAG 检索增强模块,LLM 核心能力模块,语音交互模块,以及一个简陋的 web UI 界面。
涉及到的技术包括:NIM 、langchain 、faiss 、whisper 、edge tts 以及gradio
模型选择
本项目涉及到需要 LLM 服务的内容包括:文档向量化、对话补全/聊天交互。
本项目具体选用的模型如下:
文本向量化:“nvidia/nv-embed-v1”
聊天对话:“mistralai/mixtral-8x22b-instruct-v0.1”
选择理由,随意,能用就行,但是有一点要提就是,模型名字中如果包含 -instruct-
字样,那代表它可能训练过程中增强了对指令的响应,这样的模型通常可以更好的输出格式化的内容,在构建机器人的过程中,通过指令调整模型最终的输出结果将是高频操作,所以对话模型选择一个接受指令的模型将为后续的开发提供更多帮助。
数据构建
要学习英伟达的论文,那首先你得有论文,好在 arxiv 不但收纳论文,他们还提供了一个 python 包 arxiv 来帮助开发者快速的检索他们的论文,关于这部分,我直接搜索 “NVIDIA”,得到既可以得到Nvidia 最新发表的论文了,具体可以看如下代码
import arxiv
#这里写想要获取论文的数量,调试时可以写小一点
paper_number = 2
# 创建 arXiv API 客户端
client = arxiv.Client()
search_query = "NVIDIA"
# 设置排序方式为按提交日期降序排序
sort_by = arxiv.SortCriterion.SubmittedDate
# 设置排序顺序为降序
sort_order = arxiv.SortOrder.Descending
# 创建搜索对象
search = arxiv.Search(
query=search_query,
max_results=paper_number, # 限制返回结果数量,可以根据需要调整
sort_by=sort_by,
sort_order=sort_order
)
results = client.results(search)
# 遍历结果并打印每篇论文的标题和 ID
latest_ids = []
for result in results:
print(f"Title: {result.title}")
print(f"ID: {result.get_short_id()}")
latest_ids.append(result.get_short_id())
经过以上过程,latest_ids
中已经收集了足够的 arxiv 论文id,利用这些id ,我们可以很方便的将他们分块并向量化,以便于检索
import json
from langchain_nvidia_ai_endpoints import ChatNVIDIA, NVIDIAEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.document_loaders import ArxivLoader
# NVIDIAEmbeddings.get_available_models()
#初始化一个embeder 实例
embedder = NVIDIAEmbeddings(model="nvidia/nv-embed-v1", truncate="END")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=100,
separators=["\n\n", "\n", ".", ";", ",", " "],
)
print("Loading Documents")
#此处将上一步获取的论文 id 加载为文档对象
docs = [ArxivLoader(query=paper_id).load() for paper_id in latest_ids]
接下来对于 docs 进行分块和向量化,就自然而然了(过程省略)。
对于对话机器人来说,除了它要学习的论文,还有对话历史也很重要,对于对话历史的向量化,将使得我们与机器人对话的过程中前后连贯,使得机器人看起来更像人类。
关于对话历史的处理:在对话过程中维护一个内存中的向量索引,在聊天流程中,同时检索文档和对话历史索引,将检索结果作为上下文,通过 langchain 的提示词模板技术整合成为最终发给模型的消息。
功能整合
数据向量化、存储和取回
参考下图
RAG的工作流程
语音交互流程整合
ASR
使用 openai-whisper 库进行识别
文本处理
基于上述 embeding 和 RAG 流程构建的 LLM 增强对话管道 pipeline
TTS
使用巨硬开源的 edge tts 完成
整体流程就是: 用户语音 -> ASR
-> 对话文本 -> LLM RAG -> 回答文本 -> TTS
输出语音
通过 gradio
把上述各个模块封装到一个 web 页面,效果如下:
实施步骤
环境搭建
开发环境概述
操作系统 : windows
python 版本 :3.10.0
开发工具 : Vs code / jupyter lab
主要依赖如下
arxiv 2.1.3
faiss-cpu 1.7.2
fastapi 0.112.1
huggingface-hub 0.24.5
jsonpatch 1.33
jsonpointer 3.0.0
jsonschema 4.23.0
jsonschema-specifications 2023.12.1
langchain 0.2.14
langchain-community 0.2.12
langchain-core 0.2.32
langchain-nvidia-ai-endpoints 0.2.1
langchain-text-splitters 0.2.2
langsmith 0.1.99
matplotlib 3.8.4
numpy 1.26.4
openai 1.40.6
openai-whisper 20231117
pandas 2.2.2
pathlib 1.0.1
pillow 10.4.0
PyAudio 0.2.14
pydantic 2.8.2
pydantic_core 2.20.1
pydub 0.25.1
PyMuPDF 1.24.9
regex 2024.7.24
requests 2.32.3
uvicorn 0.30.6
代码实现
这里是一些核心代码片段
ASR
通过以下代码将用户语音转换为文本
# audio_path 是用户输入的语音文件存储路径,这里有优化空间,但是初版不优化
def convert_to_text(audio_path):
result = whisper_model.transcribe(audio_path,word_timestamps=True,fp16=False,language='English',task='translate')
return result["text"]
TTS
由于对话的文本可能很长,这里做了分块和异步处理,以下是关键代码片段,流程整合略,以下是叙述顺序,不是真实代码顺序
#整体处理流程
async def async_talk(input_text):
global translate_text_flag,Language,speed,voice_name
if len(input_text)>=600:
long_sentence = True
else:
long_sentence = False
if long_sentence==True and translate_text_flag==True:
chunks_list=make_chunks(input_text,Language)
elif long_sentence==True and translate_text_flag==False:
chunks_list=make_chunks(input_text,"English")
else:
chunks_list=[input_text]
save_path="./content/audio/"+random_audio_name_generate()
edge_save_path = await async_edge_free_tts(chunks_list,speed,voice_name,save_path)
return edge_save_path
#分块操作函数
def make_chunks(input_text, language):
language="English"
if language == "English":
temp_list = input_text.strip().split(".")
filtered_list = [element.strip() + '.' for element in temp_list[:-1] if element.strip() and element.strip() != "'" and element.strip() != '"']
if temp_list[-1].strip():
filtered_list.append(temp_list[-1].strip())
return filtered_list
#tts 异步请求函数
async def async_edge_free_tts(chunks_list,speed,voice_name,save_path):
if len(chunks_list)== 1:
tts_comobj = Communicate(text=chunks_list[0].replace("-"," "),voice=voice_name)
await tts_comobj.save(save_path)
return save_path
chunk_audio_list=[]
if os.path.exists(voice_cache_home):
shutil.rmtree(voice_cache_home)
os.makedirs(voice_cache_home,exist_ok=True)
k = 1
save_tasks = []
for chk in chunks_list:
tmp_comobj = Communicate(text=chk.replace("-"," "),voice=voice_name)
_voice_path = os.path.join(voice_cache_home,f"_voice_{k}.mp3")
save_tasks.append(asyncio.create_task(tmp_comobj.save(_voice_path)))
chunk_audio_list.append(_voice_path)
k += 1
done, pending = await asyncio.wait(save_tasks, return_when=asyncio.ALL_COMPLETED)
merge_audio_files(chunk_audio_list, save_path)
return save_path
Embeding + RAG + TTS
以下为叙述顺序,并非真实代码顺序
#对话文本生成核心函数
def chat_gen(message, history=[], return_buffer=True):
buffer = ""
## 基于输入做向量取回
retrieval = retrieval_chain.invoke(message)
line_buffer = ""
## 通过promote处理成包含指令和上下文的模型输入,并以流式数据输出结果
for token in stream_chain.stream(retrieval):
buffer += token
yield buffer if return_buffer else token
## 更新缓存中的对话历史
save_memory_and_get_output({'input': message, 'output': buffer}, convstore)
#向量取回链条
retrieval_chain = (
{'input' : (lambda x: x)}
## Make sure to retrieve history & context from convstore & docstore, respectively.
| RunnableAssign({'history' : itemgetter('input') | convstore.as_retriever() | long_reorder | docs2str})
| RunnableAssign({'context' : itemgetter('input') | docstore.as_retriever() | long_reorder | docs2str})
)
# 聊天流式链条
stream_chain = chat_prompt | instruct_llm | StrOutputParser()
# 聊天promote 模板
chat_prompt = ChatPromptTemplate.from_messages([("system",
"You are a document chatbot. Help the user as they ask questions about documents."
" User messaged just asked: {input}\n\n"
" From this, we have retrieved the following potentially-useful info: "
" Conversation History Retrieval:\n{history}\n\n"
" Document Retrieval:\n{context}\n\n"
" (Answer only from retrieval. Only cite sources that are used. Make your response conversational.)"
), ('user', '{input}')])
项目成果与展示
场景应用展示
- 追踪特定技术领域或具备重要影响力的公司的论文,并高效把握他们的要点
- 可以方便的与该bot 进行交互,支持文本/语音双模态交互
效果演示
(由于这个平台的审核策略导致我的视频无法上架,效果视频略,感兴趣的话私信我吧)
问题与解决措施
- 目前语音交互的实现是 demo 级别的,将声音保存成文件,后续将把声音该文流式传输,这将提高使用者的体验。
- 目前由于网络原因,arxiv 论文获取速度很慢,没办法快速获取大量,这个解决方案目前想到的是,直接做一个单独脚本,获取论文,直接生成索引文件,把这个脚本部署到 VPS 上,直接取回索引文件。
项目总结与展望
有了这个bot ,读论文再也不用动辄数小时了,可以快速了解哪些自己感兴趣的论文的主要观点再决定要不要精读。
参考资料
https://build.nvidia.com/explore/discover
https://github.com/rany2/edge-tts?tab=readme-ov-file
https://github.com/openai/whisper
https://www.gradio.app/
https://python.langchain.com/