2026年1月30日 星期五

使用AI應用程式範本延伸模組

這篇文章將介紹如何在Visual Studio 2026開發工具中使用AI應用程式範本延伸模組(AI app template extensions),以 GitHub Models 為實作範例,快速打造智慧型AI聊天應用程式(以 GitHub Models 為例)。

核心觀念與.NET AI生態系統

隨著人工智慧技術的普及,開發人員需要更快速、更有效的方式將 AI 功能整合到應用程式中。.NET 生態系統提供了一系列強大的工具、程式庫和服務來開發 AI 應用程式,支援雲端和本機 AI 模型連線,以及各種 AI 和向量資料庫服務。


為了簡化 AI 應用程式的入門體驗,Microsoft發布了 .NET AI 應用程式範本.NET AI app template,專門處理常見的設定和組態任務。

 

.NET AI 應用程式範本(.NET AI app template是什麼?

.NET AI 應用程式範本(.NET AI app template 是一個 NuGet套件,包含兩種專案類型:AI Chat Web App Local MCP Server Console App。其中,「AI Chat Web App」範本的設計目地,是讓開發人員能快速建立一個 Blazor為基礎的互動式伺服端網站應用程式Blazor Interactive Server Web,並預先設定好常見的 AI 和資料服務。

 

這個範本的好處在於,它已經為開發人員處理了以下幾項關鍵任務:

  •  引入核心套件:在專案(.csproj檔案中已包含必要的「Microsoft.Extensions.AI套件和其他相依套件,以便開始開發 AI服務。
  •  註冊服務與相依性插入(DI:在專案的「Program.cs檔案中已建立並註冊各種 AI 服務,以便在程式中透過 DI取得這些服務。
  • 配置資料庫:專案中註冊一個 SQLite 資料庫環境服務來處理文件,並預先設定為可存取專案 wwwroot/Data 資料夾中的文件。
  • 完整聊天使用者介面:提供一個使用 Blazor 元件建置的完整聊天介面,可處理 AI 回應的豐富格式。


使用「
Microsoft.Extensions.AI」程式庫

.NET AI 應用程式範本(.NET AI app template 的核心基礎是 Microsoft.Extensions.AI程式庫。這組核心程式庫提供了一個整合的 C# 抽象層,以便與各種 AI 服務(例如 SLM LLM等等)進行互動,讓開發人員能夠選擇慣用的架構和程式庫來加速開發。

 

程式庫中核心抽象介面包含:

  • IChatClient:此介面定義了負責與提供聊天功能的 AI 服務互動的用戶端抽象概念。它用於與生成式AI 模型聊天,支援傳送和接收多模態內容(例如文字、影像)。
  • IEmbeddingGenerator<TInput,TEmbedding>:此介面代表嵌入向量的通用生成器。它用於產生內嵌(Embeddings),以應用於向量搜尋功能。

 

通過這些低階的 API,開發人員便可以使用熟悉的相依性插入(dependency injection)和中介軟體模式(middleware patterns),輕鬆地將將自動叫用函式工具、遙測和快取等元件整合到應用程式中。


檢索增強生成 (RAG) 的運作機制

使用 AI Chat Web App」範本建立的應用程式,其智慧問答能力並非僅依賴 LLM 的通用知識,而是可以透過向量搜尋Vector Search)功能,使其能夠回答有關自訂文件內容的問題。

 

這個過程涉及以下幾個關鍵類別和步驟:

  • 資料導入(Data Ingestion:當應用程式運行時,「Data Ingestor會自動讀取和處理 wwwroot/Data資料夾中的 PDF 文件。它從每個 PDF 中提取文字內容,使用選定的模型生成內嵌(向量),然後將這些內嵌儲存在 SQLite 向量資料庫中。這將文件轉換為 AI 模型可以理解和參考的可搜尋向量數據。
  • 語義搜尋(Semantic Search:當使用者輸入問題時,「Semantic Search」便開始運作。它將使用者提示轉換為內嵌,將其與儲存的文件向量進行比較,並檢索出最相關的資訊片段。
  • 生成答案:檢索到的結果隨後被傳遞給 LLMLLM 根據這些內容生成自然語言答案。應用程式通常會提供引文Citations),明確指出資訊來源於哪個文件和頁面


優點與缺點分析

.NET AI 應用程式範本(.NET AI app template 的設計旨在為 .NET 開發人員提供快速啟動 AI 專案的能力。以「AI Chat Web App範本為基礎,其優點如下:

  • 快速啟動與簡化設定:範本為你處理常見的設定任務和組態,大大簡化了使用 .NET 建置 AI 應用程式的入門體驗。
  • 內建 RAG 功能:應用程式預設配置了文件導入、內嵌生成(IEmbeddingGenerator)和向量搜尋功能。只需將 PDF 文件拖曳到 wwwroot/Data」資料夾中,應用程式就會自動處理並生成內嵌。
  • 統一的 C# 抽象層:依賴 Microsoft.Extensions.AI 程式庫,提供統一的抽象層(IChatClientIEmbeddingGenerator),確保在不同 AI 服務或模型之間切換時,程式碼可以順暢地互操作。
  • 易於擴展的功能:由於應用程式是使用 Microsoft.Extensions.AI 建置的,開發人員可以輕鬆地插入自訂 C# 函式(工具),讓聊天機器人呼叫這些函式來執行特定操作,例如擷取即時資料或連接到真實 API
  • 提供完整 UI 與引文:範本提供了一個基於 Blazor 元件的完整聊天 UI,不僅能顯示 AI 回應,還能提供資料來源的引文,告訴使用者資訊來自哪個文件和頁面。
  • 支援多種 AI 服務:範本支援多種 AI 服務提供者,包括 GitHub ModelsAzure OpenAIOpenAI 平台和 Ollama


使用.NET AI 應用程式範本專案 (GitHub Models為例)

接下來就讓我們來了解一下如何使用 .NET AI 應用程式範本(.NET AI app template)中的「AI Chat Web App」來建立一個AI聊天網站應用程式。在建立 AI Chat Web App 」之前,你需要確保開發環境滿足以下條件,並取得 GitHub Models 的存取金鑰。

 

本篇文章後續將使用Visual Studio 2026版本來進行說明。

安裝.NET AI 應用程式範本(.NET AI app template)套件

開啟作業系統命令提示字元,執行以下 dotnet new install 」命令來安裝範本套件:

dotnet new install Microsoft.Extensions.AI.Templates

這個命令執行的結果請參考下圖所示一旦安裝完成,你將看到 AI chat web app Local MCP server console app 這兩個範本。

1:安裝.NET AI 應用程式範本套件

 

設定GitHub Models個人存取令牌(Personal Access Token

由於本篇文章選擇使用 GitHub Models 作為 AI 服務提供者,因此需要一個個人存取令牌(Personal Access Token)來進行程式碼驗證


先開啟瀏覽器到 GitHub 帳戶設定中的「Marketplace」,請參考下圖所示:

2 GitHub 帳戶設定中的「Marketplace


點選「
Models>Playground」,請參考下圖所示:

3Playground


在上方的「
Model」選單中選取想要使用的模型,本文以「OpenAI gpt-5-mini」模型為例,請參考下圖所示:

4:選取想要使用的「OpenAI gpt-5-mini」模型

 

你也可以在這個頁面中的聊天介面直接測試這個模型。選好模型之後,點選右上方「Use this model」,請參考下圖所示:

5:點選右上方「Use this model


在設定驗證區塊,點選「
Create Personal Access Token」按鈕,請參考下圖所示:

6:點選「Create Personal Access Token」按鈕

 

下一步會要求登入到GitHub網站,按畫面的指示輸入帳號密碼進行驗證。驗證過後便可建立 GitHub 個人存取令牌,你會看到一個「New fine-grained personal access token」畫面,在「Token name」取一個唯一不重複的令牌名稱,請參考下圖所示:

7:「Token name」取一個唯一不重複的令牌名稱


設定令牌逾期時間,在
Permissions」區塊下方,務必將 模型Models)設定為 存取:唯讀Access: Read-only)。其它保留預設值,然後按下「Generate Token」按鈕產生令牌,請參考下圖所示:

8:按下「Generate Token」按鈕產生令牌

 

在下一個畫面按下「Generate Token」按鈕產生令牌,請參考下圖所示:

9:按下「Generate Token」按鈕產生令牌

 

接下來你會看到一個「github_pat_….」開頭的令牌字串,注意這個令牌只會出現一次,記得要馬上按下複製按鈕保存,請參考下圖所示:


10:複製令牌


使用Visual Studio 2026開發工具建立 AI 聊天應用程式

Visual Studio 2026開發工具「建立新專案」視窗右上方的下拉式清單方塊選取「AI」,你可以看到兩種專案類型:「AI Chat Web App Local MCP Server Console App,在這篇文章中以AI Chat Web App」為例,請參考下圖所示:

11建立AI Chat Web App」專案


下一步設定新的專案名稱與檔案存放資料夾,請參考下圖所示:

12:設定新的專案名稱與檔案存放資料夾


選取「
.NET 10」架構,指定 GitHub Models 」和本機向量存放區,然後按下「建立」按鈕,請參考下圖所示:

13:選取「.NET 10」架構


範例應用程式結構

檢視一下專案結構,你將會看到幾個關鍵程式:

  • Program.cs:這個檔案中的程式碼會自動配置 AI 服務和向量存放區。預設情況下,「gpt-4o-mini 」模型會被用作 LLM,而「text-embedding-3-small會用於內嵌生成,。對於儲存體,應用程式使用 SQLite 作為向量資料庫。
  • wwwroot/Data 資料夾:此資料夾包含兩個範例 PDF 文件。應用程式已預先將你新增到此資料夾的任何文件配置為內嵌。
  • Pages 資料夾:包含一個「chat.razer」元件,它是做為主聊天介面 Blazor 頁面。

 

如果需要使用 GitHub Models 中的其他模型,你可以在「Program.cs」檔案中更新名稱參數,例如我們想要使用「gpt-5-mini」作為LLM,修改這行程式為「ghModelsClient.GetChatClient("gpt-5-mini").AsIChatClient()

Program.cs

using System.ClientModel;

using Microsoft.Extensions.AI;

using OpenAI;

using MyChatApp.Components;

using MyChatApp.Services;

using MyChatApp.Services.Ingestion;

 

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents().AddInteractiveServerComponents();

 

// You will need to set the endpoint and key to your own values

// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line:

//   cd this-project-directory

//   dotnet user-secrets set GitHubModels:Token YOUR-GITHUB-TOKEN

var credential = new ApiKeyCredential(builder.Configuration["GitHubModels:Token"] ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token. See the README for details."));

var openAIOptions = new OpenAIClientOptions()

{

    Endpoint = new Uri("https://models.inference.ai.azure.com")

};

 

var ghModelsClient = new OpenAIClient(credential, openAIOptions);

var chatClient = ghModelsClient.GetChatClient("gpt-5-mini").AsIChatClient();

var embeddingGenerator = ghModelsClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();

 

var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db");

var vectorStoreConnectionString = $"Data Source={vectorStorePath}";

builder.Services.AddSqliteVectorStore(_ => vectorStoreConnectionString);

builder.Services.AddSqliteCollection<string, IngestedChunk>(IngestedChunk.CollectionName, vectorStoreConnectionString);

 

builder.Services.AddSingleton<DataIngestor>();

builder.Services.AddSingleton<SemanticSearch>();

builder.Services.AddKeyedSingleton("ingestion_directory", new DirectoryInfo(Path.Combine(builder.Environment.WebRootPath, "Data")));

builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging();

builder.Services.AddEmbeddingGenerator(embeddingGenerator);

 

var app = builder.Build();

 

// Configure the HTTP request pipeline.

if (!app.Environment.IsDevelopment())

{

    app.UseExceptionHandler("/Error", createScopeForErrors: true);

    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.

    app.UseHsts();

}

 

app.UseHttpsRedirection();

app.UseAntiforgery();

 

app.UseStaticFiles();

app.MapRazorComponents<App>()

    .AddInteractiveServerRenderMode();

 

app.Run();

 


專案中預設有一個「README.md」文件說明了令牌的設定方式,要將令牌儲存到「secrets.json」檔案,請參考下圖所示:

 

14:將令牌儲存到「secrets.json」檔案

 

Visual Studio 2026開發工具「方案總管」視窗,選取專案名字按滑鼠右鍵,從快捷選單選取「管理使用者密碼」,請參考下圖所示:

15:「管理使用者密碼」

 

這時會自動建立並開啟「secrets.json」檔案,手動在其中加入組態,設定你在GitHub申請的令牌:

 

secrets.json

{

  "GitHubModels:Token": "github_pat_11AE6…請換成你在GitHub申請的令牌"

}

請參考下圖所示:

16:設定你在GitHub申請的令牌

 

現在可以進行測試了,在Visual Studio 2026開發工具按CTRL+F5執行網站,就可以看到在瀏覽器中顯示一個Blazor聊天介面,預設網站可以存取兩個PDF檔,我們可以直接在下方文字方塊中輸入提示進行測試,例如輸入:

這個PDF檔內容是什麼?

 

應用程式將處理你的問題,在向量資料庫中運行語義搜尋,並根據相關文件內容生成答案,這個範例執行的結果請參考下圖所示:

17:範例執行的結果


使用自訂的PDF文件進行 RAG 強化

這個範本最大的實用性在於能夠輕鬆地將你自己的文件納入 AI 的知識庫中。我們可以直接將PDF 文件(例如文章、文件或規範)放入專案中的「wwwroot/data 資料夾。「Data Ingestor將自動檢視新增的文件,讀取其內容,生成內嵌,並將其儲存到向量資料庫中。接著我們就可以根據新增的文件內容提出問題,例如,如果新增了一篇關於 EF Core 分頁的文章,我們可以詢問哪種分頁方法適用於大型資料庫,應用程式將使用自訂的文章內容進行回應。

 

舉例來說,筆者加入一個「UGATS_01_Install.pdf」檔案到wwwroot/data 資料夾當網站應用程式啟動後,它會自動讀取「wwwroot/Data資料夾中的範例 PDF 文件,並生成它們的內嵌。在網站聊天介面中,使用提示如下:

總結UGATS_01_Install.pdf的內容


這個範例執行的結果請參考下圖所示:

 

18:查詢自訂的文章

 

自訂C# 函式(Tools)擴展應用程式功能

.NET AI 應用程式範本(.NET AI app template讓你能夠輕鬆地擴充聊天機器人的行為,使其能夠呼叫自定義的 C# 函式。這允許聊天機器人執行額外的能力,例如獲取即時資料或進行特定操作。

 

舉例來說,只要在專案「chat.razerBlazor 頁面,定義一個 C# 方法。例如,以下的程式中,包含一個用來獲取每日一金句的「GetGoldensentence」方法。在應用程式初始化(OnInitialized方法)方法中,需要加一行程式碼「AIFunctionFactory.Create(GetGoldensentence)」,將此函式註冊到 ChatOptions 的工具清單中,這樣聊天機器人就可以呼叫 GetGoldensentence 方法了。

 

Chat.razor

@page "/"

@using System.ComponentModel

@inject IChatClient ChatClient

@inject NavigationManager Nav

@inject SemanticSearch Search

@implements IDisposable

 

<PageTitle>Chat</PageTitle>

 

<ChatHeader OnNewChat="@ResetConversationAsync" />

 

<ChatMessageList Messages="@messages" InProgressMessage="@currentResponseMessage">

    <NoMessagesContent>

        <div>To get started, try asking about these example documents. You can replace these with your own data and replace this message.</div>

        <ChatCitation File="Example_Emergency_Survival_Kit.pdf"/>

        <ChatCitation File="Example_GPS_Watch.md"/>

    </NoMessagesContent>

</ChatMessageList>

 

<div class="chat-container">

    <ChatSuggestions OnSelected="@AddUserMessageAsync" @ref="@chatSuggestions" />

    <ChatInput OnSend="@AddUserMessageAsync" @ref="@chatInput" />

    <SurveyPrompt /> @* Remove this line to eliminate the template survey message *@

</div>

 

@code {

    private const string SystemPrompt = @"

        You are an assistant who answers questions about information you retrieve.

        Do not answer questions about anything else.

        Use only simple markdown to format your responses.

 

        Use the LoadDocuments tool to prepare for searches before answering any questions.

 

        Use the Search tool to find relevant information. When you do this, end your

        reply with citations in the special XML format:

 

        <citation filename='string'>exact quote here</citation>

 

        Always include the citation in your response if there are results.

 

        The quote must be max 5 words, taken word-for-word from the search result, and is the basis for why the citation is relevant.

        Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text.

        ";

 

    private int statefulMessageCount;

    private readonly ChatOptions chatOptions = new();

    private readonly List<ChatMessage> messages = new();

    private CancellationTokenSource? currentResponseCancellation;

    private ChatMessage? currentResponseMessage;

    private ChatInput? chatInput;

    private ChatSuggestions? chatSuggestions;

 

    protected override void OnInitialized()

    {

        statefulMessageCount = 0;

        messages.Add(new(ChatRole.System, SystemPrompt));

        chatOptions.Tools = [

            AIFunctionFactory.Create(LoadDocumentsAsync),

            AIFunctionFactory.Create(SearchAsync),

            AIFunctionFactory.Create(GetGoldensentence)

        ];

    }

 

    private async Task AddUserMessageAsync(ChatMessage userMessage)

    {

        CancelAnyCurrentResponse();

 

        // Add the user message to the conversation

        messages.Add(userMessage);

        chatSuggestions?.Clear();

        await chatInput!.FocusAsync();

 

        // Stream and display a new response from the IChatClient

        var responseText = new TextContent("");

        currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]);

        currentResponseCancellation = new();

        await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token))

        {

            messages.AddMessages(update, filter: c => c is not TextContent);

            responseText.Text += update.Text;

            chatOptions.ConversationId = update.ConversationId;

            ChatMessageItem.NotifyChanged(currentResponseMessage);

        }

 

        // Store the final response in the conversation, and begin getting suggestions

        messages.Add(currentResponseMessage!);

        statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0;

        currentResponseMessage = null;

        chatSuggestions?.Update(messages);

    }

 

    private void CancelAnyCurrentResponse()

    {

        // If a response was cancelled while streaming, include it in the conversation so it's not lost

        if (currentResponseMessage is not null)

        {

            messages.Add(currentResponseMessage);

        }

 

        currentResponseCancellation?.Cancel();

        currentResponseMessage = null;

    }

 

    private async Task ResetConversationAsync()

    {

        CancelAnyCurrentResponse();

        messages.Clear();

        messages.Add(new(ChatRole.System, SystemPrompt));

        chatOptions.ConversationId = null;

        statefulMessageCount = 0;

        chatSuggestions?.Clear();

        await chatInput!.FocusAsync();

    }

 

    [Description("Loads the documents needed for performing searches. Must be completed before a search can be executed, but only needs to be completed once.")]

    private async Task LoadDocumentsAsync()

    {

        await InvokeAsync(StateHasChanged);

        await Search.LoadDocumentsAsync();

    }

 

    [Description("Searches for information using a phrase or keyword. Relies on documents already being loaded.")]

    private async Task<IEnumerable<string>> SearchAsync(

        [Description("The phrase to search for.")] string searchPhrase,

        [Description("If possible, specify the filename to search that file only. If not provided or empty, the search includes all files.")] string? filenameFilter = null)

    {

        await InvokeAsync(StateHasChanged);

        var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5);

        return results.Select(result =>

            $"<result filename=\"{result.DocumentId}\">{result.Text}</result>");

    }

 

    private async Task<string> GetGoldensentence()

    {

        string[] sentences = {

            "知識改變命運,行動成就未來。",

            "簡單就是最高的成熟。",

            "今日的努力,成就明日的光芒。",

            "心懷熱忱,腳踏實地。",

            "小步前進,也能走完千里。",

            "失敗是成功的墊腳石。",

            "學會傾聽,方能被理解。",

            "專注於當下,掌握每一刻。",

            "勇於改變,才能看見新世界。",

            "善良與堅持,勝過所有天賦。"

        };

 

        var index = Random.Shared.Next(sentences.Length);

        return sentences[index];

    }

 

    public void Dispose()

        => currentResponseCancellation?.Cancel();

}


讓我們執行這個網站測試,在聊天介面輸入提示如下:

一日一金句


聊天機器人將在背後呼叫我們的自定義 GetGoldensentence 方法,並將其結果包含在答案中,執行的結果請參考下圖所示:

19:呼叫自定義 GetGoldensentence方法


這是一個非常強大的功能,你可以將這個概念擴展到連接真實的 API、資料庫或 IoT 設備。


總結

.NET AI 應用程式範本(.NET AI app template)(尤其是 AI Chat Web App)是一個功能強大的起點,它充分利用了 Microsoft.Extensions.AI 」套件提供的統一抽象層和 RAG 機制透過將 AI 服務、內嵌生成和 Blazor UI 預先配置在一起,這個範本降低了 .NET 開發人員建置自定義 AI 助理或聊天機器人的門檻。

無論是使用 GitHub ModelsAzure OpenAI 還是 Ollama,開發人員都能在幾分鐘內完成複雜的基礎架構設定,並將精力集中在應用程式的業務邏輯和功能擴充上,例如整合自定義 C# 工具。這使得 .NET 成為構建智慧型應用程式的強大且高效的生態系統。

沒有留言:

張貼留言