RAG實戰4-RAG過程中發生了什麼?

一蓑烟雨度平生發表於2024-03-09

RAG實戰4-RAG過程中發生了什麼?

RAG實戰3中我們介紹瞭如何追蹤哪些文件片段被用於檢索增強生成,但我們仍不知道RAG過程中到底發生了什麼,為什麼大模型能夠根據檢索出的文件片段進行回覆?本文將用一個簡單的例子來解釋前面的問題。

在閱讀本文之前,請先閱讀RAG實戰3

回答:為什麼大模型能夠根據檢索出的文件片段進行回覆?

先執行以下程式碼:

import logging
import sys
import torch
from llama_index.core import PromptTemplate, Settings, StorageContext, load_index_from_storage
from llama_index.core.callbacks import LlamaDebugHandler, CallbackManager
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.huggingface import HuggingFaceLLM

# 定義日誌
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))


# 定義system prompt
SYSTEM_PROMPT = """You are a helpful AI assistant."""
query_wrapper_prompt = PromptTemplate(
    "[INST]<<SYS>>\n" + SYSTEM_PROMPT + "<</SYS>>\n\n{query_str}[/INST] "
)

# 使用llama-index建立本地大模型
llm = HuggingFaceLLM(
    context_window=4096,
    max_new_tokens=2048,
    generate_kwargs={"temperature": 0.0, "do_sample": False},
    query_wrapper_prompt=query_wrapper_prompt,
    tokenizer_name='/yldm0226/models/Qwen1.5-14B-Chat',
    model_name='/yldm0226/models/Qwen1.5-14B-Chat',
    device_map="auto",
    model_kwargs={"torch_dtype": torch.float16},
)
Settings.llm = llm

# 使用LlamaDebugHandler構建事件回溯器,以追蹤LlamaIndex執行過程中發生的事件
llama_debug = LlamaDebugHandler(print_trace_on_end=True)
callback_manager = CallbackManager([llama_debug])
Settings.callback_manager = callback_manager

# 使用llama-index-embeddings-huggingface構建本地embedding模型
Settings.embed_model = HuggingFaceEmbedding(
    model_name="/yldm0226/RAG/BAAI/bge-base-zh-v1.5"
)

# 從儲存檔案中讀取embedding向量和向量索引
storage_context = StorageContext.from_defaults(persist_dir="doc_emb")
index = load_index_from_storage(storage_context)
# 構建查詢引擎
query_engine = index.as_query_engine(similarity_top_k=5)

# 查詢獲得答案
response = query_engine.query("不耐疲勞,口燥、咽乾可能是哪些證候?")
print(response)

# get_llm_inputs_outputs返回每個LLM呼叫的開始/結束事件
event_pairs = llama_debug.get_llm_inputs_outputs()
# print(event_pairs[0][1].payload.keys())
print(event_pairs[0][1].payload["formatted_prompt"])

輸出很長,我們一部分一部分來看。

首先找到類似下面的輸出:

**********
Trace: query
    |_query ->  14.458354 seconds
      |_retrieve ->  0.845918 seconds
        |_embedding ->  0.71383 seconds
      |_synthesize ->  13.612246 seconds
        |_templating ->  2e-05 seconds
        |_llm ->  13.60905 seconds
**********

以上的輸出記錄了我們的query在程式過程中經歷的階段和所用的時間。整個過程分為兩個階段:抽取(retrieve)和合成(synthesize)。

合成階段的templating步驟會將我們的query和抽取出來的文件片段組合成模板,構成新的query,然後呼叫LLM,得到最終的response。

所以,我們只要找到templating所構建的新query,就可以知道為什麼大模型能夠根據我們檢索出來的文件進行回覆了。

在輸出中找到response下面的部分:

[INST]<<SYS>>
You are a helpful AI assistant.<</SYS>>

Context information is below.
---------------------
file_path: document/中醫臨床診療術語證候.txt

4.6.1.1
    津液不足證  syndrome/pattern of fluid and humor insufficiency
    津虧證
    因津液生成不足,或嗜食辛辣,蘊熱化燥,邪熱灼損津液所致。臨床以口眼喉鼻及皮膚等乾燥,大便乾結,小便短少,舌質偏紅而幹,脈細數等為特徵的證候。

4.6.1.

file_path: document/中醫臨床診療術語證候.txt

臨床以口乾、舌燥,頻飲而不解其渴,食多、善飢,夜尿頻多,逐漸消瘦,舌質紅,舌苔薄黃或少,脈弦細或滑數,伴見皮膚乾燥,四肢乏力,大便乾結等為特徵的證候。

4.6.3.2
    津虧熱結證  syndrome/pattern of fluid depletion and heat binding
    液乾熱結證
    因津液虧虛,熱邪內結所致。

file_path: document/中醫臨床診療術語證候.txt

臨床以口眼喉鼻及皮膚等乾燥,大便乾結,小便短少,舌質偏紅而幹,脈細數等為特徵的證候。

4.6.1.2
    津液虧涸證  syndrome/pattern of fluid and humor scantiness
    津液虧耗證
    津液乾枯證
    因津液虧損,形體官竅失養所致。臨床以口乾、唇裂,鼻燥無涕,皮膚乾癟,目陷、螺癟,甚則肌膚甲錯,舌質紅而少津,舌中裂,脈細或數,可伴見口渴、欲飲,乾咳,目澀,大便幹,小便少等為特徵的證候。

file_path: document/中醫臨床診療術語證候.txt

臨床以鼻咽乾澀或痛,口唇燥幹,舌質紅,舌苔白或燥,脈浮或微數,伴見發熱、無汗,頭痛或肢節痠痛等為特徵的證候。

3.6.3.2
    燥幹清竅證  syndrome/pattern of dryness harassing the upper orifices
    因氣候或環境乾燥,津液耗損,清竅失濡所致。臨床以口鼻、咽喉乾燥,兩眼乾澀,少淚、少涕、少津、甚則衄血,舌質瘦小、舌苔幹而少津,脈細等為特徵的證候。

file_path: document/中醫臨床診療術語證候.txt

6.3.1
    津傷化燥證  syndrome/pattern of fluid damage transforming into dryness
    津傷燥熱證
    因燥熱內蘊,或內熱化燥,傷津耗液所致。臨床以口乾、舌燥,頻飲而不解其渴,食多、善飢,夜尿頻多,逐漸消瘦,舌質紅,舌苔薄黃或少,脈弦細或滑數,伴見皮膚乾燥,四肢乏力,大便乾結等為特徵的證候。

4.6.3.
---------------------
Given the context information and not prior knowledge, answer the query.
Query: 不耐疲勞,口燥、咽乾可能是哪些證候?
Answer: [/INST] 

上面這段很長的文字是由print(event_pairs[0][1].payload["formatted_prompt"])語句輸出的,這段文字就是templating後的新query。

現在,我們就能回答為什麼大模型能夠根據檢索出的文件片段進行回覆這個問題了:我們的原始query由"不耐疲勞,口燥、咽乾可能是哪些證候?"變成了上面這段很長的新query,由於我們給大模型提供了一些文件片段知識,並且要求大模型根據提供的先驗知識回答我們的原始query,因此大模型能夠根據檢索出的文件片段進行回覆。(這其實也就是RAG技術的本質了)

可以發現一個問題,新query中既有中文,也有英文,這是因為LlamaIndex是外國人做的,他們構建的模板都是英文的。LlamaIndex允許自定義查詢流程,構建自己的中文模板,這裡中英文混合也解決了我們的問題,因此不再贅述。

此外,event_pairs中其實還有很多對我們有用的資訊,你可以透過輸出或DEBUG的方式來尋找能夠解決你自己問題的資訊。比如,我註釋掉的#print(event_pairs[0][1].payload.keys())就可以輸出事件結束時所有相關的屬性。

下面是模型的回覆:

從提供的中醫臨床證候資訊來看,口燥、咽乾的症狀可能與以下證候相關:

1. 津液不足證:由於津液生成不足或者體內燥熱導致,表現為口眼喉鼻乾燥,咽乾是其中的一個症狀。

2. 津虧熱結證:津液虧虛加上熱邪內結,也可能出現口燥和咽乾。

3. 津液虧涸證:嚴重的津液虧損可能導致口唇乾燥、咽部乾燥,伴隨其他嚴重脫水症狀。

4. 燥幹清竅證:氣候乾燥或體質原因引起的津液缺乏,口鼻咽喉乾燥也是其特徵。

5. 津傷化燥證:燥熱內蘊或內熱化燥損傷津液,也會出現口燥、頻飲但不解渴的現象。

因此,這些證候都有可能與不耐疲勞和口燥、咽乾的症狀相符合,需要結合其他臨床表現來確定具體的證候型別。建議在中醫診斷中由專業醫生根據全人情況判斷。

進階嘗試

接下來,我們嘗試跟蹤一下更復雜的RAG過程。

前面我們提到了抽取(retrieve)和合成(synthesize)兩個階段。

抽取(retrieve)階段的retrievers模組規定了針對查詢從知識庫獲取相關上下文的技術。我們之前使用的都是預設的方法,其實LlamaIndex官方為我們提供了一些其他常用的方法:

  • SimilarityPostprocessor: 使用similarity_cutoff設定閾值。移除低於某個相似度分數的節點。
  • KeywordNodePostprocessor: 使用required_keywords和exclude_keywords。根據關鍵字包含或排除過濾節點。
  • MetadataReplacementPostProcessor: 用其後設資料中的資料替換節點內容。
  • LongContextReorder: 重新排序節點,這有利於需要大量頂級結果的情況,可以解決模型在擴充套件上下文中的困難。
  • SentenceEmbeddingOptimizer: 選擇percentile_cutoff或threshold_cutoff作為相關性。基於嵌入刪除不相關的句子。
  • CohereRerank: 使用coherence ReRank對節點重新排序,返回前N個結果。
  • SentenceTransformerRerank: 使用SentenceTransformer交叉編碼器對節點重新排序,產生前N個節點。
  • LLMRerank: 使用LLM對節點重新排序,為每個節點提供相關性評分。
  • FixedRecencyPostprocessor: 返回按日期排序的節點。
  • EmbeddingRecencyPostprocessor: 按日期對節點進行排序,但也會根據嵌入相似度刪除較舊的相似節點。
  • TimeWeightedPostprocessor: 對節點重新排序,偏向於最近未返回的資訊。
  • PIINodePostprocessor(β): 可以利用本地LLM或NER模型刪除個人身份資訊。
  • PrevNextNodePostprocessor(β): 根據節點關係,按順序檢索在節點之前、之後或兩者同時出現的節點。

合成(synthesize)階段的響應合成器(response synthesizer)會引導LLM生成響應,將使用者查詢與檢索到的文字塊混合在一起。

假設有一堆文件。現在,你問了一個問題,並希望根據這些文件得到答案。響應合成器就像人一樣,瀏覽文件,找到相關資訊,並生成回覆。

retrievers負責提取出相關的文字片段,我們已經討論過了。而響應合成器負責將這些片段收集起來,並給出一個精心設計的答案。

LlamaIndex官方為我們提供了多種響應合成器:

  • Refine: 這種方法遍歷每一段文字,一點一點地精煉答案。
  • Compact: 是Refine的精簡版。它將文字集中在一起,因此需要處理的步驟更少。
  • Tree Summarize: 想象一下,把許多小的答案結合起來,再總結,直到你得到一個主要的答案。
  • Simple Summarize: 只是把文字片段剪短,然後給出一個快速的總結。
  • No Text: 這個問題不會給你答案,但會告訴你它會使用哪些文字。
  • Accumulate: 為每一篇文章找一堆小答案,然後把它們粘在一起。
  • Compact Accumulate: 是“Compact”和“Accumulate”的合成詞。

此外,retriever和response synthesizer都支援自定義,在此不作討論。

現在,讓我們選擇一種retriever和一種response synthesizer。retriever選擇SimilarityPostprocessor,response synthesizer選擇Refine

程式碼如下所示:

import logging
import sys
import torch
from llama_index.core import PromptTemplate, Settings, SimpleDirectoryReader, \
    VectorStoreIndex, get_response_synthesizer
from llama_index.core.callbacks import LlamaDebugHandler, CallbackManager
from llama_index.core.indices.vector_store import VectorIndexRetriever
from llama_index.core.postprocessor import SimilarityPostprocessor
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.response_synthesizers import ResponseMode
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.huggingface import HuggingFaceLLM

# 定義日誌
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))

# 定義system prompt
SYSTEM_PROMPT = """You are a helpful AI assistant."""
query_wrapper_prompt = PromptTemplate(
    "[INST]<<SYS>>\n" + SYSTEM_PROMPT + "<</SYS>>\n\n{query_str}[/INST] "
)

# 使用llama-index建立本地大模型
llm = HuggingFaceLLM(
    context_window=4096,
    max_new_tokens=2048,
    generate_kwargs={"temperature": 0.0, "do_sample": False},
    query_wrapper_prompt=query_wrapper_prompt,
    tokenizer_name='/yldm0226/models/Qwen1.5-14B-Chat',
    model_name='/yldm0226/models/Qwen1.5-14B-Chat',
    device_map="auto",
    model_kwargs={"torch_dtype": torch.float16},
)
Settings.llm = llm

# 使用LlamaDebugHandler構建事件回溯器,以追蹤LlamaIndex執行過程中發生的事件
llama_debug = LlamaDebugHandler(print_trace_on_end=True)
callback_manager = CallbackManager([llama_debug])
Settings.callback_manager = callback_manager

# 使用llama-index-embeddings-huggingface構建本地embedding模型
Settings.embed_model = HuggingFaceEmbedding(
    model_name="/yldm0226/RAG/BAAI/bge-base-zh-v1.5"
)

# 讀取文件並構建索引
documents = SimpleDirectoryReader("document").load_data()
index = VectorStoreIndex.from_documents(documents)

# 構建retriever
retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=5,
)

# 構建response synthesizer
response_synthesizer = get_response_synthesizer(
    response_mode=ResponseMode.REFINE
)

# 構建查詢引擎
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[SimilarityPostprocessor(similarity_cutoff=0.6)],
)

# 查詢獲得答案
response = query_engine.query("不耐疲勞,口燥、咽乾可能是哪些證候?")
print(response)

# get_llm_inputs_outputs返回每個LLM呼叫的開始/結束事件
event_pairs = llama_debug.get_llm_inputs_outputs()
print(event_pairs[0][1].payload["formatted_prompt"])

執行程式碼後,在輸出中可以找到類似下面的內容:

**********
Trace: query
    |_query ->  33.425664 seconds
      |_synthesize ->  33.403238 seconds
        |_templating ->  2e-05 seconds
        |_llm ->  7.425154 seconds
        |_templating ->  2.5e-05 seconds
        |_llm ->  4.763223 seconds
        |_templating ->  2.4e-05 seconds
        |_llm ->  6.601226 seconds
        |_templating ->  2.2e-05 seconds
        |_llm ->  6.878335 seconds
        |_templating ->  2.2e-05 seconds
        |_llm ->  7.726241 seconds
**********

可以看出,我們將response synthesizer由預設的Compact替換為Refine之後,query在程式過程中經歷的階段發生了變化,REFINE模式會進行更多次的templating和LLM呼叫。

構建的新Query如下所示,這與之前是一樣的:

[INST]<<SYS>>
You are a helpful AI assistant.<</SYS>>

Context information is below.
---------------------
file_path: document/中醫臨床診療術語證候.txt

臨床以乾咳、痰少,或痰中帶血,口渴,鼻咽燥痛,聲音嘶啞,肌膚枯燥,舌質紅而幹,舌苔少,脈虛數,伴見低熱,神疲、乏力,語聲低微,盜汗,大便乾結等為特徵的證候。

5.4.1.5.1.1
    肺燥津傷證  syndrome/pattern of lung dryness with fluid damage
    肺燥津虧證
    因燥邪襲肺,津液虧虛,肺燥失潤所致。臨床以乾咳、少痰,咽乾,口燥,鼻燥,喉癢,舌質紅,舌苔少津,脈浮細數等為特徵的證候。

5.4.1.5.1.2
    肺燥傷陰證  syndrome/pattern of lung dryness damaging yin
    因肺熱化燥,傷及陰津所致。臨床以咳嗽,痰少或無,痰黃而黏,口乾、咽燥,煩渴、多飲,小便短少,舌質紅,舌苔焦黃,脈弦數,可伴見潮熱、顴紅等為特徵的證候。

5.4.1.5.1.3
    肺燥陰虛證  syndrome/pattern of lung dryness with yin deficiency
    陰虛肺燥證
    因陰液虧虛,肺燥失潤所致。臨床以午後潮熱,乾咳、痰少,喉癢、鼻燥、少涕,咽乾、煩渴,消瘦,舌質紅,舌苔少,脈細數,伴見盜汗浸衣,心煩、失眠等為特徵的證候。

5.4.1.5.2
    肺燥鬱熱證  syndrome/pattern of lung dryness with stagnated heat
    肺燥化熱證
    因憂勞傷肺,鬱熱化燥,傷及肺津所致。臨床以發熱、煩渴,咳嗽、痰少而黏,胸脅灼痛,大便乾結,小便短少,舌質紅而幹,舌苔薄黃,脈弦數等為特徵的證候。

5.4.1.6
    肺經證  syndrome/pattern of lung meridian (vessel)
    泛指因各種原因致使肺經循行部位異常所引起的一類證候。

5.4.1.6.1
    肺經風熱證  syndrome/pattern of wind and heat in the lung meridian
    因風熱邪客肺經,或風熱鬱滯膚腠,外發於頭面所致。
---------------------
Given the context information and not prior knowledge, answer the query.
Query: 不耐疲勞,口燥、咽乾可能是哪些證候?
Answer: [/INST] 

另外,由於我們使用了SimilarityPostprocessor的retriever,並將相似度閾值設定為0.6,因此檢索出的相似度小於0.6的文件片段會被摘除。

最後,我們看一下模型的回覆:

從中醫角度看,不耐疲勞、口燥、咽乾的症狀可能涉及多個證候,如燥邪犯肺證(4.6.3.3),由於燥氣耗傷肺津;津虧熱結證(4.6.3.2)或津枯腸結證(4.6.3.3),表現為體內津液虧損且伴有熱象,導致口乾、便秘等;肺胃陰虛證(5.6.4.4.2.2)和心腎陰虛(5.1.1.1.1),特別是心腎不交時,也會出現類似症狀。此外,心繫證中的心寒證(5.1.1.1)如心中寒證也可能表現出口乾咽燥。具體診斷需根據臨床表現、體質和相關檢查結果來確定。

從上面的案例可以看出,我們可以自由組合不同的retriever和response synthesizer,以完成我們的需求。當LlamaIndex提供的

retriever和response synthesizer不能滿足我們的需求的時候,我們還可以自定義retriever和response synthesizer,有興趣的讀者可以自行探索。

相關文章