2025年3月31日 星期一

momo 購買 Mr.Box 三層收納櫃 x 2

打算清明連假來徹底整理鄉下老家, 今天上 momo 回購 Mr.Box 的三層收納櫃兩個 :




用掉 47 元 momo 幣, 實付 1133 元. 二月中購買紀錄 :


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

本篇旨在將前面測試中所寫的命令列介面聊天機器人改成用 Gradio 實作的網頁介面版, 改寫的對象是下面這篇中的非串流版程式 cli_chatbot_search_6.py : 


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



7. 用 Gradio 製做有記憶且能自動判斷是否需要搜尋網路的聊天機器人 :  

Gradio 是一套簡單好用的 Python 網頁應用程式套件, 讓不懂網頁前端技術的 Python 程式設計師利用 Gradio 提供的元件快速搭建出 Web app 的原型, 完全不需要去學習 HTML, CSS, 與 Javascript 與熱門的前端框架 (例如 jQuery, React, Vue, Angular 等), 只要用 Python 即可搞定. HuggingFace 已經收購 Gradio 並擴充其資源, 例如可將 Gradio Playground 上開發的專案一鍵發布到 HuggingFace Space 主機上執行. 關於 Gradio 用法參考 :


Gradio 提供 Chatbot 元件用來製做具有聊天泡泡效果的介面, 同時也提供了 State 元件可用來自動儲存聊天歷史, 用法參考下面這篇 : 


下面是不使用 State 元件紀錄對話歷史, 而是自行用串列紀錄的範例程式 :

# gradio_chatbot_search_1.py
import gradio as gr
from openai import OpenAI, APIError
from dotenv import load_dotenv
import os
import json
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', [])

def chat(prompt):
    messages.append({'role': 'user', 'content': prompt})
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    reply=ask_gpt(messages)  # 第一次查詢 GPT
    messages.pop()
    try:
        result=json.loads(reply)
    except Exception as e:
        messages.append({'role': 'assistant', 'content': '發生錯誤:GPT 回傳的格式無法解析'})
        return messages, ''
    if result['need_search']=='Y':  # 需要搜尋網路
        keyword=result['keyword']  # 取得 GPT 建議的
        web_msg='以下是 Google 搜尋所得資料:\n'  # 初始化搜尋結果字串
        for item in search_google(keyword, SEARCH_ID, SEARCH_KEY):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題(**直接給出答案**):\n' + prompt
        # 記錄 Google 搜尋結果
        messages.append({'role': 'system', 'content': web_msg})
        # 再次查詢 GPT : 根據所提供的網路搜尋結果回答
        reply=ask_gpt(messages)
    else:  # 不需搜尋網路, 加入使用者提示詞
        reply=result['reply']
    # 記錄 GPT 回應到歷史訊息
    messages.append({'role': 'assistant', 'content': reply})
    return messages, ''

# 從 .env 載入環境變數
load_dotenv()
# 從環境變數取得金鑰
OPENAI_KEY=os.environ.get('OPENAI_API')
SEARCH_KEY=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
SEARCH_ID=os.environ.get('SEARCH_ENGINE_ID')
# 建立 OpenAI 物件
client=OpenAI(api_key=OPENAI_KEY)
# 設定對話歷史初始值
sys_role=sys_role='你是繁體中文AI助理'
messages=[{'role': 'system', 'content': sys_role}]

# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事: 
{}
如果知道, 請只用下列 JSON 格式回答(不要添加例如 ```json 的註記):
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答(不要添加例如 ```json 的註記):
{{
  "need_search": "Y",
  "keyword": "你建議的搜尋關鍵字",
  "reply": ""
}}
'''

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    chatbot=gr.Chatbot(type='messages',
                       height=400,
                       placeholder='我們的對話',
                       show_copy_button=True)
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")    
    send_btn.click(chat, inputs=[prompt], outputs=[chatbot, prompt])
    prompt.submit(chat, inputs=[prompt], outputs=[chatbot, prompt])
blocks.launch()

這裡我們使用 dotenv 模組來隱藏金鑰與 ID 等資訊, 其用法參考 : 


由於在前面的測試中發現, 雖然在 template 回覆模板中有特別要求模型必須回應一個 JSON 字串, 但有時候它還是會用 " ```json" 與 " ```" 將 JSON 字串包起來, 導致使用 json.loads() 解析時出現例外, 所以這修改了 template 內容, 要求不要添加這些額外字串. 

其次, 由於除了按下 send_btn 按鈕發送提問之外, 直接在 Textbox 內按下 Enter 也會提交, 所以 prompt 的 submit() 也要處理, 內容與 send_btn 相同, 輸入變數只有 prompt 一個, 輸出則有兩個, 主要是第一個的 chatbot, 用來將對話歷程 messages 傳給 chatbot 顯示更新對話泡泡, 第二個輸出 prompt 是讓 chat() 傳回一個空字串清空提示詞輸入框以便使用者輸入下一個問題, 毋須手動清除, 執行結果如下 :








可見此聊天機器人確實能在詢問超出 gpt-3.5-turbo 模型知識範圍的 2024 台灣總統大選結果時透過谷歌搜尋取得網路資料做出正確回覆, 而且能透過歷史對話將 '那 2018 年呢?' 的詢問關聯到上一個關於總統大選的議題了解是在問 2018 年總統大選結果. 

不過在第一次詢問 "甚麼是量子疊加態" 時卻出現 GPT 回應格式錯誤, 檢查這時的 reply 傳回值為 :

```json
{
  "need_search": "N",
  "keyword": "",
  "reply": "在量子物理中,量子疊加態是一種現象,表示粒子同時處於多個可能的狀態的線性組合。這意味著粒子可以同時存在於不同的狀態,直到被觀察或測量後才會確定其實際狀態。"
}
```
可見即使 template 模板中有要求不要添加額外資訊, GPT 模型偶而還是會不聽使喚. 再次詢問就能得到正常回應了. 

上面的範例我們使用了一個全域變數 messages 來紀錄對話歷史, 也可以使用 Gradio 的 State 元件來記錄狀態, 它其實是包裝成 State 類型的串列, 用法與串列相同, 將上面的程式修改為如下之 State 版 :

# gradio_chatbot_search_1.py
import gradio as gr
from openai import OpenAI, APIError
from dotenv import load_dotenv
import os
import json
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', [])

def chat(prompt, messages):
    # 添加用戶輸入到對話歷史
    messages.append({'role': 'user', 'content': prompt})
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    
    # 第一次查詢 GPT
    reply=ask_gpt(messages)
    messages.pop()  # 移除臨時的 json_query_str
    
    try:
        result=json.loads(reply)
    except Exception as e:
        messages.append({'role': 'assistant', 'content': '發生錯誤:GPT 回傳的格式無法解析'})
        return messages, messages, ''  # 更新 state, chatbot, prompt
    
    if result['need_search'] == 'Y':  # 需要搜尋網路
        keyword=result['keyword']
        web_msg='以下是 Google 搜尋所得資料:\n'
        for item in search_google(keyword, SEARCH_ID, SEARCH_KEY):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題(直接給出答案):\n' + prompt
        messages.append({'role': 'system', 'content': web_msg})
        reply=ask_gpt(messages)  # 第二次查詢 GPT
    else:
        reply=result['reply']
    
    # 添加 GPT 回應到對話歷史
    messages.append({'role': 'assistant', 'content': reply})
    return messages, messages, ''  # 更新 state, chatbot, prompt

# 從 .env 載入環境變數
load_dotenv()
# 從環境變數取得金鑰
OPENAI_KEY=os.environ.get('OPENAI_API')
SEARCH_KEY=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
SEARCH_ID=os.environ.get('SEARCH_ENGINE_ID')
# 建立 OpenAI 物件
client=OpenAI(api_key=OPENAI_KEY)
# 設定系統角色
sys_role='你是繁體中文AI助理'
# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事: 
{}
如果知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "Y",
  "keyword": "你建議的搜尋關鍵字",
  "reply": ""
}}
'''

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    chatbot=gr.Chatbot(type='messages',
                         height=400,
                         placeholder='我們的對話',
                         show_copy_button=True)
    state=gr.State([{'role': 'system', 'content': sys_role}])  # 使用 State 儲存對話歷史
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")
    
    # 將 chat 的輸入設為 prompt 和 state,輸出設為 state, chatbot, prompt
    send_btn.click(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])
    prompt.submit(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])

blocks.launch()

此例輸入有兩個, 一個式提示詞 prompt, 一個是狀態串列 state, 而 chat() 函式的傳回值則有 3 個, 新增的 state 用來更新 Blocks 區段內的對話歷史串列, 執行結果與上面的範例程式相同. 

上面的兩個範例中, 用來記錄對話歷史的不管是 messages 串列或 State 元件, 它們的長度都沒有限制, 會隨著對話持續增長. 下面的範例則是利用一個全域變數來限制其長度 (預設 10 個元素) :

# gradio_chatbot_search_3.py
import gradio as gr
from openai import OpenAI, APIError
from dotenv import load_dotenv
import os
import json
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', [])

def chat(prompt, messages):
    # 添加用戶輸入到對話歷史
    messages.append({'role': 'user', 'content': prompt})
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    
    # 第一次查詢 GPT
    reply=ask_gpt(messages)
    messages.pop()  # 移除臨時的 json_query_str
    
    try:
        result=json.loads(reply)
    except Exception as e:
        messages.append({'role': 'assistant', 'content': '發生錯誤:GPT 回傳的格式無法解析'})
        return messages, messages, ''  # 更新 state, chatbot, prompt
    
    if result['need_search'] == 'Y':  # 需要搜尋網路
        keyword=result['keyword']
        web_msg='以下是 Google 搜尋所得資料:\n'
        for item in search_google(keyword, SEARCH_ID, SEARCH_KEY):
            web_msg += f'標題: {item["title"]}\n'
            web_msg += f'描述: {item["snippet"]}\n\n'
        web_msg += '請依據上述資料回答下列問題(直接給出答案):\n' + prompt
        messages.append({'role': 'system', 'content': web_msg})
        reply=ask_gpt(messages)  # 第二次查詢 GPT
    else:
        reply=result['reply']
    
    # 添加 GPT 回應到對話歷史
    messages.append({'role': 'assistant', 'content': reply})
    
    # 限制對話歷史長度,保留第一個元素(系統角色)
    if len(messages) > MAX_HISTORY_LENGTH:
        messages=[messages[0]] + messages[-(MAX_HISTORY_LENGTH - 1):]
    #print(messages)
    return messages, messages, ''  # 更新 state, chatbot, prompt

# 從 .env 載入環境變數
load_dotenv()
# 從環境變數取得金鑰
OPENAI_KEY=os.environ.get('OPENAI_API')
SEARCH_KEY=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
SEARCH_ID=os.environ.get('SEARCH_ENGINE_ID')
# 全域變數:限制對話歷史長度
MAX_HISTORY_LENGTH=10   
# 建立 OpenAI 物件
client=OpenAI(api_key=OPENAI_KEY)
# 設定系統角色
sys_role='你是繁體中文AI助理'
# 建立 JSON 格式回覆字串模板
template='''
請確認你是否知道下面這件事: 
{}
如果知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}
如果不知道, 請只用下列 JSON 格式回答(不要添加例如 json 的註記):
{{
  "need_search": "Y",
  "keyword": "你建議的搜尋關鍵字",
  "reply": ""
}}
'''

with gr.Blocks(title="OpenAI 聊天機器人") as blocks:
    chatbot=gr.Chatbot(type='messages',
                         height=400,
                         placeholder='我們的對話',
                         show_copy_button=True)
    state=gr.State([{'role': 'system', 'content': sys_role}])  # 使用 State 儲存對話歷史
    with gr.Column():
        prompt=gr.Textbox(label="您的詢問:")
        send_btn=gr.Button("送出")
    
    # 將 chat 的輸入設為 prompt 和 state,輸出設為 state, chatbot, prompt
    send_btn.click(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])
    prompt.submit(chat, inputs=[prompt, state], outputs=[state, chatbot, prompt])

blocks.launch()

此例在 chat() 函式末尾檢查對話歷史長度, 如果超過設定值就捨棄前面的最舊紀錄, 但保留第 1 個 system 角色設定, 執行結果如下 :





可見原先它還記得我家兩隻貓的名字, 但是當對話較長後關於貓咪的紀錄被丟棄, 所以它就不記得貓咪的名字了. 

2025-04-01 補充 : 

這一周測試下來儲值的 10 美元還剩下 9.74 美元 :





從去年以來 API 只用掉 0.26 美元, 折合台幣 9 元不到, 可能是我大部分都呼叫超便宜的 gpt-3.5-turbo 模型, 且大都還是文字聊天之故, 等進到圖片測試時就會較吃錢了. 

2025年3月30日 星期日

2025 年第 13 周記事

本周主要學習重心放在 OpenAI API 的測試學習, 其實早在 2023 年底就陸續上了很多 API 的課, 知道怎麼一回事, 但就是沒動手做, 光是聽課其實通常馬耳東風, 沒複習幾乎跟沒聽過差不多. 雖然學習慢半拍, 但親自動手實測才會真正知道那是甚麼. 接下來要進入 Funtion calling 了, 但可能得暫停一下, 因為必須轉向來解決 LINE Notify 3/31 終止服務問題, 尋找解決方案, 要不然之前寫的爬蟲都會停擺了. 

週日小舅家掃墓, 今年小阿姨與左營阿姨疫情後首次回來參加掃墓, 我備辦水果餅乾跟爸去上香, 結束後還載阿姨舅舅去福善寺祭拜伯公 (外公過繼過去的兄弟, 因為無後所以由小舅負責祭祀), 我也是第一次去祭拜. 中午小舅在菸樓房訂一桌, 這家菜色真不錯, 列入以後招待訪客首選. 

三月即將結束, 回故這一季從年尾為阿蘭安金, 過年, 到掃墓, 一連串的祭祀活動終於告一段落, 下一次就是端午節了. 


2025年3月29日 星期六

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

本篇旨在將前一篇的聊天機器人改成串流版. 

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



6. 有記憶且能自動判斷是否需要搜尋網路的聊天機器人 (串流版) :  

前一篇測試中我們已利用 JSON 格式化回覆字串模板與採用對話歷史成功地製做了一個有記憶且能自動判斷是否需要搜尋網路的 OpenAI 聊天機器人, 當提出的問題超出模型的認知範圍時, 應用程式會去搜尋網路取得參考資料提供給模型去生成回應, 這種情況需要呼叫 GPT 模型兩次, 前一篇測試為了簡單起見都使用非串流回應, 事實上第二次呼叫 GPT 時可以使用串流回應 (但第一次因為要完整取得 JSON 格式的回應所以不能用串流方式), 作法參考 :


完整程式碼如下 :

# cli_chatbot_search_7.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 str(e)

def ask_gpt_s(messages, model='gpt-3.5-turbo'):
    try:
        # 啟用 stream=True 讓 GPT 逐步回應
        chunks=client.chat.completions.create(
            model=model,
            messages=messages,
            stream=True  # 啟用串流
            )
        reply=''   # 儲存完整回應
        print('GPT :', end=' ', flush=True)   # 立即輸出回應字元
        for chunk in chunks:
            if chunk.choices and chunk.choices[0].delta.content:
                text=chunk.choices[0].delta.content or ''  # 用 or 處理 None 問題
                reply += text
                print(text, end='', flush=True)  # 即時輸出
        print()  # 換行
        return reply  # 回傳完整回應
    except APIError as e:
        return str(e)

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": ""
}}
'''
# 初始化聊天訊息 (用來記憶聊天歷史) 
messages=[{'role': 'system', 'content': sys_role}]

# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    # 記錄使用者輸入到歷史訊息
    messages.append({'role': 'user', 'content': prompt})
    # 初次查詢 GPT : 確定是否需要搜尋網路
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})
    reply=ask_gpt(messages)  # 呼叫 OpenAI API
    messages.pop()  # 移除這個判斷訊息避免影響聊天歷史
    result=json.loads(reply)  # 將 JSON 字串轉成字典
    if result['need_search']=='Y':  # 需要搜尋網路
        keyword=result['keyword']  # 剔除 '#s' 取得使用者之提示詞
        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' + prompt
        # 記錄 Google 搜尋結果
        messages.append({'role': 'system', 'content': web_msg})
        # 再次查詢 GPT : 根據所提供的網路搜尋結果回答
        gpt_reply=ask_gpt_s(messages)    
    else:  # 不需搜尋網路, 加入使用者提示詞
        gpt_reply=result['reply']
        print(f'GPT : {gpt_reply}')  
    # 記錄 GPT 回應到歷史訊息
    messages.append({'role': 'assistant', 'content': gpt_reply})

主要的修改之處如下 :
  • 新增了一個串流版呼叫 GPT 模型的函式 ask_gpt_s() 給搜尋完網路後第二次呼叫 GPT 模型時使用, 它會將收到的一個個 token 即時輸出, 但同時也會串接為完整的回應字串傳回給主程式來儲存對話歷史. 
  • 由於第二次呼叫 GPT 的回應已在 ask_gpt_s() 裡面以串流方式輸出, 因此在不需要搜尋網路的 else 區塊要用 print() 輸出回應. 
執行結果如下 : 

>>> %Run cli_chatbot_search_7.py  
請設定 GPT 角色 : 
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : 賴清德當選為2024年台灣總統。
You : 那 2018 年呢?
GPT : 蔡英文當選為2018年台灣總統。
You : 那 2010 年呢?
GPT : 馬英九當選為2010年台灣總統。
You : 2024 台灣金曲歌王是誰?
GPT : 2024年台灣金曲歌王是MC HotDog。
You : 愛情是甚麼?
GPT : 愛情是強烈的依附和愛的感覺的發展,通常是對另一個人的情感結合,是受社會因素影響的生理、心理和主觀情感的複雜現象。
You : 請詳細說明
GPT : 愛情是生理、心理和主觀情感結合的複雜現象,受到社會因素影響。不同時代、文化、學科和學者對愛情有著不同的理解和定義,愛情通常包含亲密、激情、承諾這三個要素。有學者將愛情分為不同類型,包括基於對方外表的情慾之愛、不重承諾只欲征服的遊戲之愛、重情重義的友誼之愛、雙方皆可得益的現實之愛、妒忌與爭執的依附之愛以及完全無私的愛等。
You : 甚麼是量子疊加態?
GPT : 量子叠加态是指一个量子系统的几个量子态经过归一化的线性组合所得到的状态,在量子力学中,系统可以同时处于多个可能的状态之一,这种状态被称为量子叠加态。
You : 請進一步說明.
GPT : 量子叠加态是量子力学中的一个重要概念,表示量子系统可以同时处于多个可能的状态之一。这种状态的性质可以用数学上的线性组合来描述,是量子态的一个基本特征之一。量子叠加态的具体物理意义和应用在量子力学和量子信息科学等领域有着重要的应用和研究价值。
You : 
GPT : 再見。

可見此程式的回應有兩種, 如果是需要搜尋網路的, 回應會以串流方式像打字機那樣一個字一個字輸出模型的回應; 反之則會一次輸出全部 (取得 JSON 回應字串). 另外, 從後面關於量子態的回應為殘體中文可知, 即使已經設定 GPT 角色為 "繁體中文 AI 助理", 但還是有些時候輸出殘體中文 (隨機性緣故, 雖然機率較低). 

2025年3月28日 星期五

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

在上一篇測試中利用 JSON 格式回覆字串模板成功地在聊天機器人程式中自動判斷是否要搜尋網路找出參考資料讓模型能生成它認知範圍以外的知識, 但是這個聊天機器人欠缺記憶聊天歷史的能力, 無法從上下文判斷前後問題的關聯性, 本篇測試旨在解決記憶問題.

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



5. 有記憶且能自動判斷是否需要搜尋網路的聊天機器人 :  

關於如何讓聊天機器人具有記憶能力參考之前的這篇測試 :


在該篇測試中, 我們使用一個串列變數 memory 來儲存使用者的 prompt 與模型的回應, 在每一次的對話中將此對話歷史連同提示詞一起傳給 API, 這樣 GPT 模型便能從這些上下文中得知前後對話脈絡, 從而做出適當的回應. 

本篇是在前一篇測試的範例程式 cli_chatbot_search_4.py 的基礎上進行修改, 讓聊天機器人句有記憶能力, 前一篇測試參考 : 


主要的修改是將 messages 訊息串列拿到外面做為全域變數, 而且用來儲存對話歷史, 在每次對話前後都會將提示詞與模型回應存入串列中, 且每次向 GPT 查詢都是傳遞此對話歷史, 完整程式碼如下 :

# cli_chatbot_search_5.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": ""
}}
'''
# 初始化聊天訊息 (用來記憶聊天歷史) 
messages=[{'role': 'system', 'content': sys_role}]

# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    # 記錄使用者輸入到歷史訊息
    messages.append({'role': 'user', 'content': prompt})
    # 初次查詢 GPT : 確定是否需要搜尋網路
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})   
    print(messages)
    reply=ask_gpt(messages)  # 呼叫 OpenAI API
    print(reply)
    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' + prompt
        # 記錄 Google 搜尋結果
        messages.append({'role': 'system', 'content': web_msg})
        # 再次查詢 GPT : 根據所提供的網路搜尋結果回答
        reply='(正在搜尋網路...) ' + ask_gpt(messages)    
    else:  # 不需搜尋網路, 加入使用者提示詞
        reply=result['reply']
    # 記錄 GPT 回應到歷史訊息
    messages.append({'role': 'assistant', 'content': reply})
    print(f'GPT : {reply}') 

執行結果如下 : 

>>> %Run cli_chatbot_search_5.py   
請設定 GPT 角色 : 
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : (正在搜尋網路...) {
  "need_search": "N",
  "keyword": "",
  "reply": "賴清德、蕭美琴當選中華民國第16任總統、副總統。"
}
You : 那 2018 年呢?
GPT : 蔡英文當選中華民國第14任總統。

雖然透過記錄對話歷史提供給模型做為回覆參考確實讓第二個問題 '那 2018 年呢?' 關聯到前一個問題, 但是第一個問題的回答卻是 JSON 格式, 這會讓使用者覺得很奇怪. 這是因為初次查詢 GPT 之前將填充提示詞後的 JSON 格式訊息存入 messages 變數所致 :

messages.append({'role': 'user', 'content': json_query_str})

由於 JSON 格式化回覆字串中, 不管模型知不知道答案都要求只能用 JSON 格式回覆, 導致在第二次查詢 GPT 時模型已知答案, 卻受限於這個限制而回覆 JSON 格式的答案 :

如果知道, 請只用下列 JSON 格式回答:
{{
  "need_search": "N",
  "keyword": "",
  "reply":"你的答案"
}}

所以解決之道就是已經放進 memory 變數中儲存的 json_query_str 訊息字典要在初次查詢 GPT 後從 messages 串列中移除, 以免第二次查詢 GPT 時限制了模型的回覆格式, 這並不影響對話歷史的紀錄, 對話歷史只要記錄使用者輸入的提示詞與模型回應即可, 修正後的程式如下 :

# cli_chatbot_search_6.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": ""
}}
'''
# 初始化聊天訊息 (用來記憶聊天歷史) 
messages=[{'role': 'system', 'content': sys_role}]

# 聊天機器人 (無窮迴圈)
while True: 
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # 按 Enter 跳出迴圈結束聊天
        print('GPT : 再見。')
        break
    # 記錄使用者輸入到歷史訊息
    messages.append({'role': 'user', 'content': prompt})
    # 初次查詢 GPT : 確定是否需要搜尋網路
    json_query_str=template.format(prompt)
    messages.append({'role': 'user', 'content': json_query_str})  # 為了規範模型回應格式
    reply=ask_gpt(messages)  # 呼叫 OpenAI API
    messages.pop()  # 移除這個判斷訊息避免影響聊天歷史
    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' + prompt
        # 記錄 Google 搜尋結果
        messages.append({'role': 'system', 'content': web_msg})
        # 再次查詢 GPT : 根據所提供的網路搜尋結果回答
        reply=ask_gpt(messages)    
    else:  # 不需搜尋網路, 加入使用者提示詞
        reply=result['reply']
    # 記錄 GPT 回應到歷史訊息
    messages.append({'role': 'assistant', 'content': reply})
    print(f'GPT : {reply}') 

主要的修改處有三 : 
  • 初次查詢 GPT 後呼叫串列的 pop() 方法移除之前放進去的 JSON 格式化回覆訊息字典.
  • 在製做谷歌搜尋結果字串時加入 "(**直接給出答案**)" 敘述, 避免模型回應時出現 「根據搜尋結果」或「從網路資料顯示」等字眼, 這也會讓使用者覺得奇怪. 
  • 刪除了 (正在搜尋網路...) 輸出, 讓使用者不會覺得原來是在 call out. 
執行結果如下 : 

>>> %Run cli_chatbot_search_6.py     
請設定 GPT 角色 : 
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : 賴清德當選為2024年台灣總統。
You : 那 2018 年呢?
GPT : 蔡英文當選為2018年台灣總統。
You : 那 2010 年呢?
GPT : 馬英九當選為2010年台灣總統。
You : 2024 台灣金曲歌王是誰?
GPT : 2024年台灣金曲歌王是MC HotDog(熱狗)。
You : 愛情是甚麼?
GPT : 愛情是強烈的依附和愛的感覺的發展,通常是對另一個人。
You : 請詳細說明
GPT : 愛情是一種複雜的現象,涉及生理、心理和主觀情感的結合。它可以帶來強烈的依附和愛的感覺,通常是對另一個人的特殊情感,使人處於一種脆弱而幸福的狀態。愛情的定義因文化、時代和個人觀點而異,但普遍被視為人類生活中重要的情感之一。
You : 甚麼是量子疊加態?
GPT : 量子疊加態是描述量子系統可能同時處於多個可能的狀態之間的情況,直到進行量測為止。
You : 請進一步說明.
GPT : 量子疊加態是指一個量子系統的幾個量子態進行線性組合後得到的狀態,與古典力學中只能處於單一狀態不同,量子系統具有不確定性,可以同時存在多個可能狀態的性質。
You : 
GPT : 再見。

可見這個改良版機器人能夠將 "那 2018 年呢?" 與 "那 2010 年呢?" 的問題關聯到前面 "2024台灣總統大選是誰當選?" 這個跟總統大選相關的提問; "請詳細說明" 也能理解指的是要針對 "愛情是甚麼?" 來發揮; 而 "請進一步說明" 則是指量子疊加態. 

2025年3月27日 星期四

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

在前一篇測試中已透過 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 年發生了甚麼事. 

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 最優. 

2025年3月25日 星期二

Python 學習筆記 : 使用 Custom Search JSON API 取得谷歌搜尋結果

這幾天註冊了 SerpAPI 帳戶使用其 API Key 來取得 Google 搜尋結果, 但它的免費帳戶一個月只能呼叫 100 次, 平均一天只能搜尋 33 次, 這似乎是太少了, 參考 :


今天在 "ChatGPT 開發手冊 Turbo x Vision" 這本書中找到 Custom Search JSON API, 它對免費帳戶提供每天 100 次呼叫服務, 是 SerpAPI 的 3 倍, 這就佛心多了, 以下紀錄註冊免費帳戶與取得 API Key 程序以及如何用它來取得 Google 搜尋資料. 


1. 啟用谷歌 Custon Search JSON API 功能 :     

Google Custom Search JSON API 是 Google 提供的一個 REST API, 可以讓開發者透過程式存取 Google 搜尋結果 (傳回格式為 JSON), 並可根據特定的自訂搜尋引擎來篩選內容. 只要有 Google 帳號即可申請使用, 免費版每天可搜尋 100 次, 付費版 5 美元可搜尋 1000 次, 不論免費或付費, 每次搜尋最多傳回 10 筆結果 (可利用 start 參數指定傳回筆數). 

首先瀏覽器要登入 Google 帳號, 然後前往下列網址 : 


按 "搜尋引擎 ID" 項目說明中的 "程式化搜尋引擎控制台" 超連結 :  




按右上角的 "新增" 鈕新增一個自訂搜尋引擎 :




在 "為你的搜尋引擎命名" 欄中輸入搜尋引擎名稱 (自訂), 並於 "要搜尋甚麼" 欄位中勾選 "搜尋整個網路", 勾選我不是機器人後按 "建立" 鈕 : 




這樣便建立了一個搜尋引擎, 我們可以複製 HTML 碼內嵌到自己的網頁中使用 : 




按 "自訂" 鈕在下列頁面中按 "搜尋引擎 ID" 欄位右邊的複製鈕將此 ID 複製儲存到文字檔備用 : 




接下來要取得此搜尋引擎 API 的金鑰, 這時回到 "程式化搜尋引擎控制台" 網頁, 按 "API 金鑰" 欄位中的 "取得金鑰" 鈕 : 



 

在彈出的 "Enable Custom Search API" 視窗中點選 "+ Create a new project" 新增一個 GCP 專案 : 




在 "Enter a new project name" 欄位中輸入專案名稱 (只可用英數字與 - 字元), 這裡用上面的搜尋引擎相同名稱 LLM-test 後按 "NEXT" 鈕 :




這樣便完成 API Key 的啟用了, 按 "SHOW KEY" 鈕即可顯示金鑰 : 




按右邊複製鈕將 API Key 複製到文字檔中儲存備用 : 




有了自訂搜尋引擎 ID 與其 API Key 便可以用 HTTP GET 方法來取得谷歌搜尋結果了 (注意, 只能用 GET 方法, 不提供 POST 方法). 


2. 使用 Custon Search JSON API 取得谷歌搜尋結果 :       

透過 HTTP GET 方法取得谷歌自訂引擎搜尋結果可以使用內建的 urllib 或第三方爬蟲套件 requests, 以下測試使用 requests :

>>> import requests    

關於 requests 用法參考 :


向自訂的谷歌搜尋引擎提出請求的網址格式如下 : 

https://www.googleapis.com/customsearch/v1?q={query}&key={api_key}&cx={cx}&num={num}

也可以用 gl 參數指定地理位置 (台灣是 tw) 與 lr 參數指定語言 (繁體中文是 lang_zh-TW) : 

https://www.googleapis.com/customsearch/v1?q={query}&key={api_key}&cx={cx}&num={num}&gl=tw&lr=lang_zh_TW

其中 query 是要搜尋的關鍵字, api_key 是金鑰, cx 是自訂搜尋引擎的 ID, NUM 是傳回的搜尋結果筆數 (前 NUM 筆), 此兩參數都使用 ISO 國家 (3166) 與語言標碼表 (639), 參考 :


定義變數如下 :

>>> cx='我的搜尋引擎 ID'   
>>> api_key='我的 API Key'     
>>> num=3   
>>> query='2024台灣總統大選是誰當選?'    

製作 HTTP GET 請求網址並將其傳入 requests.get() 函式 : 

>>> url=f'https://www.googleapis.com/customsearch/v1?q={query}&key={api_key}&cx={cx}&num={num}'   
>>> r=requests.get(url)    

傳回值是攜帶 JSON 格式搜尋結果的 Response 物件, 呼叫其 json() 方法會轉成字典物件 :  

>>> data=r.json()    
>>> type(data)   
<class 'dict'>

呼叫 keys() 方法檢視字典的鍵 : 

>>> data.keys()  
dict_keys(['kind', 'url', 'queries', 'context', 'searchInformation', 'items'])

搜尋結果是放 items 鍵裡面, 其值為一串列 : 

>>> items=data['items']    
>>> type(items)   
<class 'list'>

檢視串列的第一個元素即可看到第一筆搜尋結果是一個字典 : 

>>> items[0]   
{'kind': 'customsearch#result', 'title': '2024年11月5日總統大選結果公布時間表| SF.gov', 'htmlTitle': '<b>2024</b>年11月5日<b>總統大選</b>結果公布時間表| SF.gov', 'link': 'https://www.sf.gov/zh-hant/november-5-2024-presidential-general-election-results-reporting-schedule?_gl=1*rpqmk7*_ga*NTA5MTY0MzM4LjE2NjU2OTYzOTg.*_ga_BT9NDE0NFC*MTcwOTU5ODgwMy4yODcuMC4xNzA5NTk4ODAzLjAuMC4w*_ga_63SCS846YP*MTcwOTU5ODgwMy4yMzguMC4xNzA5NTk4ODAzLjAuMC4w', 'displayLink': 'www.sf.gov', 'snippet': 'Nov 5, 2024 ... 投票站關閉後,選務處將會公布四份初步結果報告: ... 於選舉之夜公布的所有選舉結果均為初步性的,在往後的日子,當選務處點算數以萬計的選票後結果將會有所\xa0...', 'htmlSnippet': 'Nov 5, 2024 <b>...</b> 投票站關閉後,選務處將會公布四份初步結果報告: ... 於<b>選舉</b>之夜公布的所有<b>選舉</b>結果均為初步性的,在往後的日子,<b>當選</b>務處點算數以萬計的選票後結果將會有所&nbsp;...', 'formattedUrl': 'https://www.sf.gov/.../november-5-2024-presidential-general-election-result...', 'htmlFormattedUrl': 'https://www.sf.gov/.../november-5-<b>2024</b>-presidential-general-election-result...', 'pagemap': {'metatags': [{'next-head-count': '3', 'viewport': 'width=device-width'}]}}

我們關心的資訊放在下列三個鍵裡面 :
  • title : 網頁標題
  • link : 網頁網址
  • snippet : 網頁摘要
可以用迴圈迭代 items 串列元素印出這三個鍵的內容, 此處改用字典的 get() 方法, 並指定預設傳回值為空串列 [] :

>>> for item in data.get('items', []):
    print(f'標題: {item["title"]}')
    print(f'描述: {item["snippet"]}')
    print(f'網址: {item["link"]}\n') 
  
標題: 2024年11月5日總統大選結果公布時間表| SF.gov
描述: Nov 5, 2024 ... 投票站關閉後,選務處將會公布四份初步結果報告: ... 於選舉之夜公布的所有選舉結果均為初步性的,在往後的日子,當選務處點算數以萬計的選票後結果將會有所 ...
網址: https://www.sf.gov/zh-hant/november-5-2024-presidential-general-election-results-reporting-schedule?_gl=1*rpqmk7*_ga*NTA5MTY0MzM4LjE2NjU2OTYzOTg.*_ga_BT9NDE0NFC*MTcwOTU5ODgwMy4yODcuMC4xNzA5NTk4ODAzLjAuMC4w*_ga_63SCS846YP*MTcwOTU5ODgwMy4yMzguMC4xNzA5NTk4ODAzLjAuMC4w

標題: 2024年中华民国总统选举- 维基百科,自由的百科全书
描述: 选举结果,赖清德、萧美琴当选中华民国第16任总统、副总统。本次是继2000年后再度未有任一候选人得票率过半的总统选举,亦是自总统直选以来,首度 ...
網址: https://zh.wikipedia.org/zh-cn/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

標題: 台湾大选:一文读懂民进党赖清德为何获胜以及选后的各种看点- BBC ...
描述: Jan 14, 2024 ... 2024年台湾总统选举结果揭晓,民进党候选人赖清德及萧美琴成功当选新一任总统及副总统,总得票数达558万,得票率为40.05%。此役民进党打破台湾政坛执政党 ...
網址: https://www.bbc.com/zhongwen/simp/chinese-news-67973916

可見若未指定位置與語言, 傳回值可能出現簡體中文結果. 

另外一個做法是把 GET 的參數放在一個字典中, 然後在呼叫 requests.get() 方法時將其傳遞給 params 參數 : 

>>> params={
    'q': query,
    'key': api_key,
    'cx': cx,
    'lr': 'lang_zh-TW',  
    'gl': 'tw',          
    'num': 3            
    }  

這時 url 就不需要攜帶參數了 : 

>>> url='https://www.googleapis.com/customsearch/v1'   
>>> r=requests.get(url, params=params)    
>>> data=r.json()    
>>> for item in data.get('items', []):
    print(f'標題: {item["title"]}')
    print(f'描述: {item["snippet"]}')
    print(f'網址: {item["link"]}\n')  
  
標題: 2024年11月5日總統大選結果公布時間表| SF.gov
描述: Nov 5, 2024 ... 投票站關閉後,選務處將會公布四份初步結果報告: ... 於選舉之夜公布的所有選舉結果均為初步性的,在往後的日子,當選務處點算數以萬計的選票後結果將會有所 ...
網址: https://www.sf.gov/zh-hant/november-5-2024-presidential-general-election-results-reporting-schedule?_gl=1*rpqmk7*_ga*NTA5MTY0MzM4LjE2NjU2OTYzOTg.*_ga_BT9NDE0NFC*MTcwOTU5ODgwMy4yODcuMC4xNzA5NTk4ODAzLjAuMC4w*_ga_63SCS846YP*MTcwOTU5ODgwMy4yMzguMC4xNzA5NTk4ODAzLjAuMC4w

標題: 2024年中華民國總統選舉- 維基百科,自由的百科全書
描述: 選舉結果,賴清德、蕭美琴當選中華民國第16任總統、副總統。本次是繼2000年後再度未有任一候選人得票率過半的總統選舉,亦是自總統直選以來,首度 ...
網址: https://zh.wikipedia.org/zh-hant/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

標題: 台灣大選2024:賴清德當選總統民進黨未能控制立法院- BBC News ...
描述: Jan 13, 2024 ... 1月13日,台灣舉行2024年總統選舉與立法委員選舉,執政黨民進黨候選人賴清德以超過558萬票勝選。
網址: https://www.bbc.com/zhongwen/trad/chinese-news-67971619

因為有指定 gl 與 lr 參數, 所以傳回的搜尋結果都是繁體中文了. 

為了簡化查詢過程, 我將上述程式碼寫成如下之函式 :

>>> 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', [])

此函式預設傳回前 3 筆台灣繁體中文之搜尋結果, 使用前先匯入 requests 套件 : 

>>> import requests     

然後定義引擎 ID, 金鑰, 與關鍵字等參數值, 並將它們傳入 search_google() 即可, 它會傳回一個包含 title, link, snippet 等鍵之字典串列 : 

>>> cx='我的搜尋引擎 ID'   
>>> api_key='我的 API Key'     
>>> query='2024 台灣金曲歌王是誰?'    
>>> for item in search_google(query, cx, api_key):    
    print(f'標題: {item["title"]}')
    print(f'描述: {item["snippet"]}')
    print(f'網址: {item["link"]}\n')
    
標題: 2024金曲獎得獎名單完整公佈!MC HotDog奪歌王,歌后由孫盛希抱回
描述: Jun 30, 2024 ... 拿下最佳作詞人的MC HotDog熱狗再度成功拿下金曲歌王,適逢女兒生日,上台時他表示:「今天剛好是我女兒的生日,祝你生日快樂。我本來跟他講說,如果我沒有得獎 ...
網址: https://www.marieclaire.com.tw/entertainment/music/80150/the-35th-golden-melody-awards-winners

標題: 【金曲獎2024】金曲35完整得獎名單!MC HotDog熱狗奪歌王 ...
描述: Jun 29, 2024 ... 2024年「第35屆金曲獎」於6月29日,在台北小巨蛋盛大登場!本屆除了主持組合、表演嘉賓讓人相當期待,得獎名單也相當精采,角逐歌王歌后寶座的唱匠包括: ...
網址: https://www.elle.com/tw/entertainment/music/g61459193/gma-2024-35-award/

標題: 【2024 金曲獎】金曲35 完整得獎名單:MC HotDog、孫盛希封歌王 ...
描述: Jun 29, 2024 ... ... 歌手」封歌王、歌后,但真正的大贏家則是一舉拿下「最佳樂團獎」、「最佳華語專輯獎」、「年度專輯獎」的草東沒有派對!現在,馬上來看金曲35 的完整 ...
網址: https://www.gq.com.tw/article/%E5%BE%97%E7%8D%8E%E5%90%8D%E5%96%AE-2024%E9%87%91%E6%9B%B2%E7%8D%8E-%E9%87%91%E6%9B%B235

Custom Search JSON API 每天可以免費呼叫 100 次, 這是 SerpAPI 的 3 倍, 所以以後就改用這個吧! SerpAPI 就當備用好了.