2026年10月24日 星期六

 [AI 深度解析] 從應用到能源:看懂 AI 產業鏈與商業風險

摘要:拆解 AI 從應用、模型、晶片到電力與資本的產業鏈,理解商機與風險。



內容:


AI 不是單一產業,而是一條彼此牽動的產業鏈。 OpenAI 執行長山姆·奧特曼曾表示,自己沒有 OpenAI 的股權;但在 OpenAI 之外,他投資過核聚變發電、AI 晶片、語言學習應用等企業,也與資料、內容相關公司有投資關係。這些看似分散的布局,分別對應 AI 的應用、資料、算力與能源。要理解 AI 的發展,不能只看聊天機器人或模型排行榜,還得看支撐它們的設備、資金,以及最終由誰付費。


應用層:誰掌握使用者入口,誰就可能影響交易。 AI 購物助手是一個例子。Perplexity 的 Comet 瀏覽器能依使用者要求搜尋、比較商品;亞馬遜則對其提起訴訟,爭議包括訪問限制、使用者資訊與廣告。當消費者自行逛網站時,平台能展示商品廣告;若消費者只看第三方 AI 整理的結果,平台可能仍賣出商品,卻失去部分廣告曝光。因此,AI 助手爭奪的不只是回答問題的機會,也是影響購買決定的入口。


一套 AI 購物服務可以拆成「應用」與「模型」兩層。模型理解需求、比較資訊並提出建議;應用則連接搜尋、購物車和下單功能,在取得授權後完成操作。寫程式、做設計、處理辦公任務的 AI 工具也有類似結構。一家應用公司可以使用別家的模型,一個模型可以服務許多應用;OpenAI 這類公司則同時開發模型與經營應用。


模型層:能力取決於資料、演算法與算力。 預訓練讓模型從文章、書籍和程式碼等材料中,反覆練習預測與調整參數,建立語言、知識和程式能力的基礎。但「模型越大越好」並非唯一方向。DeepMind 的研究顯示,在相近的訓練計算量下,讓較小模型學習更多資料,也可能取得更好的結果。「規模規律」研究的,正是模型大小、資料量、計算投入與效果之間的關係。


資料多也不等於資料好。網路內容包含錯誤、重複與低品質資訊,必須篩選、清理和安排比例。模型公司也會用 AI 將既有內容改寫為題目、解答或推理練習,製作「合成資料」。這能擴充教材,卻也可能放大原始材料的錯誤,因此核驗仍不可少。


預訓練之後,模型通常還需要「後訓練」,才能更穩定地遵循指令。開發者可以提供優質回答作為示範,進行監督微調;也可以讓模型嘗試解題,再依可核對的結果給予回饋,進行強化學習。數學題較容易判分,但審合約、修改軟體等任務需要專家設計標準,並建立可反覆操作、測試的環境。Scale AI 等公司便在資料、專業回饋與訓練環境上扮演角色。


模型上線後,計算成本並不會停止。 使用者每提出一個問題,系統都要執行模型,這個過程稱為推理。處理難題時,系統還可能讓模型多次嘗試、檢查或比較答案,以更多回答階段的計算換取較好的結果。程式設計智慧體看似只收到一句指令,背後卻可能反覆修改程式、執行測試、讀取錯誤並重新修改。使用者越多、任務越複雜,持續性的算力支出就越值得重視。


降低成本有不同方法。「混合專家」(MoE)模型每次只啟用部分計算模組,減少單步運算,但未啟用的參數仍須儲存;「蒸餾」則讓較強模型提供經檢查的示範,訓練較小模型獨立完成部分任務。兩者都有助於改善效率,卻不能只憑參數或單項測試成績判斷實際設備需求。若一家公司未經授權大量利用另一家模型的輸出進行蒸餾,也可能引發商業爭議。


算力層:晶片之外,還有記憶體、軟體與網路。 GPU 原為大量並行的圖形計算而設計,恰好適合神經網路反覆進行的矩陣運算;CPU 則與 GPU 分工,負責主機程式和任務組織。輝達是重要供應商,但 AMD、Google、亞馬遜、華為等也提供 GPU 或自研 AI 晶片。企業採購時,不只比較晶片性能,也要考慮軟體相容性。許多團隊已圍繞輝達 CUDA 開發和優化,改用其他晶片可能需要重新修改、測試程式;雲服務商因此可能同時採購自研晶片與輝達晶片,服務不同客戶。


模型能不能順利執行,還取決於記憶體容量與頻寬。以 700 億參數、每個參數占兩個位元組估算,單是參數便需要約 140GB 空間,尚未計入運算資料與對話過程保留的資訊。容量決定能放多少,頻寬影響資料能多快送到計算單元。高頻寬記憶體 HBM 因此成為 AI 晶片的重要配套,也帶動 SK 海力士、三星、美光等記憶體廠商的需求。AI 資料中心同時需要系統記憶體和儲存設備,需求變化還可能影響廠商的產能分配與消費產品策略。


買下多顆晶片,也不代表它們就能高效協作。大型模型可能分布在多顆 GPU 上,訓練時也需要不同設備交換結果;資料若傳得慢,晶片便可能等待而無法繼續運算。因此,伺服器、機櫃、交換機、網路介面、NVLink 或 InfiniBand 等連接技術,都會影響整套集群的效率。資料中心的高速連接還需要光模組,在電訊號與光訊號之間轉換。集群擴張與傳輸速度升級,讓網路設備、光模組及其上游光學器件供應商一同進入 AI 產業鏈。


基礎設施層:電力與散熱決定算力何時能上線。 高密度 AI 機櫃用電量大,晶片、記憶體和網路設備工作時又持續產熱。資料中心要投入使用,除了有設備,還得有土地、許可、電網接入、變壓器、配電系統和備援電源。科技公司簽訂長期購電協議、支持核電設施重啟,目的之一是取得大規模且穩定的電力;但合約簽妥,不代表電廠整修、監管審批及輸配電建設能立即完成。自建燃氣發電也可能受到設備供應與工期限制。


電力成本會在資料中心營運的多年期間持續發生,供電地點和方案因此十分重要。變壓器、配電設備、不斷電系統等廠商,會受益於建設需求;但若關鍵設備交貨延遲,昂貴的晶片也可能因機房尚未就緒而無法提供服務。資料中心的大量用電還涉及公共成本:若電網為預期需求擴建,專案卻延後或用電不足,相關費用該由誰承擔?部分地區因此要求大型資料中心對約定供電容量承擔較長期的付款責任。


散熱同樣是一門需要細分的生意。傳統風冷愈來愈難處理高密度機櫃的集中熱量,冷板式液冷便讓液體經過晶片附近,將熱帶到換熱系統,再排出建築物。液冷並不表示所有製冷設備都會增加:若設備能接受較高的冷卻進水溫度,在合適氣候下,部分方案可能省去冷水機,卻仍需要冷板、管路、泵與換熱設備。判斷一家散熱公司的機會,必須看它賣的究竟是哪個環節。機房選址也要同時權衡電價、氣候、水資源與網路延遲;可等待的離線分析能放在較遠地點,即時業務則更重視傳輸速度。


資本層:訂單很大,不等於最終需求已獲證明。 模型研發、晶片採購和資料中心建設都要先投入巨額資金。有些晶片或算力供應商會投資自己的客戶,提供股權誘因,甚至為長期基礎設施合作提供擔保。這能幫助客戶融資、加快建設,也可能替供應商帶來設備訂單;但它讓投資、採購與收入形成相互支持的關係。看到模型公司融資、雲公司簽約、晶片公司接單,不能把它們直接當成三份彼此獨立的終端需求證明。


長期算力合約尤其考驗現金流。模型公司可能為了確保未來供應,承諾多年付款;但它自己的客戶未必願意簽同樣長的合約。若需求快速增長,提前鎖定算力可能成為優勢;若客戶減少使用,收入可以迅速下降,部分機房、算力和電力帳單卻不能同步取消。評估 AI 是否存在泡沫,關鍵不只在投資規模,更在服務能否持續創造收入,以及扣除各項成本後,現金是否足以支撐承諾。


最後仍要回到應用:AI 究竟替客戶創造多少價值? 瑞典支付公司 Klarna 在年報中估算,AI 客服處理大量對話,為公司節省可觀成本。對企業而言,是否採購 AI,最直接的判準是把軟體、人工檢查及錯誤處理都算進去後,它能否可靠地完成工作、降低總成本。這筆新支出可能來自原有的客服外包或軟體預算,因此 AI 公司的收入增長,也可能伴隨其他供應商的收入流失。


教育服務、法律研究及企業軟體都面臨這種重新分配。學生若在搜尋頁或通用 AI 裡取得解答,原本靠搜尋流量與付費題解獲客的業者可能受衝擊;若 AI 讓更少員工完成工作,按帳號數收費的軟體也可能需要調整模式。但既有企業不一定只能被取代:可信的專業資料、可核對的來源、業務系統連接與交付責任,仍可能成為它們在 AI 助手背後收費的理由。


AI 的技術影響與股票報酬,也不能畫上等號。網際網路確實改變了世界,但在熱潮高點買入相關公司的投資者,未必很快得到相稱回報。與其只問哪家公司會受惠,更實際的問題是:自己的工作有哪些步驟已能更快完成?哪些結果仍須人來核驗?客戶真正願意付費的,是便宜的初稿、可靠的專業資料,還是把問題徹底解決的能力?大型投資要經過多年才能驗證;手邊一個能用 AI 改善的工作流程,現在就可以開始測試。

[AI 解析] ChatGPT 的 Chat、Work、Codex 怎麼選?

 [AI 解析] ChatGPT 的 Chat、Work、Codex 怎麼選?

摘要:Chat 適合邊聊邊做,Work 適合完成多步驟任務,Codex 專注軟體開發與維護。



內容:


Chat、Work、Codex 不是三種按聰明程度區分的模型。它們能做的事有所重疊,差別主要在於適合怎樣的工作流程。


Chat 適合邊聊邊做。 例如規劃新咖啡上市時,可以用它發想廣告標題、討論海報方向,再透過對話反覆修改。查知識、翻譯、改文案、分析資料或撰寫程式碼,也都可以先從 Chat 開始。


Work 適合把多個步驟串成成果。 如果任務是查競品、整理價格與賣點,再依公司模板製作上市簡報,Work 更適合依目標推進整套流程。在支援的環境及取得授權後,它也能使用相關檔案、網頁或應用;過程中仍可查看進度、調整要求並驗收成果。


Codex 更適合軟體專案。 Chat 能協助寫一段程式碼,但若要替現有網站新增功能、修復錯誤,並檢查修改是否影響其他頁面,Codex 更適合檢視專案中的多個檔案、修改程式並執行測試。不會寫程式的人也能用自然語言提出需求,但涉及付費、公司系統或重要檔案時,仍應仔細確認結果。


簡單來說:想討論、發想或修改內容,選 Chat;想完成一項環環相扣的工作,考慮 Work;想開發或維護軟體,考慮 Codex。三者不是誰絕對更強,而是各有更順手的使用情境。

2026年10月23日 星期五

[AI 深度解析] 從應用到電力:看懂 AI 產業鏈與商業風險

[AI 深度解析] 從應用到電力:看懂 AI 產業鏈與商業風險

摘要:AI 不只是一場模型競賽;應用收入、晶片供應、資料中心用電與長期合約,共同決定產業能否持續成長。



內容:


OpenAI 執行長山姆・奧特曼曾表示自己沒有 OpenAI 股權,但他在公司之外投資了 AI 應用、晶片與能源企業。這些看似不同的生意,其實連在同一條產業鏈上。要理解 AI,不能只看哪個模型最聰明,還要看它如何取得資料與算力、如何交付服務,以及誰最終願意付錢。


應用是使用者接觸 AI 的入口。 以 AI 購物助手為例,模型負責理解需求、比較商品並提出建議;應用則連接商品資訊、購物車和下單功能。使用者把挑選工作交給哪個助手,也就讓它有機會影響購買決定。Perplexity 與亞馬遜的購物爭議,除了涉及存取方式與使用者資訊,也反映出一個商業問題:如果消費者跳過原本的搜尋與瀏覽流程,平台的廣告曝光可能隨之減少。


模型的能力來自資料、演算法與算力。 預訓練讓模型從文字、程式碼等大量材料中學習基礎能力;後訓練則透過示範、評分與反覆嘗試,讓它更可靠地完成特定任務。合成資料可以擴充教材,蒸餾可以讓較小的模型學習強大模型的示範,但材料仍須核驗,使用其他公司模型的輸出也可能引發授權爭議。


訓練完成不代表計算支出結束。每次使用者提問,都要執行模型;較複雜的任務可能需要多次生成、檢查,甚至反覆呼叫工具。模型公司因此既要負擔研發成本,也要負擔持續服務使用者的推理成本。混合專家架構、小模型與蒸餾等方法,都是在能力、速度和成本之間尋找平衡。


算力也不只是買 GPU。 模型引數和運算中的資料需要足夠的高速記憶體;資料送得夠不夠快,則會影響晶片能否充分工作。HBM 因而成為重要零組件。當模型或訓練任務分散到多顆晶片,伺服器之間還需要高速網路、交換機與光模組。輝達的優勢不只在 GPU,也包括 CUDA 軟體生態;雲端業者則會依客戶需求,同時採用自研晶片和其他供應商的產品。


資料中心再往下,遇到的是電力、散熱與建設時程 。高密度機櫃需要穩定供電,也要持續排出熱量,帶動變壓器、配電、不斷電系統與液冷設備的需求。發電量足夠仍不代表機房能立即上線:電網接入、設備交期、許可與冷卻設施,都可能成為瓶頸。電價、氣候、水資源和網路延遲,也會影響資料中心適合建在哪裡。


建設資金龐大,使產業形成緊密的資本關係:晶片供應商可能投資模型公司,或為相關基礎設施提供支持;模型公司則提前簽下多年算力與電力合約。這能幫助專案啟動,卻也意味著,眼前的融資、訂單和收入可能是同一輪擴張的不同面向。若終端需求不如預期,客戶可以減少使用量,部分長期付款義務卻未必能同步取消。


最終,投入能否回收,仍取決於AI 是否為客戶創造值得付費的價值 。Klarna 用 AI 處理大量客服對話,展示了降低成本的可能;Chegg 面臨的流量與訂閱壓力,則說明 AI 也可能改變既有公司的獲客方式。專業軟體未必都會被取代:如果它掌握可靠資料、業務流程和交付能力,也可以接入 AI 助手繼續提供服務。


AI 可能深刻改變世界,不代表每一筆投資都會有好回報。對企業與個人而言,比猜測產業贏家更直接的起點,是回到手上的工作:哪些步驟能變得更便宜、更快?哪些結果仍需要人核驗?又有哪些原本做不到的想法,現在可以先做出來測試?

2026年10月22日 星期四

MFA008 Session 序列化與記憶持久化

MFA008 Session 序列化與記憶持久化

在上一篇關於 Microsoft Agent Framework 系列教學文章 "MFA007 用 MAF 的 Workflow 把多個代理接成一張圖" 中,我們學會了如何使用多代理工作流將多個 AI 代理串接成一個完整的工作流,並且了解了如何設計代理之間的協作流程。

在這篇文章中,我們將學習如何將 AI 代理的對話狀態進行序列化,並將其持久化到檔案中,從而實現代理的記憶保存與恢復。在此將會產生一個睡前故事的示範,但是這個故事並不會一次說完,而是會分多次進行,每次都會保存當前的對話狀態,並在下次繼續時恢復。當在第二次重新這個專案程式碼的時候,代理將會從上次的對話狀態繼續故事,而不會忘記之前的內容。

在完成這篇文章的練習,我們將會學會如何讓 AI 代理記住之前的對話內容,並在下次繼續時恢復,從而實現持久化的對話記憶。

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

建立 Console 專案

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

安裝需要用到的 NuGet 套件

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

Microsoft.Extensions.AI

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

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

Microsoft.Extensions.AI.OpenAI

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

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

Microsoft.Agents.AI

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

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

OpenAI

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

  • 滑鼠右擊 [csPersistSessionAgent] 專案節點
  • 點選彈出功能表的 [管理 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 System.Text.Encodings.Web;
using System.Text.Json;

namespace csPersistSessionAgent;

internal class Program
{
    private const string StoryStart = "請開始說一個關於一隻叫小熊的睡前故事,只講開頭一小段(約 100 字),講到一個懸念就停下來,先不要講完。";
    private const string StoryResume = "我們昨天講到哪裡了?請先用一句話提醒我,然後接著往下講一小段。";

    private const int PreviewLines = 8;   // JSON 節錄要印幾行

    private static readonly string SessionPath = Path.Combine(AppContext.BaseDirectory, "session.json");

    private static readonly JsonSerializerOptions JsonWriteOptions = new()
    {
        WriteIndented = true,
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
    };

    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,  // 聊天用戶端
            "你是一位溫柔的睡前故事說書人,請用繁體中文,每次只講一小段(約 100 字),語氣輕柔適合小朋友入睡。",  // 系統提示詞 instructions
            "說書人",  // 代理名稱   name
            null); // 代理描述   description

        Console.WriteLine($"{DateTime.Now} 使用的模型: {model}");

        if (File.Exists(SessionPath))
        {
            await TomorrowAsync(agent);   // 明天:載入存檔,故事接著講
        }
        else
        {
            await TonightAsync(agent);    // 今晚:開一個新故事,講到一半就存檔
        }
    }

    private static async Task TonightAsync(AIAgent agent)
    {
        Header("今晚 / 找不到 session.json —— 開一個新故事");

        var session = await agent.CreateSessionAsync();

        Say("你", StoryStart);
        var answer = await agent.RunAsync(StoryStart, session);
        Say("說書人", answer.Text);

        await SaveSessionAsync(agent, session);

        Console.WriteLine();
        Console.WriteLine("👉 現在關掉程式,再執行一次 dotnet run,故事會從這裡接下去。");
    }

    private static async Task TomorrowAsync(AIAgent agent)
    {
        Header("明天 / 找到 session.json —— 故事接著講");

        Console.WriteLine();
        Console.WriteLine("── 對照組:全新 session(沒有昨晚的記憶)──");

        var freshSession = await agent.CreateSessionAsync();

        Say("你", StoryResume);
        var forgotten = await agent.RunAsync(StoryResume, freshSession);
        Say("說書人", forgotten.Text);

        Console.WriteLine();
        Console.WriteLine("---------------------------------------------------");
        Console.WriteLine();
        Console.WriteLine("── 正解:載入 session.json 還原昨晚的 session ──");

        var session = await LoadSessionAsync(agent);

        Say("你", StoryResume);
        var continued = await agent.RunAsync(StoryResume, session);
        Say("說書人", continued.Text);

        await ChatUntilBedtimeAsync(agent, session);
        await SaveSessionAsync(agent, session);

        Console.WriteLine();
        Console.WriteLine("👉 想重新開始一個新故事,把 session.json 刪掉即可。");
    }

    private static async Task ChatUntilBedtimeAsync(AIAgent agent, AgentSession session)
    {
        Console.WriteLine();
        Console.WriteLine("(想繼續聊就輸入一句話,直接按 Enter 結束並存檔)");

        while (true)
        {
            Console.Write("  你 ▸ ");
            var input = Console.ReadLine();

            if (string.IsNullOrWhiteSpace(input))
            {
                break;
            }

            var answer = await agent.RunAsync(input, session);
            Say("說書人", answer.Text);
        }
    }

    private static async Task SaveSessionAsync(AIAgent agent, AgentSession session)
    {
        JsonElement state = await agent.SerializeSessionAsync(session);
        await File.WriteAllTextAsync(SessionPath, JsonSerializer.Serialize(state, JsonWriteOptions));

        ShowSavedFile();
    }

    private static async Task<AgentSession> LoadSessionAsync(AIAgent agent)
    {
        var json = await File.ReadAllTextAsync(SessionPath);

        using var document = JsonDocument.Parse(json);
        return await agent.DeserializeSessionAsync(document.RootElement);
    }

    private static void ShowSavedFile()
    {
        var bytes = new FileInfo(SessionPath).Length;

        Console.WriteLine();
        Console.WriteLine($"💾 已存檔:{SessionPath}({bytes:N0} bytes)");

        foreach (var line in File.ReadLines(SessionPath).Take(PreviewLines))
        {
            Console.WriteLine($"  │ {(line.Length > 70 ? line[..70] + "…" : line)}");
        }

        Console.WriteLine("  │ …(略)");
    }

    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 物件,該代理人綁定的系統提示詞為 "你是一位溫柔的睡前故事說書人,請用繁體中文,每次只講一小段(約 100 字),語氣輕柔適合小朋友入睡。"。

接下來會先檢查是否有任何之前的對話 session 內容存在,若有的話,這表示存在著昨天還未說完的故事,此時,將會把昨天講的故事讀取進來,並恢復到代理的對話狀態中,從而可以接著昨天的故事繼續講下去。否則,將會從頭開始一個新的故事。

當要讀取昨天的故事內容,將會呼叫 TomorrowAsync 方法來載入並還原之前的對話 session。在這個方法內,不過,在這個方法內,將會使用兩種做法,使得可以看到兩者間的差異,第一個就是採用 對照組:全新 session(沒有昨晚的記憶) 方式。在這裡,將會使用 var freshSession = await agent.CreateSessionAsync(); 敘述來建立一個新的對話 session,然後再呼叫 var forgotten = await agent.RunAsync(StoryResume, freshSession); ,這裡使用的提示詞為這個變數 StoryResume 的字串: "我們昨天講到哪裡了?請先用一句話提醒我,然後接著往下講一小段。",當執行完之後,就可以從 forgotten.Text 看到語言模型產生的結果,如下所示。

── 對照組:全新 session(沒有昨晚的記憶)──

  你 ▸ 我們昨天講到哪裡了?請先用一句話提醒我,然後接著往下講一小段。

  說書人 ▸ 我這裡看不到昨天的故事內容,就先從小月亮來到森林邊繼續吧。

小月亮輕輕降下銀色的光,照見一隻迷路的小兔子。牠蜷在草叢旁,耳朵微微發抖。小月亮說:「別怕,我陪你找回家的路。」於是,星星們一顆接一顆亮起,像溫柔的小燈,陪著他們走過安靜的林間小徑。

從這句話可以看出 "說書人 ▸ 我這裡看不到昨天的故事內容,就先從小月亮來到森林邊繼續吧。",也就是說,語言模型並沒有記住昨天的故事內容,因此只能從新的情境開始講故事。這樣的結過並不是符合當初設計的預期,真正的目的是希望語言模型能夠記住之前的故事內容,並在新的對話中延續下去。

因此,在這個方法的後半段,將會要做到 "載入 session.json 還原昨晚的 session" 的操作,從而使語言模型能夠記住之前的故事內容,並在新的對話中延續下去。在這裡將會呼叫 LoadSessionAsync(agent) 來載入之前保存的 session,最後將其還原到 freshSession 中。

這裡首先呼叫 var json = await File.ReadAllTextAsync(SessionPath); 來讀取 session.json 的內容,接著再呼叫 JsonDocument.Parse(json) 來解析 JSON 文件,這裡將會得到型別為 JsonDocument 的物件,之後就可以從中提取出之前保存的對話歷史,並還原到 freshSession 中。最後,執行 await agent.DeserializeSessionAsync(document.RootElement) 便可以得到一個代理人物件,並且這個代理人將會記住之前的故事內容。

現在回到 TomorrowAsync 方法內,這裡將會執行 var continued = await agent.RunAsync(StoryResume, session);,這裡 StoryResume 的提示詞仍然是 "我們昨天講到哪裡了?請先用一句話提醒我,然後接著往下講一小段。",現在就來看看,這個代理人物件會得到甚麼故事內容,這裡是測試後, continued.Text 的結果。

昨天說到:小熊在草叢裡發現了一顆會呼吸、閃著藍光的星星。

小熊輕輕捧起星星,感覺它一呼一吸,掌心便暖暖地亮一下。「你是不是迷路了?」小熊小聲問。星星沒有回答,只朝森林深處飄去,留下一串銀色腳印。小熊跟著走啊走,來到一扇從沒見過的小門前,而門後竟傳來自己的聲音……

從上面的產生內容,可以看到這個代理人物件,因為有讀取之前對話的紀錄檔案內容,所以能夠記住昨天的故事,並在新的對話中延續下去。並且依照提示詞的指示,繼續往下又說了一段故事。

接著,將會呼叫 ChatUntilBedtimeAsync 方法,在這個方法內,你可以輸入一些文字內容,與這個代理人物件互動,這樣就可以依據剛剛輸入的文字來繼續產生新的故事內容出來,直到你覺得故事可以結束為止。

最後,將會呼叫 SaveSessionAsync 方法,將當前的對話 session 保存到 session.json 文件中,這樣在下次啟動程式時,就可以載入這個 session,繼續延續之前的故事。

若沒有找到之前說故事的記憶檔案,這個程式將會呼叫 TonightAsync 方法,從頭開始一個新的故事。在這個方法內,同樣的會先執行 var session = await agent.CreateSessionAsync();,然後再呼叫 var result = await agent.RunAsync(StoryStart, session); 來開始新的故事。StoryStart 的提示詞仍然是 "請開始說一個關於一隻叫小熊的睡前故事,只講開頭一小段(約 100 字),講到一個懸念就停下來,先不要講完。" 當新的故事講完之後,同樣的會呼叫 SaveSessionAsync 方法,將當前的對話 session 保存到 session.json 文件中。在這裡也是執行了 JsonElement state = await agent.SerializeSessionAsync(session) 敘述,然後再將其寫入 session.json 文件,以便下次啟動程式時可以載入這個 session。

執行結果

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

  • 在 Visual Studio 2026 下,按下 F5 鍵執行程式,將會看到程式輸出的結果。
2026/10/20 下午 01:15:56 使用的模型: gpt-5.6-luna

==========================================================================
  今晚 / 找不到 session.json —— 開一個新故事
==========================================================================

  你 ▸ 請開始說一個關於一隻叫小熊的睡前故事,只講開頭一小段(約 100 字),講到一個懸念就停下來,先不要講完。

  說書人 ▸ 月亮升上森林時,小熊抱著心愛的星星枕頭,準備回樹洞睡覺。忽然,草叢裡傳來細細的「叮咚」聲,像有人在夜裡敲著小鈴鐺。小熊循聲走去,看見一扇從沒見過的藍色小門,而門縫裡,正透出一點金色的光……

💾 已存檔:C:\Vulcan\Github\CSharp2026\csPersistSessionAgent\csPersistSessionAgent\bin\Debug\net10.0\session.json(1,045 bytes)
  │ {
  │   "stateBag": {
  │     "InMemoryChatHistoryProvider": {
  │       "messages": [
  │         {
  │           "role": "user",
  │           "contents": [
  │             {
  │ …(略)

👉 現在關掉程式,再執行一次 dotnet run,故事會從這裡接下去。

從上面的輸出內容,可以看到程式在找不到 session.json 時,會從頭開始一個新的故事。

現在停止這個專案執行,然後再次來執行這個專案

2026/10/20 下午 02:39:55 使用的模型: gpt-5.6-luna

==========================================================================
  明天 / 找到 session.json —— 故事接著講
==========================================================================

── 對照組:全新 session(沒有昨晚的記憶)──

  你 ▸ 我們昨天講到哪裡了?請先用一句話提醒我,然後接著往下講一小段。

  說書人 ▸ 我看不到昨天的對話紀錄,就先從小兔子準備走進月光森林開始吧。

小兔子抱著星星燈,沿著銀白色的小路慢慢前進。林子裡沒有吵鬧的聲音,只有風兒輕輕唱歌。忽然,一片葉子飄到牠耳邊,悄悄說:「別害怕,今晚的月亮會陪著你。」小兔子安心地笑了,踩著柔軟的月光,朝森林深處走去。

---------------------------------------------------

── 正解:載入 session.json 還原昨晚的 session ──

  你 ▸ 我們昨天講到哪裡了?請先用一句話提醒我,然後接著往下講一小段。

  說書人 ▸ 昨天我們說到:小熊在古老樹洞裡,看見深處亮起了兩盞藍色的燈。

小熊屏住呼吸,抱緊蜂蜜罐,輕聲問:「你是誰呀?」藍光慢慢靠近,照出一對圓圓的眼睛,還有一頂歪歪的小帽子。那個神秘身影小聲說:「別害怕,我一直在等你,因為森林的月亮……不見了。」小熊抬頭一看,夜空果然少了一輪月亮,而樹洞裡忽然傳來咚、咚、咚的聲音……

(想繼續聊就輸入一句話,直接按 Enter 結束並存檔)
  你 ▸ 我還想繼續聽一段故事

  說書人 ▸ 小熊望著那頂歪歪的小帽子,鼓起勇氣問:「月亮去哪裡了呢?」小身影指向樹洞深處,說:「它被一陣淘氣的風,吹進了沉睡的雲谷。」於是,小熊牽起神秘朋友的手,沿著微弱的藍光往前走。走著走著,前方出現一扇小小的銀色門,門上刻著一行字:只有說出最溫柔的夢,才能打開它……
  你 ▸

💾 已存檔:C:\Vulcan\Github\CSharp2026\csPersistSessionAgent\csPersistSessionAgent\bin\Debug\net10.0\session.json(3,136 bytes)
  │ {
  │   "stateBag": {
  │     "InMemoryChatHistoryProvider": {
  │       "messages": [
  │         {
  │           "role": "user",
  │           "contents": [
  │             {
  │ …(略)

👉 想重新開始一個新故事,把 session.json 刪掉即可。

在文字 "── 正解:載入 session.json 還原昨晚的 session ──" 與 "(想繼續聊就輸入一句話,直接按 Enter 結束並存檔)" 之間,程式會嘗試讀取 session.json,並將其中的對話歷史還原到當前的 session 中,這樣就能接著昨天的故事繼續講下去。我在這裡輸入了 "我還想繼續聽一段故事",所以就看到了程式便接著又講了一段故事。這也說明了 session 的持久化功能,能夠讓對話在程式重啟後仍然保持連貫。最後,我不想在聽故事了,就可以直接按 Enter 結束,程式會將當前的 session 保存到 session.json 中,以便下次繼續使用。