文章目录
概要
NVIDIA AI-AGENT夏季训练营
项目名称:AI-AGENT夏季训练营 — RAG智能词典助手(多模态)
报告日期:2024年8月18日
项目负责人:赵冠齐
项目概述
技术栈
本项目基于NIM的模型接口,结合Langchain的链式编程,Gradio作为UI界面以及Funasr,Edge-tts等开源模型,实现了一套完整的RAG智能对话助手(多模态)
应用场景及亮点
本项目以构建一个智能词典助手为目标,通过多模态输入方式来实现查询词典的功能。
用户通过前端输入要查询的词语,可以是文字输入,或通过语音输入,甚至图片输入。
技术方案与实施步骤
模型选择:
本项目基于RAG构建,因此Embedding模型选择如下:
NVIDIAEmbeddings(model="nvidia/nv-embedqa-mistral-7b-v2")
在vectorstores,向量存储时,使用工具为FAISS
from langchain_community.vectorstores import FAISS
在调用llm模型时选择如下,由于llama-3.1-405b是当前llama最新最大模型,多方面生成效果都很优秀,因此优先选择:
ChatNVIDIA(model="meta/llama-3.1-405b-instruct", nvidia_api_key=nvapi_key, max_tokens=512)
考虑到多模态输入,在读取图片信息的时候,ai-phi-3-vision-128k-instruct对于图像分析有专门的优化,整体效果很好,因此优先选择:
ChatNVIDIA(model="ai-phi-3-vision-128k-instruct")
考虑到多模态输入,在音频输入时,选择了开源模型Funasr,由阿里达摩院出品,因为本次查询词典场景更加偏向中文环境,因此选择了对于中文能力更加优秀的Funasr,而不是Whisper。同时Funasr作为开源模型,可快速本地部署,对于低成本的测试也很友好:
from funasr import AutoModel
考虑到多模态输入,在音频输出时,选择了开源模型Edge-TTS,由微软出品,
同样可快速本地部署,节约成本。(由Day2培训时,老师推荐,因此就拿来测试了)
import edge_tts
数据的构建:
因为应用场景为查询词典助手,因此我选择了《现代汉语词典》(第7版)的txt文本作为数据源。以下为数据源来源:
https://github.com/CNMan/XDHYCD7th?tab=readme-ov-file
文本效果如下:
优化: 拥有数据源后,需要进行数据的embeding以及后续的向量化存储,再此之前,我们先要对数据进行一定的预处理,优化他的存储结构,才能在后续的向量化检索中得到更加优秀的表现:
在上图中我将文档按照规则切分,使词典中每一个词语作为个单独的数据块,并提取词语本身作为title,提取数据所在文件路径作为sources。
在上图中我们在切分文本后,考虑到模型的最大输入512,因此需要再次对文本进行大小上的限制二次切分,并将切分结果,结合之前的元数据,一起封装为可以直接embeding的document格式。
使用FAISS工具调用embeding模型将数据向量化并存储在本地作为向量库。
注:由于整个词典切分后块数过多,导致embeding时间过长。所以可以适当的减少文档中的数据条数
功能整合:
语音输入:
语音功能的实现策略参考了一下项目作为框架:
https://github.com/Ikaros-521/voice_talk_chatgpt
替换了其中的模型以及模型调用的方式。
方案流程为:
- 前端输入录音文件,或直接语音输入。后端通过读取音频文件,调用Funasr模型来转化为文字。
- 将文本结合Prompt,调用LLM模型来进行回答。
- 后端获取回复后调用Edge-TTS模型将文本转化为语音输出。最终在前端呈现。
图片输入:
图片功能的实现,参考了培训day3的multi_agent_demo.ipynb作为框架:
替换了其中的模型以及模型调用的逻辑。
方案流程为:
- 前端输入图片。后端读取图片,调用模型来解释图片中的物品信息(返回为英文)
- 调用LLM模型来翻译物品信息(转为中文),获取文本。
- 调用本地向量库进行搜索匹配数据,将匹配数据结合Prompt调用LLM模型进行总结,整合,润色。
实施步骤
环境搭建:
python环境推荐3.8+(本人使用的3.10)
其他库如下
gradio
openai
colorlog
scipy
langchain-nvidia-ai-endpoints
langchain_core
langchain
langchain-community
matplotlib
faiss-cpu
openai
# Funasr包含后续调用中使用的推理模型,切分模型等安装
Funasr
edge_tts
部署内容:
- 注册获取NIM平台,并获取模型API KEY
- Rag的数据向量化及本地向量库存储
- 部署Funasr及Edge-TTS模型
- 部署Gradio及相关配置
代码实现:
整个数据向量化及存储过程已在前面的章节展示了。
音频处理的核心代码如下图:
def send_msg(audio, text):
try:
# 创建一个临时文件来存储录制的音频数据
output_file = "out/" + common.get_bj_time(4) + ".wav"
if text:
stt_ret = text
if audio:
# 创建一个示例的 int16 音频数据
int16_audio_data = np.array(audio[1], dtype=np.int16)
# 使用 scipy.io.wavfile 将数据保存为 WAV 文件
wavfile.write(output_file, audio[0], int16_audio_data)
res = model.generate(output_file)
stt_ret = res[0]['text']
logging.info(f"语音识别内容:{stt_ret}")
# 数据回显
text_input.value = stt_ret
'''
llm调用stt_ret
返回值chat_ret
'''
chat_ret = chain1.invoke(stt_ret)
logging.info(f"对话返回:{chat_ret}")
audio_path = './'+change_wav_to_mp3(output_file)
communicate = edge_tts.Communicate(chat_ret, VOICE)
communicate.save_sync(audio_path)
logging.info(f"合成音频输出在:{audio_path}")
return audio_path, chat_ret
except Exception as e:
logging.error(f"Error processing audio: {str(e)}")
return None
Funasr的模型配置组合如下:
model = AutoModel(model="paraformer-zh", vad_model="fsmn-vad", punc_model="ct-punc")
Prompt及chain的构建如下:
prompt1 = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a dictionary assistant, capable of helping answer questions"
" related to the explanations of characters and words. Answer solely "
"based on the following context:{context}",
),
("user", "{question}"),
]
)
chain1 = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt1
| llm
| StrOutputParser()
)
图片处理的核心代码如下:
def chart_agent_gr(image_b64, user_input):
embedder = NVIDIAEmbeddings(model="nvidia/nv-embedqa-mistral-7b-v2")
folderpath="./data/"
store=FAISS.load_local(folderpath+"/nv_embedding", embedder,allow_dangerous_deserialization=True)
retriever = store.as_retriever(search_type="mmr",search_kwargs={'k':10,'fetch_k':100})
image_b64 = image2b64(image_b64)
# Chart reading Runnable
chart_reading = ChatNVIDIA(model="microsoft/phi-3-vision-128k-instruct")
chart_reading_prompt = ChatPromptTemplate.from_template(
'Tell me the objects recognized based on the following image data, only respond with nouns, do not describe, : <img src="data:image/png;base64,{image_b64}" />'
)
chart_chain = chart_reading_prompt | chart_reading
translate_llm = ChatNVIDIA(model="meta/llama-3.1-405b-instruct")
translate_prompt = ChatPromptTemplate.from_template(
'Only translate the following English words into Chinese, do not reply with any other description:<content {content}>'
)
translate_chain = translate_prompt | translate_llm
instruct_chat = ChatNVIDIA(model="meta/llama-3.1-405b-instruct")
prompt1 = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a dictionary assistant, capable of helping answer questions related to the explanations of characters and words. Answer solely based on the following context:\n<Documents>\n{context}\n</Documents>",
),
("user", "{question}"),
]
)
chain1 = (
chart_chain
| RunnableLambda(print_and_return)
| translate_chain
)
chain2 = (
{"context": retriever, "question": RunnablePassthrough()}
| RunnableLambda(print_and_return)
| prompt1
| instruct_chat
| StrOutputParser()
)
return chain2.invoke(chain1.invoke({"image_b64": image_b64}).content)
调用chain1来解读图片内容,并翻译图片内容从英文–>中文。
调用chain2来搜索本地向量库并将匹配内容交给LLM进行最终总结。
调试与优化
整个项目的多模态由于时间问题,拆分为了音频搜索,和图片搜索两个页面,独立功能都可以正常运行。
但没有来的及整合在同一页面下,完成最终的集成测试。
项目成果与展示
应用场景展示:
首先对比在没有使用RAG的方案下,大模型本身对于汉语生僻字查询的能力:
是否正确?我们看一下正确答案
显然大模型没有正确回答问题,而且给了我们错误的导向
再来看一下加入了RAG方案的问答场景(首先是文字输入):
可以看到,在调用了同一个模型的情况下,RAG明显回答正确,并且还有语音播报。来看一下后台的日志:
程序将模型调用后的返回文字保存为out目录中的3.MP3
好的,我们来继续展示音频输入的效果如何:
首先我来对着麦克风说一段话,说了什么呢。在前端还在等待最终输出的时候,我们可以看一下后端服务打印的日志来理解,录音上传后的中间过程
程序成功的识别了我的录音内容,并将模型调用后的返回文字保存为out目录中的4.MP3
看来过程很顺利,这时,前端应该也开始播放我的搜索结果了:
好的,我们打开图片输入的页面(时间问题,两个输入模式没有统一在一个页面中):
那么识别什么呢,好像刚刚结束的奥运会,我们表现得还不错:
看了下,好像跑通了,但是结果却没找到奥运五环。我们来看下后台的日志
可以看到,图片识别出了奥运五环,但是在向量库匹配时却只能找到奥运会的解释。所以最终输出没有对于奥运五环的解释。
那这样的搜索结果对么?去看看材料原文是什么样的:
看来RAG没有骗我,原文中确实没有奥运五环的解释。
问题与解决方案
问题:首先,基于我们效果展示的最后一个案例。在没有得到确切答案时,模型最终回复了没有找到答案。
解决方案:这样的回复显然不是很好,我们可以在PE工程中,添加对于没有找到答案的相应处理方式,再告知没有准确答案的同时可以推荐一些意思相近的词语的解释,或是跳过向量库提供的材料,自己回答。
这需要我们提供一套完备的prompt模板以及多次测试(尚未完成)
问题:对于文本过大时,切分策略值得我们思考,如何能让切分的效果最佳,不截断原来文本连贯的意思。同时保证搜索时,能返回切分后的多个文本块。
解决方案:这需要我们深入理解业务场景下文本的结构,在切分同时,我们可以提取所切文本的抽象信息,如标题,其中的关键字啊,所属段落啊等等来构成元数据metadata,再从搜索时加入一些元数据的匹配策略,从而更提高召回质量
项目总结与展望
项目评估:
项目整体完成了对于本次培训内容的实际应用以及场景构想。
成功的让我接触了很多以往没有测试过的模型。
熟悉了langchain的基于chain的快速搭建流程(熟悉了以后确实很方便)
实现了文字,图片,语音等多模态输入方式。
未来方向:
由于培训时间有限,对于RAG的很多内容,止步于搭建和测试。在实际中如召回的优化,数据的清洗,向量化等多个环节的优化讨论是缺少的。
因此,未来希望更多的从RAG的能力深入到RAG的质量。不断地优化,提高正确率。