2026年11月11日 星期三

[AI 觀點] Linux 核心撤除 AI 標註規定:工具不是問題,責任才是關鍵

[AI 觀點] Linux 核心撤除 AI 標註規定:工具不是問題,責任才是關鍵

摘要:Linux 核心撤除 AI 使用標註要求,引發工具使用與開發者責任之爭。



內容:


2026 年 10 月,Linux 核心社群決定撤除一項實施不到一年的規定:開發者提交程式碼時,不必再於變更紀錄中標註是否使用人工智慧工具。這項決定引發爭論。反對者擔心,缺少揭露機制會讓維護者更難辨識不負責任的提交;Linux 創始人林納斯·托瓦茲則主張,審查重點應是開發者是否理解並能為程式碼負責,而不是他使用了什麼工具。


爭議始於 2026 年初。Linux 核心 7.0 版的開發文件加入〈寫程式助手指南〉,要求開發者若使用進階寫程式工具協助開發或修補,便在變更紀錄中加入「Assisted-by」標籤,說明所用模型。根據 Linux 週刊主編強納森·科貝特的統計,Linux 7.3 版共合併 18,533 筆非合併變更,其中 1,884 筆帶有這項標籤,約占一成。


標註數量雖然可觀,部分維護者卻質疑它的實際價值。紅帽公司的核心維護者傑夫·雷頓於 7 月提議廢除標籤,理由包括:規定無法確保所有人如實遵守;標籤可能使核心變更紀錄成為商業模型的免費宣傳;更重要的是,知道開發者用了哪個模型,既不能證明程式碼正確,也不能取代漏洞審查。


穩定版核心維護者葛雷格·克羅哈特曼起初反對廢除,認為標籤至少能提示哪些變更值得特別留意。但到了 10 月 8 日布拉格核心維護者峰會,他改變立場,認為這套做法無法有效運作,反而增加維護者負擔。峰會形成共識後,維護者薩莎·萊文隔天送出修補程式,刪除提交指南中強制加入「Assisted-by」標籤的要求。


影像子系統開發者洛朗·潘夏爾反對直接拿掉規定。他擔心有人不理解核心運作,只把大型語言模型產生的內容複製貼上至郵件列表,成為他所稱的「人肉代理」(meat proxies)。他主張,即使取消原有標籤,也應建立其他揭露或約束機制,例如要求提交說明由人類親自撰寫,藉此表明提交者確實參與並理解工作。他也指出,規則未必能百分之百防弊,但仍能向社群清楚傳達期望;開發者的 Signed-off-by 簽名同樣無法從技術上杜絕作弊,卻仍有其規範意義。


這份憂慮並非憑空而來。科貝特的峰會記錄提到,Linux 7.3 版週期出現將近 800 名首次貢獻者;其中有些人在短時間內跨多個子系統提交複雜變更,卻未必理解修改內容,也不一定會留下來維護。對維護者而言,真正令人不安的是程式碼湧入的速度,可能超過社群理解、審查與長期維護的能力。


然而,核心安全維護者基斯·庫克質疑,靠提交說明的文風辨識 AI 並不可靠。他指出,自己也曾與模型協作撰寫提交紀錄,還曾把另一位維護者親手寫的詳細說明誤認為模型產物。人類撰寫與工具協助之間,已很難僅憑肉眼劃出清楚界線。


10 月 13 日,林納斯·托瓦茲回信反駁禁止工具的思路。他以反諷方式表示:若認為不用 AI 才能證明開發者理解程式碼,那是否也該禁止編譯器,要求所有人手寫組合語言?若要確保提交說明是親手寫的,是否連可修改錯字的文字編輯器都該禁止?他的核心論點是,社群應要求貢獻者全力以赴、真正理解自己的修改,不能把「未使用某種工具」誤當成能力證明。


到了 10 月 26 日,面對大型語言模型輸出不具確定性的質疑,托瓦茲再次指出,人類提交的結果同樣不具確定性,不同版本的編譯器也可能產生不同輸出;單靠這一點,無法構成全面排斥工具的理由。在他看來,問題是有人不理解核心,卻提交自己無法解釋或維護的變更,而不是工具本身。


企業界也出現相近觀點。Shopify 執行長託比·呂特克認為,反 AI 討論有時高估了人類日常撰寫程式碼的品質:人寫的程式碼一樣可能粗糙,AI 工具也可能協助改善。這並不表示 AI 產出可以免於審查,而是提醒人們,純手寫不等於高品質,使用工具也不必然代表缺乏能力。


目前開源社群對此仍有不同路線。System76 的桌面環境與 NetBSD 等專案,選擇限制提交 AI 生成的程式碼;Linux 核心則撤除強制標註要求,採取更重視成果與責任的做法,並嘗試運用 Sashiko 等代理式工具協助審查。


撤除標籤不代表撤除責任。Linux 核心仍保留開發者來源證明與 Signed-off-by 要求。當工具能迅速產生大量程式碼,工程師的價值不只在於寫出程式,更在於理解修改、說明判斷、修正問題,並以自己的名字承擔後果。

[.NET11 新功能] .NET 11 System.Text.Json:聯集、封閉類別與 AOT 支援

[.NET11 新功能] .NET 11 System.Text.Json:聯集、封閉類別與 AOT 支援

摘要:整理 .NET 11 System.Text.Json 的聯集與封閉類別支援、來源產生器改進及新增的 JSON 註記功能。



內容:


Microsoft .NET Libraries 團隊的首席工程師 Eric 介紹了 .NET 11 中 System.Text.Json(STJ)的更新。今年的重點包括支援聯集型別與封閉類別、改善來源產生器和原生 AOT 的使用體驗、提供更具表達力的型別註記,以及修正可靠性問題。團隊共合併 60 個 PR,其中 17 個涉及新功能或 API、16 個改善來源產生器與 AOT、8 個改善效能、19 個修正錯誤。


聯集型別的序列化與反序列化


STJ 能辨識聯集型別,並直接依照目前聯集成員的型別輸出 JSON。例如,一個可容納整數、字串或一般物件的聯集,序列化後分別會得到 JSON 數字、字串或物件;不會額外加入包裝結構或型別識別欄位。


反序列化時,STJ 通常根據 JSON 值的類型選擇聯集成員。因此,包含數字、字串與物件的 JSON 陣列,可以反序列化成對應的聯集陣列。但這也帶來限制:如果多個聯集成員都對應同一種 JSON 類型,STJ 就無法僅憑 JSON 值判斷目標型別。例如 DateOnly 與 Guid 都會表示為 JSON 字串;由兩者組成的聯集可以正常序列化,反序列化時卻可能因目標型別不明確而失敗。


STJ 採取這種保守的型別推斷方式,是為了避免不受限制的多型反序列化所帶來的安全問題。若需要處理同一 JSON 類型對應多個 .NET 型別的情況,可以註冊型別分類器,讓程式檢查 JSON 內容,再決定要繫結至哪個型別。例如,分類器可以嘗試將字串解析為日期或 GUID,並據此選擇聯集成員;若兩者都不符合,則回報錯誤。


對於由不同物件組成的聯集,STJ 也提供內建的結構分類方式。以 Dog 與 Cat 兩種物件為例,若它們具有足以區分彼此的屬性,反序列化時就能依 JSON 中出現的屬性選擇型別。這種分類只看屬性結構,不會深入分析屬性值,因此較為脆弱:一旦兩種物件日後新增相同屬性,原本可區分的結構就可能變得模糊。


封閉類別與多型 JSON 契約


講者接著介紹封閉類別。過去若要序列化多型的事件模型,通常必須為每個衍生型別明確加入 JsonDerivedType 等註記,並設定型別識別值。一般類別的繼承階層是開放的,編譯時無法得知所有可能的衍生型別,因此需要逐一宣告。


封閉類別的衍生型別則限定於目前的編譯範圍,編譯器可以提供完整清單。STJ 因而能在啟用封閉型別多型推斷後,自動取得這些衍生型別,減少隨模型擴大而增加的註記程式碼。


若能自行決定 JSON 契約的設計,講者建議優先使用封閉類別階層來表達多種選擇。封閉類別同樣能提供窮舉處理的保證,且可使用型別識別欄位,使結構在日後擴充時較為穩定。聯集型別不使用這類識別欄位;當它必須依內容或屬性結構判別型別時,契約演進便較容易受到影響。若既有結構無法更改,例如必須與 TypeScript 風格的聯集資料互通,聯集仍是合適選擇。


來源產生器與原生 AOT 改進


.NET 11 縮小了 STJ 反射式序列化與來源產生器之間的功能差距。過去,透過 JsonConstructor 或 JsonInclude 存取私有成員的情境,來源產生器未能完整支援;現在可在較新的執行環境利用 unsafe accessors 存取,在較舊環境則透過相容方式處理,同時兼顧 AOT 安全性。


init 屬性的指派行為也獲得改善。過去即使 JSON 中沒有對應屬性,來源產生器仍可能指派預設值,導致原有資料被覆蓋;現在可以依欄位是否出現在 JSON 中,有條件地進行指派。此外,JSON 序列化內容類別可宣告為 partial,方便將較大的內容定義拆分至多個檔案。


另一項重要改進是泛型轉換器註記。以往泛型物件若需要自訂 JSON 轉換器,往往還得撰寫使用反射的 JsonConverterFactory;這不但程式碼繁複,也不利於原生 AOT。新版允許在轉換器註記中使用開放泛型宣告,由來源產生器補入對應的泛型參數。使用時,轉換器的泛型參數必須與目標型別相符。


更具表達力的 JSON 註記


.NET 11 新增可套用於型別的命名原則註記,使特定型別的 JSON 屬性名稱不再完全取決於執行時的 JsonSerializerOptions。例如,可以要求某個型別的屬性一律使用 camelCase,同時針對個別屬性覆寫為 kebab-case。這有助於讓 JSON 契約不受外部選項變動影響。


原本主要用於個別屬性的忽略條件註記,也擴充至型別層級。若希望某個型別的所有 null 屬性都不輸出,便不必在每個屬性上重複加入相同註記。


其他更新與使用建議


其他更新包括:新增 JSON Lines 序列化支援,以補齊既有的反序列化能力;加入 F# 辨識式聯集的內建支援,但講者提醒聯集支援目前使用反射,尚不支援來源產生器;支援泛型多型階層及相關的開放泛型註記;新增 PascalCase 命名原則,以及支援 .NET 11 新增的數值與集合型別。


整體而言,這次更新一方面讓 STJ 配合新的語言型別模型,另一方面著重減少來源產生器與 AOT 情境中的限制。實際設計資料模型時,若 JSON 結構由自己掌控,應優先考慮可使用型別識別欄位的封閉階層;若必須遵循既有、沒有識別欄位的 JSON 結構,再選用聯集,並留意反序列化時的型別歧義。

[.NET11 技術解析] .NET MAUI 在 .NET 11 轉向 CoreCLR:原理、效能與遷移重點

[.NET11 技術解析] .NET MAUI 在 .NET 11 轉向 CoreCLR:原理、效能與遷移重點

摘要 : .NET MAUI 在 .NET 11 改用 CoreCLR,整合診斷能力,並透過 Ready to Run 改善行動應用體驗。



內容:


.NET MAUI 與 .NET 工程師 John Peppers 分享了 .NET MAUI 在 .NET 11 轉向 CoreCLR 的歷程。這項工作橫跨多年、涉及多個團隊,核心目標是讓行動應用開發不再像獨立於 .NET 之外的體系,而是真正融入統一的 .NET 平台。他以較熟悉的 Android 為主要例子說明;其中許多概念也適用於 iOS。


從 Xamarin 走向統一的 .NET


過去的 Xamarin 由多個部分組成:Xamarin.Forms 讓開發者使用 XAML 或 C# 建立跨平台介面,角色相當於今日的 .NET MAUI;Xamarin.Android 與 Xamarin.iOS 負責銜接各平台 API,並將應用程式封裝成可提交至商店的形式;底層則使用開源的 Mono 執行階段及其基礎類別庫。由於 Xamarin 原本有一套平行實作,微軟在 2016 年收購 Xamarin 後,便持續推動行動平台與 .NET 的整合。


2020 年左右,Mono 執行階段的程式碼被移入 .NET runtime 儲存庫,並保留原專案歷史。隨後 .NET 6 於 2021 年推出,.NET MAUI 先以預覽版亮相,再於隔年 5 月正式發布。團隊也提供升級工具,協助使用者從 Xamarin 遷移。儘管如此,行動應用底層仍持續使用 Mono;轉向 CoreCLR,是這段整合歷程的重要一步。


執行階段究竟負責什麼?


C# 程式碼編譯後會成為組件中的中介語言(IL)。.NET 執行階段負責載入並執行這些程式碼,依裝置架構與作業系統運作,同時提供垃圾回收、型別系統及例外處理等能力。它也透過基礎類別庫銜接底層平台:例如開發者呼叫 System.IO.File 時,.NET 會使用所在作業系統對應的功能完成檔案操作。


CoreCLR 是已廣泛用於 .NET 伺服器與桌面應用的成熟執行階段。將它帶到 .NET MAUI,除了統一底層架構,也能讓行動應用逐步受益於相同的偵錯、追蹤及診斷能力。過去 Mono 有自己的偵錯器,部分追蹤事件及當機報告的呈現方式也與 CoreCLR 不同;改用 CoreCLR 後,這些工具體驗有機會更加一致。


切換後,應用程式如何啟動?


行動應用首先由作業系統啟動,接著才輪到 .NET 程式碼執行。以 Android 為例,建置程序會替應用程式設定一個用來啟動 .NET 執行階段的 Content Provider,使執行階段能在主要 Application.OnCreate 等生命週期程式碼執行前就緒。之後,Android 才會呼叫應用程式及 Activity 的相關方法。這些細節多半已由專案範本和建置工具處理,開發者通常不必手動介入。


在這個啟動環節,底層原本會啟動 Mono,改版後則啟動 CoreCLR。團隊希望這項替換對一般開發者盡可能透明:既有 C# 程式碼與大多數 NuGet 套件,原則上應維持原有行為。


兩種執行階段仍有較底層的差異。Mono 提供豐富的嵌入 API,讓原生 C 或 C++ 程式碼載入型別、呼叫方法並檢查 .NET 型別;CoreCLR 沒有完全對等的 API 集合。因此,依賴 Mono 嵌入 API 的特殊專案,例如某些遊戲引擎整合方式,可能需要重新設計。不過,這通常不是一般 .NET MAUI 應用會碰到的層次。


Mono AOT、Ready to Run 與 Native AOT 有何不同?


這次轉換容易令人混淆的地方,是「AOT」並非只指一種技術。一般而言,C# 程式碼先編譯成 IL;若在執行時由即時編譯器將 IL 轉為原生碼,稱為 JIT。Android 可以使用 JIT;但 iOS 對執行記憶體中的即時產生程式碼有限制,因此不能以相同方式使用純粹的 JIT,必要時也會用到直譯器。相對地,預先編譯(AOT)會在建置期間先產生原生碼。


過去使用 Mono AOT 時,建置工具 mono-aot-cross 會處理應用程式的 DLL,並為各 DLL 產生對應的原生函式庫。改用 CoreCLR 後,對應的重要技術是 Ready to Run(R2R):CrossGen2 會在建置時產生原生碼,並將其放入仍含有 IL 的 DLL。這也是啟用 Ready to Run 後,DLL 可能變大的原因。


在 Android 上,.NET MAUI 預設採用部分 Ready to Run,而非將所有程式碼都預先編譯。團隊透過具有代表性的 MAUI 應用記錄啟動時用到的方法,將資料保存為 MIBC 檔案,交給 Ready to Run 編譯器使用。如此可優先加快啟動路徑,同時避免完整預先編譯造成應用程式體積大幅增加。開發者仍可評估自己的應用,決定是否以較大的檔案換取完整 Ready to Run 可能帶來的效能收益。iOS 則因無法以相同方式使用 JIT,沒有採用 Android 的這種部分 Ready to Run 預設方案。


應用程式的封裝方式也會隨之變化。在 Android 上,.NET 組件會被放入名為 libassemblystore 的原生函式庫中,便於依裝置架構封裝與配送。使用 CoreCLR 時,其中除了 IL,也可能包含 Ready to Run 映像;一般開發者通常不需要直接處理這些內部檔案。


Native AOT 是另一條路,不等於這次的 CoreCLR 切換


Native AOT 不只是「把 Ready to Run 開得更完整」,而是另一種執行階段與建置模式。它在建置時完成編譯,不依賴 JIT,並積極移除未使用的程式碼。其成果通常是啟動更快、體積更小的應用;例如 Android 可產生一個包含執行階段與已編譯 .NET 程式碼的 .so 檔案。不過,在這場分享所述的階段,Android 上的 Native AOT 仍屬預覽功能。


Native AOT 也要求開發者更仔細處理動態行為。例如,執行時才從磁碟載入 DLL 的外掛模式,便不適合直接沿用。序列化也是常見例子:若程式依賴 System.Text.Json 在執行時動態檢查物件屬性,程式碼裁剪器可能無法判斷哪些成員必須保留;改用 System.Text.Json 的原始碼產生器,則能提供明確的型別資訊,使裁剪與 Native AOT 建置更可靠。既有應用若要導入 Native AOT,除了自身程式碼,也須檢查第三方 NuGet 套件是否相容。


目前觀察到的效能與體積變化


John 展示了以 Samsung Galaxy A16 等裝置測試的代表性結果,涵蓋 MAUI 專案範本、加入更多控制項與圖表的範例,以及實際以 .NET MAUI 建置的 Microsoft 365 Admin 應用。在 Android 上,Mono、預設的部分 Ready to Run,以及完整 Ready to Run 的啟動表現大致可比;部分與完整 Ready to Run 哪個較快,會因應用而異。在 iOS 的測試中,展示的應用啟動時間則普遍有所改善。


體積方面,Android 的部分 Ready to Run 與原先 Mono 版本相近,完整 Ready to Run 通常會讓 APK 稍微變大;展示的 iOS 測試則呈現應用程式體積縮小的趨勢。這些結果並不保證每個應用都會相同,因此團隊希望開發者以自己的實際專案測量;若出現明顯異常,也歡迎回報以便調查。


升級至 .NET 11 時應檢查什麼?


分享指出,在 .NET 10 中,開發者可透過將 UseMonoRuntime 設為 false,以實驗性方式試用 CoreCLR;而在其介紹的 .NET 11 預覽版規劃中,切換目標框架即可預設改用 CoreCLR。CoreCLR 自 Preview 4 起成為預設,Preview 6 起則移除了在 .NET 11 中指定 Mono 的能力。若遇到必須依賴 Mono 的罕見相容性問題,團隊希望開發者回報,並可暫時留在 .NET 10。


理想情況下,升級不需要修改應用程式碼,但仍應完整執行測試,特別留意啟動時間、記憶體用量、安裝檔大小及實際功能行為。診斷流程也值得重新確認:改用不同執行階段後,當機報告與堆疊追蹤的格式或內容可能有所不同。若專案使用了 Mono 嵌入 API,則需要另外評估替代設計;特定原生程式碼呼叫 .NET 的情境,也可研究 Native AOT 與 UnmanagedCallersOnly 是否適用。


從 Xamarin、.NET MAUI 到 CoreCLR,這次轉換的方向始終一致:讓行動應用成為 .NET 平台更完整的一部分。團隊期望開發者在盡量不更動既有程式碼的前提下,取得更一致的執行階段、診斷能力與具競爭力的效能,並持續推進 Native AOT 等後續工作。

2026年11月10日 星期二

[AI 趨勢] AI 降低創業門檻後,什麼能力變得更值錢?

[AI 趨勢] AI 降低創業門檻後,什麼能力變得更值錢?

摘要:YC 觀察到獨自創業、硬科技與營收成長加速;當寫程式碼變便宜,產業經驗與選題能力更顯珍貴。



內容:


二十年前,Y Combinator(YC)聯合創始人 Paul Graham 曾把「一個人創業」列為可能害死新創公司的頭號錯誤。如今,YC 總裁 Gary Tan 卻在播客《Lightcone》中說:「YC 自己就是獨自創業者的 YC。」這並不代表找搭檔已經過時,而是 AI 正在改變創業初期需要多少人、哪些能力最難取得。


**第一個變化,是獨自創業者變多了。**YC 合夥人 Diana 表示,過去獲選公司的獨自創業者約占 5%,如今已超過 18% 至 19%,接近原來的四倍。過去創業往往需要一人擅長銷售與推廣,另一人具備頂尖技術能力;兩種本事很難集中在同一人身上,因此找一位技術共同創始人,常被視為必要條件。現在,AI 程式設計代理能承擔更多開發工作,創業者更有機會先把產品做出來。難題也因此轉向:要知道該解決什麼問題、該讓 AI 做什麼,以及什麼產品值得做。


不過,YC 仍認為共同創始人是加分項。Instacart 和 Coinbase 進入 YC 時都只有一位創始人,之後才找到搭檔。因此,更貼切的說法或許是「先一個人開工,跑通後再找人」。數據也須留意口徑:有人逐一統計某屆冬季班的 199 家公司,其中 22 家是獨自創業,約占 11%,低於 YC 提到的比例;但相較於過去約 5%,增加的方向一致。


**第二個變化,是資深創業者更能發揮優勢。**Gary Tan 說,他最近見到的一些出色創始人,年齡在三十多、四十多歲,甚至五十多歲。他舉 OpenCLAW 作者 Peter Steinberger 為例:對方四十歲,先前有十年經營公司的經驗。合夥人 Jared 指出,管理 AI 程式設計代理,在某些方面與管理人員相似:都要拆解任務、訂立標準、驗收成果,並預判容易出錯之處。這些能力往往來自長年的實務經驗。當「不會寫程式碼」或「找不到技術搭檔」不再像以前那樣難以克服,熟悉一個行業的人,也更有機會親手驗證自己的想法。


**第三個變化,是更多新創走向硬科技。**Diana 表示,YC 批次中硬科技公司的占比,從約 8% 升至 20%。機器人、工廠、國防、晶片與電力等領域,都出現顯著增長。YC 將這股趨勢稱為「從位元走向原子」:不只開發螢幕裡的軟體,也開始處理真實世界中摸得到的設備與設施。


AI 讓軟體更容易開發,為什麼反而有利於硬體?原因之一是,許多硬科技公司的瓶頸不只在硬體本身。無人機或機器人背後,都需要控制系統、韌體及後台軟體;過去要組建足夠強的軟體團隊,往往得與大型科技公司爭奪人才。如今,少數熟練工程師配合 AI,可能就能完成更多開發工作,讓團隊有餘力處理製造與現場部署等難題。


YC 提到的 Knox Metals,便在底特律閒置多年的老工廠中重新從事金屬加工。Paul Graham 曾形容,這家金屬加工公司的成長速度像軟體公司;其需求來自一批急需供貨、傳統供應商卻難以跟上的國防科技新創。節目還提到,某屆夏季班的創始人約每六人就有一位博士,顯示不少人選擇挑戰技術門檻更高的題目。


**第四個變化,是新創更快取得收入,產品也從「工具」走向「完成工作」。**YC 投資的公司剛加入時,通常尚無收入;三個月後參加 Demo Day,過去月收入中位數約為 8,000 美元,如今約為 20,000 美元,是原來的兩倍半。節目也提到,部分公司能在這三個月內,從零成長至百萬美元級別營收;過去達到類似規模,可能需要十八個月以上。


YC 同時觀察到,提供「端到端完成一份工作」服務的公司,占比從約 10% 升至超過 25%。招聘產品 Juicebox 是一個例子:過去它主要幫客戶找到符合條件的人選,後續聯絡、安排面試仍由招聘人員處理;現在,其 AI 代理也能發訊息、約面試,接手更多流程。同一位客戶願意支付的費用可能因此增加兩到三倍,而招聘人員則能把時間用在判斷人選是否適合團隊等較難交給 AI 的工作。


這也是 YC 對「軟體已死」的回應:軟體並沒有消失,而是在改變角色。它可以像員工一樣直接把事辦完,也可以成為讓 AI 穩定工作的環境與流程,也就是所謂的 harness。客戶付費購買的,越來越不只是操作工具的權利,而是工作完成的結果。


**第五個變化,是模型所需的資料與練習環境,正在形成生意。**Jared 表示,過去兩年 YC 投資了十多家相關公司,每家年收入都超過一千萬美元,部分達到數億美元。它們主要向大模型公司提供兩類東西:資料,以及強化學習環境。


強化學習環境可以理解為 AI 的模擬考場。例如建立一套虛構的公司財務系統,放入帳本、發票和待辦事項,讓 AI 練習對帳、報稅,並依結果自動評分。要讓 AI 學會處理真實工作,除了讀取資料,還需要能反覆操作、檢驗對錯的場景;環境越貼近現實,就越有價值。機器人領域也有類似需求:有公司在各地工廠,以第一人稱視角拍攝工人的操作,再將資料提供給機器人實驗室。YC 認為,部分業者不願公開談論這門生意,正因為不想讓競爭對手注意到機會;YC 於 2016 年投資的 Scale AI,則是這個領域的早期代表。


這五個變化,可以用同一條線串起來:**當寫程式碼的成本下降,人才與資金會流向仍然昂貴、仍然困難的地方。**一個人更容易開始創業;多年產業經驗更容易轉化為產品;硬科技團隊能跨過部分軟體門檻;軟體能直接交付工作成果;而越強的模型,越需要真實資料與練習環境。


當然,這些數字主要來自 YC 自己挑選與投資的公司,不能直接代表所有新創。若 YC 主動增加硬科技投資,批次中的硬科技比例也自然會上升。即使如此,背後的問題仍值得思考:當技術實作變得更容易,你在一個行業裡累積的判斷——客戶真正頭痛什麼、哪些坑不能踩、什麼需求不值得做——或許比「會不會寫程式碼」更難被取代。

[AI 分享] 從單一對話到多代理協作:四招打造 AI 工作流程

 [AI 分享] 從單一對話到多代理協作:四招打造 AI 工作流程

摘要:文章分享以總管代理、技能庫、系統串接與任務複述,建立可重複運作的 AI 協作流程。



內容:


一段談論 AI 工作方式的影片近期引發討論。原文介紹,一名網名「土豆女神」的工程師 Lauren Tan,展示了由一個 AI 協調多個 AI 代理(Agent)處理工作的做法。原文也提到「指揮 50 多個 AI」、「完成 20 多名工程師的工作量」及「公司九成以上員工都在使用」等說法;這些是原文轉述的宣稱,不能僅憑這段文字確認。


這套方法的核心,不是同時打開更多對話視窗,而是把常做的工作拆分、交給不同代理執行,並建立可重複使用的規則。原文將其歸納為以下四個做法。


第一招:先建立「總管」代理,讓 AI 協調 AI。 與其自己在多個視窗之間分派、追蹤任務,不如先設定一個負責規劃的總管代理。它不必親自完成每項工作,而是先詢問使用者:每天最耗時的三件事是什麼?哪個環節最容易卡住?釐清需求後,再協助設計分工,例如讓不同代理分別負責蒐集資料、修改程式碼及撰寫通知。


在原文描述的流程中,總管也會定期檢查各代理的產出,發現偏離目標或耗用過多資源時調整任務。這樣做的目的,是讓使用者從逐一指揮每個步驟,轉向設定目標、檢查結果與處理例外狀況。


第二招:把做成功的方法存成獨立技能。 原文認為,最值得養成的習慣,是在 AI 完成一項工作後,不只保留結果,也把有效的處理方法整理成可複用的說明。做完會議紀要,就記下整理格式與判斷規則;排除一次系統故障,就記下檢查順序;將長文改寫成短影片腳本,也可以留下轉換步驟和品質標準。


這些「技能」可以存放在私有程式碼倉庫或其他知識庫。日後遇到類似任務,便能提供既有規則,減少反覆解釋背景與要求的時間。原文強調的是:不要讓一次成功的對話,隨著視窗關閉而消失。


第三招:串接帳號與工具,減少人工複製貼上。 如果一項工作需要人把郵件內容複製給 AI,再把 AI 的結果貼進表格、手動新增日程,流程仍高度依賴人工搬運。原文提出,可讓負責特定任務的代理透過工作帳號與系統連接器,直接取得所需資訊並執行後續步驟。


例如,收到客戶郵件後,由代理擷取關鍵資訊、更新線上表格,再建立跟進提醒。這類自動化的價值在於讓資訊於工具之間順暢流動。不過,實作時仍應依任務給予必要權限,並對寄信、修改資料等重要動作設置審核,避免因授權過大或判讀錯誤造成損失。


第四招:交代任務時說清楚背景,並要求 AI 先複述。 原文指出,AI 答非所問,有時不是缺少複雜的提示詞,而是使用者沒有交代完整情境。對於背景較多的任務,可以用語音一次說明業務場景、對象、現有資料,以及怎樣的成果才算合格。


交代完後,先請 AI 用自己的話重述需求,不要立刻開始執行。如果它理解正確,再讓它開工;若有遺漏或誤解,就先修正。這一步旨在把確認需求的工作提前,減少完成後才發現方向錯誤、反覆修改的情況。


整體而言,原文想傳達的觀點是:AI 的用途不只是一問一答,也可以透過分工、保存方法、串接工具與確認需求,形成較穩定的工作流程。真正需要人持續把關的,則是任務目標、權限範圍與最終成果的品質。

2026年11月9日 星期一

MFA015 - Responses API 代理

MFA015 - Responses API 代理

在上一篇關於 Microsoft Agent Framework 系列教學文章 "MFA014 - 多模態輸入(圖片)" 中,我們學會了使用 Microsoft Agent Framework 建立能夠處理圖片輸入的 AI 代理。讓模型接收到多模態的資料,在這裡使用的圖片,這裡的圖片將會是冰箱的照片。透過這個練習,我們了解了如何讓 AI 代理分析圖片內容並給出建議。


在完成這篇文章的練習之後,你將能夠使用 Microsoft Agent Framework 建立一個能夠處理多輪對話的 AI 代理,並了解 Responses API 與 Chat Completions 代理在多輪對話中的差異。所謂的 Chat Completion API 則是每輪都需要將完整對話歷史送給模型,服務端不會保存對話狀態。而對於 Responses API,服務端會保存對話狀態,因此每輪不需要重送完整對話歷史,且能保留上一輪的推論摘要。那麼對於使用者而言,面對問題需求的時候,該如何挑選適合的代理,就可以根據是否需要服務端保存對話歷史以及對訊息數與 Token 使用量的考量來決定。

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

建立 Console 專案

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

安裝需要用到的 NuGet 套件

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

Microsoft.Agents.AI.OpenAI

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

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

宣告 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 OPENAI001 // OpenAI SDK 2.14 的 ResponsesClient 仍標記為實驗性 API

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Chat;
using OpenAI.Responses;
using System.ClientModel;
using System.Text;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage; // OpenAI.Chat 也有 ChatMessage,這裡指定用 MEAI 的

namespace csResponsesAgent;

internal class Program
{
    private const string Instructions = "你是邏輯老師,請用繁體中文作答,先給結論再用三句話內說明理由。";

    // 同一道邏輯題:三種情境都問它,數字才有比較基礎
    // 刻意選一題「需要想一下」的題目:推論太短(約 200 token 以內)時,服務端常常不回推論摘要
    private const string Puzzle =
        "有四個盒子,標籤分別是「蘋果」「橘子」「香蕉」「混合」,但四個標籤全部貼錯了。" +
        "你只能從其中一個盒子拿出一顆水果來看(不能看盒子裡面)," +
        "能不能保證推出四個盒子的正確內容?請說明。";

    // 第二輪追問:故意不重述規則,只有記得上一輪的代理才答得出來
    private const string FollowUp = "那如果只有三個盒子(蘋果、橘子、混合),規則都一樣,應該從哪個盒子拿?";

    // 由聊天用戶端中介層記錄:這一輪「實際送出」幾則訊息、帶出去的對話 id 是什麼
    private static int lastSentMessages;
    private static string? lastSentConversationId;

    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;
        }

        // ── Responses API 代理:ResponsesClient + AsAIAgent()(Microsoft.Agents.AI.OpenAI 提供的擴充方法)
        AIAgent responsesAgent =
            new ResponsesClient(
                    new ApiKeyCredential(apiKey),
                    new ResponsesClientOptions { Endpoint = new Uri(endpoint) })
                .AsAIAgent(
                    model: model,
                    instructions: Instructions,
                    name: "邏輯老師(Responses)",
                    clientFactory: client => client.AsBuilder().Use(CountMessages, null).Build());

        // ── Chat Completions 代理:就是 MFA001~MFA003 用的 ChatClient,只是改用同一個套件的 AsAIAgent() 建立
        AIAgent chatAgent =
            new ChatClient(
                    model,
                    new ApiKeyCredential(apiKey),
                    new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
                .AsAIAgent(
                    instructions: Instructions,
                    name: "邏輯老師(Chat Completions)",
                    clientFactory: client => client.AsBuilder().Use(CountMessages, null).Build());

        Console.WriteLine($"使用的模型:{model}");
        Console.WriteLine($"邏輯題:{Puzzle}");

        // ── 示範一:設定 reasoning effort 與推論摘要,同一道題比較 Low 與 High
        Header("示範一:Responses API + 推論摘要(同一道題,effort = Low vs High)");

        foreach (var effort in new[] { ReasoningEffort.Low, ReasoningEffort.High })
        {
            Section($"reasoning effort = {effort}");
            AgentResponse response = await responsesAgent.RunAsync(Puzzle, options: ReasoningRunOptions(effort));
            PrintReasoningAndAnswer(response);
            PrintUsage(response.Usage);
        }

        // ── 示範二:服務端保存對話(Responses API)
        Header("示範二:服務端保存對話(Responses API,只傳上一輪的 response id)");
        var responsesTurns = await RunTwoTurnsAsync(responsesAgent);

        // ── 示範三:本機保存對話(Chat Completions,MFA003 的作法)
        Header("示範三:對照組 Chat Completions(每一輪都把整段歷史重送)");
        var chatTurns = await RunTwoTurnsAsync(chatAgent);

        // ── 對照小結:只看第二輪(追問那一輪)
        Header("對照小結:第二輪(追問)");
        Row("實際送出的訊息數", $"{responsesTurns[1].SentMessages} 則", $"{chatTurns[1].SentMessages} 則");
        Row("帶出的對話 id", responsesTurns[1].SentConversationId is null ? "(無)" : "上一輪的 response id", chatTurns[1].SentConversationId ?? "(無)");
        Row("輸入 token(其中快取)", $"{responsesTurns[1].InputTokens}({responsesTurns[1].CachedTokens})", $"{chatTurns[1].InputTokens}({chatTurns[1].CachedTokens})");
        Row("推論 token", $"{responsesTurns[1].ReasoningTokens}", $"{chatTurns[1].ReasoningTokens}");
        Row("兩輪中拿到推論摘要", responsesTurns.Any(t => t.HasSummary) ? "是" : "否", chatTurns.Any(t => t.HasSummary) ? "是" : "否");
        Row("對話歷史存在哪裡", "OpenAI 服務端", "本機 AgentSession");

        // 這是最容易誤會的地方:「不重送歷史」省的是傳輸量和自己管理歷史的工作,不是 token
        Console.WriteLine();
        Console.WriteLine("  觀察:Responses 第二輪只送 1 則訊息,但服務端會把上一輪的完整內容(含推論項目)接回上下文,");
        Console.WriteLine("        所以輸入 token 照樣計費,甚至可能比 Chat Completions 多;Chat Completions 重送的只有看得見的文字,");
        Console.WriteLine("        上一輪的推論內容直接丟掉。(提示快取要前綴超過約 1,024 token 才會命中,這個範例太短所以是 0)");
    }

    // 兩種 API 都用 MEAI 的 ChatOptions.Reasoning 設定:Effort 是推論力道,Output = Summary 是要求回傳推論摘要
    private static ChatClientAgentRunOptions ReasoningRunOptions(ReasoningEffort effort) =>
        new(new ChatOptions
        {
            Reasoning = new ReasoningOptions { Effort = effort, Output = ReasoningOutput.Summary }
        });

    private static async Task<List<TurnStat>> RunTwoTurnsAsync(AIAgent agent)
    {
        AgentSession session = await agent.CreateSessionAsync();
        var turns = new List<TurnStat>();

        foreach (var (title, question) in new[] { ("第一輪:原題", Puzzle), ("第二輪:追問(不重述題目)", FollowUp) })
        {
            Section(title);
            AgentResponse response = await agent.RunAsync(question, session, ReasoningRunOptions(ReasoningEffort.Low));
            PrintReasoningAndAnswer(response);

            // Responses API 存在服務端時,ConversationId 就是這一輪的 response id;Chat Completions 則一直是 null
            var conversationId = (session as ChatClientAgentSession)?.ConversationId;
            Console.WriteLine($"  實際送出訊息數:{lastSentMessages} 則");
            Console.WriteLine($"  帶出的對話 id :{lastSentConversationId ?? "(無)"}");
            Console.WriteLine($"  回來後 session.ConversationId:{conversationId ?? "(無,歷史存在本機 session)"}");
            PrintUsage(response.Usage);

            turns.Add(new TurnStat(
                lastSentMessages,
                lastSentConversationId,
                response.Usage?.InputTokenCount ?? 0,
                response.Usage?.CachedInputTokenCount ?? 0,
                response.Usage?.ReasoningTokenCount ?? 0,
                GetReasoningSummary(response).Length > 0));
        }

        return turns;
    }

    // 聊天用戶端中介層:記下這一輪真正送到 API 的訊息數,以及帶出去的對話 id(Responses 的 previous_response_id)
    private static Task<ChatResponse> CountMessages(
        IEnumerable<ChatMessage> messages, ChatOptions? options, IChatClient innerClient, CancellationToken cancellationToken)
    {
        var list = messages.ToList();
        lastSentMessages = list.Count;
        lastSentConversationId = options?.ConversationId;
        return innerClient.GetResponseAsync(list, options, cancellationToken);
    }

    // 推論摘要放在回應訊息裡的 TextReasoningContent;答案則是一般的 TextContent(response.Text)
    private static string GetReasoningSummary(AgentResponse response) =>
        string.Join(Environment.NewLine, response.Messages
            .SelectMany(message => message.Contents)
            .OfType<TextReasoningContent>()
            .Select(reasoning => reasoning.Text)
            .Where(text => !string.IsNullOrWhiteSpace(text)));

    private static void PrintReasoningAndAnswer(AgentResponse response)
    {
        var summary = GetReasoningSummary(response);

        Console.WriteLine("  [推論摘要]");
        Console.WriteLine(summary.Length > 0
            ? Indent(summary)
            : "    (沒有收到推論摘要:Chat Completions 不提供;Responses 則在推論太短時會省略)");
        Console.WriteLine("  [答案]");
        Console.WriteLine(Indent(response.Text));
    }

    private static void PrintUsage(UsageDetails? usage)
    {
        Console.WriteLine(
            $"  token:輸入 {usage?.InputTokenCount}(快取 {usage?.CachedInputTokenCount ?? 0})" +
            $"/輸出 {usage?.OutputTokenCount}(其中推論 {usage?.ReasoningTokenCount ?? 0})" +
            $"/總計 {usage?.TotalTokenCount}");
    }

    private static string Indent(string text) =>
        string.Join(Environment.NewLine, text.Split('\n').Select(line => "    " + line.TrimEnd('\r')));

    private static void Header(string title)
    {
        Console.WriteLine();
        Console.WriteLine(new string('=', 70));
        Console.WriteLine($"  {title}");
        Console.WriteLine(new string('=', 70));
    }

    private static void Section(string title)
    {
        Console.WriteLine();
        Console.WriteLine($"【{title}】");
    }

    private static void Row(string label, string responses, string chat) =>
        Console.WriteLine($"  {label}:Responses = {responses}|Chat Completions = {chat}");
}

internal sealed record TurnStat(
    int SentMessages,
    string? SentConversationId,
    long InputTokens,
    long CachedTokens,
    long ReasoningTokens,
    bool HasSummary);

在上面的範例程式碼中,將會展示如何使用 Responses API 代理與 Chat Completions 代理,並比較它們在處理同一個邏輯題時的行為差異。為什麼會想要這麼來比較呢?因為,這裡提供兩種類型的 API,當我們遇到問題的時候,究竟要如何選擇採用哪種 API 呢?透過這個範例,我們可以觀察到兩者在訊息傳遞、對話管理以及推論摘要上的差異,進而做出更適合的選擇。

這個程式一開始將會讀取 API Key 與服務端點的數據,使用 ResponsesClient 類別 (這個類別定義在 OpenAI.Responses 命名空間中) 來建立一個 AIAgent 代理物件,變數名稱為 responsesAgent,這將會是一個採用 Responses API 代理,也就是採用 ResponsesClient + AsAIAgent()(Microsoft.Agents.AI.OpenAI 提供的擴充方法),這裡使用的系統提示詞為 "你是邏輯老師,請用繁體中文作答,先給結論再用三句話內說明理由。"

第二個 AIAgent 物件,變數名稱為 chatAgent,將會是一個採用 Chat Completions 代理(定義在 OpenAI.Chat 命名空間中),也就是使用 ChatClient + AsAIAgent() 建立,系統提示詞同樣為 "你是邏輯老師,請用繁體中文作答,先給結論再用三句話內說明理由。"

現在來針對定義在 Puzzle 這個字串的邏輯題,將其內容設為:"有四個盒子,標籤分別是「蘋果」「橘子」「香蕉」「混合」,但四個標籤全部貼錯了。你只能從其中一個盒子拿出一顆水果來看(不能看盒子裡面),能不能保證推出四個盒子的正確內容?請說明。",並將其傳遞給 responsesAgent 與 chatAgent 進行推論。單就這裡提出的提示詞而言,兩個代理都應該能夠給出正確的推論結果。但是,兩者在訊息傳遞、對話管理以及推論摘要上的行為可能會有所不同,這正是我們想要觀察的重點。

首先將會要來測試理解 API,這裡會分別對 responsesAgent 物件,傳入 ReasoningEffort.Low, ReasoningEffort.High 這兩個參數,ReasoningEffort 這個型別將會用來控制代理在推論時的努力程度,Low 代表低推理努力,High 代表高推理努力。這裡使用的程式碼將會為 await responsesAgent.RunAsync(Puzzle, options: ReasoningRunOptions(effort)); , 當執行完成後便會得到 AgentResult 物件,裡面包含了代理的推論結果以及相關的訊息傳遞與對話管理資訊。PrintReasoningAndAnswer 方法將會用來輸出推論過程與最終答案。在此方法內,將會呼叫 GetReasoningSummary 方法,這個方法透過 Linq 對訊息傳遞的內容進行分析,並整理出推論摘要,方便我們觀察代理在不同努力程度下的推理行為。GetReasoningSummary 方法執行結果將會儲存到 summary 物件內,也就是一段字串內容,代表了推論摘要

**Explaining the puzzle setup**

I’m considering a standard puzzle with three labeled boxes: apple, orange, and mixed, but they’re all incorrectly labeled. Typical strategies involve drawing from the mixed box to find the correct fruit, but here there are four types with three fruit kinds, which complicates things. It seems like one sample isn’t enough to solve this, and I should clarify using counterexamples to support that idea while keeping the explanation concise and clear.

上面的推論摘要之中文意思將會為:

**謎題設定說明**

我正在考慮一個標準的謎題,其中包含三個標有“蘋果”、“橙子”和“混合”的盒子,但它們的標籤都貼錯了。通常的解題策略是從「混合」盒子中抽取水果來找到正確的水果,但這裡卻有四種盒子,每種盒子裡又有三種水果,這使得問題變得複雜。似乎僅憑一個例子不足以解決這個問題,我應該用反例來闡明我的觀點,同時保持解釋的簡潔明了。

這次的推論結果將會儲存在 response.Text 這個屬性內

**結論:不能保證推出四個盒子的正確內容。**
應從標籤「混合」的盒子取水果,因為它不可能是混合盒,若取到蘋果,只能確定該盒是純蘋果,但其餘三盒仍可能有多種符合「全錯」的排列,例如「蘋果標籤盒」可為混合或橘子盒。
因此一次取樣所得資訊不足以唯一確定四個盒子的內容。

對於採用 ReasoningEffort.Low 方式來進行推論的使用摘要如下

token:輸入 118(快取 0)/輸出 418(其中推論 295)/總計 536

底下將會為採用 ReasoningEffort.High 的推論過程

(為什麼要採用兩種不同的推論努力程度來解題?ReasoningEffort.Low 代表快速、簡單的推論,可能忽略一些細節;ReasoningEffort.High 則代表更深入、全面的推論,會考慮更多可能性。透過比較兩種不同的推論努力程度,可以觀察代理在不同情境下的表現差異。)

【reasoning effort = High】
  [推論摘要]
    **Solving the labeling puzzle**

    I need to find a solution involving boxes labeled incorrectly with their contents. The mixed-labeled box will help identify what’s inside, which will then allow me to label the others correctly. If I draw an apple from the mixed box, I can eliminate that as a possibility for the remaining boxes, given the constraints of labeling. For instance, if I label one box correctly, I can’t have the others labeled correctly. I’ll explore all possible arrangements next.**Analyzing the box labeling puzzle**

    I’m trying to determine if the arrangement A-box B, O-box M, and B-box O is valid. Drawing from the mixed box could help identify one pure box, but it might not uniquely solve the problem with three remaining boxes. To answer the user’s question about guaranteeing the correct contents of four boxes, I need to explain clearly that it’s not possible with just one draw from the mixed box. I have to demonstrate this effectively in three sentences.**Examining labeling strategies**

    I need to illustrate that no strategy guarantees accurate labeling, although drawing from the mixed-labeled box seems to be the best option. If I draw an apple, I can identify the mixed box but face ambiguity with two potential assignments. To clarify, I'll label the boxes as Apple A, Orange O, Banana B, and Mixed M. If I draw an apple from M, the remaining contents could suggest two valid assignments for the other labels, which complicates things further.
  [答案]
    **不能保證推出四個盒子的正確內容。**例如從「混合」盒取出蘋果,可知該盒是純蘋果;但剩下仍可能是「蘋果標籤盒=混合、橘子標籤盒=香蕉、香蕉標籤盒=橘子」,也可能是「蘋果標籤盒=香蕉、橘子標籤盒=混合、香蕉標籤盒=橘子」,兩者都符合所有標籤貼錯,因此一次取樣不足以唯一判定。
  token:輸入 118(快取 0)/輸出 652(其中推論 516)/總計 770

從上面的 Low & High 的執行結果可以看出,ReasoningEffort.Low 的推論較為簡單快速,但可能忽略一些細節,例如:無法完整展示所有可能的盒子排列,對於保證正確標籤的推論不夠全面。而 ReasoningEffort.High 則提供了更完整的分析,考慮了多種可能性,能更清楚地說明為什麼無法保證推出四個盒子的正確內容。

接下來將會展示服務端保存對話(Responses API),這裡的目的將會是示範如何在同一個對話中,依序向代理提出多個問題,並追蹤每輪的訊息與用量。在此將會呼叫 RunTwoTurnsAsync 方法,在這個方法內,將會於傳入 responsesAgent 的 AIAgent 物件,呼叫 CreateSessionAsync 方法,取得一個 AgentSession 物件,接下來會使用同一個 AgentSession,依序向代理提出兩個問題,並記錄每輪的傳送與用量資訊:

  1. 依序詢問 Puzzle 原題和 FollowUp 追問;追問刻意不重述前題,藉此測試代理能否記住上下文。
  2. 每輪印出推論摘要、答案、實際送出的訊息數、對話 ID,以及 token 用量。
  3. 把這些數據整理成 TurnStat,最後回傳兩輪的統計清單。 這個方法本身不比較兩種 API;Main 會分別用 Responses API 代理和 Chat Completions 代理呼叫它,再比較兩者的結果。

另外一個將會是呼叫 RunTwoTurnsAsync 方法,但傳入的是 Chat Completions 代理,也就是 chatAgent 這個物件,這裡將會把每一輪的完整對話歷史重送給代理,藉此比較兩種代理在同一個對話中的表現差異。

當這兩輪呼叫完成 RunTwoTurnsAsync 之後,將會分別把結果存入 responsesResult 和 chatResult 兩個變數,之後可以比較兩者在同一個對話中的表現差異。底下將會為實際輸出內容

======================================================================
  對照小結:第二輪(追問)
======================================================================
  實際送出的訊息數:Responses = 1 則|Chat Completions = 3 則
  帶出的對話 id:Responses = 上一輪的 response id|Chat Completions = (無)
  輸入 token(其中快取):Responses = 347(0)|Chat Completions = 242(0)
  推論 token:Responses = 0|Chat Completions = 22
  兩輪中拿到推論摘要:Responses = 是|Chat Completions = 否
  對話歷史存在哪裡:Responses = OpenAI 服務端|Chat Completions = 本機 AgentSession

  觀察:Responses 第二輪只送 1 則訊息,但服務端會把上一輪的完整內容(含推論項目)接回上下文,
        所以輸入 token 照樣計費,甚至可能比 Chat Completions 多;Chat Completions 重送的只有看得見的文字,
        上一輪的推論內容直接丟掉。(提示快取要前綴超過約 1,024 token 才會命中,這個範例太短所以是 0)

觀察上面的對照小結:第二輪(追問)可以發現,Responses API 只送出一則訊息,但服務端會自動帶入上一輪的完整對話歷史,因此能保留推論摘要;而 Chat Completions 代理則需要每輪重送完整對話歷史,導致訊息數增加,且無法保留上一輪的推論內容。這也說明了兩種代理在處理多輪對話時的差異:Responses API 依賴服務端保存對話,Chat Completions 則依賴本機管理對話歷史。對於 Token 使用量而言,Responses API 可能因為帶入完整對話歷史而增加輸入 token,但仍能減少訊息數;Chat Completions 則每輪都需重送完整對話歷史,訊息數增加,且無法保留推論摘要。

執行結果

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

  • 在 Visual Studio 2026 下,按下 F5 鍵執行程式,將會看到程式輸出的結果。
使用的模型:gpt-5.6-luna
邏輯題:有四個盒子,標籤分別是「蘋果」「橘子」「香蕉」「混合」,但四個標籤全部貼錯了。你只能從其中一個盒子拿出一顆水果來看(不能看盒子裡面),能不能保證推出四個盒子的正確內容?請說明。

======================================================================
  示範一:Responses API + 推論摘要(同一道題,effort = Low vs High)
======================================================================

【reasoning effort = Low】
  [推論摘要]
    **Explaining the puzzle setup**

    I’m considering a standard puzzle with three labeled boxes: apple, orange, and mixed, but they’re all incorrectly labeled. Typical strategies involve drawing from the mixed box to find the correct fruit, but here there are four types with three fruit kinds, which complicates things. It seems like one sample isn’t enough to solve this, and I should clarify using counterexamples to support that idea while keeping the explanation concise and clear.
  [答案]
    **結論:不能保證推出四個盒子的正確內容。**
    應從標籤「混合」的盒子取水果,因為它不可能是混合盒,若取到蘋果,只能確定該盒是純蘋果,但其餘三盒仍可能有多種符合「全錯」的排列,例如「蘋果標籤盒」可為混合或橘子盒。
    因此一次取樣所得資訊不足以唯一確定四個盒子的內容。
  token:輸入 118(快取 0)/輸出 418(其中推論 295)/總計 536

【reasoning effort = High】
  [推論摘要]
    **Solving the labeling puzzle**

    I need to find a solution involving boxes labeled incorrectly with their contents. The mixed-labeled box will help identify what’s inside, which will then allow me to label the others correctly. If I draw an apple from the mixed box, I can eliminate that as a possibility for the remaining boxes, given the constraints of labeling. For instance, if I label one box correctly, I can’t have the others labeled correctly. I’ll explore all possible arrangements next.**Analyzing the box labeling puzzle**

    I’m trying to determine if the arrangement A-box B, O-box M, and B-box O is valid. Drawing from the mixed box could help identify one pure box, but it might not uniquely solve the problem with three remaining boxes. To answer the user’s question about guaranteeing the correct contents of four boxes, I need to explain clearly that it’s not possible with just one draw from the mixed box. I have to demonstrate this effectively in three sentences.**Examining labeling strategies**

    I need to illustrate that no strategy guarantees accurate labeling, although drawing from the mixed-labeled box seems to be the best option. If I draw an apple, I can identify the mixed box but face ambiguity with two potential assignments. To clarify, I'll label the boxes as Apple A, Orange O, Banana B, and Mixed M. If I draw an apple from M, the remaining contents could suggest two valid assignments for the other labels, which complicates things further.
  [答案]
    **不能保證推出四個盒子的正確內容。**例如從「混合」盒取出蘋果,可知該盒是純蘋果;但剩下仍可能是「蘋果標籤盒=混合、橘子標籤盒=香蕉、香蕉標籤盒=橘子」,也可能是「蘋果標籤盒=香蕉、橘子標籤盒=混合、香蕉標籤盒=橘子」,兩者都符合所有標籤貼錯,因此一次取樣不足以唯一判定。
  token:輸入 118(快取 0)/輸出 652(其中推論 516)/總計 770

======================================================================
  示範二:服務端保存對話(Responses API,只傳上一輪的 response id)
======================================================================

【第一輪:原題】
  [推論摘要]
    **Explaining a classic puzzle**

    I need to answer in traditional Chinese, starting with a clear conclusion followed by an explanation of three sentences maximum. The puzzle involves labeling where the contents are pure apple, orange, banana, or a mixed fruit, all with wrong labels. The strategy is to draw from the box labeled mixed, as it cannot contain mixed fruit, allowing identification of the true contents of the other boxes too.
  [答案]
    可以保證。先從標示「混合」的盒子取一顆,因標籤全錯,它不可能是混合盒,因此看到的水果即可確定該盒是對應的單一水果盒;再依「其餘標籤全錯」的條件,排除已知內容,唯一確定另外三盒的正確配置。
  實際送出訊息數:1 則
  帶出的對話 id :(無)
  回來後 session.ConversationId:resp_09570987efccb64b006af16ce2a45c8193bf29d2ab8c90fe24
  token:輸入 118(快取 0)/輸出 172(其中推論 81)/總計 290

【第二輪:追問(不重述題目)】
  [推論摘要]
    (沒有收到推論摘要:Chat Completions 不提供;Responses 則在推論太短時會省略)
  [答案]
    應從標示「混合」的盒子拿。因為標籤全錯,該盒必定是純蘋果或純橘子;若拿到蘋果,它就是蘋果盒,標示「橘子」的盒子只能是混合盒,剩下的是橘子盒(拿到橘子時反之亦然)。
  實際送出訊息數:1 則
  帶出的對話 id :resp_09570987efccb64b006af16ce2a45c8193bf29d2ab8c90fe24
  回來後 session.ConversationId:resp_09570987efccb64b006af16d266f98819398624f4df0d0cb1b
  token:輸入 331(快取 0)/輸出 89(其中推論 0)/總計 420

======================================================================
  示範三:對照組 Chat Completions(每一輪都把整段歷史重送)
======================================================================

【第一輪:原題】
  [推論摘要]
    (沒有收到推論摘要:Chat Completions 不提供;Responses 則在推論太短時會省略)
  [答案]
    **不能保證推出四個盒子的正確內容。**
    例如從「混合」盒取出蘋果,只能確定該盒是「蘋果」,但其餘仍可能是「蘋果標籤=橘子、橘子標籤=香蕉、香蕉標籤=混合」,也可能是「蘋果標籤=混合、橘子標籤=香蕉、香蕉標籤=橘子」,兩者都符合所有標籤貼錯。
    因此一次取出一顆水果的資訊不足以唯一確定四盒內容。
  實際送出訊息數:1 則
  帶出的對話 id :(無)
  回來後 session.ConversationId:(無,歷史存在本機 session)
  token:輸入 118(快取 0)/輸出 660(其中推論 512)/總計 778

【第二輪:追問(不重述題目)】
  [推論摘要]
    (沒有收到推論摘要:Chat Completions 不提供;Responses 則在推論太短時會省略)
  [答案]
    **應該從標籤為「混合」的盒子拿一顆水果。**
    因為所有標籤都錯,「混合」盒不可能是混合;若拿出蘋果,它就是蘋果盒,接著標籤「橘子」的盒只能是混合,標籤「蘋果」的盒則是橘子。
    若拿出橘子,推理對調即可,因此一定能推出三盒內容。
  實際送出訊息數:3 則
  帶出的對話 id :(無)
  回來後 session.ConversationId:(無,歷史存在本機 session)
  token:輸入 307(快取 0)/輸出 177(其中推論 58)/總計 484

======================================================================
  對照小結:第二輪(追問)
======================================================================
  實際送出的訊息數:Responses = 1 則|Chat Completions = 3 則
  帶出的對話 id:Responses = 上一輪的 response id|Chat Completions = (無)
  輸入 token(其中快取):Responses = 331(0)|Chat Completions = 307(0)
  推論 token:Responses = 0|Chat Completions = 58
  兩輪中拿到推論摘要:Responses = 是|Chat Completions = 否
  對話歷史存在哪裡:Responses = OpenAI 服務端|Chat Completions = 本機 AgentSession

  觀察:Responses 第二輪只送 1 則訊息,但服務端會把上一輪的完整內容(含推論項目)接回上下文,
        所以輸入 token 照樣計費,甚至可能比 Chat Completions 多;Chat Completions 重送的只有看得見的文字,
        上一輪的推論內容直接丟掉。(提示快取要前綴超過約 1,024 token 才會命中,這個範例太短所以是 0)