構建RAG應用-day05: 如何評估 LLM 應用 評估並最佳化生成部分 評估並最佳化檢索部分

passion2021發表於2024-04-27

評估 LLM 應用

1.一般評估思路

首先,你會在一到三個樣本的小樣本中調整 Prompt ,嘗試使其在這些樣本上起效。

隨後,當你對系統進行進一步測試時,可能會遇到一些棘手的例子,這些例子無法透過 Prompt 或者演算法解決。

最終,你會將足夠多的這些例子新增到你逐步擴大的開發集中,以至於手動執行每一個例子以測試 Prompt 變得有些不便。

然後,你開始開發一些用於衡量這些小樣本集效能的指標,例如平均準確度。

也就是說總結一下大模型應用的開發過程,可以得出測試用例隨著時間發展的變化:
基於一兩個小測試用例構建應用 --> 解決棘手的測試用例(bad case) --> 足夠多的測試用例集 --> 針對測試用例集合設定指標(自動評估)

image-20240427000115182

2. 大模型評估方法

在具體的大模型應用開發中,我們可以找到 Bad Cases,並不斷針對性最佳化 Prompt 或檢索架構來解決 Bad Cases,從而最佳化系統的表現。

我們會將找到的每一個 Bad Case 都加入到我們的驗證集中,每一次最佳化之後,我們會重新對驗證集中所有驗證案例進行驗證,從而保證最佳化後的系統不會在原有 Good Case 上失去能力或表現降級。

核心:找到棘手的情況(大模型解決不了、效果不佳的情況),將其加入驗證集。利用這個驗證集,每次最佳化都進行驗證,保證是在正確的方向。

import sys
sys.path.append("../C3 搭建知識庫") # 將父目錄放入系統路徑中

# 使用智譜 Embedding API,注意,需要將上一章實現的封裝程式碼下載到本地
from zhipuai_embedding import ZhipuAIEmbeddings

from langchain.vectorstores.chroma import Chroma
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv, find_dotenv
import os

_ = load_dotenv(find_dotenv())    # read local .env file
zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]

# 定義 Embeddings
embedding = ZhipuAIEmbeddings()

# 向量資料庫持久化路徑
persist_directory = '../../data_base/vector_db/chroma'

# 載入資料庫
vectordb = Chroma(
    persist_directory=persist_directory,  # 允許我們將persist_directory目錄儲存到磁碟上
    embedding_function=embedding
)

# 使用 OpenAI GPT-3.5 模型
llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0)

2.1 人工評估的一般思路

應用開發的初期,可以採用人工評估,也就是人來觀察大模型的輸出是否達到好的效果。
但是人工評估也需要有一些準則。

準則一 量化評估

方法1:打分。對llm的輸出進行打分,比如1-5分,1-100分。
方法2:評估規範。 在不同評估員中達成共識,例如在幻覺的情況下,直接打0分。
方法3:平均值。對於單個prompt,使用多個測試用例進行測試,最後去評分的平均值。選擇平均值高的prompt。

如下是使用兩個測試用例(query),對兩個prompt進行測試,然後人工觀察輸出結果進行打分,最終取平均值,然後決定選擇哪個prompt。

import openai
from embed import ZhipuAIEmbeddings
from langchain.vectorstores.chroma import Chroma
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv, find_dotenv
import os

openai.base_url = 'https://api.chatanywhere.tech/v1'
_ = load_dotenv(find_dotenv())  # read local .env file
zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]

# 定義 Embeddings
embedding = ZhipuAIEmbeddings()

# 向量資料庫持久化路徑
persist_directory = 'data_base/vector_db/chroma'

# 載入資料庫
vectordb = Chroma(
    persist_directory=persist_directory,  # 允許我們將persist_directory目錄儲存到磁碟上
    embedding_function=embedding
)

# 使用 OpenAI GPT-3.5 模型
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0, base_url=openai.base_url)

from langchain.prompts import PromptTemplate
from langchain.chains import RetrievalQA

template_v1 = """使用以下上下文來回答最後的問題。如果你不知道答案,就說你不知道,不要試圖編造答
案。最多使用三句話。儘量使答案簡明扼要。總是在回答的最後說“謝謝你的提問!”。
{context}
問題: {question}
"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"],
                                 template=template_v1)

qa_chain = RetrievalQA.from_chain_type(llm,
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt": QA_CHAIN_PROMPT})

print("問題一:")
question = "南瓜書和西瓜書有什麼關係?"
result = qa_chain({"query": question})
print(result["result"])

print("問題二:")
question = "應該如何使用南瓜書?"
result = qa_chain({"query": question})
print(result["result"])

template_v2 = """使用以下上下文來回答最後的問題。如果你不知道答案,就說你不知道,不要試圖編造答
案。你應該使答案儘可能詳細具體,但不要偏題。如果答案比較長,請酌情進行分段,以提高答案的閱讀體驗。
{context}
問題: {question}
有用的回答:"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context", "question"],
                                 template=template_v2)

qa_chain = RetrievalQA.from_chain_type(llm,
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt": QA_CHAIN_PROMPT})

print("問題一:")
question = "南瓜書和西瓜書有什麼關係?"
result = qa_chain({"query": question})
print(result["result"])

print("問題二:")
question = "應該如何使用南瓜書?"
result = qa_chain({"query": question})
print(result["result"])

準則二 多維評估

什麼是多維評估?有哪些維度?

知識、回答的正確性

① 知識查詢正確性。該維度需要檢視系統從向量資料庫查詢相關知識片段的中間結果,評估系統查詢到的知識片段是否能夠對問題做出回答。該維度為0-1評估,即打分為0指查詢到的知識片段不能做出回答,打分為1指查詢到的知識片段可以做出回答。

② 回答一致性。該維度評估系統的回答是否針對使用者問題展開,是否有偏題、錯誤理解題意的情況,該維度量綱同樣設計為0~1,0為完全偏題,1為完全切題,中間結果可以任取。

③ 回答幻覺比例。該維度需要綜合系統回答與查詢到的知識片段,評估系統的回答是否出現幻覺,幻覺比例有多高。該維度同樣設計為0~1,0為全部是模型幻覺,1為沒有任何幻覺。

④ 回答正確性。該維度評估系統回答是否正確,是否充分解答了使用者問題,是系統最核心的評估指標之一。該維度可以在0~1之間任意打分。

擬人性、語法正確性

⑤ 邏輯性。該維度評估系統回答是否邏輯連貫,是否出現前後衝突、邏輯混亂的情況。該維度為0-1評估。

⑥ 通順性。該維度評估系統回答是否通順、合乎語法,可以在0~1之間任意打分。

⑦ 智慧性。該維度評估系統回答是否擬人化、智慧化,是否能充分讓使用者混淆人工回答與智慧回答。該維度可以在0~1之間任意打分。

所以基本上就是按照評分標準,人工逐個去確認核實,最終得出分數。

# 問題:
應該如何使用南瓜書?

# 模型回答:
應該將南瓜書作為西瓜書的補充,主要在遇到自己無法推導或理解的公式時進行查閱。對於初學機器學習的小白來說,建議先簡單過一下南瓜書的第1章和第2章,等學得更深入後再回來深究。每個公式的解析和推導都以本科數學基礎的視角進行講解,超綱的數學知識會在附錄和參考文獻中給出,感興趣的同學可以繼續深入學習。如果南瓜書中沒有你想要查閱的公式,或者發現有錯誤,可以在GitHub的Issues中提交反饋,通常會在24小時內得到回覆。最終目的是幫助讀者更好地理解和應用機器學習知識,成為合格的理工科學生。

# 我們做出相應評估:
① 知識查詢正確性—— 1
② 回答一致性—— 0.8(解答了問題,但是類似於“反饋”的話題偏題了)
③ 回答幻覺比例—— 1
④ 回答正確性—— 0.8(理由同上)
⑤ 邏輯性—— 0.7(後續內容與前面邏輯連貫性不強)
⑥ 通順性—— 0.6(最後總結囉嗦且無效)
⑦ 智慧性—— 0.5(具有 AI 回答的顯著風格)

我們也可以針對不同維度的不同重要性賦予權值,再計算所有維度的加權平均來代表系統得分。

對系統每一個版本的每一個案例,我們都需要進行七次評估。如果我們有兩個版本的系統,驗證集中有10個驗證案例,那麼我們每一次評估就需要 140 次;但當我們的系統不斷改進迭代,驗證集會迅速擴大,一般來說,一個成熟的系統驗證集應該至少在幾百的體量,迭代改進版本至少有數十個,那麼我們評估的總次數會達到上萬次,帶來的人力成本與時間成本就很高了。因此,我們需要一種自動評估模型回答的方法。

3.2 簡單自動評估

核心思想:將複雜的沒有標準答案的主觀題進行轉化,從而變成有標準答案的問題,進而透過簡單的自動評估來實現。

方法一 構造客觀題

主觀題的評估是非常困難的,但是客觀題可以直接對比系統答案與標準答案是否一致,從而實現簡單評估。
簡單來說,就是直接讓llm輸出ABCD,做選擇題,根據llm的選擇來進行打分。而不是讓llm回覆一大串文字。

例如,對於問題:

【問答題】南瓜書的作者是誰?

我們可以將該主觀題構造為如下客觀題(也是我們的測試用例,看看llm回答的如何):

【多項選擇題】南瓜書的作者是誰?   A 周志明 B 謝文睿 C 秦州 D 賈彬彬

然後構造一個函式,來根據llm的回答進行打分:

def multi_select_score_v2(true_answer : str, generate_answer : str) -> float:
    # true_anser : 正確答案,str 型別,例如 'BCD'
    # generate_answer : 模型生成答案,str 型別
    true_answers = list(true_answer)
    '''為便於計算,我們假設每道題都只有 A B C D 四個選項'''
    # 先找出錯誤答案集合
    false_answers = [item for item in ['A', 'B', 'C', 'D'] if item not in true_answers]
    # 如果生成答案出現了錯誤答案
    for one_answer in false_answers:
        if one_answer in generate_answer:
            return -1
    # 再判斷是否全選了正確答案
    if_correct = 0
    for one_answer in true_answers:
        if one_answer in generate_answer:
            if_correct += 1
            continue
    if if_correct == 0:
        # 不選
        return 0
    elif if_correct == len(true_answers):
        # 全選
        return 1
    else:
        # 漏選
        return 0.5

方法二:計算答案相似度

這個方法,需要你首先自己寫一個問題和一個正確答案,然後使用一些ai演算法來比較,llm的輸出和你準備好的答案的文字相似度。(類似embedding計算餘弦距離?)

例如,對問題:

南瓜書的目標是什麼?

我們可以首先人工構造一個標準回答:

周志華老師的《機器學習》(西瓜書)是機器學習領域的經典入門教材之一,周老師為了使盡可能多的讀者透過西瓜書對機器學習有所瞭解, 所以在書中對部分公式的推導細節沒有詳述,但是這對那些想深究公式推導細節的讀者來說可能“不太友好”,本書旨在對西瓜書裡比較難理解的公式加以解析,以及對部分公式補充具體的推導細節。

計算相似度的方法有很多,我們一般可以使用 BLEU 來計算相似度,其原理詳見:知乎|BLEU詳解,對於不想深究演算法原理的同學,可以簡單理解為主題相似度。

我們可以呼叫 nltk 庫中的 bleu 打分函式來計算:

from nltk.translate.bleu_score import sentence_bleu
import jieba

def bleu_score(true_answer : str, generate_answer : str) -> float:
    # true_anser : 標準答案,str 型別
    # generate_answer : 模型生成答案,str 型別
    true_answers = list(jieba.cut(true_answer))
    # print(true_answers)
    generate_answers = list(jieba.cut(generate_answer))
    # print(generate_answers)
    bleu_score = sentence_bleu(true_answers, generate_answers)
    return bleu_score

問題:

  • 需要人工構造標準答案。對於一些垂直領域而言,構造標準答案可能是一件困難的事情;
  • 透過相似度來評估,可能存在問題。例如,如果生成回答與標準答案高度一致但在核心的幾個地方恰恰相反導致答案完全錯誤,bleu 得分仍然會很高;
  • 透過計算與標準答案一致性靈活性很差,如果模型生成了比標準答案更好的回答,但評估得分反而會降低;
  • 無法評估回答的智慧性、流暢性。如果回答是各個標準答案中的關鍵詞拼接出來的,我們認為這樣的回答是不可用無法理解的,但 bleu 得分會較高。

2.3 使用大模型進行評估

就是讓llm扮演評估員打分,就這麼簡單:

prompt = '''
你是一個模型回答評估員。
接下來,我將給你一個問題、對應的知識片段以及模型根據知識片段對問題的回答。
請你依次評估以下維度模型回答的表現,分別給出打分:

① 知識查詢正確性。評估系統給定的知識片段是否能夠對問題做出回答。如果知識片段不能做出回答,打分為0;如果知識片段可以做出回答,打分為1。

② 回答一致性。評估系統的回答是否針對使用者問題展開,是否有偏題、錯誤理解題意的情況,打分分值在0~1之間,0為完全偏題,1為完全切題。

③ 回答幻覺比例。該維度需要綜合系統回答與查詢到的知識片段,評估系統的回答是否出現幻覺,打分分值在0~1之間,0為全部是模型幻覺,1為沒有任何幻覺。

④ 回答正確性。該維度評估系統回答是否正確,是否充分解答了使用者問題,打分分值在0~1之間,0為完全不正確,1為完全正確。

⑤ 邏輯性。該維度評估系統回答是否邏輯連貫,是否出現前後衝突、邏輯混亂的情況。打分分值在0~1之間,0為邏輯完全混亂,1為完全沒有邏輯問題。

⑥ 通順性。該維度評估系統回答是否通順、合乎語法。打分分值在0~1之間,0為語句完全不通順,1為語句完全通順沒有任何語法問題。

⑦ 智慧性。該維度評估系統回答是否擬人化、智慧化,是否能充分讓使用者混淆人工回答與智慧回答。打分分值在0~1之間,0為非常明顯的模型回答,1為與人工回答高度一致。

你應該是比較嚴苛的評估員,很少給出滿分的高評估。
使用者問題:
```
{}
```
待評估的回答:
```
{}
```
給定的知識片段:
```
{}
```
你應該返回給我一個可直接解析的 Python 字典,字典的鍵是如上維度,值是每一個維度對應的評估打分。
不要輸出任何其他內容。
'''

程式碼:

from openai import OpenAI

client = OpenAI(
    # This is the default and can be omitted
    api_key=os.environ.get("OPENAI_API_KEY"),
)


def gen_gpt_messages(prompt):
    '''
    構造 GPT 模型請求引數 messages
    
    請求引數:
        prompt: 對應的使用者提示詞
    '''
    messages = [{"role": "user", "content": prompt}]
    return messages


def get_completion(prompt, model="gpt-3.5-turbo", temperature = 0):
    '''
    獲取 GPT 模型呼叫結果

    請求引數:
        prompt: 對應的提示詞
        model: 呼叫的模型,預設為 gpt-3.5-turbo,也可以按需選擇 gpt-4 等其他模型
        temperature: 模型輸出的溫度係數,控制輸出的隨機程度,取值範圍是 0~2。溫度係數越低,輸出內容越一致。
    '''
    response = client.chat.completions.create(
        model=model,
        messages=gen_gpt_messages(prompt),
        temperature=temperature,
    )
    if len(response.choices) > 0:
        return response.choices[0].message.content
    return "generate answer error"

question = "應該如何使用南瓜書?"
result = qa_chain({"query": question})
answer = result["result"]
knowledge = result["source_documents"]

response = get_completion(prompt.format(question, answer, knowledge))
response

2.4 混合評估

將上述方法綜合起來使用

例如,針對本專案個人知識庫助手,我們可以設計以下混合評估方法:

  1. 客觀正確性。客觀正確性指對於一些有固定正確答案的問題,模型可以給出正確的回答。我們可以選取部分案例,使用構造客觀題的方式來進行模型評估,評估其客觀正確性。
  2. 主觀正確性。主觀正確性指對於沒有固定正確答案的主觀問題,模型可以給出正確的、全面的回答。我們可以選取部分案例,使用大模型評估的方式來評估模型回答是否正確。
  3. 智慧性。智慧性指模型的回答是否足夠擬人化。由於智慧性與問題本身弱相關,與模型、Prompt 強相關,且模型判斷智慧效能力較弱,我們可以少量抽樣進行人工評估其智慧性。
  4. 知識查詢正確性。知識查詢正確性指對於特定問題,從知識庫檢索到的知識片段是否正確、是否足夠回答問題。知識查詢正確性推薦使用大模型進行評估,即要求模型判別給定的知識片段是否足夠回答問題。同時,該維度評估結果結合主觀正確性可以計算幻覺情況,即如果主觀回答正確但知識查詢不正確,則說明產生了模型幻覺。

使用上述評估方法,基於已得到的驗證集示例,可以對專案做出合理評估。限於時間與人力,此處就不具體展示了。

3.評估並最佳化生成部分

1. 提升直觀回答質量

這部分是教學如何調prompt。
首先給出一個例子:

問題:什麼是南瓜書
初始回答:南瓜書是對《機器學習》(西瓜書)中難以理解的公式進行解析和補充推導細節的一本書。謝謝你的提問!
存在不足:回答太簡略,需要回答更具體;謝謝你的提問感覺比較死板,可以去掉

我們需要找到回覆中存在的問題,這也就是最佳化的方向,然後依據問題修改prompt。
核心:圍繞具體業務展開思考,找出初始回答中不足以讓人滿意的點,並針對性進行提升改進

2. 標明知識來源,提高可信度

示例:

強化學習是一種機器學習方法,旨在讓智慧體透過與環境的互動學習如何做出一系列好的決策。在這個過程中,智慧體會根據環境的反饋(獎勵)來調整自己的行為,以最大化長期獎勵的總和。強化學習的目標是在不確定的情況下做出最優的決策,類似於讓一個小孩透過不斷嘗試來學會走路的過程。強化學習的互動過程由智慧體和環境兩部分組成,智慧體根據環境的狀態選擇動作,環境根據智慧體的動作輸出下一個狀態和獎勵。強化學習的應用非常廣泛,包括遊戲玩法、機器人控制、交通管理等領域。【來源:蘑菇書一語二語二強化學習教程】。

但是,附上原文來源往往會導致上下文的增加以及回覆速度的降低,我們需要根據業務場景酌情考慮是否要求附上原文。可以在資料庫層面就對於原文進行標註,只要檢索匹配到,就能獲取原文的位置,不過這也有很多工作要做。

3. 構造思維鏈

問題:我們應該如何去構造一個 LLM 專案
初始回答:略
存在不足:事實上,知識庫中中關於如何構造LLM專案的內容是使用 LLM API 去搭建一個應用,模型的回答看似有道理,實則是大模型的幻覺,將部分相關的文字拼接得到,存在問題

構建思維鏈的示例:也就是告訴llm怎麼做,怎麼思考。如下是個兩部的思維,1根據上下文回答問題,2進行反思

template_v4 = """
請你依次執行以下步驟:
① 使用以下上下文來回答最後的問題。如果你不知道答案,就說你不知道,不要試圖編造答案。
你應該使答案儘可能詳細具體,但不要偏題。如果答案比較長,請酌情進行分段,以提高答案的閱讀體驗。
如果答案有幾點,你應該分點標號回答,讓答案清晰具體。
上下文:
{context}
問題: 
{question}
有用的回答:
② 基於提供的上下文,反思回答中有沒有不正確或不是基於上下文得到的內容,如果有,回答你不知道
確保你執行了每一個步驟,不要跳過任意一個步驟。
"""

4. 增加一個指令解析

即我們需要模型以我們指定的格式進行輸出。但是,由於我們使用了 Prompt Template 來填充使用者問題,使用者問題中存在的格式要求往往會被忽略,例如:

question = "LLM的分類是什麼?給我返回一個 Python List"

模型沒有返回list,而是在答案中包含list:

根據上下文提供的資訊,LLM(Large Language Model)的分類可以分為兩種型別,即基礎LLM和指令微調LLM。基礎LLM是基於文字訓練資料,訓練出預測下一個單詞能力的模型,通常透過在大量資料上訓練來確定最可能的詞。指令微調LLM則是對基礎LLM進行微調,以更好地適應特定任務或場景,類似於向另一個人提供指令來完成任務。

根據上下文,可以返回一個Python List,其中包含LLM的兩種分類:["基礎LLM", "指令微調LLM"]。

解決這個問題的方案是:再加上一次llm呼叫
使用者問題 --> 指令解析llm --> 知識庫llm --> 輸出格式解析llm

指令解析前:

question = "LLM的分類是什麼?給我返回一個 Python List"

指令解析後(也就是呼叫一個指令解析llm,讓其生成一個list):

'```\n["給我返回一個 Python List", "LLM的分類是什麼?"]\n```'

然後根據拆分得到的問題部分,進行檢索。最後根據拆分得到的輸出格式要求,按照輸出格式再次呼叫llm進行輸出。

評估並最佳化檢索部分(瞭解)

檢索部分的檢索精確率和召回率其實更大程度影響了應用的整體效能。

回顧整個 RAG:

image-20240427010709220

如何評估檢索效果?
檢索部分的核心功能是找到存在於知識庫中、能夠正確回答使用者 query 中的提問的文字段落

兩個標準:

  1. query的答案確實存在於知識庫。
    如果正確答案本就不存在,那我們應該將 Bad Case 歸因到知識庫構建部分,說明知識庫構建的廣度和處理精度還有待提升。
  2. 該答案能被成功檢索到。
    什麼叫成功檢索?假設對於每一個 query,系統找到了 K 個文字片段,如果正確答案在 K 個文字片段之一,那麼我們認為檢索成功;

問題:

  • 有的 query 可能需要聯合多個知識片段才能做出回答,對於這種 query,我們如何評估?
  • 檢索到的知識片段彼此之間的順序其實會對大模型的生成帶來影響,我們是否應該將檢索片段的排序納入考慮?
  • 除去檢索到正確的知識片段之外,我們的系統還應儘量避免檢索到錯誤的、誤導性知識片段,否則大模型的生成結果很可能被錯誤片段誤導。我們是否應當將檢索到的錯誤片段納入指標計算?

相關文章