這一系列文章其實就是 Andrew Ng 大大與 OpenAI 合作的一門免費課程【Building Systems with the ChatGPT API】的筆記。很建議大家直接看原本的課程影片,內容蠻淺顯易懂的。
對話系統的開發,整體上就是一個持續且循環的改進過程。因此,能在持續變動之下,確保提示訊息的回應依然如我們預期的方式運作,無疑是一個關鍵步驟。
在這份文章中,我們將以大賣場的銷售助理為例子進行說明,讓大家可以看到在正式的系統開發時,能夠以數值來打分數的這類訊息,它的評估作業是如何運作的。
我們先取得賣場的所有產品類別以及各類別內的產品清單:
# 取得賣場所有的產品類別以及類別內產品清單
products_and_category = utils.get_products_and_category()
--- 以下是 products_and_category 的內容 ---
{'電腦與筆記型電腦': ['TechPro Ultrabook',
'BlueWave 電競筆電',
'PowerLite 二合一電腦',
'TechPro 桌機',
'BlueWave Chromebook'],
... 中間省略 ...
'相機和攝影機': ['FotoSnap 單眼相機',
'ActionCam 4K',
'FotoSnap 無反光鏡相機',
'ZoomMaster 攝影機',
'FotoSnap 拍立得相機']}
然後設計第一個版本的提示訊息,這個提示訊息主要目的是從客戶訊息提取對方要詢問的產品類別以及產品清單:
def find_category_and_product_v1(user_input,products_and_category):
delimiter = "####"
system_message = f"""
您將會收到客戶服務的查詢。
該客戶服務查詢將以 {delimiter} 字符進行分隔。
請輸出一個 Python 列表,其中每個物件都是一個 JSON 物件,每個物件具有以下格式:
'category': <以下其中一種:電腦和筆記本,智慧手機和配件,電視和家庭影院系統,遊戲機和配件,音響設備,相機和攝像機>
和
'products': <必須在下面允許的產品中找到的產品列表內>
類別和產品必須在客戶服務查詢中找到。
如果提到了產品,它必須與下面允許的產品列表中的正確類別相關聯。
如果找不到產品或類別,則輸出一個空列表。
根據產品名稱和產品類別與客戶服務查詢的相關程度,列出所有相關的產品。
不要從產品的名稱推測任何特性或屬性,如相對品質或價格。
允許的產品以 JSON 格式提供。
每個項目的鍵代表類別。
每個項目的值是該類別中的產品列表。
允許的產品:{products_and_category}
"""
few_shot_user_1 = """我想要你們最貴的電腦。"""
few_shot_assistant_1 = """
[{'category': '電腦與筆記型電腦', \
'products': ['TechPro Ultrabook', 'BlueWave 電競筆電', 'PowerLite 二合一電腦', 'TechPro 桌機', 'BlueWave Chromebook']}]
"""
messages = [
{'role':'system', 'content': system_message},
{'role':'user', 'content': f"{delimiter}{few_shot_user_1}{delimiter}"},
{'role':'assistant', 'content': few_shot_assistant_1 },
{'role':'user', 'content': f"{delimiter}{user_input}{delimiter}"},
]
return get_completion_from_messages(messages)
測試幾個客戶的訊息,的確如我們預期的回應:
customer_msg_0 = f"""如果我有預算限制的話我可以購買哪種電視?"""
products_by_category_0 = find_category_and_product_v1(customer_msg_0,
products_and_category)
print(products_by_category_0)
--- 以下是回應內容 ---
[{'category': '電視和家庭劇院系統', 'products': ['CineView 4K 液晶電視',
'SoundMax 家庭劇院', 'CineView 8K 液晶電視', 'SoundMax 音箱',
'CineView OLED 電視']}]
第二個訊息也是正常:
customer_msg_1 = f"""我需要幫我的手機買個充電器"""
products_by_category_1 = find_category_and_product_v1(customer_msg_1,
products_and_category)
print(products_by_category_1)
--- 以下是回應內容 ---
[{'category': '手機和配件', 'products': ['MobiTech 行動充電王',
'MobiTech 無線充電器']}]
但是下方這個比較複雜的訊息,它就不能如預期的回覆了:
customer_msg_4 = f"""
跟我說明一下 CineView 電視,8K 的那一款,還有 Gamesphere 遊戲機,X 一款。
我有預算的限制,你們有什麼電腦可以選擇?
"""
products_by_category_4 = find_category_and_product_v1(customer_msg_4,
products_and_category)
# 預期只回覆 python 物件列表, 但是這個回覆有問題。
print(products_by_category_4)
--- 以下是回應內容 ---
[{'category': '電視和家庭劇院系統', 'products': ['CineView 8K 液晶電視']}, {'category': '遊戲機和配件', 'products': ['GameSphere X']}]
對於您的預算限制,我們有以下電腦選擇:
[{'category': '電腦與筆記型電腦', 'products': ['TechPro Ultrabook',
'BlueWave 電競筆電', 'PowerLite 二合一電腦', 'TechPro 桌機',
'BlueWave Chromebook']}]
你可以看到,它的回覆訊息參雜了【 對於您的預算限制,我們有以下電腦選擇:】這段訊息。針對這個問題,我們的改善方案則是在提示訊息裏面加上
請不要輸出任何不是 JSON 格式的額外文字。
輸出要求的 JSON 之後,請不要寫任何解釋性的文字。
這段要求,完整提示訊息如下:
def find_category_and_product_v2(user_input,products_and_category):
"""
增加: 不要輸出任何不是 JSON 格式的額外文字。
"""
delimiter = "####"
system_message = f"""
您將會收到客戶服務的查詢。
該客戶服務查詢將以 {delimiter} 字符進行分隔。
請輸出一個 Python 列表,其中每個物件都是一個 JSON 物件,每個物件具有以下格式:
'category': <以下其中一種:電腦和筆記本,智能手機和配件,電視和家庭影院系統,遊戲機和配件,音響設備,相機和攝像機>
和
'products': <必須在下面允許的產品中找到的產品列表內>
請不要輸出任何不是 JSON 格式的額外文字。
輸出要求的 JSON 之後,請不要寫任何解釋性的文字。
類別和產品必須在客戶服務查詢中找到。
如果提到了產品,它必須與下面允許的產品列表中的正確類別相關聯。
如果找不到產品或類別,則輸出一個空列表。
根據產品名稱和產品類別與客戶服務查詢的相關程度,列出所有相關的產品。
不要從產品的名稱推測任何特性或屬性,如相對品質或價格。
允許的產品以 JSON 格式提供。
每個項目的鍵代表類別。
每個項目的值是該類別中的產品列表。
允許的產品:{products_and_category}
"""
few_shot_user_1 = """我想要最貴的電腦,你有什麼建議?"""
few_shot_assistant_1 = """
[{'category': '電腦與筆記型電腦', \
'products': ['TechPro Ultrabook', 'BlueWave 電競筆電',
'PowerLite 二合一電腦', 'TechPro 桌機', 'BlueWave Chromebook']}]
"""
messages = [
{'role':'system', 'content': system_message},
{'role':'user', 'content': f"{delimiter}{few_shot_user_1}{delimiter}"},
{'role':'assistant', 'content': few_shot_assistant_1 },
{'role':'user', 'content': f"{delimiter}{user_input}{delimiter}"},
]
return get_completion_from_messages(messages)
我們以剛剛發現的,有問題的訊息測試是否已經修正:
customer_msg_4 = f"""
跟我說明一下 CineView 電視,8K 的那一款,還有 Gamesphere 遊戲機,X 一款。
我有預算的限制,你們有什麼電腦可以選擇?
"""
products_by_category_4 = find_category_and_product_v2(customer_msg_4,
products_and_category)
print(products_by_category_4)
--- 以下是回應內容 ---
[{'category': '電視和家庭劇院系統', 'products': ['CineView 8K 液晶電視']},
{'category': '遊戲機和配件', 'products': ['GameSphere X']},
{'category': '電腦與筆記型電腦', 'products': ['TechPro Ultrabook',
'BlueWave 電競筆電', 'PowerLite 二合一電腦', 'TechPro 桌機',
'BlueWave Chromebook']}]
確定剛有問題的訊息,已經修正無誤。
這時候有一點需要特別提醒的,當你的提示訊息改變後,也記得要重新確認其他沒有問題的部分也一樣正常運作:
customer_msg_0 = f"""如果我有預算限制的話我可以購買哪種電視?"""
products_by_category_0 = find_category_and_product_v2(customer_msg_0,
products_and_category)
print(products_by_category_0)
--- 以下是回應內容 ---
[{'category': '電視和家庭劇院系統', 'products': ['CineView 4K 液晶電視',
'SoundMax 家庭劇院', 'CineView 8K 液晶電視', 'SoundMax 音箱',
'CineView OLED 電視']}]
修正版本的提示訊息,第一個測試訊息也一樣正常運作。
提示訊息的開發、驗證、重複確認就大概是前面這樣的狀況。到這裏,比較敏感的朋友可能會感受到,這樣不是很繁瑣嗎?
沒有錯,而且當你的對話系統發展一段時間後,你將會蒐集越來越多重要,而且對整體運作來說,不驗證不行的對話內容,所以你接下來就可以試著使用批次的驗證方式,以下是一個簡單的批次驗證範例。
我們先建立一個自己的測試集:
msg_ideal_pairs_set = [
# eg 0
{'customer_msg':"""如果我有預算限制的話我可以購買哪種電視?""",
'ideal_answer':{
'電視和家庭劇院系統':set(
['CineView 4K 液晶電視', 'SoundMax 家庭劇院', 'CineView 8K 液晶電視', 'SoundMax 音箱', 'CineView OLED 電視']
)}
},
# eg 1
{'customer_msg':"""我需要幫我的手機買個充電器""",
'ideal_answer':{
'手機和配件':set(
['MobiTech 行動充電王', 'MobiTech 無線充電器']
)}
},
... 中間省略 ...
# eg 9
{'customer_msg':f"""我想要一台時光機浴缸。""",
'ideal_answer': []
}
]
下面這個函式則是來協助我們來評估【回應的內容】與【預期的內容】的差異性:
def eval_response_with_ideal(response,
ideal,
debug=False):
if debug:
print("回應內容: ")
print(response)
# json.loads() 只能讀取雙引號字串,無法讀取單引號字串
json_like_str = response.replace("'",'"')
# 讀取為 dict list
l_of_d = json.loads(json_like_str)
# 如果兩者都是空值時
if l_of_d == [] and ideal == []:
return 1
# 如果兩者只有其中一個是空值時,明顯有錯誤。
elif l_of_d == [] or ideal == []:
return 0
correct = 0
if debug:
print("l_of_d: ")
print(l_of_d)
for d in l_of_d:
cat = d.get('category')
prod_l = d.get('products')
if cat and prod_l:
# 轉換為 set 以便比較
prod_set = set(prod_l)
# 取得理想回覆中的產品類別
ideal_cat = ideal.get(cat)
if ideal_cat:
# 轉換為 set 以便比較
prod_set_ideal = set(ideal.get(cat))
else:
if debug:
print(f"無法在理想回覆中找到 {cat} 類別")
print(f"理想回覆: {ideal}")
continue
if debug:
print("prod_set\n",prod_set)
print()
print("prod_set_ideal\n",prod_set_ideal)
# 檢查實際回應是否為理想回覆的子集
if prod_set == prod_set_ideal:
# 如果實際回應是理想回覆的子集,則正確
if debug:
print("正確")
correct +=1
else:
# 如果實際回應不是理想回覆的子集,則檢查是否為理想回覆的超集
print("不正確")
print(f"prod_set: {prod_set}")
print(f"prod_set_ideal: {prod_set_ideal}")
if prod_set <= prod_set_ideal:
print("實際回應只是理想回覆的子集")
elif prod_set >= prod_set_ideal:
print("實際回應是理想回覆的超集")
# 計算正確率
pc_correct = correct / len(l_of_d)
return pc_correct
這是單一驗證時的回應結果,最後也打了一個分數,1.0 代表完全相同:
response = find_category_and_product_v2(msg_ideal_pairs_set[7]["customer_msg"],
products_and_category)
print(f'Response: {response}')
eval_response_with_ideal(response,
msg_ideal_pairs_set[7]["ideal_answer"])
--- 以下是回應內容 ---
Response:
[{'category': '遊戲機和配件', 'products': ['GameSphere X', 'ProGamer 搖桿',
'GameSphere Y', 'ProGamer 賽車方向盤', 'GameSphere VR 頭盔']}]
1.0
最後我們就可以自動化驗證所有的測試項目了:
import time
# 請注意,這個在任一個 api 呼叫逾時時,就會失敗。
score_accum = 0
for i, pair in enumerate(msg_ideal_pairs_set):
print(f"範例 {i}")
customer_msg = pair['customer_msg']
ideal = pair['ideal_answer']
# 從 customer_msg 找出產品類別和產品名稱
# print("Customer message",customer_msg)
# print("ideal:",ideal)
response = find_category_and_product_v2(customer_msg,
products_and_category)
# 計算正確率
# print("products_by_category",products_by_category)
score = eval_response_with_ideal(response, ideal, debug=False)
print(f"{i}: {score}")
score_accum += score
# 等待 20 秒。免費版的 openapi 有每分鐘 3 次的呼叫限制,如果確定沒有問題,可以把這行註解掉。
#print("等待 20 秒。\n")
time.sleep(20)
# 計算所有範例的正確率
n_examples = len(msg_ideal_pairs_set)
fraction_correct = score_accum / n_examples
print(f"{n_examples} 個測試案例的正確率 : {fraction_correct}")
``` 以下是輸出內容 ---
範例 0
0: 1.0
範例 1
1: 1.0
範例 2
2: 1.0
範例 3
3: 1.0
範例 4
4: 1.0
範例 5
5: 1.0
範例 6
6: 1.0
範例 7
7: 1.0
範例 8
8: 1.0
範例 9
9: 1
10 個測試案例的正確率 : 1.0
我們可以看到如上最後的輸出,我們測試集裏面目前有10個待驗證項目,而驗證結果是全數通過(最後一個 1.0 代表的是 100% 通過)。
下一篇則會跟大家介紹只能模糊評估的訊息它的評估方式,這也是我們這個系列的最後一篇了,敬請期待~