在前一篇測試中已透過 API 從谷歌取得搜尋資料提供給 GPT 模型, 要求它針對所提供的搜尋結果做出回應, 但那是利用提示詞開頭自訂的 "#s" 字串來辨別是否要搜尋網路, 本篇則是要測試如何讓模型自動判別是否要 Call out 搜尋網路.
4. 利用 JSON 回覆字串模板自動判斷是否要搜尋網路 :
首先匯入 OpenAI 類別與建立物件 :
>>> from openai import OpenAI, APIError
>>> api_key='填入 API key'
>>> client=OpenAI(api_key=api_key)
然後寫一個 ask_gpt() 函式 :
>>> def ask_gpt(messages, model='gpt-3.5-turbo'):
try:
reply=client.chat.completions.create(
model=model,
messages=messages
)
return reply.choices[0].message.content
except APIError as e:
return e.message
此處參數 messages 是一個字典串列, 格式如下 :
[{'role': 'system', 'content': '<系統角色>'}, {'role': 'user', 'content': '<提示詞>'}, ...]
其中 user 角色的提示詞字典視需要可以有多個.
要怎麼讓 API 給出一個可以讓應用程式判斷模型知不知道這個問題的答案呢? 這可以透過刻意設計的 JSON 格式之回覆字串來規範其回應格式, 因為 JSON 資料可以輕易地轉成 Python 字典, 利用字典鍵就可以取得到底模型知不知道我們所詢問的問題, 如果模型初次的回應是不知道, 那麼程式便可啟動網路搜尋取得資料後提供給模型再次生成回應; 若知道就把答案直接放在 JSON 裡. 這種要求用 JSON 格式回覆的字串模板範例如下所示 :
>>> template='''
請確認你是否知道下面這件事:
{}
如果知道, 請只用下列 JSON 格式回答:
{{
"need_search": "N",
"keyword": "",
"reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答:
{{
"need_search": "Y",
"keyword": "你建議的搜尋關鍵字",
"reply": ""
}}
'''
注意, 這裡我們以 "只用" 字眼來約束模型只能有這兩種回應格式, 其次, JSON 格式要求鍵必須用雙引號括起來, 如果用單引號的話, 在使用 json 模組轉成 Python 字典時會出現錯誤.
這個長字串中的 {} 表示一個占位符, 它可以用字串的 format() 方法把變數丟到這個占位符所在的位置填充; 而 {{}} 則分別用來表示 { 與 } 字元, 因為 { 與 } 在字串中是特殊字元, 如果要表示 { 這個字元要用 {{; 而要表示 } 字元則是要用 }}. 注意,
先以詢問 2024 台灣總統大選結果為例來測試此模板 :
>>> query='2024台灣總統大選是誰當選?'
>>> prompt=template.format(query)
>>> print(prompt)
請確認你是否知道下面這件事:
2024台灣總統大選是誰當選?
如果知道, 請只用下列 JSON 格式回答:
{
"need_search": "N",
"keyword": "",
"reply":"你的答案"
}
如果不知道, 請只用下列 JSON 格式回答:
{
"need_search": "Y",
"keyword": "你建議的搜尋關鍵字",
"reply": ""
}
可見 format() 方法已經將提示詞填充至 {} 占位符所在之處了. 然後將此模板字串放進 OpenAI API 訊息的字典串列中傳給模型 :
>>> messages=[
{'role': 'system', 'content': '你是一個繁體中文AI助理'},
{'role': 'user', 'content': prompt}
]
>>> print(ask_gpt(messages))
{
"need_search": "Y",
"keyword": "2024台灣總統大選結果",
"reply": ""
}
可見因為 2024 年的事件超出 gpt-3.5-turbo 所認知的時間範圍, 模型果然回應了一個需要網路搜尋的 JSON 格式字串, 而且 keyword 鍵也傳回了模型建議的搜尋關鍵字.
如果將問題改為 gpt-3.5-turbo 認知範圍內的 2018 總統大選結果 :
>>> query='2018台灣總統大選是誰當選?'
>>> prompt=template.format(query)
>>> messages=[
{'role': 'system', 'content': '你是一個繁體中文AI助理'},
{'role': 'user', 'content': prompt}
]
>>> print(ask_gpt(messages))
{
"need_search": "N",
"keyword": "",
"reply":"蔡英文"
}
這次因為 gpt-3.5-turbo 模型知道答案, 所以傳回的 JSON 字串中 need_search 鍵的值為 'N', 表示不需要搜尋網路, 當然 keyword 鍵的值就為空, 而 reply 鍵的值就是模型所知的答案.
可以用 json 模組的 loads() 將 JSON 字串轉成字典 :
>>> import json
>>> result=json.loads(ask_gpt(messages))
>>> type(result)
<class 'dict'>
>>> result
{'need_search': 'N', 'keyword': '', 'reply': '蔡英文'}
>>> result['need_search']
'N'
這樣就可以依據第一次查詢回應中的 need_search 鍵來判斷是否需要搜尋網路了, 如果需要搜尋網路, 那就用模型建議的關鍵字 (keyword 鍵之值) 取得谷歌搜尋結果, 然後將其做為參考資料對模型發出第二次查詢並要求它根據所提供之搜尋資料做出回覆, 完整程式碼如下 :
# cli_chatbot_search_4.py
from openai import OpenAI, APIError
import requests
import json
def ask_gpt(messages, model='gpt-3.5-turbo'):
try:
reply=client.chat.completions.create(
model=model,
messages=messages
)
return reply.choices[0].message.content
except APIError as e:
return e.message
def search_google(query, cx, api_key, num=3, gl='tw', lr='lang_zh_TW'):
params={
'q': query,
'key': api_key,
'cx': cx,
'gl': gl,
'lr': lr,
'num': num
}
url='https://www.googleapis.com/customsearch/v1'
r=requests.get(url, params=params)
data=r.json()
return data.get('items', [])
# 設定金鑰變數
openai_key='你的 OpenAI API Key'
custom_search_key='你的 Custom Search JSON API Key'
cx='你的 Custom Search Engine ID' # Custom Search Engine ID
# 建立 OpenAI 物件
client=OpenAI(api_key=openai_key)
# 設定 GPT 系統角色
sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '':
sys_role='你是繁體中文AI助理'
print(sys_role)
# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事:
{}
如果知道, 請只用下列 JSON 格式回答:
{{
"need_search": "N",
"keyword": "",
"reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答:
{{
"need_search": "Y",
"keyword": "你建議的搜尋關鍵字",
"reply": ""
}}
'''
# 聊天機器人 (無窮迴圈)
while True:
prompt=input('You : ') # 輸入提示詞
if prompt.strip() == '': # 按 Enter 跳出迴圈結束聊天
print('GPT : 再見。')
break
messages=[{'role': 'system', 'content': sys_role}] # 初始化訊息
json_query_str=template.format(prompt)
messages.append({'role': 'user', 'content': json_query_str})
# 第一次查詢 : 確定是否需要搜尋網路
reply=ask_gpt(messages)
result=json.loads(reply) # 將 JSON 字串轉成字典
if result['need_search']=='Y': # 需要搜尋網路
keyword=result['keyword'] # 取得 GPT 建議之搜尋關鍵字
web_msg='以下是 Google 搜尋所得資料:\n' # 初始化搜尋結果字串
for item in search_google(keyword, cx, custom_search_key):
web_msg += f'標題: {item["title"]}\n'
web_msg += f'描述: {item["snippet"]}\n\n'
web_msg += '請依據上述資料直接回答下列問題:\n' # 此為必要提示詞
web_msg += prompt # 串接使用者提示詞
messages=[
{'role': 'system', 'content': sys_role},
{'role': 'user', 'content': web_msg}
]
# 第二次查詢 : 根據所提供的網路搜尋結果回答
reply='(正在搜尋網路...) ' + ask_gpt(messages)
else: # 不需搜尋網路, 加入使用者提示詞
reply=result['reply']
print(f'GPT : {reply}')
此例首先會將使用者輸入的提示詞嵌入 JSON 格式回覆字串模板中產生第一次查詢的字串, 透過 JSON 回應中的 need_search 鍵來判斷到底模型知不知道答案, 如果知道就直接輸出回應, 否則依據模型建議的關鍵字透過 Custom Search JSON API 去搜尋谷歌, 然後對模型發起第二次查詢, 要求它根據這些搜尋結果做出回覆. 關於 Custom Search JSON API 用法參考 :
上面的程式執行結果如下 :
>>> %Run cli_chatbot_search_4.py
請設定 GPT 角色 :
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : (正在搜尋網路...) 2024年台灣總統大選中,賴清德當選為第16任中華民國總統。
You : 2018台灣總統大選是誰當選?
GPT : 你的答案是蔡英文
You : 2024 台灣金曲歌王是誰?
GPT : (正在搜尋網路...) 2024 台灣金曲歌王是 MC HotDog(熱狗)。
You :
GPT : 再見。
可見當需要搜尋網路時就會顯示 "(正在搜尋網路...)", 然後輸出模型根據搜尋結果給出的答案, 否則就會直接顯示模型回覆之答案.
但是, 上面的程式沒有記憶能力, 每組對話彼此獨立並無脈絡可循, 所以我們自以為上下文相關的對話 GPT 模型會認為是完全無關, 例如 :
>>> %Run cli_chatbot_search_4.py
請設定 GPT 角色 :
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : (正在搜尋網路...) 2024年台灣總統大選中,賴清德當選為第16任中華民國總統。
You : 那 2018 年呢?
GPT : (正在搜尋網路...) 根據上述資料顯示,2018年發生了各種重要事件和災害,包括自然災害、國際會議、國際新聞等。在2018年發生了許多重要的事件,影響了全球各地的人們。
You :
GPT : 再見。
可見模型根本不會認為第二個問題是接續前面的問題, 因為 OpenAI API 不會紀錄對話歷史 (ChatGPT 登入後則會), 雖然它緊接在 2024 年大選之後, 但 GPT 模型不會將它與上一個問題關聯起來, 會以為我們是在問關於 2018 年發生了甚麼事.
沒有留言 :
張貼留言