2025年3月26日 星期三

OpenAI API 學習筆記 : 結合搜尋引擎的聊天機器人 (一)

今天繼續學習 OpenAI API 的用法, 進度來到製作可以 Call out 搜尋引擎的 OpenAI 聊天機器人, 由於語言模型受限於訓練資料的時間範圍, 對於時事相關的詢問若超出範圍就無法生成答案, 這時可以先從搜尋引擎取得相關資訓提供給模型來生成回應. 

本系列全部文章索引參考 : 


首先匯入 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 角色的提示詞字典可以有多個, 例如 : 

>>> messages=[
    {'role': 'system', 'content': '你是一個繁體中文AI助理'},
    {'role': 'user', 'content': '2024台灣總統大選是誰當選?'}
    ] 

因為 gpt-3.5-turbo 模型的訓練數據截止時間為 2021 年 9 月, 因此若問它超出此期限的問題時 (例如 2024 年台灣總統大選結果) 它會回覆無法預測未來 : 

>>> print(ask_gpt(messages))   
對不起,作為一個AI助理,我無法提供未來事件的資料或預測。2024年台灣總統大選的結果將取決於當時的政治情勢、候選人表現以及選民的選擇。讓我們一起期待未來的發展吧!如果您有任何其他問題,歡迎讓我知道。 

解決辦法是先透過 API 向搜尋引擎詢問, 然後將搜尋結果作為提示詞的一部分提供給模型, 並要求它依據所提供的資料回答. 


1. 利用 SerpAPI 取得谷歌搜尋資料 :  

使用 SerpAPI 須先註冊帳號取得 API Key 並用 pip 安裝 google-search-results 套件才行, 免費帳戶享有每個月 100 次搜尋的服務, 做法參考 :  


首先從 serpapi 模組匯入 GoogleSearch 類別 :

>>> from serpapi import GoogleSearch   

定義 SerpAPI 的存取金鑰變數 :

>>> serpapi_key='我的 SerpAPI key'   

然後建立 GoogleSearch 物件並呼叫其所提供之方法取得搜尋結果, 我將此程序寫成如下函式 search_google() :

>>> def search_google(query, serpapi_key, num=3, gl='tw', hl='zh-tw'):
    params={
        'q': query,
        'api_key': serpapi_key,
        'num': num,
        'gl': gl,
        'hl': hl
        }
    search=GoogleSearch(params)
    results=search.get_dict()
    return results.get('organic_results', [])

此函式已預設指定傳回最前面 3 筆繁體中文結果, 呼叫時只要傳入要搜尋的關鍵字 query 與 API Key 即可, 若有搜尋到資料會傳回一個包含 title (標題), snippet (描述), 與 url (網址) 等鍵的字典串列, 若沒有搜尋結果就傳回空字串, 例如 :

>>> query='2024台灣總統大選是誰當選?'   
>>> results=search_google(query, serpapi_key)   
>>> type(results)  
<class 'list'>  
>>> len(results)  
3
>>> type(results[0])   
<class 'dict'>

可見傳回了包含 3 筆搜尋結果字典的串列, 用迴圈迭代此串列元素並顯示 title, snippet, 與 url 這三個鍵之值 : 

>>> for result in results:   
    print(f'標題: {result["title"]}')
    print(f'描述: {result["snippet"]}')
    print(f'網址: {result["link"]}\n')  
  
標題: 2024年中華民國總統選舉
描述: 本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度由同一政黨連續三次獲勝。蔡英文八年執政雖成功交棒,但維持執政地位的民進黨則在同日 ...
網址: https://zh.wikipedia.org/zh-tw/2024%E5%B9%B4%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E7%B8%BD%E7%B5%B1%E9%81%B8%E8%88%89

標題: 第16任總統副總統選舉
描述: 第16任總統副總統選舉 ; 金門縣. 柯文哲. /吳欣盈. 賴清德. /蕭美琴. 侯友宜. /趙少康. 1. 2. 3. 13,038. 4,569. 28,005. 28.58%. 10.02%. 61.40% ; 基隆市. 柯文哲. /吳欣盈.
網址: https://db.cec.gov.tw/ElecTable/Election/ElecTickets?dataType=tickets&typeId=ELC&subjectId=P0&legisId=00&themeId=4d83db17c1707e3defae5dc4d4e9c800&dataLevel=C&prvCode=00&cityCode=000&areaCode=00&deptCode=000&liCode=0000

標題: 【Data Reporter】35張圖表,帶你看2024大選關鍵結果
描述: 2024年1月15日 —
網址: https://www.twreporter.org/a/2024-election-results-chart

將這些搜尋結果提供給模型並要求它依據這些資料回答便能生成正確的回應了. 參考下面這篇的命令列聊天機器人來改寫 :


先測試無記憶聊天機器人, 用 input() 輸入模型的 system 角色 : 

>>> sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)
請設定 GPT 角色 : 
你是繁體中文AI助理

直接按 Enter 用預設值 :

>>> sys_role   
'你是繁體中文AI助理'

用 system 角色初始化提示詞字典串列 messages : 

>>> messages=[{'role': 'system', 'content': sys_role}] 
>>> messages   
[{'role': 'system', 'content': '你是繁體中文AI助理'}]

用 input() 輸入提示詞, 若以 '#s' 開頭表示要搜尋網路 : 

>>> prompt=input('You : ')  
You : #s2024台灣總統大選是誰當選?  
>>> prompt   
'#s2024台灣總統大選是誰當選?'

然後判斷提示詞 prompt 是否以 '#s' 開頭, 是的話剔除 '#s' 傳給上面的 search_google() 透過 SerpAPI 搜尋谷歌, 將結果字典中的 title 與 description 鍵之值串起來與提示詞一起做為查詢字串傳給上面的 ask_gpt() 函式詢問模型, 並要求其依據搜尋資料回覆 :  

>>> if prompt[:2].lower()=='#s':
    user_msg=prompt[2:]    # 剔除開頭的 '#s' 字元
    web_msg='以下是 Google 搜尋所得資料:\n'   # 初始化搜尋結果字串
    for result in search_google(user_msg, serpapi_key):   # 迭代搜尋結果字典串列
        web_msg += f'標題: {result["title"]}\n'   # 串接標題
        web_msg += f'描述: {result["snippet"]}\n\n'   # 串接描述
    web_msg += '請依據上述資料回答下列問題:\n'   # 要求模型依據搜尋結果回覆
    web_msg += user_msg   # 串接提示詞
    messages.append({'role': 'user', 'content': web_msg})   # 加入搜尋結果+提示詞
else:   # 不搜尋網路
    messages.append({'role': 'user', 'content': prompt})   # 只加入提示詞
reply=ask_gpt(messages)   # 查詢 GPT 
print(f'GPT : {reply}')   

GPT : 根據你提供的資訊,2024年台灣總統大選中當選的是蔡英文。

結果模型根據搜尋結果的回答是錯的 (應該是賴清德啦). 

檢視傳給模型之 messages 字典串列 :

>>> messages    
[{'role': 'system', 'content': '你是繁體中文AI助理'}, {'role': 'user', 'content': '以下是 Google 搜尋所得資料:\n標題: 2024年中華民國總統選舉\n描述: 本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度由同一政黨連續三次獲勝。蔡英文八年執政雖成功交棒,但維持執政地位的民進黨則在同日 ...\n\n標題: 第16任總統副總統選舉\n描述: 第16任總統副總統選舉 ; 金門縣. 柯文哲. /吳欣盈. 賴清德. /蕭美琴. 侯友宜. /趙少康. 1. 2. 3. 13,038. 4,569. 28,005. 28.58%. 10.02%. 61.40% ; 基隆市. 柯文哲. /吳欣盈.\n\n請依據上述資料回答下列問題:\n2024台灣總統大選是誰當選?'}]

可見原因可能出在 SerpAPI 的搜尋結果, 其 snippet 欄位的摘要似乎太簡短, 很容易誤導模型做出錯誤的回覆. 另外 serach_google() 預設參數 num=3 也可能使提供給模型的資料不夠充分. 

將上述測試寫成如下完整的程式碼 : 

# cli_chatbot_search_1.py 
from openai import OpenAI, APIError
from serpapi import GoogleSearch

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, serpapi_key, num=5, gl='tw', hl='zh-tw'):
    params={
        'q': query,
        'api_key': serpapi_key,
        'num': num,
        'gl': gl,
        'hl': hl
        }
    search=GoogleSearch(params)
    results=search.get_dict()
    return results.get('organic_results', [])

# 設定金鑰變數
openai_key='你的 OpenAI API Key'
serpapi_key='你的 SerpAPI Key'
# 建立 OpenAI 物件
client=OpenAI(api_key=openai_key)
# 設定 GPT 系統角色
sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)
# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    messages=[{'role': 'system', 'content': sys_role}]  # 初始化訊息
    if prompt[:2].lower()=='#s':  # 前兩個字元 '#s' 表示要搜尋網路
        user_msg=prompt[2:]  # 剔除 '#s' 取得使用者之提示詞
        web_msg='以下是 Google 搜尋所得資料:\n'  # 初始化搜尋結果字串
        for result in search_google(user_msg, serpapi_key):
            web_msg += f'標題: {result["title"]}\n'
            web_msg += f'描述: {result["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題:\n'  # 此為必要提示詞
        web_msg += user_msg  # 串接使用者提示詞
        messages.append({'role': 'user', 'content': web_msg})
    else:  # 不搜尋網路, 加入使用者提示詞
        messages.append({'role': 'user', 'content': prompt})
    reply=ask_gpt(messages)
    print(f'GPT : {reply}') 

注意, 在 search_google() 函式中, num 參數的預設值已經增加為 5, 執行結果如下 : 

>>> %Run cli_chatbot_search_1.py   
請設定 GPT 角色 : 
你是繁體中文AI助理
You : #s2024台灣總統大選是誰當選?
GPT : 根據上述資料,2024年台灣總統大選中,民進黨候選人賴清德以超過558萬票勝選,成功當選第16屆中華民國總統。
You : #s2024 台灣金曲歌王是誰?
GPT : 根據上述資料,2024 台灣金曲歌王是 MC HotDog(熱狗)
You : 2024台灣總統大選是誰當選?
GPT : 抱歉,我無法預測未來的事件,包括2024年的台灣總統大選結果。以上信息僅供參考,具體情況仍需等待實際情況發生後才能確認。
You : 2024 台灣金曲歌王是誰?
GPT : 2024年的金曲獎尚未舉辦,所以還不清楚誰會成為當年的金曲歌王。如果想知道過去的金曲歌王,我可以幫忙查詢。
You : 
GPT : 再見。

可見只要提供較多的搜尋結果, 模型應該就可以回覆正確答案了. 


2. 利用 Custom Search JSON API 取得谷歌搜尋資料 :  

在前一篇測試中我們已利用 Custom Search JSON API 順利取得谷歌搜尋結果, 參考 : 


直接將上面的程式修改為如下的 Custon Search JSON API 版本 : 

# cli_chatbot_search_2.py
from openai import OpenAI, APIError
import requests 

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)
# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    messages=[{'role': 'system', 'content': sys_role}]  # 初始化訊息
    if prompt[:2].lower()=='#s':  # 前兩個字元 '#s' 表示要搜尋網路
        user_msg=prompt[2:]  # 剔除 '#s' 取得使用者之提示詞
        web_msg='以下是 Google 搜尋所得資料:\n'  # 初始化搜尋結果字串
        for item in search_google(user_msg, cx, custom_search_key):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題:\n'  # 此為必要提示詞
        web_msg += user_msg  # 串接使用者提示詞
        messages.append({'role': 'user', 'content': web_msg})
    else:  # 不搜尋網路, 加入使用者提示詞
        messages.append({'role': 'user', 'content': prompt})
    reply=ask_gpt(messages)
    print(f'GPT : {reply}') 

此處搜尋函式名稱雖然與上面範例一樣, 但內容不同, 執行結果如下 : 

>>> %Run cli_chatbot_search_2.py    
請設定 GPT 角色 : 
你是繁體中文AI助理
You : #s2024台灣總統大選是誰當選?
GPT : 根據上述資料,2024年台灣總統大選中,賴清德當選為中華民國第16任總統。
You : #s2024 台灣金曲歌王是誰?
GPT : 根據資料顯示,2024年台灣金曲歌王是MC HotDog熱狗。
You : 
GPT : 再見。

可見使用 Custom Search JSON API 搜尋的結果讓模型對這兩個提問均回覆了正確答案 (參考前一篇測試可知對於 2024 總統當選人的搜尋結果, 其中第二筆摘要有明確指出總統當選人為賴清德). 此例 search_google() 函式預設參數 num=3 只提供前三筆搜尋結果模型就給出正確答案, 而上面使用 SerpAPI 卻要放大到 5 筆, 感覺 Custom Search JSON API 較優啊! 


3. 利用 DuckDuckGo API 取得網路搜尋資料 :  

除了 Google 搜尋引擎外, 也可以透過 DuckDuckGo 搜尋引擎取得網路資料, 參考 : 


使用前須先用 pip 安裝 duckduckgo-search 套件, 然後匯入 DDGS 類別, 呼叫其建構式 DDGS() 建立 DDGS 物件 : 

>>> from duckduckgo_search import DDGS     
>>> ddgs=DDGS()     

搜尋文字資料可呼叫其 text() 方法, 並傳入必要參數 query (關鍵字), 也可傳入區域語系參數 region 與傳回筆數 max_results 參數 : 

>>> results=ddgs.text(query, region='zh-tw', max_results=5)     

傳回值是一個包含 title (標題), href (網址), body (摘要) 三鍵的字典串列, 不過其標題非常簡短, 提供的訊息量很少, 如果要提供給模型據此生成回覆的話要用其 body 鍵, 可用迴圈顯示 body 內容如下 :

>>> for result in results:   
    print(result["body"])   
    
2024年中華民國總統選舉,即中華民國第十六任總統副總統選舉,於2024年(民國113年)1月13日舉行,與第11屆立法委員選舉在同日舉行,為中華民國第8次正、副總統公民直選,採用普通、直接、平等、無記名、單記、相對多數投票制度。 第14、15任總統蔡英文將於2024年5月20日任期屆滿,因已連任一次 ...
2024年中華民國總統選舉,即中華民國第十六任總統副總統選舉,於2024年(民國113年)1月13日舉行,與第11屆立法委員選舉在同日舉行,為中華民國第8次正、副總統公民直選,採用普通、直接、平等、無記名、單記、相對多數投票制度。 第14、15任總統蔡英文將於2024年5月20日任期屆滿,因已連任一次 ...
2024年台灣總統選舉結果揭曉,民進黨候選人賴清德及蕭美琴成功當選新一任總統及副總統,總得票數達558萬,得票率為40.05%。 ... 參與總統大選舉 ...
新總統解讀》賴清德未來4年辛苦執政路,會和「偶像級」柯文哲合作? 野島剛專訪》柯文哲才是隱形主角、年輕人打破藍綠 野島剛:2024是一場沒有敗北者的選舉 開票看天下》得票地圖、國會席次、ai分析選情一次看 賴清德、蕭美琴以超過558萬得票,打破8年連任魔咒,當選中華民國第16任總統 ...
1月13日,台灣舉行2024年總統選舉與立法委員選舉。執政黨民進黨候選人賴清德以超過558萬票勝選,打破台灣八年政黨輪替「魔咒」。但在立委選舉中 ...

只要將這些搜尋結果傳給模型並要求它據此作出回應即可, 完整程式碼如下 :

# cli_chatbot_search_3.py
from openai import OpenAI, APIError
from duckduckgo_search import DDGS

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_duckduckgo(query, region='zh-tw', max_results=5):
    try:
        results=ddgs.text(query, region=region, max_results=max_results)
    except Exception as e:
        results=[]
    return results

# 設定金鑰變數
openai_key='你的 OpenAI API Key'
# 建立 OpenAI 與 DDGS 物件
client=OpenAI(api_key=openai_key)
ddgs=DDGS()
# 設定 GPT 系統角色
sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)
# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    messages=[{'role': 'system', 'content': sys_role}]  # 初始化訊息
    if prompt[:2].lower()=='#s':  # 前兩個字元 '#s' 表示要搜尋網路
        user_msg=prompt[2:]  # 剔除 '#s' 取得使用者之提示詞
        web_msg='以下是 DuckDuckGo 搜尋所得資料:\n'  # 初始化搜尋結果字串
        results=search_duckduckgo(user_msg)
        if results:   # 傳回非空串列時
            for result in results:
                web_msg += f'標題: {result["title"]}\n'
                web_msg += f'標題: {result["body"]}\n\n'
            web_msg += '請依據上述資料回答下列問題:\n'  # 此為必要提示詞
            web_msg += user_msg  # 串接使用者提示詞
        else:   # 傳回空串列時
            web_msg += '搜尋引擎發生錯誤, 無資料'
        messages.append({'role': 'user', 'content': web_msg})
    else:  # 不搜尋網路, 加入使用者提示詞
        messages.append({'role': 'user', 'content': prompt})
    reply=ask_gpt(messages)
    print(f'GPT : {reply}') 

此處因為 DuckDuckGo 搜尋引擎有時會出現 RateLimitError (預設使用免費 Lite 版), 所以加上 try except 攔截例外, 若出現錯誤傳回空串列, 否則傳回搜尋結果的字典串列, 結果如下 : 

>>> %Run cli_chatbot_search_3.py   
請設定 GPT 角色 : 
你是繁體中文AI助理
You : #s2024台灣總統大選是誰當選?
GPT : 2024年台灣總統大選中,民進黨候選人賴清德當選為新一任台灣總統。
You : #s2024 台灣金曲歌王是誰?
GPT : 根據資料顯示,2024年台灣金曲歌王是MC HotDog熱狗。
You : 
GPT : 再見。

有時會出現例外無法傳回搜尋結果 :

You : #s2024台灣總統大選是誰當選?
GPT : 抱歉,看起來出現了問題,我們可以嘗試使用其他搜尋引擎或者試著提供不同的關鍵字來搜尋。請告訴我您需要什麼樣的資訊,我們可以一起來尋找解答。
You : #s2024台灣總統大選是誰當選?
GPT : 抱歉,看來搜尋引擎出現了錯誤無法提供相關資訊。你還有其他問題或需要協助嗎?

綜合上述測試結果, 結論是 Custom Search JSON API 最優. 

沒有留言 :