在上一篇關於 Microsoft Agent Framework 系列教學文章 "MFA005 使用自訂 DelegatingHandler 查看通訊內容" 中,我們學會了如何使用自訂的 DelegatingHandler 來記錄每一次 HTTP 請求與回應的內容,從而觀察底層的通訊細節。在這篇 MAF 教學文章中,我們將學習如何在多回合對話中,讓 AI 代理記住之前的對話內容,從而實現更自然的對話體驗。為什麼需要說明多回合對話呢?這是因為在大語言模型的原始設計架構中,就是為了單一回合對話而設計的,當使用者送入一份提示詞之後,模型只會根據這一次的輸入生成回應,而不會記住之前的對話內容。這樣的設計雖然簡單,但在實際應用中,往往需要 AI 代理能夠記住對話歷史,從而提供更連貫的回應。
在真實環境應用中,這些系統是需要能夠記憶住之前對話的內容,如此便可以降低幻覺產生更大的機率,舉的例子來說,在一個使用AI為基礎的客服系統中,如果代理能夠記住使用者之前的問題與回應,那麼在後續的對話中,代理就能提供更精準的建議與解答,而不會因為每次都是單一回合對話而遺忘之前的上下文。若是系統忘記了之前對話的內容,將會造成使用者需要重複提供相同的資訊,降低使用者體驗,並可能導致錯誤的建議或解答。不過,若要記住之前所有的詢問與回答內容,將會面臨到記憶空間的問題,對於大語言模型而言,這是一個需要平衡的挑戰。在大語言模型中,每一輪對話將會受限於大語言模型的上下文長度(context length),也就是模型在生成回應時,能夠考慮的歷史對話內容的最大範圍。當對話歷史超過這個限制時,模型將無法記住更早的對話內容,因此在設計多回合對話系統時,需要考慮如何有效管理和利用對話歷史。
有了這樣的背景說明之後,在這個練習專案內,將會示範如何建立一個能夠記住多回合對話歷史的 AI 代理,從而實現更自然的對話體驗。這裡將會進行兩輪的對話,第一個使用沒有多回合記憶的 AI 代理,第二個使用具備多回合記憶的 AI 代理,從而比較兩者在對話連貫性上的差異。
接下來看看如何做到這樣的效果。
- 開啟 Visual Studio 2026
- 選擇「建立新專案」
- 在 [建立新的專案] 視窗中,在右方清單內,找到並選擇「主控台應用程式」 項目
- 然後點擊右下方「下一步」按鈕
- 此時將會看到 [設定新的專案] 對話窗
- 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csConversationAgent"
- 然後點擊右下方「下一步」按鈕
- 接著會看到 [其他資訊] 對話窗
- 在這個對話窗內,確認使用底下的選項
- 架構:.NET 10.0 (或更新版本)
- 勾選 不要使用最上層陳述式 (這是我的個人習慣)
- 然後點擊右下方「建立」按鈕
- 現在,已經完成了這個 Console 主控台 專案的建立
要完成這個第一次接觸 Microsoft Agent Framework 練習專案,需要安裝以下幾個 NuGet 套件:
這個套件將會為生成式 AI 定義的一層「統一抽象介面 + Middleware 管線」,讓開發者可以更方便地整合不同的生成式 AI 模型,並且在應用程式中使用一致的方式進行呼叫與管理。
- 滑鼠右擊 [csConversationAgent] 專案節點
- 點選彈出功能表的 [管理 NuGet 套件] 項
- 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
- Microsoft.Extensions.AI
這個套件是微軟官方為 .NET 整合 AI 服務所提供的擴充套件。它實現了 IChatClient 與 IEmbeddingGenerator 核心抽象介面,讓開發人員能以一致的程式碼,輕鬆串接 OpenAI 或相容的 API 端點(例如 Azure OpenAI、GitHub Models 等)。
- 滑鼠右擊 [csConversationAgent] 專案節點
- 點選彈出功能表的 [管理 NuGet 套件] 項
- 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
- Microsoft.Extensions.AI.OpenAI
這個套件是微軟官方推出 Microsoft Agent Framework(微軟智慧代理框架)的核心 .NET 套件。它主要用於在 .NET 環境中建構、協調與部署 AI 代理(AI Agents) 及多代理工作流(Multi-agent workflows),是用來取代舊版 AutoGen 的企業級解決方案。
- 滑鼠右擊 [csConversationAgent] 專案節點
- 點選彈出功能表的 [管理 NuGet 套件] 項
- 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
- Microsoft.Agents.AI
這個套件為官方的 OpenAI .NET 客戶端類別庫,是 OpenAI NuGet 套件。此套件由 OpenAI 與 Microsoft 合作開發,提供直接存取 OpenAI 官方 REST API 的便利管道。
- 滑鼠右擊 [csConversationAgent] 專案節點
- 點選彈出功能表的 [管理 NuGet 套件] 項
- 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
- OpenAI
關於這個小節的內容,將會在這個系列每篇文章中都會用到,因此,可以參考 MFA001 Microsoft Agent Framework 初次體驗開發出 AI 代理 文章中的 [宣告 API 端點與 Key 環境變數] 說明內容,若這裡提到的環境變數已經建立好了,則可以跳過這個小節內容。
為了要完成這個需求 : 如何用 Azure OpenAI 的 gpt-5.6-luna 聊天模型建立一個簡單 AI 代理,並產生一篇與鵝有關的詩。 因此,我們需要修改 Program.cs 程式碼,這時需要完成底下的操作。
- 在專案內,找到並且開啟 [Program.cs] 檔案
- 使用底下程式碼內容來取代 [Program.cs] 檔案的內容
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.Text;
namespace csConversationAgent;
internal class Program
{
private const string Turn1 = "我叫 Vulcan,我養了三隻鵝,分別叫大白、小白和阿花。";
private const string Turn2 = "我養了幾隻鵝?牠們叫什麼名字?";
static async Task Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
var apiKey = Environment.GetEnvironmentVariable("AzureOpenAI_Key");
var endpoint = Environment.GetEnvironmentVariable("AzureOpenAI_Endpoint");
var model = "gpt-5.6-luna";
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(endpoint))
{
Console.WriteLine("請先設定環境變數 AzureOpenAI_Key 與 AzureOpenAI_Endpoint 後再執行。");
return;
}
IChatClient chatClient =
new ChatClient(
model,
new ApiKeyCredential(apiKey),
new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.AsIChatClient();
AIAgent agent = new ChatClientAgent(
chatClient, // 聊天用戶端
"你是一位友善的助理,請用繁體中文簡短回答。", // 系統提示詞 instructions
"助理", // 代理名稱 name
null); // 代理描述 description
Console.WriteLine($"{DateTime.Now} 使用的模型: {model}");
await RunWithoutSessionAsync(agent); // 對照組:會失憶
await RunWithSessionAsync(agent); // 正解:記得
}
// ────────────────────────────────────────────────────────────────
// 執行 1:不傳 session —— 代理會失憶
// ────────────────────────────────────────────────────────────────
private static async Task RunWithoutSessionAsync(AIAgent agent)
{
Header("執行 1 / 不傳 session —— 代理會失憶");
Say("你", Turn1);
var answer1 = await agent.RunAsync(Turn1);
Say("助理", answer1.Text);
Say("你", Turn2);
var answer2 = await agent.RunAsync(Turn2);
Say("助理", answer2.Text);
}
// ────────────────────────────────────────────────────────────────
// 執行 2:兩輪共用同一個 session —— 代理記得
// ────────────────────────────────────────────────────────────────
private static async Task RunWithSessionAsync(AIAgent agent)
{
Header("執行 2 / 共用同一個 session —— 代理記得");
var session = await agent.CreateSessionAsync();
Say("你", Turn1);
var answer1 = await agent.RunAsync(Turn1, session);
Say("助理", answer1.Text);
Say("你", Turn2);
var answer2 = await agent.RunAsync(Turn2, session);
Say("助理", answer2.Text);
Console.WriteLine();
Console.WriteLine("【session 裡實際存放的對話歷史】");
if (session.TryGetInMemoryChatHistory(out var history) && history is not null)
{
foreach (var message in history)
{
Say(message.Role.ToString(), message.Text);
}
}
else
{
Console.WriteLine(" 這個 session 沒有把歷史存在記憶體(例如改由服務端保存時)。");
}
}
private static void Header(string title)
{
Console.WriteLine();
Console.WriteLine(new string('=', 74));
Console.WriteLine($" {title}");
Console.WriteLine(new string('=', 74));
}
private static void Say(string who, string? text)
{
Console.WriteLine();
Console.WriteLine($" {who} ▸ {text}");
}
}這個程式一開始將會讀取 API Key 與服務端點的數據,接著如同前面的範例相同,先建立一個 IChatClient 物件,接者建立一個 AIAgent 物件,並使用它來與模型進行互動。在建立 AIAgent 物件時,使用的系統提示詞為 "你是一位友善的助理,請用繁體中文簡短回答。"
首先將會來體驗沒有記憶的多回合對話,這裡將會呼叫 await RunWithoutSessionAsync(agent); 來執行第一輪對話,觀察代理在沒有 session 的情況下是否能記住之前的對話內容。
在這個 RunWithoutSessionAsync 方法內,使用不同的提示詞,呼叫了兩次 RunAsync 方法來與代理互動,但由於沒有傳入 session,因此代理不會記住之前的對話內容,每次都是獨立的回合。
Say("你", Turn1);
var answer1 = await agent.RunAsync(Turn1);
Say("助理", answer1.Text);
Say("你", Turn2);
var answer2 = await agent.RunAsync(Turn2);
Say("助理", answer2.Text);由於 LLM 產生的結果是隨機的,因此,底下為可能的輸出範例,實際結果可能會有所不同。
==========================================================================
執行 1 / 不傳 session —— 代理會失憶
==========================================================================
你 ▸ 我叫 Vulcan,我養了三隻鵝,分別叫大白、小白和阿花。
助理 ▸ 你好,Vulcan!你養了三隻鵝,分別是大白、小白和阿花,真可愛!
你 ▸ 我養了幾隻鵝?牠們叫什麼名字?
助理 ▸ 我目前不知道你養了幾隻鵝,也不知道牠們的名字。
從這裡的可以看到,在第一輪對話中,使用者說明自己的名字與養的三隻鵝的名字,但由於代理沒有記憶,因此在第二個回合中 這裡送出提示詞 "我養了幾隻鵝?牠們叫什麼名字?",代理無法回答使用者的問題,這裡看到LLM回答的是 "我目前不知道你養了幾隻鵝,也不知道牠們的名字。" ,一般人看到這樣結果,都會覺得 AI 患了失憶症。開始在胡說八道。
接著,將會呼叫 await RunWithSessionAsync(agent); 來執行第二輪對話,在這個方法一開始,將會透過 var session = await agent.CreateSessionAsync(); 程式碼建立一個 session,之後的每一次與代理的互動都會傳入這個 session,從而讓代理能夠記住之前的對話歷史。接著將剛剛 RunWithoutSessionAsync 方法內同樣的呼叫 AIAgent 物件的方法,都在執行一次,但這次會傳入 session,這樣可以看看會有甚麼變化,這裡用的程式碼如下:
Say("你", Turn1);
var answer1 = await agent.RunAsync(Turn1, session);
Say("助理", answer1.Text);
Say("你", Turn2);
var answer2 = await agent.RunAsync(Turn2, session);
Say("助理", answer2.Text);底下為執行結果範例
==========================================================================
執行 2 / 共用同一個 session —— 代理記得
==========================================================================
你 ▸ 我叫 Vulcan,我養了三隻鵝,分別叫大白、小白和阿花。
助理 ▸ 你好,Vulcan!大白、小白和阿花,三隻鵝的名字都很可愛呢。
你 ▸ 我養了幾隻鵝?牠們叫什麼名字?
助理 ▸ 你養了三隻鵝,牠們叫大白、小白和阿花。
【session 裡實際存放的對話歷史】
user ▸ 我叫 Vulcan,我養了三隻鵝,分別叫大白、小白和阿花。
assistant ▸ 你好,Vulcan!大白、小白和阿花,三隻鵝的名字都很可愛呢。
user ▸ 我養了幾隻鵝?牠們叫什麼名字?
assistant ▸ 你養了三隻鵝,牠們叫大白、小白和阿花。
在這個文字 "【session 裡實際存放的對話歷史" 之前,可以看到這兩輪對話,AI是有記住之前的對話內容的,因此能夠在第二個回合中正確回答使用者的問題。他回答的是 : "你養了三隻鵝,牠們叫大白、小白和阿花。" 這樣回答的結果是正確的,這也說明了在共用同一個 session 的情況下,代理能夠保持對話的連貫性。
在這個文字 "【session 裡實際存放的對話歷史】" 之後,可以看到代理是如何將使用者與助理的對話歷史存放在 session 中的。這也說明了在共用同一個 session 的情況下,代理能夠保持對話的連貫性。
其實,在這個程式碼的背後運作方式為,在第二輪對話的時候,AIAgent 不只送出 "我養了幾隻鵝?牠們叫什麼名字?" 這個提示詞,還會將整個 session 中的對話歷史一併送出給 LLM,這樣 LLM 才能根據之前的對話內容,給出連貫且正確的回應。

沒有留言:
張貼留言