2025年10月27日 星期一

Google Gemini API 學習筆記 (二) : 有記憶的聊天機器人

以前曾對 Gemini 做過初步文字聊天測試, 但後來主要重心放在 OpenAI API 的串接測試, 所以就沒繼續做 Gemini 的測試, 參考 : 


前陣子在 render.com 上佈署的 serverless 平台撰寫了一個 LINE Bot 聊天機器人程式 linebot_gemini 來串接 Gemini, 參考 : 


但這兩個測試中的每個對話都是獨立無記憶的, 如果要讓聊天機器人記得前後上下文對話脈絡 (即保留前幾輪對話), 必須自行管理對話記憶 (模型本身無記憶). 


1. 使用串列管理對話記憶 : 

使用串列來對話紀錄是最直覺的方法,  首先匯入 google.generativeai 模型的 API 套件與 dotenv 套件來從環境變數檔 .env 讀取 Gemini 金鑰 : 

>>> import google.generativeai as genai   
>>> from dotenv import dotenv_values   

讀取 Gemini 金鑰 : 

>>> config=dotenv_values('.env')   
>>> api_key=config.get('GEMINI_API_KEY')   

然後自訂一個 GeminiChat 類別來串接 Gemini API : 

>>> class GeminiChat: 
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20):
        genai.configure(api_key=api_key)
        self.model=genai.GenerativeModel(model)
        self.max_history=max_history
        self.history=[]  # 用 list 儲存 (user, reply) 對話
    def ask(self, prompt):
        # 整理上下文(最近 N 輪對話)
        context=''  # 初始值
        for user_msg, ai_msg in self.history[-self.max_history:]:  # 拜訪記憶串列
            context += f'使用者:{user_msg}\n助理:{ai_msg}\n'  # 串接對話記憶為字串
        # 組成最終 prompt
        full_prompt=f'{context}使用者:{prompt}\n助理:'
        # 發送給 Gemini (同時驗證金鑰)
        response=self.model.generate_content(full_prompt)
        reply=response.text.strip()  # 去除左右空格
        # 將 (提示詞, 回應) 放入記憶串列
        self.history.append((prompt, reply))  
        # 限制記憶長度 : 刪除記憶串列中的舊對話, 只留最後 max_history 個對話
        if len(self.history) > self.max_history:
            self.history=self.history[-self.max_history:] 
        return reply

在此自訂類別的初始化函式 __init__() 中, 我們先呼叫 genai.configure() 來將傳入的 API Key 存進 SDK 的全域設定裡, 這樣之後所有透過 genai.GenerativeModel, genai.list_models(), model.generate_content() 等方法發出的 API 請求都會自動帶上這個 API Key (只有呼叫這些方法時才會驗證金鑰是否有效). 然後呼叫 genai.GenerativeModel() 建構式建立模型物件, 用來與指定 之 Gemini 模型互動. 最後定義 history 與 max_history 屬性來記錄與管控對話記憶, history 是一個串列, 用來儲存對話紀錄; max_history 則是一個整數, 用來設定記憶長度, 預設是 20 個對話. 

接著定義一個 ask() 方法來處理對話與管理記憶, 每次詢問都會從記憶串列重新組成上下文字串, 串接目前提問後組成完整之提問上下文, 然後呼叫 Gemini 模型物件的 generate_content() 方法生成回應, 測試如下 : 

>>> print(chat.ask('你好'))  
你好!有什么我能帮助你的吗?
>>> print(chat.ask('我叫 Tony'))    
很高兴认识你,Tony!有什么我能为你做的吗?
>>> print(chat.ask('我叫什麼名字?'))     
你叫 Tony。
>>> print(chat.ask('我有兩隻貓, 名叫萬萬與小咪'))  
好的,Tony。我記下了你有兩隻貓,名叫萬萬和小咪。還有什麼我能為你做的嗎?
>>> print(chat.ask('你是誰?'))  
我是一个大型语言模型,由 Google 训练。
>>> print(chat.ask('我有幾隻貓? 名叫甚麼?'))     
你有兩隻貓,名叫萬萬和小咪。

可見模型透過記憶串列中的脈絡得知上下文資訊, 故能回答正確答案. 

完整程式碼如下 : 

# gemini_chat_memory_1.py
import google.generativeai as genai
from dotenv import dotenv_values

class GeminiChat:
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20):
        genai.configure(api_key=api_key)
        self.model=genai.GenerativeModel(model)
        self.max_history=max_history
        self.history=[]  # 用 list 儲存 (user, reply) 對話
    def ask(self, prompt):
        # 整理上下文(最近 N 輪對話)
        context=''  # 初始值
        for user_msg, ai_msg in self.history[-self.max_history:]: 拜訪記憶串列
            context += f'使用者:{user_msg}\n助理:{ai_msg}\n'  # 串接對話記憶為字串
        # 組成最終 prompt
        full_prompt=f'{context}使用者:{prompt}\n助理:'
        # 發送給 Gemini (同時驗證金鑰)
        response=self.model.generate_content(full_prompt)
        reply=response.text.strip()  # 去除左右空格
        # 將 (提示詞, 回應) 放入記憶串列
        self.history.append((prompt, reply))  
        # 限制記憶長度:刪除記憶串列中的舊對話, 只留最後 max_history 個對話
        if len(self.history) > self.max_history:
            self.history=self.history[-self.max_history:]  
        return reply

if __name__ == '__main__':
    config=dotenv_values('.env')
    api_key=config.get('GEMINI_API_KEY')
    chat=GeminiChat(api_key, max_history=5)
    print(chat.ask('你好'))
    print(chat.ask('請記住我叫 Tony'))
    print(chat.ask('我叫什麼名字?'))


2. 使用 ChatSession 管理對話記憶 : 

除了使用串列自行管理對話記憶, Gemini 官方文件建議使用 genai.GenerativeModel 類別實例的 start_chat() 方法建立一個有上下文的 ChatSession 對話物件來達成同樣目的, 程式碼如下 :

# gemini_chat_memory_2.py
import google.generativeai as genai
from dotenv import dotenv_values

class GeminiChat:
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20):
        genai.configure(api_key=api_key)
        self.model=genai.GenerativeModel(model)
        self.max_history=max_history
        self.chat=self.model.start_chat(history=[])  # 初始化空歷史紀錄
    def ask(self, prompt):
        # 若超過 max_history 則移除最舊的訊息
        if len(self.chat.history) > self.max_history * 2:
            # 限制歷史對話長度 :每輪對話有 user+model 各一筆故要乘以 2
            self.chat.history=self.chat.history[-self.max_history * 2:]
        response=self.chat.send_message(prompt)
        return response.text

if __name__ == "__main__":
    config=dotenv_values('.env')
    api_key=config.get('GEMINI_API_KEY')
    chat=GeminiChat(api_key, max_history=20)
    print(chat.ask('你好'))
    print(chat.ask('請記住我叫 Tony'))
    print(chat.ask('我叫什麼名字?'))

首先在 GeminiChat 類別初始化時, 呼叫 GenerativeModel 物件的 start_session() 方法並傳入一個 history 參數 (預設空串列) 來建立一個 ChatSession 對話物件, 所以此物件實際上也是使用串列來記錄對話歷史, 然後將此 ChatSession 儲存在 GeminiChat 物件的 chat 屬性裡, 這樣便能自動記錄對話歷史, 在 ask() 方法中只要管理對話歷史的長度即可, 毋須像上例那樣手動串接上下文. 測試如下 : 

>>> %Run gemini_chat_memory_2.py
你好!有什么可以帮助你的吗?
好的,我記住了。你叫 Tony。

很高興認識你!
你叫 Tony。  

可見效果與上例相同, 但程式碼更簡潔. 


3. 加入系統提示 system_instruction : 

從上面範例可知, 即使 prompt 是繁體中文, Gemini 的回應幾乎都是用殘體中文回應, 如果要強制它用正體中文回應, 可在呼叫 GenerativeModel() 建構式時傳入系統提示詞參數 system_instruction 解決 :

# gemini_chat_memory_3.py
import google.generativeai as genai
from dotenv import dotenv_values

class GeminiChat:
    def __init__(self, api_key, model='gemini-2.5-flash', max_history=20,
                 system_instruction=None):
        genai.configure(api_key=api_key)
        # 在建立模型時傳入 system_instruction
        self.model=genai.GenerativeModel(
            model,
            system_instruction=system_instruction
            )
        self.max_history=max_history
        self.chat=self.model.start_chat(history=[])
    def ask(self, prompt):
        # 限制歷史紀錄長度(每輪 user+model 各一筆)
        if len(self.chat.history) > self.max_history * 2:
            self.chat.history=self.chat.history[-self.max_history * 2:]
        response=self.chat.send_message(prompt)
        return response.text

if __name__ == '__main__':
    config=dotenv_values('.env')
    api_key=config.get('GEMINI_API_KEY')
    # 加上系統提示
    system_instruction='你是一個繁體中文AI助理,請以台灣人的習慣用語回答。'
    chat=GeminiChat(api_key, max_history=20, system_instruction=system_instruction)
    print(chat.ask('你好'))
    print(chat.ask('請記住我叫 Tony'))
    print(chat.ask('我叫什麼名字?'))

測試結果確實能生成繁體中文回應 : 

>>> %Run gemini_chat_memory_3.py   
哈囉,你好!有什麼需要我幫忙的嗎?
好的,Tony 我記住了!之後就叫你 Tony 囉。

有什麼需要我幫忙的嗎?
你叫 Tony 呀!

沒有留言 :