做個有記憶力的 AI 機器人,實作對話記憶
# 使用生成式 AI 結合 RAG 技術實做屬於自己的 LLM 知識庫系列文章
前情提要
這篇文章是【Hello Gemini,串接第一個 Gemini 生成式 AI】的延伸篇,在完成上一篇的設定後,我們已經成功透過程式呼叫 Google Gemini,串起了第一個生成式 AI 的小程式了。
但,事情並不總是這麼順利... 請看下圖的情境,我一開始問了一個問題,而它也正確的回答,接著再問「那他的背景是什麼」,結果 Gemini 居然回了一個讓人摸不著頭緒回答,不知道它是害怕想起來還是擔心對岸會對它不利 XD,它似乎完全忘了我們上一個問題在問什麼了。
你可能會有疑問?我明明在 ChatGPT 或 Gemini 使用者介面都用的好好的,怎麼會串個 API 就不知道我第一個問題是什麼呢?難道我的 API 版本是金魚腦比較笨!!其實,這是因為像 ChatGPT 或 Gemini 這類平台的使用者介面,內建了「對話記憶」機制來解決這個問題。
也就是說,這些平台它會自動把你之前說過的話保留下來,讓模型能理解整段對話的上下文,回應自然又有連貫性,就像是跟朋友說話一樣自然。當然,這種記憶也是有限度的(畢竟模型的上下文長度是有限的,因此記憶也是有限的,這部分我們之後有機會可以再深入介紹),不過至少最近幾輪的對話它都還能記住。
但當你透過 API 來串接 Gemini 或 ChatGPT 時,模型並不會主動幫你記住對話的上下文,你得自己把對話的歷史維護好,並在每一次請求時都一併傳送過去,因此如同這篇的主題「做個有記憶力的 AI 機器人」,這篇將會示範如何實作基本的對話記憶機制。
先用 API 看懂請求與回應
照慣例,在開始實作記憶功能之前,我們先不使用任何套件,直接透過 API 來與 Gemini 對話,這邊我會先使用 Postman 來手動發送請求,觀察請求與回應的資料格式,搞清楚模型實際需要什麼樣的輸入,以及會回應什麼樣的輸出。
畢竟,了解這些資料結構之後,後續我們在使用 Semantic Kernel 來整合時,才更能掌握整個流程,知道每一層背後實際發生了什麼事,在本文範例中,我們將使用 Gemini API 當作範例來,讓我們一步一步開始吧。
提出第一個問題
當使用者提出第一個問題時,請求的內容必須清楚地標示出訊息的角色。也就是說,我們要明確告訴 API,這句話是誰說的?是系統的設定語句?是使用者的問題?還是模型先前的回應?
在 Gemini 或 ChatGPT 的 API 結構中,每一則對話訊息都會有一個 role 欄位,常見的二個角色如下:
- user:使用者提出的問題 。
- system:AI 模型的回應,在 Gimini 中該角色則是使用 model。
另外在某些情況下,為了要限制 GPT 的語氣、身份,或是回答的風格,我們也可以指定系統指令,這個通常是由應用程式在背後自動補上的,但這牽涉到 Prompt Engineering(提示工程) 的技巧,後續我們會再另外說明。
這邊你只需要先有個概念,system_instruction 不是必填欄位,可加也可不加,實務上是否使用,取決於你的應用場景與需求,更多參數請自行參考官方提供的系統指令與設定。
因此在送出第一個問題時,你的請求內容可能會長的類似下面的結構:
{
"system_instruction": {
"parts": [
{
"text": "你是一位專業政治新聞助理,請用清楚簡潔的方式回答問題。"
}
]
},
"contents": [
{
"role": "user",
"parts": [
{
"text": "請問台灣總統是誰"
}
]
}
]
}
觀察第一個回應
當送出第一個請求後,Gemini 會回傳一段 JSON 結構的回應資料,而我們主要先把重點放在回應內容的部分,其它欄位之後有機會再說明,格式大致如下:
{
"candidates": [
{
"content": {
"parts": [
{
"text": "台灣總統是蔡英文。\n"
}
],
"role": "model"
}
}
]
}
Gemini 收到我們的請求後,會回應 台灣總統是蔡英文, 這是因為 Gemini 的訓練資料可能停留在某個特定時間點,而當時的總統是蔡英文(不是它搞錯了,只是它活在過去 XD),我們要把這個回應記錄下來。
啟動第二輪對話
因為我們即將提出下一個問題,而這時候模型並不知道上一輪你問了什麼,也不記得它自己講過什麼內容。為了讓模型「有記憶」。
所以我們要做的事情是:把上一句模型的回應也一併帶入下一次的請求中,並且指定它的角色是 "model",這樣 Gemini 才會知道「剛剛是我自己說了這句話」,才能理解使用者現在接著問的內容是延續上文的,然後,再加入新的問題,角色是 "user",請求內容可能會長的類似下面的結構:
{
"system_instruction":
{
"parts":
[
{ "text": "你是一位專業政治新聞助理,請用清楚簡潔的方式回答問題。"}
]
},
"contents":
[
{ "role": "user", "parts": [ { "text": "請問台灣總統是誰" } ] },
{ "role": "model", "parts": [ { "text": "台灣總統是蔡英文。" } ] },
{ "role": "user", "parts": [ { "text": "那請問她的背景是什麼?" } ] }
]
}
關於後續的對話
如果你要進行後續對話,就只需要持續「重複第二輪對話階段」的循環,把上一輪的使用者問題與模型回應一起帶進下一輪請求中,讓模型知道這是一段有來有往的對話。
但記憶其實也不是無限量的,每次請求都會佔用一定的 Token 數(對於 Gemini 模型,一個符號大約等於 4 個字元。100 個符記大約等於 60 到 80 個英文單字),而模型本身也有上下文長度的限制(不同模型有不同的 Max Token,詳細規格可自行至 API 說明文件查詢 ),當整段對話過長超出限制時,前面的內容就可能會被截斷或忽略,導致模型又變回金魚腦模式 🐟。
所以實務上,如果你的對話太長,可能需要自行做一些記憶壓縮、摘要處理,或只保留關鍵訊息來維持對話的流暢與準確性,這部分可以視你的應用需求再進一步設計。
Token 計算的小工具
ChatGPT 有提供一個官方網站,可以讓我們試算不同模型所計算出來的 Token 數量,像是 gpt-4o 或 gpt-3.5 等等,因此你只要輸入一段文字,它就會告訴你這段話會佔用多少 token,非常方便(如下圖所示),可惜目前我並沒有找到 Gemini 上面找到類似的工具。
![]() |
來源網站 : https://platform.openai.com/tokenizer |
開始實作吧
這篇文章花了稍微多一點時間帶大家熟悉 Gemini API 的請求與回應格式,接下來我們就要正式動手寫程式了。這次我們一樣使用 .NET 8,並搭配 Semantic Kernel 來幫我們管理聊天的上下文,同時也請先安裝好底下的套件。
<PackageReference Include="Microsoft.SemanticKernel" Version="1.54.0" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.54.0-alpha" />
然後來看看底下這段核心的對話程式碼,跟一般對話功能差不多,一樣是送出對話、接收回應,只是我們要額外自己維護對話歷史,在每一次呼叫 API 的時候,把整段過去的訊息都一併帶過去,因此下面的程式碼主要做了幾件事情:
- 記錄使用者的發言:使用 chatHistory.AddUserMessage(userInput),把這一輪的問題記下來,讓模型知道使用者現在問了什麼。
- 送出整段對話給模型:使用 chatService.GetChatMessageContentAsync(chatHistory),把目前累積的對話歷史全部送出,讓模型能理解上下文來產生連貫的回應。
- 接收並印出模型的回答:取得回應後輸出到 Console,同時也用 chatHistory.AddAssistantMessage(content) 把它加入歷史紀錄中,這樣下一輪的對話就能擁有先前的對話紀錄了。
private static async Task GetChatMessageContentAsync(IChatCompletionService chatService, ChatHistory chatHistory, string userInput)
{
// 輸出使用者輸入內容
Console.WriteLine("User: " + userInput);
chatHistory.AddUserMessage(userInput);
// 呼叫 Gemini 模型並取得回應
var response = await chatService.GetChatMessageContentAsync(chatHistory);
// 輸出 Gemini 回應的內容
var content = response.Content ?? "";
Console.WriteLine("Assistant: " + content);
chatHistory.AddAssistantMessage(content);
}
![]() |
本篇的目標 :打造一個「有記憶力」的 AI 聊天機器人 |
本文範例使用 NetCore 8,請至 HelloGeminiMemory 下載 。
礙於篇幅的關係,本系列規劃用底下幾篇文章來說明。
- 前言及流程規劃。
- 建置本地端 Ollama 服務及 LLM 知識庫所需的環境設置。
- 蝦咪系 Word Embedding?詞嵌入模型概念及實作。
- Hello Gemini,串接第一個 Gemini 生成式 AI。
- 做個有記憶力的 AI 機器人,實作對話記憶。
- 來跟 AI 玩玩角色扮演吧,提示工程(Prompt engineering)實作。
- 解決 AI 幻覺,讓 RAG 幫你吧。
- AI 也能認識你是誰喔,實作自定義 Function。
- 來吧,開始建立基於生成式 AI 的 KM 系統了。
- 番外篇 - 我想換個生成模型呢。
留言
張貼留言
您好,我是 Lawrence,這裡是我的開發筆記的網誌,如果你對我的文章有任何疑問或者有錯誤的話,歡迎留言讓我知道。