在前一篇測試中, 我們成功地利用 Function calling 功能實作了一個能讓 gpt-3.5-turbo 模型在不知答案時呼叫外部工具函式求解的聊天機器人, 但那個機器人沒有記憶功能, 本篇旨在前一篇基礎上添加記憶功能, 參考 :
1. 無限制記憶長度的聊天機器人 :
這是單純地將訊息串列 messages 拿到外面做為全域變數來記憶對話歷史, 包含提示詞, 可能的搜尋結果, 以及模型的回應等, 都依序丟進串列中儲存, 程式碼如下 :
# cli_chatbot_function_calling_2.py
from openai import OpenAI, APIError
import requests
import json
from dotenv import load_dotenv
import os
def ask_gpt(messages, tools=[], model='gpt-3.5-turbo'):
try:
reply=client.chat.completions.create(
model=model,
messages=messages,
tools=tools
)
return reply
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', [])
# 載入環境變數
load_dotenv()
openai_api_key=os.environ.get('OPENAI_API')
custom_search_key=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
cx=os.environ.get('SEARCH_ENGINE_ID')
# 設定外部工具函式串列
tools=[{
"type": "function",
"function": {
"name": "search_google",
"description": "用 Google Custom Search API 取得搜尋結果",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "要搜尋的關鍵字"
},
"cx": {
"type": "string",
"description": "Google Custom Search 搜尋引擎 ID"
},
"api_key": {
"type": "string",
"description": "Google Custom Search API 金鑰"
},
"num": {
"type": "integer",
"description": "回傳的搜尋結果筆數(最多 10 筆)",
"default": 3
},
"gl": {
"type": "string",
"description": "地理區域 (預設 'tw' 台灣)",
"default": "tw"
},
"lr": {
"type": "string",
"description": "語言 (預設 'lang_zh_TW' 繁體中文)",
"default": "lang_zh_TW"
}
},
"required": ["query", "cx", "api_key"]
}
}
}]
# 建立 OpenAI 物件
client=OpenAI(api_key=openai_api_key)
# 設定 GPT 系統角色
sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '':
sys_role='你是繁體中文AI助理'
print(sys_role)
# 初始化聊天訊息 (用來記憶聊天歷史)
messages=[{'role': 'system', 'content': sys_role}]
# 聊天機器人 (無窮迴圈)
while True:
prompt=input('You : ') # 輸入提示詞
if prompt.strip() == '': # 按 Enter 跳出迴圈結束聊天
print('GPT : 再見。')
del messages # 結束對話時主動刪除對話歷史
break
messages.append({'role': 'user', 'content': prompt}) # 加入提示詞
# 第一次查詢 : 確定是否需要搜尋網路
reply=ask_gpt(messages, tools=tools)
reply_message=reply.choices[0].message # 取出回應訊息
if reply_message.content == None: # 內容為空表示需要搜尋網路
messages.append(reply_message) # 回應放入 messages 串列
tool_calls=reply_message.tool_calls # 取得外部工具函式物件
function_name=tool_calls[0].function.name # 取得函式名稱
args=json.loads(tool_calls[0].function.arguments) # 取得參數
args['api_key']=custom_search_key # 設定搜尋引擎金鑰
args['cx']=cx # 設定搜尋引擎ID
func=globals().get(function_name) # 取得函式參考
results=func(**args) # 呼叫外部函式
web_msg=[] # 用來儲存搜尋結果
for result in results:
web_msg.append(f'標題: {result["title"]}')
web_msg.append(f'描述: {result["snippet"]}')
content='\n'.join(web_msg) # 搜尋結果串接為字串
results_dict={ # 製作 JSON 格式的搜尋結果字典
"tool_call_id": tool_calls[0].id,
"role": "tool",
"name": function_name,
"content": content
}
messages.append(results_dict) # 將搜尋結果字典放進 messages
# 第二次查詢 : 根據所提供的網路搜尋結果回答
reply2=ask_gpt(messages, tools=tools)
response='搜尋網路...' + reply2.choices[0].message.content
else: # 不需搜尋網路, 直接回應答案
response=reply_message.content
messages.append({'role': 'assistant', 'content': response})
print(f'GPT : {response}')
此例在取得外部函式傳回的結果前冠上 '搜尋網路...' 表示模型有呼叫外掛工具 search_google() 協助 (雖然模型回應通常會以 '根據搜尋結果' 開頭), 第一次執行發現即使是它知道的也呼叫外掛 :
>>> %Run cli_chatbot_function_calling_2.py
請設定 GPT 角色 :
你是繁體中文AI助理
You : 2024台灣總統大選是誰當選?
GPT : 搜尋網路...根據搜尋結果,2024年台灣總統大選中,賴清德當選為中華民國第16任總統,蕭美琴當選為副總統。賴清德總統當選,將預計於2024年5月20日宣誓就職。
You : 那 2018 年呢?
GPT : 搜尋網路...根據搜尋結果,2018年台灣總統大選並未找到明確的結果,但顯示在2018年的「九合一」選舉中,民進黨遭遇大敗,國民黨拿下逾三分之二的地方席位。
You : 那 2016 年呢?
GPT : 搜尋網路...根據搜尋結果,2016年台灣總統大選中,蔡英文當選為中華民國第14任總統,成為台灣史上首位女性最高統治者,也是中華民國首位女性、未婚、擁有獲選總統的人。
You : 那 2012 年呢?
GPT : 搜尋網路...根據搜尋結果,2012年台灣總統大選中,馬英九擊敗蔡英文當選為中華民國第13任總統,副總統為吳敦義。馬英九的得票率為51.6%。
You : 2012台灣總統大選是誰當選?
GPT : 搜尋網路...根據搜尋結果,2012年台灣總統大選中,馬英九當選為中華民國第13任總統,副總統為吳敦義。馬英九在該次選舉中獲得51.6%的得票率。
You : 那 2008 年呢?
GPT : 搜尋網路...根據搜尋結果,2008年台灣總統大選中,國民黨提名的馬英九以壓倒性優勢當選為中華民國第12任總統,副總統為蕭萬長。馬英九在該次選舉中獲得58.45%的得票率。
You :
GPT : 再見。
第二次執行時知道的有時查網路, 有時又自行回答 :
>>> %Run cli_chatbot_function_calling_2.py
請設定 GPT 角色 :
你是繁體中文AI助理
You : 2008台灣總統大選是誰當選?
GPT : 搜尋網路...2008年台灣總統大選中,馬英九當選為台灣的第12任總統。
You : 那2012年呢?
GPT : 搜尋網路...搜尋網路...2012年台灣總統大選中,馬英九連任為台灣的第13任總統。
You : 那2016年呢?
GPT : 2016年台灣總統大選中,蔡英文當選為台灣的第14任總統,也是台灣史上首位女性最高統治者。
You : 那2020年呢?
GPT : 2020年台灣總統大選中,蔡英文當選為台灣的第15任總統。
You : 那2024年呢?
GPT : 搜尋網路...抱歉,我目前沒有2024年台灣總統大選的資訊。您可以稍後再查詢或到網路上尋找最新的資料。如果您有其他問題,歡迎告訴我。我會盡力協助您的。
You : 那2024年呢?
GPT : 2024年台灣總統大選中,賴清德當選為台灣的第16任總統。
You :
GPT : 再見。
上面 2024 年的去查網路, 但不知為何無法根據搜尋結果做出確定之回覆, 但這些搜尋結果都存入對話歷史中了, 所以第二次問 2024 年大選結果它沒查網路, 而是根據對話歷史中上一次搜尋的紀錄直接做出回應.
第三次測試結果如下 :
>>> %Run cli_chatbot_function_calling_2.py
請設定 GPT 角色 :
你是繁體中文AI助理
You : 我養了兩隻貓, 名叫咪咪與萬萬
GPT : 搜尋網路...那兩隻貓的名字很可愛! 有什麼關於咪咪和萬萬的資訊需要我幫忙查詢嗎?
You : 愛情是甚麼?
GPT : 根據愛情心理學的分析,愛情由親密、激情和承諾三個要素組成。親密表示雙方之間的緊密聯繫,激情則是指對彼此的熱情和慾望,而承諾則是指對關係的堅定承諾。愛情的發展通常是對另一個人的強烈依附和愛的感覺,讓人陷入一種脆弱且讓人無法控制的狀態。結合這些要素,愛情被認為是一種深刻的情感與關係的結合。
You : 2024 台灣金曲歌王是誰?
GPT : 根據2024年台灣金曲獎的得獎名單,金曲歌王是 MC HotDog(熱狗),而歌后則由孫盛希獲得。祝賀他們在音樂界取得了傑出的成就!
You : 你還記得我那兩隻貓的名字嗎?
GPT : 搜尋網路...當然記得!你養的兩隻貓的名字是咪咪和萬萬。有關咪咪和萬萬需要我為你查找更多資訊嗎?
You :
GPT : 再見。
很奇怪的是, 2024 金曲歌王它應該不知道, 要去查網路才對, 結果居然直接回答! 還有, 我那兩隻貓的名字是啥幹嘛去搜尋網路? 可見即使不傳 tool_choice 參數, 它預設就是 'auto', 對於它知識範圍的詢問要不要查網路是不確定的.
2. 限制記憶長度的聊天機器人 :
上面的程式沒有對紀錄聊天歷史的串列 messages 的長度做限制, 它的長度就會隨著對話次數增加而擴增, 這樣會使得耗費的 token 數也會逐漸增加, 扣款數字就會攀升, 因此有必要限制此串列之長度. 我們可以使用一個全域變數來設定對話歷史串列的最大長度 (預設 10), 程式碼如下 :
# cli_chatbot_function_calling_2.py
from openai import OpenAI, APIError
import requests
import json
from dotenv import load_dotenv
import os
def ask_gpt(messages, tools=[], model='gpt-3.5-turbo'):
try:
reply=client.chat.completions.create(
model=model,
messages=messages,
tools=tools
)
return reply
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', [])
# 載入環境變數
load_dotenv()
openai_api_key=os.environ.get('OPENAI_API')
custom_search_key=os.environ.get('GOOGLE_CUSTOM_SEARCH_API')
cx=os.environ.get('SEARCH_ENGINE_ID')
# 設定外部工具函式串列
tools=[{
"type": "function",
"function": {
"name": "search_google",
"description": "用 Google Custom Search API 取得搜尋結果",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "要搜尋的關鍵字"
},
"cx": {
"type": "string",
"description": "Google Custom Search 搜尋引擎 ID"
},
"api_key": {
"type": "string",
"description": "Google Custom Search API 金鑰"
},
"num": {
"type": "integer",
"description": "回傳的搜尋結果筆數(最多 10 筆)",
"default": 3
},
"gl": {
"type": "string",
"description": "地理區域 (預設 'tw' 台灣)",
"default": "tw"
},
"lr": {
"type": "string",
"description": "語言 (預設 'lang_zh_TW' 繁體中文)",
"default": "lang_zh_TW"
}
},
"required": ["query", "cx", "api_key"]
}
}
}]
# 建立 OpenAI 物件
client=OpenAI(api_key=openai_api_key)
# 設定 GPT 系統角色
sys_role=input('請設定 GPT 角色 : ')
if sys_role.strip() == '':
sys_role='你是繁體中文AI助理'
print(sys_role)
# 初始化聊天訊息 (用來記憶聊天歷史)
messages=[{'role': 'system', 'content': sys_role}]
# 全域變數:限制對話歷史長度
MAX_HISTORY_LENGTH=10
# 聊天機器人 (無窮迴圈)
while True:
prompt=input('You : ') # 輸入提示詞
if prompt.strip() == '': # 按 Enter 跳出迴圈結束聊天
print('GPT : 再見。')
del messages # 結束對話時主動刪除對話歷史
break
messages.append({'role': 'user', 'content': prompt}) # 加入提示詞
# 第一次查詢 : 確定是否需要搜尋網路
reply=ask_gpt(messages, tools=tools)
reply_message=reply.choices[0].message # 取出回應訊息
if reply_message.content == None: # 內容為空表示需要搜尋網路
messages.append(reply_message) # 回應放入 messages 串列
tool_calls=reply_message.tool_calls # 取得外部工具函式物件
function_name=tool_calls[0].function.name # 取得函式名稱
args=json.loads(tool_calls[0].function.arguments) # 取得參數
args['api_key']=custom_search_key # 設定搜尋引擎金鑰
args['cx']=cx # 設定搜尋引擎ID
func=globals().get(function_name) # 取得函式參考
results=func(**args) # 呼叫外部函式
web_msg=[] # 用來儲存搜尋結果
for result in results:
web_msg.append(f'標題: {result["title"]}')
web_msg.append(f'描述: {result["snippet"]}')
content='\n'.join(web_msg) # 搜尋結果串接為字串
results_dict={ # 製作 JSON 格式的搜尋結果字典
"tool_call_id": tool_calls[0].id,
"role": "tool",
"name": function_name,
"content": content
}
messages.append(results_dict) # 將搜尋結果字典放進 messages
# 第二次查詢 : 根據所提供的網路搜尋結果回答
reply2=ask_gpt(messages, tools=tools)
response=reply2.choices[0].message.content
else: # 不需搜尋網路, 直接回應答案
response='搜尋網路...' + reply_message.content
messages.append({'role': 'assistant', 'content': response})
# 限制對話歷史長度,保留第一個元素(系統角色)
if len(messages) > MAX_HISTORY_LENGTH:
messages=[messages[0]] + messages[-(MAX_HISTORY_LENGTH - 1):]
print(f'GPT : {response}')
此程式使用一個全域變數 MAX_HISTORY_LENGTH 來限制對話歷史的長度, 在程式最後面儲存完模型的回應後進行 messages 串列裁減, 保留地一個 system 角色訊息, 然後擷取倒數 MAX_HISTORY_LENGTH - 1 個對話歷史組成新的 MAX_HISTORY_LENGTH 個對話歷史訊息. 執行結果如下 :
>>> %Run cli_chatbot_function_calling_3.py
請設定 GPT 角色 :
你是繁體中文AI助理
You : 我養了兩隻貓, 名叫咪咪與萬萬
GPT : 搜尋網路...咪咪與萬萬是很可愛的名字!有什麼關於你的貓咪想讓我知道或幫忙的嗎?
You : 我養的兩隻貓名叫甚麼?
GPT : 搜尋網路...你養的兩隻貓的名字分別是咪咪和萬萬。有什麼其他問題或需要幫忙的嗎?
You : 2016台灣總統大選是誰當選?
GPT : 2016年台灣總統大選中,蔡英文當選為台灣史上首位女性最高統治者。如果有其他問題或需要幫忙,請告訴我!
You : 你還記得我養的兩隻貓嗎?
GPT : 搜尋網路...當然記得!你養的兩隻貓的名字是咪咪和萬萬。有任何其他問題或需要幫忙的,歡迎告訴我!
You : 2024年呢?
GPT : 根據搜尋結果,2024年台灣總統大選中,賴清德當選為中華民國第16任總統。如果有其他問題或需要幫忙,請告訴我!
You : 2024 台灣金曲歌王是誰?
GPT : 根據搜尋結果,2024年台灣金曲歌王是 MC HotDog。如果您有其他問題或需要進一步資訊的話,請告訴我!
You : 考考你的記性, 我那兩隻貓名字是甚麼?
GPT : 搜尋網路...你養的兩隻貓的名字分別是咪咪和萬萬。有其他問題或需要我幫忙查詢資訊嗎?
You : 那 2020 年呢?
GPT : 搜尋網路...很抱歉,我無法追蹤您在2020年養的兩隻貓的名字。如果您需要任何其他幫助或資訊,請隨時告訴我!
You : 2020 年金曲獎歌王啦!
GPT : 根據搜尋結果,2020年台灣金曲獎歌王是周華健。如果您有其他問題或需要進一步資訊的話,請告訴我!
You : 簡單說明何謂量子態?
GPT : 搜尋網路...量子態是描述量子系統在某一特定時刻的狀態。在量子力學中,一個系統的狀態可以用一個稱為波函數的數學函數來描述,波函數包含有關系統可能出現在各種不同狀態的機率分佈。量子態的特點是具有超越經典物理觀念的性質,如超位置性、量子糾纏等特徵。量子態是量子力學中非常重要的概念,用於描述微觀粒子的行為和性質。
You : 請問下一次五星連珠時間?
GPT : 搜尋網路...抱歉,我目前無法提供五星連珠的下一次活動時間。您可以查詢遊戲的官方網站或社群媒體以獲取最新的活動資訊。如果您有其他問題或需要幫忙,請隨時告訴我!
You : 愛情是甚麼?
GPT : 搜尋網路...愛情是一種情感和關係,通常在人與人之間產生。它包含了深厚的情感連結、照顧、尊重、支持和傾聽。愛情可以是各種形式,包括親情、友情和愛情關係。在愛情中,人們分享他們的喜悅、悲傷、希望和夢想,共同建立彼此之間的情感聯繫和相互扶持。愛情是人類最基本和深刻的情感之一,為生活帶來溫暖、意義和幸福感。
You : 你會不會忘記我那兩隻貓了?
GPT : 搜尋網路...我不會忘記您提到的兩隻貓!如果您需要我提醒您有關它們的任何資訊或需要幫助,請隨時告訴我。您可以提供更多關於您的貓咪的資訊,讓我更好地了解它們。
You : 它們的名字叫甚麼?
GPT : 搜尋網路...抱歉,我無法記住先前的對話內容,因此沒有你提到兩隻貓的名字。如果你方便的話,可以提供一下它們的名字,讓我記住以便日後提醒。
You :
GPT : 再見。
由於限制對話歷史的長度為 10 個, 如果哈拉太久的話, 它就會忘記我那兩隻貓的名字了.