背景
隨著生成式 AI 的興起,和大語(yǔ)言模型對(duì)話聊天的應(yīng)用變得非常熱門,但這類應(yīng)用往往只能簡(jiǎn)單地和你“聊聊家常”,并不能針對(duì)某些特定的行業(yè),給出非常專業(yè)和精準(zhǔn)的答案。這也是由于大語(yǔ)言模型(以下簡(jiǎn)稱 LLM)在時(shí)效性和專業(yè)性上的局限所導(dǎo)致,現(xiàn)在市面上大部分開(kāi)源的 LLM 幾乎都只是使用某一個(gè)時(shí)間點(diǎn)前的公開(kāi)數(shù)據(jù)進(jìn)行訓(xùn)練,因此它無(wú)法學(xué)習(xí)到這個(gè)時(shí)間點(diǎn)之后的知識(shí),并且也無(wú)法保證在專業(yè)領(lǐng)域上知識(shí)的準(zhǔn)確性。那有沒(méi)有辦法讓你的模型學(xué)習(xí)到新的知識(shí)呢?
當(dāng)然有,這里一般有 2 種方案:
Fine-tuning 微調(diào)
微調(diào)通過(guò)對(duì)特定領(lǐng)域數(shù)據(jù)庫(kù)進(jìn)行廣泛的訓(xùn)練來(lái)調(diào)整整個(gè)模型。這樣可以內(nèi)化專業(yè)技能和知識(shí)。然后,微調(diào)也需要大量的數(shù)據(jù)、大量的計(jì)算資源和定期的重新訓(xùn)練以保持時(shí)效性。
RAG 檢索增強(qiáng)生成
RAG的全稱是 Retrieval-Augmented Generation,它的原理是通過(guò)檢索外部知識(shí)來(lái)給出上下文響應(yīng),在無(wú)需對(duì)模型進(jìn)行重新訓(xùn)練的情況,保持模型對(duì)于特定領(lǐng)域的專業(yè)性,同時(shí)通過(guò)更新數(shù)據(jù)查詢庫(kù),可以實(shí)現(xiàn)快速地知識(shí)更新。但 RAG 在構(gòu)建以及檢索知識(shí)庫(kù)時(shí),會(huì)占用更多額外的內(nèi)存資源,其回答響應(yīng)延時(shí)也取決于知識(shí)庫(kù)的大小。
從以上比較可以看出,在沒(méi)有足夠 GPU 計(jì)算資源對(duì)模型進(jìn)行重新訓(xùn)練的情況下,RAG 方式對(duì)普通用戶來(lái)說(shuō)更為友好。因此本文也將探討如何利用 OpenVINO 以及 LangChain 工具來(lái)構(gòu)建屬于你的 RAG 問(wèn)答系統(tǒng)。
RAG 流程
雖然 RAG 可以幫助 LLM “學(xué)習(xí)”到新的知識(shí),并給出更可靠的答案,但它的實(shí)現(xiàn)流程并不復(fù)雜,主要可以分為以下兩個(gè)部分:
01構(gòu)建知識(shí)庫(kù)檢索
圖:構(gòu)建知識(shí)庫(kù)流程
Load 載入:
讀取并解析用戶提供的非結(jié)構(gòu)化信息,這里的非結(jié)構(gòu)化信息可以是例如 PDF 或者 Markdown 這樣的文檔形式。
Split 分割:
將文檔中段落按標(biāo)點(diǎn)符號(hào)或是特殊格式進(jìn)行拆分,輸出若干詞組或句子,如果拆分后的單句太長(zhǎng),將不便于后期 LLM 理解以及抽取答案,如果太短又無(wú)法保證語(yǔ)義的連貫性,因此我們需要限制拆分長(zhǎng)度(chunk size),此外,為了保證 chunk 之間文本語(yǔ)義的連貫性,相鄰 chunk 會(huì)有一定的重疊,在 LangChain 中我可以通過(guò)定義 Chunk overlap 來(lái)控制這個(gè)重疊區(qū)域的大小。
圖:Chunk size 和 Chunk overlap 示例
Embedding 向量化:
使用深度學(xué)習(xí)模型將拆分后的句子向量化,把一段文本根據(jù)語(yǔ)義在一個(gè)多維空間的坐標(biāo)系里面表示出來(lái),以便知識(shí)庫(kù)存儲(chǔ)以及檢索,語(yǔ)義將近的兩句話,他們所對(duì)應(yīng)的向量相似度會(huì)相對(duì)較大,反之則較小,以此方式我們可以在檢索時(shí),判斷知識(shí)庫(kù)里句子是否可能為問(wèn)題的答案。
Store 存儲(chǔ):
構(gòu)建知識(shí)庫(kù),將文本以向量的形式存儲(chǔ),用于后期檢索。
02檢索和答案生成
圖:答案生成流程
Retrieve 檢索:
當(dāng)用戶問(wèn)題輸入后,首先會(huì)利用 embedding 模型將其向量化,然后在知識(shí)庫(kù)中檢索與之相似度較高的若干段落,并對(duì)這些段落的相關(guān)性進(jìn)行排序。
Generate 生成:
將這個(gè)可能包含答案,且相關(guān)性最高的 Top K 個(gè)檢索結(jié)果,包裝為 Prompt 輸入,喂入 LLM 中,據(jù)此來(lái)生成問(wèn)題所對(duì)應(yīng)的的答案。
關(guān)鍵步驟
在利用 OpenVINO構(gòu)建 RAG 系統(tǒng)過(guò)程中有以下一些關(guān)鍵步驟:
01封裝 Embedding 模型類
由于在 LangChain 的 chain pipeline 會(huì)調(diào)用 embedding 模型類中的embed_documents和 embed_query 來(lái)分別對(duì)知識(shí)庫(kù)文檔和問(wèn)題進(jìn)行向量化,而他們最終都會(huì)調(diào)用 encode 函數(shù)來(lái)實(shí)現(xiàn)每個(gè) chunk 具體的向量化實(shí)現(xiàn),因此在自定義的 embedding 模型類中也需要實(shí)現(xiàn)這樣幾個(gè)關(guān)鍵方法,并通過(guò) OpenVINO進(jìn)行推理任務(wù)的加速。
圖:embedding 模型推理示意
由于在 RAG 系統(tǒng)中的各個(gè) chunk 之間的向量化任務(wù)往往沒(méi)有依賴關(guān)系,因此我們可以通過(guò) OpenVINO 的 AsyncInferQueue 接口,將這部分任務(wù)并行化,以提升整個(gè) embedding 任務(wù)的吞吐量。
for i, sentence in enumerate(sentences_sorted): inputs = {} features = self.tokenizer( sentence, padding=True, truncation=True, return_tensors='np') for key in features: inputs[key] = features[key] infer_queue.start_async(inputs, i) infer_queue.wait_all() all_embeddings = np.asarray(all_embeddings)
左滑查看更多
此外,從 HuggingFace Transfomers 庫(kù)中(https://hf-mirror.com/sentence-transformers/all-mpnet-base-v2#usage-huggingface-transformers)導(dǎo)出的 embedding 模型是不包含 mean_pooling 和歸一化操作的,因此我們需要在獲取模型推理結(jié)果后,再實(shí)現(xiàn)這部分后處理任務(wù)。并將其作為 callback function 與 AsyncInferQueue 進(jìn)行綁定。
def postprocess(request, userdata): embeddings = request.get_output_tensor(0).data embeddings = np.mean(embeddings, axis=1) if self.do_norm: embeddings = normalize(embeddings, 'l2') all_embeddings.extend(embeddings) infer_queue.set_callback(postprocess)
左滑查看更多
02封裝 LLM 模型類
由于 LangChain 已經(jīng)可以支持 HuggingFace 的 pipeline 作為其 LLM 對(duì)象,因此這里我們只要將 OpenVINO 的 LLM 推理任務(wù)封裝成一個(gè) HF 的 text generation pipeline 即可(詳細(xì)方法可以參考我的上一篇文章)。此外為了流式輸出答案(逐字打印),需要通過(guò) TextIteratorStreamer 對(duì)象定義一個(gè)流式生成器。
streamer = TextIteratorStreamer( tok, timeout=30.0, skip_prompt=True, skip_special_tokens=True ) generate_kwargs = dict( model=ov_model, tokenizer=tok, max_new_tokens=256, streamer=streamer, # temperature=1, # do_sample=True, # top_p=0.8, # top_k=20, # repetition_penalty=1.1, ) if stop_tokens is not None: generate_kwargs["stopping_criteria"] = StoppingCriteriaList(stop_tokens) pipe = pipeline("text-generation", **generate_kwargs) llm = HuggingFacePipeline(pipeline=pipe)
左滑查看更多
03設(shè)計(jì) RAG prompt template
當(dāng)完成檢索后,RAG 會(huì)將相似度最高的檢索結(jié)果包裝為 Prompt,讓 LLM 進(jìn)行篩選與重構(gòu),因此我們需要為每個(gè) LLM 設(shè)計(jì)一個(gè) RAG prompt template,用于在 Prompt 中區(qū)分這些檢索結(jié)果,而這部分的提示信息我們又可以稱之為 context 上下文,以供 LLM 在生成答案時(shí)進(jìn)行參考。以 ChatGLM3 為例,它的 RAG prompt template 可以是這樣的:
"prompt_template": f"""<|system|> {DEFAULT_RAG_PROMPT_CHINESE }""" + """ <|user|> 問(wèn)題: {question} 已知內(nèi)容: {context} 回答: <|assistant|>""",
左滑查看更多
其中:
● {DEFAULT_RAG_PROMPT_CHINESE}為我們事先根據(jù)任務(wù)要求定義的系統(tǒng)提示詞。
●{question}為用戶問(wèn)題。
●{context} 為 Retriever 檢索到的,可能包含問(wèn)題答案的段落。
例如,假設(shè)我們的問(wèn)題是“飛槳的四大優(yōu)勢(shì)是什么?”,對(duì)應(yīng)從飛槳文檔中獲取的 Prompt 輸入就是:
“<|system|> 基于以下已知信息,請(qǐng)簡(jiǎn)潔并專業(yè)地回答用戶的問(wèn)題。如果無(wú)法從中得到答案,請(qǐng)說(shuō) "根據(jù)已知信息無(wú)法回答該問(wèn)題" 或 "沒(méi)有提供足夠的相關(guān)信息"。不允許在答案中添加編造成分。另外,答案請(qǐng)使用中文。 <|user|> 問(wèn)題: 飛槳的四大領(lǐng)先技術(shù)是什么? 已知內(nèi)容: ## 安裝 PaddlePaddle最新版本: v2.5 跟進(jìn)PaddlePaddle最新特性請(qǐng)參考我們的版本說(shuō)明 四大領(lǐng)先技術(shù) 開(kāi)發(fā)便捷的產(chǎn)業(yè)級(jí)深度學(xué)習(xí)框架 飛槳深度學(xué)習(xí)框架采用基于編程邏輯的組網(wǎng)范式,對(duì)于普通開(kāi)發(fā)者而言更容易上手,符合他們的開(kāi)發(fā)習(xí)慣。同時(shí)支持聲明式和命令式編程,兼具開(kāi)發(fā)的靈活性和高性能。網(wǎng)絡(luò)結(jié)構(gòu)自動(dòng)設(shè)計(jì),模型效果超越人類專家。 支持超大規(guī)模深度學(xué)習(xí)模型的訓(xùn)練 飛槳突破了超大規(guī)模深度學(xué)習(xí)模型訓(xùn)練技術(shù),實(shí)現(xiàn)了支持千億特征、萬(wàn)億參數(shù)、數(shù)百節(jié)點(diǎn)的開(kāi)源大規(guī)模訓(xùn)練平臺(tái),攻克了超大規(guī)模深度學(xué)習(xí)模型的在線學(xué)習(xí)難題,實(shí)現(xiàn)了萬(wàn)億規(guī)模參數(shù)模型的實(shí)時(shí)更新。 查看詳情 支持多端多平臺(tái)的高性能推理部署工具 … <|assistant|>“
左滑查看更多
04創(chuàng)建 RetrievalQA 檢索
在文本分割這個(gè)任務(wù)中,LangChain 支持了多種分割方式,例如按字符數(shù)的 CharacterTextSplitter,針對(duì) Markdown 文檔的 MarkdownTextSplitter,以及利用遞歸方法的 RecursiveCharacterTextSplitter,當(dāng)然你也可以通過(guò)繼成 TextSplitter 父類來(lái)實(shí)現(xiàn)自定義的 split_text 方法,例如在中文文檔中,我們可以采用按每句話中的標(biāo)點(diǎn)符號(hào)進(jìn)行分割。
class ChineseTextSplitter(CharacterTextSplitter): def __init__(self, pdf: bool = False, **kwargs): super().__init__(**kwargs) self.pdf = pdf def split_text(self, text: str) -> List[str]: if self.pdf: text = re.sub(r" {3,}", " ", text) text = text.replace(" ", "") sent_sep_pattern = re.compile( '([﹒﹔﹖﹗.。???]["’”」』]{0,2}|(?=["‘“「『]{1,2}|$))') # del :; sent_list = [] for ele in sent_sep_pattern.split(text): if sent_sep_pattern.match(ele) and sent_list: sent_list[-1] += ele elif ele: sent_list.append(ele) return sent_list
左滑查看更多
接下來(lái)我們需要載入預(yù)先設(shè)定的好的 prompt template,創(chuàng)建 rag_chain。
圖:Chroma 引擎檢索流程
這里我們使用 Chroma 作為檢索引擎,在 LangChain 中,Chroma 默認(rèn)使用 cosine distance 作為向量相似度的評(píng)估方法,同時(shí)可以通過(guò)調(diào)整 db.as_retriever(search_type= "similarity_score_threshold"),或是 db.as_retriever(search_type= "mmr")來(lái)更改默認(rèn)搜索策略,前者為帶閾值的相似度搜索,后者為 max_marginal_relevance 算法。當(dāng)然 Chroma 也可以被替換為 FAISS 檢索引擎,使用方式也是相似的。
此外通過(guò)定義 as_retriever函數(shù)中的 {"k": vector_search_top_k},我們還可以改變檢索結(jié)果的返回?cái)?shù)量,有助于幫助 LLM 獲取更多有效信息,但也為增加 Prompt 的長(zhǎng)度,提高推理延時(shí),因此不建議將該數(shù)值設(shè)定太高。創(chuàng)建 rag_chain 的完整代碼如下:
documents = load_single_document(doc.name) text_splitter = TEXT_SPLITERS[spliter_name]( chunk_size=chunk_size, chunk_overlap=chunk_overlap ) texts = text_splitter.split_documents(documents) db = Chroma.from_documents(texts, embedding) retriever = db.as_retriever(search_kwargs={"k": vector_search_top_k}) global rag_chain prompt = PromptTemplate.from_template( llm_model_configuration["prompt_template"]) chain_type_kwargs = {"prompt": prompt} rag_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, chain_type_kwargs=chain_type_kwargs, )
左滑查看更多
05答案生成
創(chuàng)建以后的 rag_chain 對(duì)象可以通過(guò) rag_chain.run(question) 來(lái)響應(yīng)用戶的問(wèn)題。將它和線程函數(shù)綁定后,就可以從 LLM 對(duì)象的 streamer 中獲取流式的文本輸出。
def infer(question): rag_chain.run(question) stream_complete.set() t1 = Thread(target=infer, args=(history[-1][0],)) t1.start() partial_text = "" for new_text in streamer: partial_text = text_processor(partial_text, new_text) history[-1][1] = partial_text yield history
左滑查看更多
最終效果
最終效果如下圖所示,當(dāng)用戶上傳了自己的文檔文件后,點(diǎn)擊 Build Retriever 便可以創(chuàng)建知識(shí)檢索庫(kù),同時(shí)也可以根據(jù)自己文檔的特性,通過(guò)調(diào)整檢索庫(kù)的配置參數(shù)來(lái)實(shí)現(xiàn)更高效的搜索。當(dāng)完成檢索庫(kù)創(chuàng)建后就可以在對(duì)話框中與 LLM 進(jìn)行問(wèn)答交互了。
圖:基于 RAG 的問(wèn)答系統(tǒng)效果
總結(jié)
在醫(yī)療、工業(yè)等領(lǐng)域,行業(yè)知識(shí)庫(kù)的構(gòu)建已經(jīng)成為了一個(gè)普遍需求,通過(guò) LLM 與 OpenVINO 的加持,我們可以讓用戶對(duì)于知識(shí)庫(kù)的查詢變得更加精準(zhǔn)與高效,帶來(lái)更加友好的交互體驗(yàn)。
審核編輯:湯梓紅
-
英特爾
+關(guān)注
關(guān)注
60文章
9745瀏覽量
170639 -
AI
+關(guān)注
關(guān)注
87文章
28867瀏覽量
266188 -
數(shù)據(jù)庫(kù)
+關(guān)注
關(guān)注
7文章
3711瀏覽量
64021 -
OpenVINO
+關(guān)注
關(guān)注
0文章
73瀏覽量
139
原文標(biāo)題:基于 OpenVINO? 和 LangChain 構(gòu)建 RAG 問(wèn)答系統(tǒng) | 開(kāi)發(fā)者實(shí)戰(zhàn)
文章出處:【微信號(hào):英特爾物聯(lián)網(wǎng),微信公眾號(hào):英特爾物聯(lián)網(wǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論