2025年3月15日 星期六

OpenAI API 學習筆記 : 命令列聊天機器人

測試完常用的 OpenAI API 參數後, 接下來要來寫一個在命令列執行的聊天機器人. 

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



1. 無記憶的 GPT 聊天機器人 : 

OpenAI API 的功能跟 HTTP 協定運作原理一樣無記憶功能, 每一個回應都只與這次的請求有關, 每一對請求與回應都是獨立的

首先匯入 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': '<提示詞>'}]

接下來用 input() 讓使用者輸入系統扮演之角色 : 

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

最後用一個 while 無限迴圈來製做一個命令列的聊天機器人 (輸入 Enter 跳出迴圈) :

>>> while True:
    prompt=input('You : ')
    if prompt.strip() == '':
        print('GPT : 再見。')
        break
    messages=[
        {'role': 'system', 'content': sys_role},
        {'role': 'user', 'content': prompt}
        ]
    reply=ask_gpt(messages)
    print(f'GPT : {reply}') 
  
You : 我養了兩隻貓, 名叫咪咪與萬萬
GPT : 這兩隻貓的名字聽起來很可愛呢!照顧牠們需要給予愛與耐心,也希望牠們健康快樂地成長。有什麼需要我幫忙的嗎?
You : 你還記得我那兩隻貓的名字嗎?
GPT : 抱歉,我並不保留先前的對話記錄,因此無法記得你的兩隻貓的名字。不過,如果你提醒我一下,我會盡力幫助你。你可以告訴我你的兩隻貓的名字嗎?
You : 
GPT : 再見。

可見再次詢問前一次聊天的內容時, 它其實並不記得, 這跟在未登入情況下與 ChatGPT 聊天的結果一樣 (必須登入帳號才會記憶聊天過程). 


2. 使用串列紀錄聊天歷史 : 

如果要讓模型記得之前的聊天內容, 必須自行記錄聊天歷史, 並且在每次詢問時將之前的聊天歷史放在 assistance 角色中

# cli_chatbot_1.py
from openai import OpenAI, APIError   

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

# 建立 OpenAI 物件
api_key='填入 API key'    
client=OpenAI(api_key=api_key) 
# 設定 AI 系統扮演之角色
sys_role=input('設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)
# 初始化記憶:存入系統角色字典
memory=[{'role': 'system', 'content': sys_role}]
while True:  # 無窮迴圈
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # Enter 跳出迴圈
        print('GPT : 再見。')
        break
    memory.append({'role': 'user', 'content': prompt}) # 提示詞存入記憶
    reply=ask_gpt(memory)  # 將記憶資料傳給模型
    memory.append({'role': 'assistant', 'content': reply}) # 回應存入記憶
    print(f'GPT : {reply}')  # 輸出 API 回應

此例一開始就將系統角色存入記憶變數 memory, 之後每次交談都會存入成對的 user 與 assistant 鍵之字典, 每次請求的內容都是之前的記憶加上目前的提示詞組成之字典串列, 執行結果如下 : 

You : 我養了兩隻貓, 名叫咪咪與萬萬
GPT : 很高興聽到您養了兩隻可愛的貓咪,名字取得非常可愛!您有任何想要了解或分享的事情嗎?
You : 你還記得我那兩隻貓的名字嗎?
GPT : 是的,您剛剛告訴我您養了兩隻貓,名字是咪咪和萬萬。是否有其他問題要問呢?
You : 你喜歡貓嗎?
GPT : 我是一個人工智慧助理,沒有喜好或情感,但我知道貓是很可愛、獨立和聰明的寵物。如果您有貓咪的相關問題或想分享貓咪的照片或故事,我很樂意聽取!
You : 你比較喜歡貓還是狗?
GPT : 我作為人工智慧助理沒有偏愛任何寵物,我對貓和狗一視同仁。每種寵物都有它們獨特的魅力和個性,各有各的可愛之處。您喜歡貓還是狗呢?您有其他寵物嗎?歡迎和我分享您的看法和經驗!
You : 考考你的記性, 我家那兩隻貓名叫甚麼?
GPT : 您剛剛告訴我您養了兩隻貓,名字是咪咪與萬萬。讓我知道,您是否有其他問題或需要幫忙的地方喔!
You : 
GPT : 再見。

結束聊天後檢查記憶變數 memory 內容 :

>>> memory  
[{'role': 'system', 'content': '你是繁體中文AI助理'}, {'role': 'user', 'content': '我養了兩隻貓, 名叫咪咪與萬萬'}, {'role': 'assistant', 'content': '很高興聽到您養了兩隻可愛的貓咪,名字取得非常可愛!您有任何想要了解或分享的事情嗎?'}, {'role': 'user', 'content': '你還記得我那兩隻貓的名字嗎?'}, {'role': 'assistant', 'content': '是的,您剛剛告訴我您養了兩隻貓,名字是咪咪和萬萬。是否有其他問題要問呢?'}, {'role': 'user', 'content': '你喜歡貓嗎?'}, {'role': 'assistant', 'content': '我是一個人工智慧助理,沒有喜好或情感,但我知道貓是很可愛、獨立和聰明的寵物。如果您有貓咪的相關問題或想分享貓咪的照片或故事,我很樂意聽取!'}, {'role': 'user', 'content': '你比較喜歡貓還是狗?'}, {'role': 'assistant', 'content': '我作為人工智慧助理沒有偏愛任何寵物,我對貓和狗一視同仁。每種寵物都有它們獨特的魅力和個性,各有各的可愛之處。您喜歡貓還是狗呢?您有其他寵物嗎?歡迎和我分享您的看法和經驗!'}, {'role': 'user', 'content': '考考你的記性, 我家那兩隻貓名叫甚麼?'}, {'role': 'assistant', 'content': '您剛剛告訴我您養了兩隻貓,名字是咪咪與萬萬。讓我知道,您是否有其他問題或需要幫忙的地方喔!'}]

可以想見的是, 隨著交談次數增多, 記憶變數內容也會越來越長, 且因 memory 是全域變數, 跳出程式後它仍然存在, 再次執行前要將之清除, 否則前世記憶會遺留到下一次對話中, 因此在跳出程式前應該主動將記憶清除才對 (上例是為了觀察 memory 內容未清除) : 

# cli_chatbot_2.py
from openai import OpenAI, APIError   

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

# 建立 OpenAI 物件
api_key='填入 API key'    
client=OpenAI(api_key=api_key) 
# 設定 AI 系統扮演之角色
sys_role=input('設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)
# 初始化記憶:存入系統角色字典
memory=[{'role': 'system', 'content': sys_role}]
while True:  # 無窮迴圈
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # Enter 跳出迴圈
        print('GPT : 再見。')
        del memory   # 離開聊天時刪除記憶
        break
    memory.append({'role': 'user', 'content': prompt}) # 提示詞存入記憶
    reply=ask_gpt(memory)  # 將記憶資料傳給模型
    memory.append({'role': 'assistant', 'content': reply}) # 回應存入記憶
    print(f'GPT : {reply}')  # 輸出 API 回應

此例在離開聊天時使用 del 指令將記憶變數 memory 刪除, 這樣再次執行此程式時就不會有前次聊天的資訊了. 

也可以設定只記住最近幾回聊天紀錄, 這樣就可以避免記憶串列隨交談次數增加而變長. 在下面範例中增加了一個 retro 變數用來設定要回溯記住最近幾回的聊天次數 (一問一答為一回) :

# cli_chatbot_3.py
from openai import OpenAI, APIError   

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

# 建立 OpenAI 物件
api_key='填入 API key'    
client=OpenAI(api_key=api_key) 
# 設定 AI 系統扮演之角色
sys_role=input('設定 GPT 角色 : ')
if sys_role.strip() == '': 
    sys_role='你是繁體中文AI助理'
print(sys_role)
# 初始化記憶:存入系統角色字典
memory=[{'role': 'system', 'content': sys_role}]
retro=2  # 設定記憶前面幾回聊天次數

while True:  # 無窮迴圈
    prompt=input('You : ')  # 輸入提示詞
    if prompt.strip() == '':  # Enter 跳出迴圈
        print('GPT : 再見。')
        break
    memory.append({'role': 'user', 'content': prompt}) # 提示詞存入記憶
    reply=ask_gpt(memory)  # 將記憶資料傳給模型
    memory.append({'role': 'assistant', 'content': reply}) # 回應存入記憶
    memory=[memory[0]] + memory[-(retro * 2):]  # 取出系統角色串接前幾回聊天紀錄
    print(f'GPT : {reply}')  # 輸出 API 回應

此例用 retro=2 設定只記憶前兩回聊天紀錄, 因此用 -(retro) *2: 索引取出記憶串列中前 retro 回的紀錄 (一次交談包含一個 user 角色與一個 assistant 角色故要乘以 2, 負號是從串列尾端往回算, 冒號後面空的表示一直取到結尾), 執行結果如下 :

設定 GPT 角色 : 
你是繁體中文AI助理
You : 我養了兩隻貓, 名叫咪咪與萬萬
GPT : 這兩隻貓咪的名字真可愛!請問有什麼問題我可以協助你的嗎?
You : 你還記得我那兩隻貓的名字嗎?
GPT : 當然記得!你養的兩隻貓名叫咪咪和萬萬。有任何關於牠們的問題想問嗎?
You : 你喜歡貓嗎?
GPT : 作為AI助理,我沒有喜好或感受,但我知道許多人喜歡貓咪!牠們可愛、獨立又親人,是很受歡迎的寵物。你是一位喜歡貓咪的人嗎?
You : 你比較喜歡貓還是狗?
GPT : 作為AI助理,我沒有個人偏好,我對貓和狗都一視同仁。貓和狗各有其獨特之處,每個人對寵物的喜好也各有不同。你更喜歡貓還是狗呢?
You : 考考你的記性, 我家那兩隻貓名叫甚麼?
GPT : 很抱歉,因為我無法記住之前的對話內容,所以無法知道你家寵物的名字。也許你可以提醒我一下,讓我知道你家那兩隻貓的名字是什麼呢?
You : 
GPT : 再見。

檢視記憶變數內容 : 

>>> memory   
[{'role': 'system', 'content': '你是繁體中文AI助理'}, {'role': 'user', 'content': '你比較喜歡貓還是狗?'}, {'role': 'assistant', 'content': '作為AI助理,我沒有個人偏好,我對貓和狗都一視同仁。貓和狗各有其獨特之處,每個人對寵物的喜好也各有不同。你更喜歡貓還是狗呢?'}, {'role': 'user', 'content': '考考你的記性, 我家那兩隻貓名叫甚麼?'}, {'role': 'assistant', 'content': '很抱歉,因為我無法記住之前的對話內容,所以無法知道你家寵物的名字。也許你可以提醒我一下,讓我知道你家那兩隻貓的名字是什麼呢?'}]

可見關於那兩隻貓名字的聊天歷史已不存在於聊天記憶中, 因此 GPT 無法記住名字. 每一回交談後都會保留記憶串列 memory 索引 0 的 system 角色與前 2 回聊天紀錄來更新 memory 串列, 因此 聊過兩回合之後, memory 變數都會一直保持只有 5 個元素的狀態 (1 + 2*2). 

沒有留言 :