2026年11月4日 星期三

MFA013 - 對話壓縮

MFA013 - 對話壓縮

在上一篇關於 Microsoft Agent Framework 系列教學文章 "MFA012 - 自己寫中介層(Middleware)" 中,我們學會了使用 MAF 提供的中介層機制,能夠在 AI 代理的執行流程中,插入自訂的邏輯與控制。

當在進行多回合對話時,訊息會隨著回合數增加而累積,若不加以管理,將會有可能造成模型 Context Windows 超過大小的問題,又或者模型無法有效記住早期的對話內容,影響回答的準確性。因此,我們需要透過對話壓縮策略,來有效管理訊息的累積,保留關鍵資訊,同時降低模型的記憶負擔。然而,當在進行多回合對話時,如何選擇適當的壓縮策略,並確保模型仍能取得核心資訊,便成為一個重要的課題。

在完成這篇文章的練習,將會體驗三種作法,分別是完整保留、滑動視窗壓縮以及摘要壓縮。這三種做法各有其優缺點,開發者可以根據實際需求選擇最適合的策略。簡單說明這三種做法的差異與優缺點如下:

  • 完整保留:保留所有對話訊息,模型可以參考完整的對話內容,但容易造成 Context Windows 過大,增加計算成本。
  • 滑動視窗壓縮:只保留最近幾個回合的訊息,過去的訊息會被丟棄,能有效控制 Context Windows 大小,但模型可能遺忘早期的對話內容。
  • 摘要壓縮:將過去的對話內容進行摘要,保留核心資訊,既能控制 Context Windows 大小,又能讓模型參考歷史訊息,但摘要的品質可能影響模型的理解。

接下來看看如何做到這樣的效果。

建立 Console 專案

  • 開啟 Visual Studio 2026
  • 選擇「建立新專案」
  • 在 [建立新的專案] 視窗中,在右方清單內,找到並選擇「主控台應用程式」 項目
  • 然後點擊右下方「下一步」按鈕
  • 此時將會看到 [設定新的專案] 對話窗
  • 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csCompactionAgent"
  • 然後點擊右下方「下一步」按鈕
  • 接著會看到 [其他資訊] 對話窗
  • 在這個對話窗內,確認使用底下的選項
    • 架構:.NET 10.0 (或更新版本)
    • 勾選 不要使用最上層陳述式 (這是我的個人習慣)
  • 然後點擊右下方「建立」按鈕
  • 現在,已經完成了這個 Console 主控台 專案的建立

安裝需要用到的 NuGet 套件

要完成這個第一次接觸 Microsoft Agent Framework 練習專案,需要安裝以下幾個 NuGet 套件:

Microsoft.Extensions.AI

這個套件將會為生成式 AI 定義的一層「統一抽象介面 + Middleware 管線」,讓開發者可以更方便地整合不同的生成式 AI 模型,並且在應用程式中使用一致的方式進行呼叫與管理。

  • 滑鼠右擊 [csCompactionAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • Microsoft.Extensions.AI

Microsoft.Extensions.AI.OpenAI

這個套件是微軟官方為 .NET 整合 AI 服務所提供的擴充套件。它實現了 IChatClient 與 IEmbeddingGenerator 核心抽象介面,讓開發人員能以一致的程式碼,輕鬆串接 OpenAI 或相容的 API 端點(例如 Azure OpenAI、GitHub Models 等)。

  • 滑鼠右擊 [csCompactionAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • Microsoft.Extensions.AI.OpenAI

Microsoft.Agents.AI

這個套件是微軟官方推出 Microsoft Agent Framework(微軟智慧代理框架)的核心 .NET 套件。它主要用於在 .NET 環境中建構、協調與部署 AI 代理(AI Agents) 及多代理工作流(Multi-agent workflows),是用來取代舊版 AutoGen 的企業級解決方案。

  • 滑鼠右擊 [csCompactionAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • Microsoft.Agents.AI

OpenAI

這個套件為官方的 OpenAI .NET 客戶端類別庫,是 OpenAI NuGet 套件。此套件由 OpenAI 與 Microsoft 合作開發,提供直接存取 OpenAI 官方 REST API 的便利管道。

  • 滑鼠右擊 [csCompactionAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • OpenAI

宣告 API 端點與 Key 環境變數

關於這個小節的內容,將會在這個系列每篇文章中都會用到,因此,可以參考 MFA001 Microsoft Agent Framework 初次體驗開發出 AI 代理 文章中的 [宣告 API 端點與 Key 環境變數] 說明內容,若這裡提到的環境變數已經建立好了,則可以跳過這個小節內容。

修改 Program.cs 程式碼

為了要完成這個需求 : 如何用 Azure OpenAI 的 gpt-5.6-luna 聊天模型建立一個簡單 AI 代理,並產生一篇與鵝有關的詩。 因此,我們需要修改 Program.cs 程式碼,這時需要完成底下的操作。

  • 在專案內,找到並且開啟 [Program.cs] 檔案
  • 使用底下程式碼內容來取代 [Program.cs] 檔案的內容
#pragma warning disable MAAI001

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.Text;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;

namespace csCompactionAgent;

internal class Program
{
    private const string Instructions =
        "你是一位資深記者,正在逐段研讀一篇訪談稿並回答追問。請用繁體中文在 120 字內回答。" +
        "只能根據對話中出現過的內容回答;對話中找不到的資訊請直說「不記得」,不要猜。";

    private static readonly string[] Segments =
    [
        """
        記者:林阿伯,您是怎麼開始養鵝的?
        林阿伯:1987 年我退伍回到濁水溪邊的老家,田裡種稻賺不了錢。我跟舅舅借了三萬塊,
        買了 12 隻白羅曼鵝的雛鵝,就養在豬舍旁邊的空地。其中有一隻特別貪吃、走路一搖一擺,
        每天跟在我後面討飯吃,我就叫牠「阿肥」。那時候什麼都不懂,晚上還抱著手電筒去看牠們有沒有著涼,
        我媽都笑我說養鵝比養小孩還認真。
        """,
        """
        記者:後來規模是怎麼擴大的?
        林阿伯:前十年都是一點一點加,最多養到一千多隻。可是 1998 年那個颱風,溪水一夜之間漫進來,
        鵝舍整個泡在水裡,我划著塑膠桶去救,最後還是死了八百多隻。那陣子我真的想放棄,
        欠了農會一屁股債,每天凌晨三點就醒來,看著空空的鵝舍發呆。是隔壁的陳伯拉我去喝酒,
        跟我說「鵝會再生,人不能倒」,我才咬牙重新來過。
        """,
        """
        記者:重建之後,經營方式有什麼改變?
        林阿伯:我學乖了,鵝舍全部墊高一公尺,也不再只賣活鵝給盤商,因為價格被壓得很低。
        2003 年我太太在路口擺了一個小攤子賣鵝肉飯和煙燻鵝,自己養、自己殺、自己賣,
        一隻鵝的利潤多了快三倍。後來連台中的客人都專程開車來買,攤子變成店面,
        店名就叫「溪邊鵝」,到現在還是假日要排隊的店。
        """,
        """
        記者:這中間有遇過比颱風更難的關卡嗎?
        林阿伯:2015 年的禽流感。附近鵝場驗出病毒,防疫人員來了,整個場區三公里內全部撲殺,
        我們家兩千多隻鵝一天之內就沒了。那種感覺跟颱風不一樣,颱風是天災,這次是眼睜睜看著。
        政府有補償,但錢補不回心情。之後我把場區改成全密閉的負壓鵝舍,進出都要消毒換鞋,
        花了將近四百萬,但從那之後再也沒出過事。
        """,
        """
        記者:聽說現在場裡主要是女兒在管?
        林阿伯:對,小惠在台北做了八年的軟體工程師,2020 年疫情那年決定回來。
        她在鵝舍裝了溫溼度感測器和攝影機,手機上就能看每一區的狀況,飼料也改成依體重自動配給,
        一年省下快兩成成本。她還開了網路商店賣真空包的煙燻鵝,現在網購已經佔營收一半。
        我一開始覺得那些東西是花拳繡腿,後來半夜不用再起來巡場,才知道年輕人是對的。
        """,
        """
        記者:最後,您對鵝場的未來有什麼想像?
        林阿伯:我今年六十多歲了,想做的事只剩一件:讓小孩子知道食物是從哪裡來的。
        我們明年要開放鵝場導覽,跟附近三間國小合作食農教育,讓學生來餵鵝、撿鵝蛋,
        再到店裡吃一碗鵝肉飯。小惠說要做成 APP 預約,我說好啦好啦,你們年輕人決定。
        只要鵝場還在,這條溪邊就會一直有鵝叫聲,這樣我就滿足了。
        """
    ];

    private static readonly string[] FollowUps =
    [
        "從這段看來,林阿伯當初創業最大的風險是什麼?",
        "颱風之後他能重新站起來,關鍵是什麼?",
        "自產自銷為什麼能讓利潤變成將近三倍?",
        "禽流感和颱風對他的打擊,本質上有什麼不同?",
        "小惠導入的改變裡,哪一項對林阿伯的生活影響最大?",
        "林阿伯對未來的想像,和他當初創業的動機有什麼不同?"
    ];

    private const string DetailQuestion = "回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?";
    private const string SynthesisQuestion = "請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。";

    private const string SummaryPrompt =
        "你是對話摘要員。請用繁體中文把以下對話濃縮成一份摘要。" +
        "如果對話中已經有先前的摘要([Summary] 開頭),要保留它的重點,再把新內容合併進去。" +
        "每段訪談只寫一行、不超過 40 字,只留人名、數量、金額與年份等關鍵事實,不要寫分析與問答過程。";

    private static int lastSentMessages;

    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();

        IChatClient agentClient = chatClient.AsBuilder().Use(CountMessages, null).Build();

        IChatClient summarizerClient = chatClient.AsBuilder().Use(PrintSummary, null).Build();

        var trigger = CompactionTriggers.TurnsExceed(3);

        Console.WriteLine($"使用的模型:{model}/訪談共 {Segments.Length} 段,加上 2 題總結追問");

        await RunInterviewAsync(agentClient, "執行 1 / 不壓縮(對照組)", null);

        await RunInterviewAsync(agentClient, "執行 2 / 滑動視窗:只留最近 2 個回合",
            new SlidingWindowCompactionStrategy(trigger, 2));

        await RunInterviewAsync(agentClient, "執行 3 / 摘要:舊回合交給 LLM 濃縮成一則摘要",
            new SummarizationCompactionStrategy(summarizerClient, trigger, 4, SummaryPrompt));
    }

    private static async Task RunInterviewAsync(IChatClient client, string title, CompactionStrategy? strategy)
    {
        Header(title);

        AIAgent agent = new ChatClientAgent(client, new ChatClientAgentOptions
        {
            Name = "訪談分析師",
            ChatOptions = new ChatOptions { Instructions = Instructions },
            // 重點就這一行:把壓縮策略包成 CompactionProvider 掛到代理上
            AIContextProviders = strategy is null ? null : [new CompactionProvider(strategy)]
        });

        var session = await agent.CreateSessionAsync();
        var rows = new List<(string Turn, int Sent, long Input, long Output)>();

        async Task AskAsync(string turn, string question, string prompt)
        {
            Say($"🧑 {turn}", question);
            var response = await agent.RunAsync(prompt, session);
            Say("🤖 分析師", response.Text);
            rows.Add((turn, lastSentMessages,
                response.Usage?.InputTokenCount ?? 0, response.Usage?.OutputTokenCount ?? 0));
        }

        for (int i = 0; i < Segments.Length; i++)
        {
            // 畫面上只印追問,實際送給代理的是「整段訪談 + 追問」
            await AskAsync($"第 {i + 1} 段", FollowUps[i], $"【第 {i + 1} 段】\n{Segments[i]}\n\n問題:{FollowUps[i]}");
        }
        await AskAsync("細節題", DetailQuestion, DetailQuestion);
        await AskAsync("綜合題", SynthesisQuestion, SynthesisQuestion);

        Console.WriteLine();
        Console.WriteLine($"  【{title}】每回合統計");
        Console.WriteLine("  回合      送出訊息數  輸入 tokens  輸出 tokens");
        foreach (var row in rows)
        {
            Console.WriteLine($"  {row.Turn,-6}  {row.Sent,10}  {row.Input,11}  {row.Output,11}");
        }
        Console.WriteLine($"  {"合計",-6}  {"",10}  {rows.Sum(r => r.Input),11}  {rows.Sum(r => r.Output),11}");
    }

    private static Task<ChatResponse> CountMessages(
        IEnumerable<ChatMessage> messages, ChatOptions? options, IChatClient innerClient, CancellationToken cancellationToken)
    {
        var list = messages.ToList();
        lastSentMessages = list.Count;
        return innerClient.GetResponseAsync(list, options, cancellationToken);
    }

    private static async Task<ChatResponse> PrintSummary(
        IEnumerable<ChatMessage> messages, ChatOptions? options, IChatClient innerClient, CancellationToken cancellationToken)
    {
        var response = await innerClient.GetResponseAsync(messages, options, cancellationToken);
        Console.WriteLine();
        Console.WriteLine($"  📝 產生摘要(輸入 {response.Usage?.InputTokenCount} /輸出 {response.Usage?.OutputTokenCount} tokens):");
        Console.WriteLine($"     {response.Text.ReplaceLineEndings("\n     ")}");
        return response;
    }

    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 物件。接者建立 IChatClient 物件,變數名稱為 chatClient,接著將會建立第二個 IChatClient 物件,變數名稱為 agentClient ,並透過 CountMessages 中介函式來統計每回合送出的訊息數。

在這裡先來了解 CountMessages 中介函式做了甚麼事情:

在這個方法內,將會收到這四個參數,分別是,1. messages:傳入的聊天訊息集合。2. options:聊天選項,可能包含模型、溫度等設定。3. innerClient:實際的聊天客戶端,用來取得模型回應。4. cancellationToken:取消操作的標記。

  1. 它會將傳入的訊息集合轉成清單。 var list = messages.ToList();
  2. 記錄清單的數量到 lastSentMessages 變數,這樣就能統計每回合送出的訊息數。由聊天用戶端中介層記錄:這一輪模型「實際收到」幾則訊息
  3. 最後呼叫 innerClient 的 GetResponseAsync 方法,將訊息傳給實際的聊天模型,並回傳回應。

回到主程式進入點,接者建立摘要用的用戶端 summarizerClient ,同樣的,這裡也會多包一層,把每次產生的摘要印出來,方便追蹤每回合的摘要內容。同樣的,這裡也是透過中介軟體來做到,這裡將會使用 PrintSummary 這個方法來處理。

對於這個方法,會收到與 PrintSummary 相同的四個參數,分別是 messages、options、innerClient 與 cancellationToken。它的主要功能將會提供做到摘要策略呼叫 LLM 時,把產生的摘要與花費的 token 印出來,方便追蹤每回合的摘要內容。底下為其處理流程:

  1. 呼叫 innerClient 的 GetResponseAsync 方法,取得模型回應。
  2. 將回應的摘要內容印出,方便追蹤每回合的摘要。在此將會取得模型回應的使用統計數據中的輸入與輸出 token 數,並印出來。
  3. 回傳模型的回應。

再度回到主程式的流程,接下來將會依序執行 CompactionTriggers.TurnsExceed(3) 這個壓縮觸發條件,當對話回合數超過 3 回合時,將會觸發對話壓縮策略。在此將會得到 trigger 物件,這個物件將會在執行2階段,傳入到 RunInteractionAsync 方法中,在第二個參數內,將會傳入到 SlidingWindowCompactionStrategy 建構函式內。這個方法位於 Microsoft.Agents.AI.Compaction 命名空間內,而這個類別將會用於管理對話壓縮的策略,確保在對話回合數超過設定值時,能夠自動進行摘要與壓縮。

這樣的設計可以確保在對話過程中,當訊息量累積到一定程度時,系統會自動進行摘要與壓縮,避免訊息過多導致模型處理效率下降,同時也能保留對話的核心內容,提升整體互動體驗。

現在又回到主程式的流程,接下來會開始執行 RunInterviewAsync 方法三次,第一次將會做到不壓縮(對照組),第二次則會依照設定的壓縮觸發條件進行對話壓縮,第三次則會再次執行,觀察壓縮策略的效果。

現在要來看看 RunInterviewAsync 做了甚麼事情。這個方法主要負責執行整個訪談流程,首先在這裡建立了一個 AI Agent 物件,agent,在這裡提供的系統提示詞為 "你是一位資深記者,正在逐段研讀一篇訪談稿並回答追問。請用繁體中文在 120 字內回答。只能根據對話中出現過的內容回答;對話中找不到的資訊請直說「不記得」,不要猜。" , 這個系統提示詞將會引導模型在整個訪談過程中,依照設定的規則進行回答。這些規則將會做到:

  1. 只能根據對話中出現過的內容回答,對話中找不到的資訊要直說「不記得」。
  2. 回答需控制在 120 字以內。
  3. 以資深記者的角度,逐段研讀訪談稿並回答追問。

對於 ChatClientAgentOptions 這個類別,在建立的時候,對於第三個傳入的參數,AIContextProviders,若得到的 strategy 為非 null,將會把它加入到上下文提供者的集合中,這樣在每次與模型互動時,都能夠依照設定的策略來管理對話上下文,確保對話壓縮與摘要策略能夠正確運作。在此將會建立一個 CompactionProvider,並將其加入到 AIContextProviders 的集合中。在這個建構函式將會傳入 strategy 物件,以便在每次對話中依照策略進行壓縮與摘要。

接下來,將會透過代理人物件的 CreateSessionAsync 方法來建立一個新的對話會話,這個方法會回傳一個 session 物件,代表一次完整的對話流程。透過這個 session,將會依序處理每個回合的對話,並根據設定的壓縮策略決定是否進行對話壓縮。

對於接下來的 AskAsync 方法,將會是一個 Inline 方法,用來向模型提出問題並取得回應。這個方法會接收使用者的提問,並將其轉換成模型可理解的訊息格式,然後透過代理用的聊天用戶端,也就是 agent 物件,發送給模型,同時也會傳送對話物件 session 到 RunAsync 內,以便模型能夠知道當前的對話上下文,最後回傳模型的回應結果。

現在將會根據 Segments 這個字串型別的集合型別物件,這裡代表虛構的長篇訪談稿:鵝農林阿伯的三十多年(第一段埋了最後要考的細節:12 隻、阿肥),逐一將每一個字串取出來,透過迴圈依序呼叫剛剛說明的 Inline 方法,也就是 AskAsync 方法,將每個段落的內容傳給模型,並取得模型的回應。這樣可以模擬整個訪談過程,逐段與模型互動,並在每回合結束後,依照設定的壓縮策略決定是否進行對話壓縮。

對於 FollowUps 這個字串型別的集合型別物件,表示每一段配一個預先寫好的追問:三次執行的輸入完全相同,比較才公平。每個追問都對應到 Segments 中的相應段落。將會依序處理每一個追問,透過迴圈依序呼叫 AskAsync 方法,將每個追問傳給模型,並取得模型的回應。這樣可以模擬整個訪談過程中,使用者對每個段落的追問,並在每回合結束後,依照設定的壓縮策略決定是否進行對話壓縮。

例如,在第一輪呼叫 RunInterviewAsync 方法時,對於第一次觸發 AskAsync 方法,對於 prompt 參數,將會是底下內容

【第 1 段】
記者:林阿伯,您是怎麼開始養鵝的?
林阿伯:1987 年我退伍回到濁水溪邊的老家,田裡種稻賺不了錢。我跟舅舅借了三萬塊,
買了 12 隻白羅曼鵝的雛鵝,就養在豬舍旁邊的空地。其中有一隻特別貪吃、走路一搖一擺,
每天跟在我後面討飯吃,我就叫牠「阿肥」。那時候什麼都不懂,晚上還抱著手電筒去看牠們有沒有著涼,
我媽都笑我說養鵝比養小孩還認真。

問題:從這段看來,林阿伯當初創業最大的風險是什麼?

在整個迴圈執行完成後,將會對模型提出 "細節題" 與 "綜合題",以檢驗模型對整篇訪談的理解與摘要能力。前者將會詢問 "回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?" 後者將會詢問 "請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。",接下來看看三次呼叫 RunInterviewAsync 方法的執行結果。

第一輪(不壓縮對照組)將會得到 "細節題" 與 "綜合題" 的回答,並且每回合的訊息都不會被壓縮,完整保留所有對話內容。

  🧑 細節題 ▸ 回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?

  🤖 分析師 ▸ 一開始養了12隻白羅曼鵝雛鵝,那隻愛跟在他後面的鵝叫「阿肥」。

  🧑 綜合題 ▸ 請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。

  🤖 分析師 ▸ 1. 1987年:退伍返鄉,借錢買12隻雛鵝,開始養鵝

在第一輪的執行中,所有回合的訊息都完整保留,沒有進行任何壓縮,因此模型可以參考整篇訪談的內容來回答細節題與綜合題。

第二輪(滑動視窗:只留最近 2 個回合)將會得到 "細節題" 與 "綜合題" 的回答,每回合只保留最近兩個回合的對話內容,其餘的會被壓縮或丟棄。

  🧑 細節題 ▸ 回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?

  📝 產生摘要(輸入 497 /輸出 224 tokens):
     [Summary]
     1987年林阿伯借3萬元買12隻雛鵝創業。
     前十年擴至千餘隻;1998年颱風死800多隻。
     2003年改養鵝並販售鵝肉飯、煙燻鵝,成立溪邊鵝。
     2015年禽流感撲殺2,000多隻,改建密閉鵝舍花近400萬元。

  🤖 分析師 ▸ 一開始養了12隻雛鵝;那隻愛跟在他後面的鵝叫什麼名字,我不記得。

  🧑 綜合題 ▸ 請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。

  📝 產生摘要(輸入 481 /輸出 292 tokens):
     1987年林阿伯借3萬元買12隻雛鵝創業。
     前十年擴至千餘隻;1998年颱風死800多隻。
     2003年改養鵝並販售鵝肉飯、煙燻鵝,成立溪邊鵝。
     2015年禽流感撲殺2,000多隻,改建密閉鵝舍花近400萬元。
     2020年女兒小惠返鄉,導入智慧養鵝,省近兩成成本。
     小惠經營網路商店,網購營收占一半,林阿伯免夜間巡場。

  🤖 分析師 ▸ 1. 1987年:借3萬元買12隻雛鵝,開始養鵝創業。
2. 1998年:颱風造成800多隻鵝死亡,成為重大打擊。
3. 2003年至2020年:轉型販售鵝肉;2015年禽流感後升級鵝舍,2020年女兒返鄉導入智慧養鵝。

在第二輪的執行中,由於採用了滑動視窗的壓縮策略,只保留最近兩個回合的對話內容,其餘的會被壓縮或丟棄,因此模型在回答細節題時,對於第一段的資訊可能無法完整回憶。這也說明了在使用滑動視窗策略時,模型的記憶是有限的,過去的訊息可能會被遺忘。

第三輪(壓縮策略:保留最近 2 個回合,其他回合壓縮)將會得到 "細節題" 與 "綜合題" 的回答,每回合只保留最近兩個回合的對話內容,其餘的會被壓縮,但壓縮後的內容仍可供模型參考。

  🧑 細節題 ▸ 回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?

  📝 產生摘要(輸入 478 /輸出 224 tokens):
     1987年林阿伯借3萬元買12隻白羅曼鵝創業。
     1998年颱風淹鵝舍,死800多隻;陳伯鼓勵重振。
     2003年自產自銷鵝肉飯、燻鵝,利潤增近3倍,成立溪邊鵝。
     2015年禽流感撲殺兩千多隻鵝,花近400萬元改建負壓鵝舍。

  🤖 分析師 ▸ 一開始養了12隻白羅曼鵝;那隻愛跟在他後面的鵝叫什麼名字,我不記得。

  🧑 綜合題 ▸ 請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。

  📝 產生摘要(輸入 491 /輸出 265 tokens):
     1987年林阿伯借3萬元買12隻白羅曼鵝創業。
     1998年颱風淹鵝舍,死800多隻;陳伯鼓勵重振。
     2003年自產自銷鵝肉飯、燻鵝,利潤增近3倍,成立溪邊鵝。
     2015年禽流感撲殺兩千多隻鵝,花近400萬元改建負壓鵝舍。
     2020年小惠返鄉導入智慧養鵝,省近兩成成本,網購占營收半數。

  🤖 分析師 ▸ 1. 1987年:借3萬元買12隻鵝,開始養鵝創業。

在第三輪的執行中,採用了壓縮策略,保留最近兩個回合的對話內容,其餘的回合會被壓縮,但壓縮後的內容仍可供模型參考。這樣可以在有限的記憶下,仍保留對話的核心資訊,幫助模型回答細節題與綜合題。

在實務應用中,這種壓縮策略適合用於長篇訪談或多回合對話,能有效降低模型的記憶負擔,同時保留關鍵資訊,提升回答的準確性與一致性。

執行結果

現在來看看這個範例程式碼的執行結果。

  • 在 Visual Studio 2026 下,按下 F5 鍵執行程式,將會看到程式輸出的結果。
使用的模型:gpt-5.6-luna/訪談共 6 段,加上 2 題總結追問

==========================================================================
  執行 1 / 不壓縮(對照組)
==========================================================================

  🧑 第 1 段 ▸ 從這段看來,林阿伯當初創業最大的風險是什麼?

  🤖 分析師 ▸ 文中未明說「最大風險」。可看出他借三萬元投入,且當時毫無養鵝經驗,若飼養失敗,可能面臨資金損失與生計壓力。

  🧑 第 2 段 ▸ 颱風之後他能重新站起來,關鍵是什麼?

  🤖 分析師 ▸ 關鍵是陳伯的支持與鼓勵,讓他沒有因重大損失和債務倒下;他也靠著堅持重新開始養鵝。

  🧑 第 3 段 ▸ 自產自銷為什麼能讓利潤變成將近三倍?

  🤖 分析師 ▸ 因為不再只把活鵝低價賣給盤商,而是自行屠宰、加工成鵝肉飯與煙燻鵝,再直接銷售給消費者,省去盤商抽成並提高產品附加價值。

  🧑 第 4 段 ▸ 禽流感和颱風對他的打擊,本質上有什麼不同?

  🤖 分析師 ▸ 颱風屬於突發天災,鵝舍遭洪水淹沒;禽流感則是疫情防疫下的撲殺,雖非直接天災,卻是眼睜睜看著鵝群被消失,心理打擊更深。

  🧑 第 5 段 ▸ 小惠導入的改變裡,哪一項對林阿伯的生活影響最大?

  🤖 分析師 ▸ 溫溼度感測器和攝影機影響最大,讓林阿伯能用手機掌握各區狀況,半夜不用再起來巡場,直接改善了生活作息。

  🧑 第 6 段 ▸ 林阿伯對未來的想像,和他當初創業的動機有什麼不同?

  🤖 分析師 ▸ 當初創業是因稻作收入不佳,為了改善生計而養鵝;如今則不以賺錢為主要目標,而是希望透過導覽與食農教育,讓孩子認識食物來源,延續鵝場與地方記憶。

  🧑 細節題 ▸ 回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?

  🤖 分析師 ▸ 一開始養了12隻白羅曼鵝雛鵝;那隻愛跟在他後面的鵝叫「阿肥」。

  🧑 綜合題 ▸ 請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。

  🤖 分析師 ▸ 1. **1987年**:退伍返鄉,借款三萬元開始養鵝。
2. **1998年**:颱風淹沒鵝舍、死傷八百多隻,在陳伯鼓勵下重新振作。
3. **2003年後**:改採自產自銷,創立「溪邊鵝」;2015年禽流感後升級防疫,2020年由女兒導入科技經營。

  【執行 1 / 不壓縮(對照組)】每回合統計
  回合      送出訊息數  輸入 tokens  輸出 tokens
  第 1 段            1          285          136
  第 2 段            3          544           45
  第 3 段            5          791           66
  第 4 段            7         1064           72
  第 5 段            9         1350           83
  第 6 段           11         1608           71
  細節題             13         1723           39
  綜合題             15         1792          229
  合計                         9157          741

==========================================================================
  執行 2 / 滑動視窗:只留最近 2 個回合
==========================================================================

  🧑 第 1 段 ▸ 從這段看來,林阿伯當初創業最大的風險是什麼?

  🤖 分析師 ▸ 不記得。段落只提到借三萬元買雛鵝、缺乏養鵝經驗,未明確說明最大的風險。

  🧑 第 2 段 ▸ 颱風之後他能重新站起來,關鍵是什麼?

  🤖 分析師 ▸ 關鍵是陳伯的支持與鼓勵。陳伯拉他喝酒,告訴他「鵝會再生,人不能倒」,讓他在欠債、幾乎想放棄時咬牙重新開始。

  🧑 第 3 段 ▸ 自產自銷為什麼能讓利潤變成將近三倍?

  🤖 分析師 ▸ 因為不再只把活鵝低價賣給盤商,而是自行養殖、屠宰並製成鵝肉飯和煙燻鵝直接販售,省去盤商壓價與中間環節,因此每隻鵝利潤多了快三倍。

  🧑 第 4 段 ▸ 禽流感和颱風對他的打擊,本質上有什麼不同?

  🤖 分析師 ▸ 颱風屬於突如其來的天災,鵝舍被洪水淹沒;禽流感則是眼睜睜看著防疫撲殺,兩千多隻鵝一天全沒了。前者偏向意外破壞,後者帶來更強的心理煎熬。

  🧑 第 5 段 ▸ 小惠導入的改變裡,哪一項對林阿伯的生活影響最大?

  🤖 分析師 ▸ 對林阿伯生活影響最大的是裝設溫溼度感測器和攝影機,讓他能用手機掌握各區狀況,半夜不用再起來巡場,生活因此輕鬆許多。

  🧑 第 6 段 ▸ 林阿伯對未來的想像,和他當初創業的動機有什麼不同?

  🤖 分析師 ▸ 對話中沒有提到林阿伯當初創業的動機,因此無法比較。不過,他現在的想像是透過鵝場導覽與食農教育,讓孩子了解食物來源,並守住溪邊的鵝叫聲。

  🧑 細節題 ▸ 回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?

  🤖 分析師 ▸ 不記得,對話中沒有提供第一段的內容。

  🧑 綜合題 ▸ 請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。

  🤖 分析師 ▸ 目前對話只提供第六段,無法總結整篇或列出三個轉折與年份。已知內容是:

1. 林阿伯今年六十多歲。
2. 明年將開放鵝場導覽。
3. 將與附近三間國小合作食農教育。



  【執行 2 / 滑動視窗:只留最近 2 個回合】每回合統計
  回合      送出訊息數  輸入 tokens  輸出 tokens
  第 1 段            1          285           42
  第 2 段            3          528           60
  第 3 段            5          790           76
  第 4 段            5          815           83
  第 5 段            5          851          114
  第 6 段            5          839          112
  細節題              5          657           17
  綜合題              5          432          124
  合計                         5197          628

==========================================================================
  執行 3 / 摘要:舊回合交給 LLM 濃縮成一則摘要
==========================================================================

  🧑 第 1 段 ▸ 從這段看來,林阿伯當初創業最大的風險是什麼?

  🤖 分析師 ▸ 最大的風險是缺乏養鵝經驗,卻借了三萬元投入購買十二隻雛鵝;一旦飼養失敗,可能無法回收資金。

  🧑 第 2 段 ▸ 颱風之後他能重新站起來,關鍵是什麼?

  🤖 分析師 ▸ 關鍵是陳伯的支持與鼓勵,讓他沒有被重大損失和債務擊倒,重新振作、咬牙再來。

  🧑 第 3 段 ▸ 自產自銷為什麼能讓利潤變成將近三倍?

  🤖 分析師 ▸ 因為不再只把活鵝低價賣給盤商,而是自行屠宰、加工成鵝肉飯與煙燻鵝,再直接販售給消費者,省去中間商並提高產品附加價值。

  🧑 第 4 段 ▸ 禽流感和颱風對他的打擊,本質上有什麼不同?

  📝 產生摘要(輸入 373 /輸出 115 tokens):
     1987年林阿伯借3萬元買12隻白羅曼鵝雛鵝創業。

  🤖 分析師 ▸ 颱風是突發天災,鵝舍被洪水淹沒;禽流感則因鄰近鵝場染病,遭防疫撲殺,眼睜睜看著鵝消失,心理打擊更深。

  🧑 第 5 段 ▸ 小惠導入的改變裡,哪一項對林阿伯的生活影響最大?

  📝 產生摘要(輸入 383 /輸出 158 tokens):
     1987年林阿伯借3萬元買12隻白羅曼鵝雛鵝創業。
     1998年颱風淹死800多隻,陳伯鼓勵他重振養鵝事業。

  🤖 分析師 ▸ 裝設溫溼度感測器和攝影機,讓他能用手機掌握鵝舍狀況,半夜不用再起來巡場,對生活影響最大。

  🧑 第 6 段 ▸ 林阿伯對未來的想像,和他當初創業的動機有什麼不同?

  📝 產生摘要(輸入 437 /輸出 146 tokens):
     [Summary]
     1987年林阿伯借3萬元買12隻白羅曼鵝創業。
     1998年颱風淹死800多隻,陳伯鼓勵重振養鵝。
     2003年墊高鵝舍1公尺,自產自銷,利潤增近3倍。
     「溪邊鵝」由攤變店,台中客人專程來買。

  🤖 分析師 ▸ 當初創業是為了經營鵝場、謀生;如今則不只追求生意,而是希望透過導覽與食農教育,讓孩子認識食物來源,延續鵝場與地方記憶。

  🧑 細節題 ▸ 回到第一段:林阿伯一開始養了幾隻鵝?那隻愛跟在他後面的鵝叫什麼名字?

  📝 產生摘要(輸入 488 /輸出 291 tokens):
     [Summary]
     1987年林阿伯借3萬元買12隻白羅曼鵝創業。
     1998年颱風淹死800多隻,陳伯鼓勵重振養鵝。
     2003年墊高鵝舍1公尺,自產自銷,利潤增近3倍。
     「溪邊鵝」由攤變店,台中客人專程來買。
     2015年禽流感撲殺2000多隻鵝,改建負壓鵝舍近400萬元。

  🤖 分析師 ▸ 一開始養了12隻鵝;那隻愛跟在他後面的鵝叫什麼名字,我不記得。

  🧑 綜合題 ▸ 請用三點總結整篇訪談中,林阿伯人生的重大轉折與年份。

  📝 產生摘要(輸入 509 /輸出 267 tokens):
     [Summary]
     1987年林阿伯借3萬元買12隻白羅曼鵝創業。
     1998年颱風淹死800多隻,陳伯鼓勵重振養鵝。
     2003年墊高鵝舍1公尺,自產自銷,利潤增近3倍。
     「溪邊鵝」由攤變店,台中客人專程來買。
     2015年禽流感撲殺2000多隻,改建負壓鵝舍近400萬元。
     2020年小惠返鄉導入智慧管理,省近兩成成本,網購占營收一半。

  🤖 分析師 ▸ 1. **1987年**:借3萬元買12隻白羅曼鵝,開始養鵝創業。
2. **1998年**:颱風淹死800多隻,在陳伯鼓勵下重振鵝場。
3. **2015年至2020年**:歷經禽流感撲殺與改建,後由小惠返鄉導入智慧管理,轉型升級。

  【執行 3 / 摘要:舊回合交給 LLM 濃縮成一則摘要】每回合統計
  回合      送出訊息數  輸入 tokens  輸出 tokens
  第 1 段            1          285          109
  第 2 段            3          538           41
  第 3 段            5          781           64
  第 4 段            6          820           65
  第 5 段            6          887           83
  第 6 段            6          922          197
  細節題              6          786           70
  綜合題              6          613          163
  合計                         5632          792

[AI 實踐] 在 Skill 流程加入人為檢查點

[AI 實踐] 在 Skill 流程加入人為檢查點

摘要:在高價值節點讓人及早審核,能減少誤差累積,提升 Skill 的穩定性。



內容:


設計 Skill 時,很容易把「完全自動化」當成目標:把 SOP 交給 Agent 後,從開始到產出結果都不再介入,也不必查看中間過程。但在反覆打磨 Skill 後,我發現這種想法反而可能是失敗的原因。若想提升 Skill 的魯棒性與穩定性,不能一味把人從流程中抽離,而應在適當位置加入人為檢查點(checkpoint)。


我在製作問卷生成 Skill 時遇過這個問題。經過多個版本的迭代,產出的問卷仍不符合預期。我能感覺到設計有問題、某些地方需要調整,卻很難把這種綜合判斷整理成固定而明確的規則,寫進 Skill。當時流程往往有十幾個步驟,執行一次可能要等十多分鐘;等到最終結果出來才發現不對,再要求重跑,又得耗費一輪時間。


後來我改變了設計方式:不再要求 Agent 一口氣做完,而是把人的經驗判斷放進流程中最有價值的節點。現在,這個 Skill 會先分析客戶背景、提取關鍵資訊,再讀取既有的通用問卷模板,思考如何針對該客戶調整。接著,它不會立刻生成完整問卷,而是先提出一份簡短的調整方向,停下來請我確認。這就是第一道人為檢查點。


把檢查點放在早期很重要。假設流程共有十步,第三步的方向已經偏了,卻等到第七步甚至最後才檢查,後續工作可能已經建立在錯誤的基礎上。Agent 也不是一開始就完整想好每一步,而是在執行過程中根據前一步結果繼續推進;早期的小誤差,可能隨著步驟增加而放大。因此,越早在關鍵處校正方向,越能避免浪費後續的時間與 token。


檢查點的中間產物必須方便人閱讀,因為它的服務對象是人,不只是 Agent。如果要求人審核,卻產出一份冗長的 Markdown 文件,就失去了快速判斷的意義。在我的流程中,Agent 會把問卷的調整方向整理成簡潔的 HTML。我可以迅速看出它提出了哪些改進點,告訴它哪些方向可以保留、哪些需要修改;有需要的話,HTML 也可以設計成可勾選確認的互動形式,但不必為了互動而讓流程過度複雜。


收到我的意見後,Agent 會修正方案,再生成問卷初版。初版同時有 Markdown 與 HTML 兩種形式:Markdown 供 Agent 後續處理,HTML 則供人預覽。這時進入第二道檢查點,我會在最終產出前檢視問卷。如果整體符合預期,只有個別題目需要微調,就直接提出意見,讓 Agent 在目前流程中修改,不必從頭重跑,也不一定需要再安排第三次審核。確認後,它才呼叫後續工具生成最終問卷,並把連結交給我分享。


這與「全部做完後,在流程外要求重做」不同。檢查點是 Skill 內部預先設計好的環節,讓人能在尚可低成本調整時介入,而不是等到所有步驟完成才回頭修正。


這種設計也承認了 Skill 的限制:即使指示寫得清楚,Agent 的執行仍可能有變動;有些人類能察覺的問題,也很難事先寫成可穩定遵循的規則。與其無止境地修改指示、期待流程永遠自行產出理想結果,不如設計一套讓人與 Agent 各自發揮所長的協作方式。


實作時,我會掌握三個原則:盡量及早設置檢查點;選在需要高價值判斷的節點介入;讓中間產物簡短、易讀,足以支持人的審核。 經過多次使用後,人於檢查點提出的意見和修改紀錄,也可以再沉澱成改進 Skill 的素材。


人為檢查點不是放棄自動化,而是避免過度相信全自動化。當流程走到能看出雛形、也最需要判斷的地方,讓人及時介入,通常比等到最後才檢查,更能得到穩定且符合預期的結果。

[AI 觀點] AI 投資狂潮下,價值與利潤為何對不上?

摘要:AI 提升效率,卻未必帶來相稱利潤;高額投資、成本轉嫁與流量流失,正考驗產業的商業模式。



內容:


近期美股出現一個值得注意的現象:原文指出,谷歌、Intel 與 Sandisk 的財報表現優於預期,股價卻仍大幅下跌。作者認為,市場擔心的並非眼前的營收或獲利,而是企業持續加碼 AI 後,投入的資金能否在合理時間內回收。AI 能創造價值,不代表投資 AI 的每家公司都能賺到足夠的利潤。


首先是資本支出的壓力。原文以谷歌為例,指出資料中心與晶片採購支出快速增加,甚至使自由現金流轉負。其他科技公司也持續投入巨額資金建置算力,即使已經從 AI 應用取得收入,仍可能需要把賺來的錢重新投入基礎設施。作者據此提出疑問:當企業必須不斷投入更多資金,卻還說不清何時回本,市場是否會開始重新評估它們的估值?


第二筆帳是 AI 成本由誰承擔。原文認為,資料中心擴張會推高記憶體等零組件需求,進而影響手機與電腦的成本和價格。從模型公司的角度看,將更多資源集中於資料中心或許能提高產出;但從一般消費者的角度看,可能先感受到設備變貴,還未享受到同等程度的 AI 效益。換句話說,AI 的投資成本不一定只留在科技公司的帳上,也可能沿著供應鏈轉嫁給使用者。


第三筆帳,也是作者最重視的問題:AI 創造的價值,最後由誰取得?AI 確實能協助寫程式、作圖、剪影片與撰寫文案,讓原本需要一天的工作縮短到幾個小時。然而,效率提升未必能直接轉化為企業利潤。當更多人都能用 AI 快速製作產品,市場供給與競爭也會同步增加,產品更容易降價,甚至免費提供。使用者因此得到更好、更便宜的服務,開發產品的人卻未必更容易賺錢。


原文用藍莓豐收、價格下跌作比喻:當某種供給大幅增加,單位價格便可能受到壓抑。AI 正在讓部分過去昂貴的「智慧服務」變得容易取得。若模型運算與基礎設施的投入是真金白銀,而多數效益以低價或免費服務的形式流向使用者,投入與回收之間就可能出現落差。


第四筆帳是網際網路原有的流量分配方式。過去,網站提供內容給搜尋引擎索引;搜尋引擎將使用者導回網站;網站再透過廣告、訂閱或會員收入,支持內容創作。如今,AI 搜尋可以直接整理答案,使用者不一定需要點進原始網站。原文以 Reddit 為例,擔憂搜尋導流減少,將使提供內容的社群與網站更難維持收入。若內容來源持續失去流量,長期也可能削弱支撐網路內容生態的誘因。


作者進一步擔心,AI 若同時取代部分工作、壓低產品價格,又讓原本依靠流量獲利的人難以取得收入,整體經濟中的消費與收入循環可能受到影響。不過,這是一項對未來的推論,而不是已經確定會發生的結果。


至於「AI 泡沫」何時會破,作者也承認無法預測。他回顧網際網路泡沫:市場早在見頂前數年就開始討論估值過高,但股價仍持續上漲,直到泡沫破裂才大幅修正。這段歷史提醒人們,即使判斷某項技術的市場估值過熱,也不代表能準確預測轉折時間。原文提到 Anthropic 上市可能成為市場情緒的觀察點,但相關上市時程與估值傳聞,仍應以公司及監管機構的正式資訊為準。


最後,作者強調:**即使泡沫破裂,被修正的可能是估值,而不是技術本身。**網際網路泡沫破裂後,網際網路仍持續發展;AI 也可能在市場淘汰難以證明商業模式的公司後,逐漸找到更有價值的用途。


對個人而言,值得思考的不是「會不會使用 AI」而已,而是 AI 正在讓哪些能力變得普遍,又讓哪些能力更顯稀缺。程式碼、圖片、影片與文案的製作門檻可能持續降低;判斷力、審美、信任,以及對問題和人的理解,反而更難被直接複製。與其把使用 AI 當成唯一優勢,不如用它放大自己原本就具備的獨特能力。


*本文整理原文觀點;其中個別公司財務數字、股價變動、離職方案與上市消息未經核實,不構成投資建議。

2026年11月3日 星期二

[AI 洞察] AI 時代的創新人才與企業護城河

 [AI 洞察] AI 時代的創新人才與企業護城河

摘要 : 當 AI 讓執行變得便宜,企業更需要容納獨特人才,並將隱性知識轉化為可驗證的業務成果。



內容:


企業最想得到的創造力,可能恰恰來自它最容易淘汰的人。Palantir 執行長亞歷克斯·卡普在訪談中提出一個值得企業正視的問題:為什麼組織一面宣稱重視創新,一面又用標準化的招聘、考核與晉升制度,排除最可能帶來突破的人?在 AI 能力愈來愈強、使用成本愈來愈低的時代,企業真正稀缺的,或許不是執行力,而是獨特的判斷力,以及把判斷轉化為成果的能力。


卡普把 Palantir 形容為「藝術家社群」。對一家以複雜資料系統和企業 AI 應用著稱的公司而言,這個說法看似奇特,卻反映了他對人才的看法:突破性成果未必來自各項能力平均、容易管理的人,而可能來自某個領域特別突出的「鋸齒型人才」(jagged talent)。這類人可能不擅長溝通、不喜歡管理,也難以適應標準流程,卻能在特定問題上展現旁人難以取代的創造力。


傳統企業偏好能力全面、表現穩定的員工,因為他們較容易被既有制度衡量與管理。但全面不等於卓越,穩定也不等於創新。如果企業用同一套能力模型招聘、用同一套績效標準考核,再期待員工產出非標準化的突破,結果可能是:愈符合制度的人愈容易留下,愈有特殊才能的人愈容易被視為「不合格」。標準化提高了管理效率,也可能限制組織的創新上限。


接納創造力的難處,還不只是制度設計。真正有價值的新想法,往往會質疑既有流程,甚至指出管理者過去的判斷有誤。當提出挑戰的人資歷較淺、表達方式又不符合慣例,組織面對的便不只是「這個想法對不對」,還有「我們能不能接受他可能比自己更懂這件事」。卡普認為,許多組織並非缺少人才,而是缺少容納人才、承受權威被挑戰的能力。


這並不代表每個挑戰權威的人都是天才,也不代表創新組織不需要管理。更困難、也更重要的管理能力,是在不盲從特殊人才的前提下,認真檢驗其想法的價值;同時不因對方難以配合,就先消磨其獨特性。好的管理不是要求所有人服從領導者的認知,而是讓各有所長的人共同創造結果,避免組織的認知上限被少數管理者鎖住。


然而,聚集人才、做出先進技術,仍不等於創造商業價值。卡普以「如何從罐子裡取出第一顆橄欖」比喻爭取首位客戶:讓客戶願意試用固然重要,更難的是讓他理解產品的價值,進而真正採用。一次精彩的展示可以令人驚訝,卻不會自動改變企業的工作方式。導入新技術通常還涉及流程重設、職責調整、責任劃分,甚至既有權力結構的改變。


這也是 Palantir 重視前線部署工程師(FDE)的原因。他們不只交付軟體,也深入客戶的業務現場,理解具體問題、調整系統,協助技術進入實際工作流程。現場經驗又反過來影響產品開發:企業不只是先做出技術,再尋找使用場景,而是在解決真實問題的過程中,辨認下一步應該開發什麼。產品有多少功能不是最終目的;它改變了哪些業務結果,才是價值所在。


AI 的普及讓「使用」與「創造價值」之間的落差更加明顯。企業可以購買更多 token、生成更多報告,讓 AI agent 自動執行更多任務;但如果報告沒有人使用,任務也沒有改善業務結果,增加的只是產出數量。把原本無效的流程自動化,並不會使它變得有價值。企業衡量 AI 成效時,不能只看呼叫量或執行速度,更應比較導入前後的決策品質、工作流程與實際成果。


AI 的成本也不只有帳單上的模型費用。企業多年累積的供應商判斷、生產排程方法、客戶辨識經驗等專有知識,未必記錄在財務報表上,卻可能是競爭優勢的核心。使用 AI 時,企業需要清楚掌握這些知識的使用範圍、流向與控制權。這不表示外部 AI 必然造成資訊外洩,而是提醒企業:節省了多少費用,不能脫離可能失去什麼來衡量。


當不同公司都能取得相近的先進模型,難以複製的差異就更可能來自組織長期累積的隱性知識(tribal knowledge)。資深採購經理如何察覺供應商風險、產品負責人如何判斷哪些需求不值得做、銷售團隊如何辨認真正有購買意願的客戶,這些能力往往源自長期實踐與反覆試錯,未必能從文件中直接讀到。


AI 提供了一種可能:讓原本只存在於個人經驗中的知識,逐步成為組織可運用、可檢驗、可持續修正的判斷能力。這不只是把資料放進知識庫,還需要連結業務關係、決策規則、工作流程與結果回饋。Palantir 強調的 ontology(本體系統),正是其組織企業資料、業務關係及專有知識的產品路線。更普遍的啟示是:兩家公司即使使用相同模型,若只有一家持續沉澱經驗、驗證判斷並改善流程,長期形成的智慧資產仍可能截然不同。


不過,知識不會自動變成能力,能力也不會自動變成價值。隱性知識必須進入實際工作流程,接受結果檢驗與持續修正,才可能構成競爭優勢。卡普也提醒,若 AI 創造的財富高度集中、成果無法讓更多人分享,技術進步可能引發社會反彈。創新除了要在技術上可行,也必須能被組織採用,並以更廣泛、可持續的方式創造價值。


回頭看,「藝術家社群」、鋸齒型人才、前線部署工程師與企業知識系統,其實指向同一條創新價值鏈:獨特人才發現別人看不到的問題,組織讓新想法落地,知識系統則把一次性的成果沉澱為可持續的能力。只有人才而沒有組織支持,創造力可能停留在個人身上;只有高效組織而沒有獨特判斷,企業可能只是更快地執行舊規則;只有 AI 工具而沒有知識積累和真實回饋,也難以形成長期優勢。


當 AI 讓標準化執行愈來愈便宜,發現問題、質疑慣例、判斷價值並創造新標準的能力,便顯得愈來愈重要。企業未來的風險,可能不是找不到有才能的人,而是用一套看似合理的制度,將最有價值的人判定為不合格。

2026年11月2日 星期一

MFA012 - 自己寫中介層(Middleware)

MFA012 - 自己寫中介層(Middleware)

在上一篇關於 Microsoft Agent Framework 系列教學文章 "MFA011 - 工具核准(代理層的人機互動)" 中,我們學會了使用 MAF 提供的工具核准機制,讓 AI 代理在執行修改或刪除操作前,先取得使用者的核准。這裡舉出的例子為資料庫助理在執行修改或刪除訂單前,會先向使用者確認。但是若要對資料庫做查詢需求時,則可以自動放行,不需要額外的核准。

在完成這篇文章的練習,將會學會與理解到如何在 MAF 中使用與開發出適合自己使用的中介層(Middleware),並且能夠在 AI 代理的執行流程中,靈活地加入自訂的邏輯與控制。在這篇文章中,所建立起來的中介層方法將會用來檢查 AI 代理的回應是否超過字數上限,若超過則自動重試。

接下來看看如何做到這樣的效果。

建立 Console 專案

  • 開啟 Visual Studio 2026
  • 選擇「建立新專案」
  • 在 [建立新的專案] 視窗中,在右方清單內,找到並選擇「主控台應用程式」 項目
  • 然後點擊右下方「下一步」按鈕
  • 此時將會看到 [設定新的專案] 對話窗
  • 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csRetryMiddlewareAgent"
  • 然後點擊右下方「下一步」按鈕
  • 接著會看到 [其他資訊] 對話窗
  • 在這個對話窗內,確認使用底下的選項
    • 架構:.NET 10.0 (或更新版本)
    • 勾選 不要使用最上層陳述式 (這是我的個人習慣)
  • 然後點擊右下方「建立」按鈕
  • 現在,已經完成了這個 Console 主控台 專案的建立

安裝需要用到的 NuGet 套件

要完成這個第一次接觸 Microsoft Agent Framework 練習專案,需要安裝以下幾個 NuGet 套件:

Microsoft.Extensions.AI

這個套件將會為生成式 AI 定義的一層「統一抽象介面 + Middleware 管線」,讓開發者可以更方便地整合不同的生成式 AI 模型,並且在應用程式中使用一致的方式進行呼叫與管理。

  • 滑鼠右擊 [csRetryMiddlewareAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • Microsoft.Extensions.AI

Microsoft.Extensions.AI.OpenAI

這個套件是微軟官方為 .NET 整合 AI 服務所提供的擴充套件。它實現了 IChatClient 與 IEmbeddingGenerator 核心抽象介面,讓開發人員能以一致的程式碼,輕鬆串接 OpenAI 或相容的 API 端點(例如 Azure OpenAI、GitHub Models 等)。

  • 滑鼠右擊 [csRetryMiddlewareAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • Microsoft.Extensions.AI.OpenAI

Microsoft.Agents.AI

這個套件是微軟官方推出 Microsoft Agent Framework(微軟智慧代理框架)的核心 .NET 套件。它主要用於在 .NET 環境中建構、協調與部署 AI 代理(AI Agents) 及多代理工作流(Multi-agent workflows),是用來取代舊版 AutoGen 的企業級解決方案。

  • 滑鼠右擊 [csRetryMiddlewareAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • Microsoft.Agents.AI

OpenAI

這個套件為官方的 OpenAI .NET 客戶端類別庫,是 OpenAI NuGet 套件。此套件由 OpenAI 與 Microsoft 合作開發,提供直接存取 OpenAI 官方 REST API 的便利管道。

  • 滑鼠右擊 [csRetryMiddlewareAgent] 專案節點
  • 點選彈出功能表的 [管理 NuGet 套件] 項
  • 在 [瀏覽] 索引標籤中,搜尋並且安裝底下的 NuGet 套件
    • OpenAI

宣告 API 端點與 Key 環境變數

關於這個小節的內容,將會在這個系列每篇文章中都會用到,因此,可以參考 MFA001 Microsoft Agent Framework 初次體驗開發出 AI 代理 文章中的 [宣告 API 端點與 Key 環境變數] 說明內容,若這裡提到的環境變數已經建立好了,則可以跳過這個小節內容。

修改 Program.cs 程式碼

為了要完成這個需求 : 如何用 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;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage; // OpenAI.Chat 也有 ChatMessage,這裡指定用 MEAI 的

namespace csRetryMiddlewareAgent;

internal class Program
{
    // 回答的字數上限(直接用 response.Text.Length 計算,中文一個字算 1,標點與換行也算)
    private const int MaxChars = 100;

    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 poet = new ChatClientAgent(
            chatClient,
            "你是一位詩人,創作引人入勝、富有創意的詩,而且特別喜歡寫長詩。", // 系統提示詞
            "詩人");                                                         // 代理名稱

        AIAgent guardedPoet = poet.AsBuilder()
            .Use(runFunc: RetryIfTooLong, runStreamingFunc: null)
            .Build();

        Console.WriteLine($"使用的模型:{model}/字數上限:{MaxChars} 字");

        Console.WriteLine();
        Console.WriteLine("===== 對照組:沒有中介層 =====");
        await AskAsync(poet, "寫一個關於鵝的詩。");

        Console.WriteLine();
        Console.WriteLine("===== 套上中介層 =====");
        await AskAsync(guardedPoet, "寫一個關於鵝的詩。");
    }

    private static async Task AskAsync(AIAgent agent, string question)
    {
        Console.WriteLine($"🧑 {question}");
        var response = await agent.RunAsync(question);
        Console.WriteLine($"🤖({response.Text.Length} 字)");
        Console.WriteLine(response.Text);
    }

    private static async Task<AgentResponse> RetryIfTooLong(
        IEnumerable<ChatMessage> messages,
        AgentSession? session,
        AgentRunOptions? options,
        AIAgent innerAgent,
        CancellationToken cancellationToken)
    {
        session ??= await innerAgent.CreateSessionAsync(cancellationToken);

        var response = await innerAgent.RunAsync(messages, session, options, cancellationToken);
        if (response.Text.Length <= MaxChars)
        {
            return response;
        }

        Console.WriteLine($"🛡️ 中介層:回答 {response.Text.Length} 字,超過上限 {MaxChars} 字,要求重答");
        var retry = await innerAgent.RunAsync(
            $"你的回答有 {response.Text.Length} 字,超過 {MaxChars} 字的上限。請把同一首詩精簡到 {MaxChars} 字以內重新回答。",
            session, options, cancellationToken);

        if (retry.Text.Length > MaxChars)
        {
            Console.WriteLine($"⚠️ 中介層:重答後仍有 {retry.Text.Length} 字,照樣回傳");
        }
        return retry;
    }
}

這個程式一開始將會讀取 API Key 與服務端點的數據,接著如同前面的範例相同,先建立一個 IChatClient 物件。接者建立 AIAgent 物件,變數名稱為 poet,這裡將會指定這個 AI Agent 的系統提示詞為 : 你是一位詩人,創作引人入勝、富有創意的詩,而且特別喜歡寫長詩。"。

接著,對於這個代理人物件,使用 Use 方法,套用一個中介層,這個中介層會在回答過長時自動要求 AI 代理人重答,直到回答符合字數上限為止。也就是說,我們不需要擔心這個 AI 代理人會給出過長的回答,超過我們設定的字數限制。在以往若要做出這樣的效果,將會需要擁有非常有經驗的開發人員與擁有豐富的技術知識與能力,現在,當透過AI代理人這樣工具之後,就可以輕鬆的處理這樣的需求。

這裡開發出一個 RetryIfTooLong 方法,這個方法內會收到 ChatMessage 集合參數、AgentSession、AgentRunOptions、AIAgent 以及 CancellationToken。對於 messages 這個參數是用於傳遞使用者與 AI 代理人之間的對話內容,有了這個物件便可以取得完整的對話歷史,session 則是用來維持代理人的對話狀態,所以,這個物件可以提供對話的上下文,因此,在這個方法內使用這個物件可以做到保持對話的連貫性,options 則包含執行時的選項,在這個範例專案中,這個參數將會為 null,也就是不使用額外的執行選項,innerAgent 是實際執行請求的 AI 代理人,因此有了這個物件便可以發送請求並取得回答,而 cancellationToken 則用於取消操作。

首先,透過 innerAgent.CreateSessionAsync 方法建立一個對話會話,如果 session 為 null,便會使用這個方法創建新的會話。接著,使用 innerAgent.RunAsync 方法發送請求,在這裡,對於 messages 這個引數將會為 "寫一個關於鵝的詩。",session 因為為第一次使用到,所以裡面沒有任何資料,options 為 null,cancellationToken 則用於取消操作。中介層會檢查回答的字數是否超過 MaxChars,如果超過,便會要求 AI 代理人重答,直到回答符合字數上限為止。

當 await innerAgent.RunAsync 這個非同步方法執行完成後,接下來就是檢查回應的詩是否符合字數上限。如果回答的字數沒有超過 MaxChars,便直接回傳這個回答;否則,螢幕上將會顯示警告訊息文字,例如: "🛡️ 中介層:回答 942 字,超過上限 100 字,要求重答" ,接著會再次呼叫 innerAgent.RunAsync,並告知 AI 代理人回答過長,要求重新生成符合字數上限的詩。若第二次再度執行 AI Agent 之後,得到的回應詩文字長度超過 MaxChars,仍會照樣回傳,但會在螢幕上顯示警告訊息,例如: "⚠️ 中介層:重答後仍有 115 字,照樣回傳" 。

現在回到程式進入點的程式碼, 將會得到第二個 AI 代理人物件 guardedPoet ,接下來就是分別使用 poet 與 guardedPoet 來傳入到 AsAsync 方法內,用來生成詩。這裡對於 poet 來說,沒有套用中介層,因此可能會得到超過字數上限的長詩;而 guardedPoet 則套用了 RetryIfTooLong 中介層,會自動檢查回答的字數,並在回答過長時要求 AI 代理人重答,直到符合字數上限為止。

執行結果

現在來看看這個範例程式碼的執行結果。

  • 在 Visual Studio 2026 下,按下 F5 鍵執行程式,將會看到程式輸出的結果。
使用的模型:gpt-5.6-luna/字數上限:100 字

===== 對照組:沒有中介層 =====
🧑 寫一個關於鵝的詩。
🤖(868 字)
### 《鵝渡長河》

清晨,霜還伏在井沿,
一群鵝從柴門後走出來——
白羽像未寄出的信,
一封一封,
在薄霧裡慢慢展開。

牠們踱過石橋,
踱過昨夜落滿楓葉的路,
腳掌敲著秋天的鼓點:
喀、喀、喀——
把沉睡的村莊叫醒。

最前面的老鵝,
頸項高得像一枝彎月,
牠看過許多河流,
看過南方潮濕的雨,
看過北方雪原上
一盞孤獨的燈。

牠知道,
天空並不總是藍的,
河水也不總是溫柔;
有時風從遠山奔來,
把羽毛吹亂,
把方向吹得像一個謎。

可是鵝仍然叫著——
嘎!嘎!
那聲音粗糲、明亮,
像一把生鏽的鑰匙,
卻能打開清晨的門。

牠們走到河邊,
先把影子放進水裡,
再把身體放進水裡,
最後才把天空
一寸一寸地帶走。

河面忽然遼闊,
彷彿大地在此刻
有了兩隻白色的翅膀;
蘆葦低頭,
魚兒停在水下,
連風也屏住呼吸,
看牠們穿過倒映的雲。

有一隻小鵝落在後面,
牠的羽毛還不夠潔白,
腳步也總被水草纏住。
老鵝回過頭,
沒有催促,
只是把長長的脖子
彎成一座溫柔的橋。

於是小鵝跟上了。
牠們一起游向河心,
游向遠處閃耀的水路,
游向沒有人知道名字的地方。

午後,村人回家,
看見籬笆旁少了幾片羽毛;
孩子撿起它們,
插在草帽上,
以為自己也能飛翔。

夕陽沉入稻田,
鵝群沿著土路歸來,
身上沾著水光,
像從另一個世界
帶回了幾枚金色的鐘。

牠們仍然高聲叫著:
嘎!嘎!
彷彿在說:

不要因為翅膀沉重,
就忘了天空;
不要因為道路漫長,
就停止歌唱。

夜裡,月亮升過屋脊,
鵝群睡在溫暖的草垛旁。
牠們把脖子藏進羽翼,
把白日的河流
夢成一條銀色的長路。

而在夢的盡頭,
仍有一隻老鵝
站在星光下守望——
守望潮汐,守望遠方,
守望每一個
尚未學會飛翔的黎明。

===== 套上中介層 =====
🧑 寫一個關於鵝的詩。
🛡️ 中介層:回答 1102 字,超過上限 100 字,要求重答
🤖(91 字)
### 《鵝渡晚雲》

一隻鵝踏過清晨,
白羽載著微光。
牠在池中划碎雲影,
昂首喊醒村莊。
風起時萬物沉睡,
唯牠守著水岸,
以不滅的潔白,
迎向黎明。

這次的執行結果可以看到,guardedPoet 透過中介層的檢查,成功生成了符合字數上限的詩,而不會像 poet 那樣生成過長的詩。接下來再度重新執行一次,可以從底下的輸出結果看到,guardedPoet 依然會檢查回答的字數,並在超過上限時要求重答。

使用的模型:gpt-5.6-luna/字數上限:100 字

===== 對照組:沒有中介層 =====
🧑 寫一個關於鵝的詩。
🤖(1262 字)
### 《鵝,穿過黃昏的河流》

一隻鵝從村口走來,
帶著白色的風,
帶著一小片未曾融化的冬天,
牠的腳掌踩過泥土,
踩過昨夜的露水,
也踩過孩子們遺落在路旁的笑聲。

牠走得很慢,
彷彿大地是一張尚未寫完的信,
每一步,
都要先讀過石頭的沉默,
再讀過稻穗低頭的原因。

「嘎——」
牠忽然叫了一聲。

池塘裡的月亮便碎了,
碎成許多銀色的小魚,
游向蘆葦深處;
遠山也微微抬頭,
像一位年老的牧人,
聽見遙遠年代的召喚。

鵝不知道自己有多潔白。
牠只知道清晨很冷,
知道霜落在羽毛上時,
世界會暫時變得安靜;
知道一顆穀子,
可以使漫長的午後
有了溫暖的方向。

牠曾在春天見過雨,
雨絲像細小的琴弦,
一根一根落在背上;
牠曾在夏天見過荷花,
荷花把粉紅的火焰
舉到水面之上;
牠曾在秋天見過稻田,
金色的波浪一陣陣湧來,
彷彿整片大地
正在為誰鼓掌。

只有冬天,
冬天沒有掌聲。

冬天的河面結著薄冰,
天空低低地壓著村莊,
屋簷下的風
把每一個名字吹得很遠。
鵝便站在雪裡,
把脖子彎成一枚問號,
問白茫茫的世界:

春天呢?
春天還會回來嗎?

沒有人回答。
但遠處的炊煙升起,
一縷,兩縷,三縷,
像有人在灰色天幕上
慢慢縫補破洞。

於是鵝又叫了一聲——
不是為了恐嚇誰,
也不是為了宣告自己的勇敢,
只是因為沉默太厚,
厚得像一扇關久了的門,
總該有人
用一點聲音把它推開。

牠的叫聲穿過院牆,
穿過枯井,
穿過睡夢中翻身的孩子,
穿過老樹上最後一片葉子,
最後落進河裡,
成為水波一圈一圈
向外擴張的心。

黃昏時,鵝回到籬笆旁。
牠的影子被夕陽拉得很長,
長得像一條白色的道路,
通往屋後,
通往田野,
通往那些尚未發生的日子。

牠低下頭,
在草叢裡尋找一顆遲到的種子。
天邊有雲,
雲中有金色的火,
火焰慢慢熄滅,
而鵝仍然站著。

多麼平凡的一隻鵝啊——
白羽毛,紅腳掌,
一雙總是略帶驚訝的眼睛;
牠不懂星辰的名字,
不懂人間為何有戰爭、離別與謊言,
不懂人們為何匆忙地愛,
又匆忙地告別。

但牠懂得守住門前的清晨,
懂得在黑夜靠近時發出聲響,
懂得把一條小路
走成回家的方向。

所以,當夜色終於落下,
當村莊一盞盞亮起燈,
當每扇窗後
都有人把孤單藏進晚餐的熱氣裡——

請聽,
那隻鵝又在叫了。

嘎——
像一面小小的旗幟,
插在寂靜的土地上;

嘎——
像遠方尚未歸來的船,
在黑暗裡試探潮汐;

嘎——
像一顆不肯熄滅的心,
對著沉沉的世界說:

我在這裡。
我還醒著。
我仍然相信,
明天的河流,
會把天上的光
帶回人間。

===== 套上中介層 =====
🧑 寫一個關於鵝的詩。
🛡️ 中介層:回答 942 字,超過上限 100 字,要求重答
⚠️ 中介層:重答後仍有 115 字,照樣回傳
🤖(115 字)
### 《河岸上的鵝》

晨霧未散,鵝群抖落露水,
嘎聲喚醒沉睡的村莊。
牠們沿河而行,
足印如花,翅膀如帆。
夕陽西下,白影歸草坡;
老鵝仰望星空:
即使道路泥濘,
仍向風與光,
唱出活著的響亮。