本文將帶你了解如何打造一個使用 Phi3 模型和 ONNX Runtime Generative AI 函式庫來實作簡單生成式 AI 聊天應用程式的 WinUI 應用程式。 大型語言模型 (LLM) 可讓您將文字產生、轉換、推理和翻譯功能新增至您的應用程式。 如需在 Windows 應用程式中使用 AI 和機器學習模型的詳細資訊,請參閱在 Windows 上開始使用 AI。 如需 ONNX runtime 和生成式 AI 的詳細資訊,請參閱 Generative AI 使用 ONNX Runtime。
使用 AI 功能時,建議您檢閱:在 Windows上開發負責任的產生式 AI 應用程式和功能。
什麼是 ONNX Runtime
ONNX Runtime 是跨平臺機器學習模型加速器,具有彈性介面來整合硬體特定連結庫。 ONNX Runtime 可以搭配來自 PyTorch、Tensorflow/Keras、TFLite、scikit-learn和其他架構的模型使用。 如需詳細資訊,請參閱 ONNX Runtime 網站,網址為 https://onnxruntime.ai/docs/。
先決條件
- 您的裝置必須啟用開發人員模式。 欲知詳情,請參閱啟用您的設備以進行開發。
- Visual Studio 2022 或更新版本搭配 .NET 桌面開發工作負載。
建立新的 C# WinUI 應用程式
在 Visual Studio 中,建立新的專案。 在 [建立新專案] 對話框中,將語言篩選設定為 [C#],並將專案類型篩選設定為 [winui],然後選取 [空白應用程式,封裝 (Desktop 中的 WinUI3)] 範本。 將新專案命名為 「GenAIExample」。
將參考新增至 ONNX Runtime Generative AI NuGet 套件
在 方案總管 中,右鍵點擊「相依關係」並選擇「管理 NuGet 套件......」。在 NuGet 套件管理器中,選擇「瀏覽」標籤。搜尋「Microsoft.ML.OnnxRuntimeGenAI.WinML」,在版本下拉選單選擇最新的穩定版本,然後點選安裝。
此套件利用Windows機器學習自動選擇最佳的硬體執行提供者(NPU → GPU → CPU)。 不需要在 DirectML、QNN 或 CPU 專用套件間做選擇——Windows ML 都能處理。
備註
.WinML 套件需要 Windows 專屬目標架構(例如 net8.0-windows10.0.19041.0 或更新版本以上)。 如果你需要跨平台套件或想針對特定執行提供者,請參考本文末尾 的替代套件 章節。
將模型和詞彙檔案新增至您的專案
在 [方案總管]中,以滑鼠右鍵按下您的專案,然後選取 [新增>新資料夾]。 將新資料夾命名為 「Models」。 在此範例中,我們將使用來自 https://huggingface.co/microsoft/Phi-3-mini-4k-instruct-onnx/tree/main/directml/directml-int4-awq-block-128的模型。
有數種不同的方法來擷取模型。 在本逐步解說中,我們將使用 Hugging Face 命令列介面(CLI)。 如果您使用其他方法取得模型,您可能必須調整範例程序代碼中的模型檔案路徑。 如需安裝 Hugging Face CLI 並設定您的帳戶以使用它的資訊,請參閱 命令行介面 (CLI)。
安裝 CLI 之後,開啟終端機,流覽至您所建立的 Models 目錄,然後輸入下列命令。
huggingface-cli download microsoft/Phi-3-mini-4k-instruct-onnx --include directml/* --local-dir .
當作業完成時,請確認下列檔案存在:[Project Directory]\Models\directml\directml-int4-awq-block-128\model.onnx。
在 [方案總管]中,展開 “directml-int4-awq-block-128” 資料夾,然後選取資料夾中的所有檔案。 在 [檔案屬性] 窗格中,將 [複製到輸出目錄] 設定為 [若較新則複製]。
新增簡單的UI以與模型互動
在此範例中,我們將建立一個非常簡單的UI,其中具有 TextBox 來指定提示、提交提示的 Button,以及用於顯示狀態消息和模型的回應 TextBlock。 使用下列 XAML 取代 中預設 MainWindow.xaml 元素。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column ="0">
<TextBox x:Name="promptTextBox" Text="Compose a haiku about coding."/>
<Button x:Name="myButton" Click="myButton_Click">Submit prompt</Button>
</StackPanel>
<Border Grid.Column="1" Margin="20">
<TextBlock x:Name="responseTextBlock" TextWrapping="WrapWholeWords"/>
</Border>
</Grid>
初始化模型
在 MainWindow.xaml.cs中,為 Microsoft.ML.OnnxRuntimeGenAI 命名空間新增 using 指示詞。
using Microsoft.ML.OnnxRuntimeGenAI;
在 MainPage 類別定義中,宣告針對 Model 和 Tokenizer的成員變數。 設定我們在先前步驟中新增之模型檔案的位置。
private Model? model = null;
private Tokenizer? tokenizer = null;
private readonly string ModelDir =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
@"Models\directml\directml-int4-awq-block-128");
建立協助程式方法,以異步方式初始化模型。 這個方法會呼叫 Model 類別的構造函式,並將模型目錄的路徑傳入。 接下來,它會從模型建立新的 Tokenizer。
public Task InitializeModelAsync()
{
DispatcherQueue.TryEnqueue(() =>
{
responseTextBlock.Text = "Loading model...";
});
return Task.Run(() =>
{
var sw = Stopwatch.StartNew();
model = new Model(ModelDir);
tokenizer = new Tokenizer(model);
sw.Stop();
DispatcherQueue.TryEnqueue(() =>
{
responseTextBlock.Text = $"Model loading took {sw.ElapsedMilliseconds} ms";
});
});
}
在此範例中,我們會在啟動主視窗時載入模型。 更新頁面建構函式,以註冊 Activated 事件的處理程式。
public MainWindow()
{
this.InitializeComponent();
this.Activated += MainWindow_Activated;
}
Activated 事件可以多次引發,因此在事件處理程式中,請先檢查模型是否為 null,再初始化它。
private async void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
{
if (model == null)
{
await InitializeModelAsync();
}
}
將提示提交至模型
建立一個輔助方法,將提示提交給模型,然後以異步方式使用 IAsyncEnumerable將結果回傳給呼叫者。
在此方法中,迴圈會使用 產生器 類別,在每個階段中呼叫 generateNextToken ,以擷取模型預測接下來的幾個字元,稱為令牌的字元,應以輸入提示為基礎。 輸入的標記序列會透過 AppendTokenSequences 提供給產生器。 迴圈會執行,直到產生器 IsDone 方法傳回 true,或收到任何令牌 "<|end|>"、"<|system|>" 或 "<|user|>",這表示我們可以停止生成令牌。
public async IAsyncEnumerable<string> InferStreaming(string prompt)
{
if (model == null || tokenizer == null)
{
throw new InvalidOperationException("Model is not ready");
}
var generatorParams = new GeneratorParams(model);
var sequences = tokenizer.Encode(prompt);
generatorParams.SetSearchOption("max_length", 2048);
using var tokenizerStream = tokenizer.CreateStream();
using var generator = new Generator(model, generatorParams);
generator.AppendTokenSequences(sequences);
StringBuilder stringBuilder = new();
while (!generator.IsDone())
{
string part;
try
{
await Task.Delay(10).ConfigureAwait(false);
generator.GenerateNextToken();
part = tokenizerStream.Decode(generator.GetSequence(0)[^1]);
stringBuilder.Append(part);
if (stringBuilder.ToString().Contains("<|end|>")
|| stringBuilder.ToString().Contains("<|user|>")
|| stringBuilder.ToString().Contains("<|system|>"))
{
break;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
break;
}
yield return part;
}
}
新增UI程式代碼以提交提示並顯示結果
在 [按鈕] 點擊事件處理程式中,先確認模型不是空值。 使用系統和使用者提示創建提示字串,然後呼叫 InferStreaming,並更新 TextBlock 以反映回應的每個部分。
此範例中使用的模型已定型為接受下列格式的提示,其中 systemPrompt 是模型應如何運作的指示,而 userPrompt 是用戶的問題。
<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>
模型應該記錄其提示詞規範。 針對此模型,格式記載於 Huggingface 模型卡片。
private async void myButton_Click(object sender, RoutedEventArgs e)
{
responseTextBlock.Text = "";
if(model != null)
{
var systemPrompt = "You are a helpful assistant.";
var userPrompt = promptTextBox.Text;
var prompt = $@"<|system|>{systemPrompt}<|end|><|user|>{userPrompt}<|end|><|assistant|>";
await foreach (var part in InferStreaming(prompt))
{
responseTextBlock.Text += part;
}
}
}
執行範例
在 Visual Studio 中,在 [解決方案平臺] 下拉式清單中,確定目標處理器設定為 x64。 ONNXRuntime Generative AI 程式庫不支援 x86。 建置並執行專案。 等待 TextBlock 顯示模型已載入。 在提示文字框中輸入提示,然後按兩下 [提交] 按鈕。 您應該會看到結果逐漸填入文字塊。
替代性生成式 AI 套件
如果你需要鎖定特定的執行提供者,而非使用 Windows ML 的自動選擇,你可以使用以下套件之一代替.WinML:
| Package | 應用案例 |
|---|---|
Microsoft.ML.OnnxRuntimeGenAI.DirectML |
僅限顯示卡(NVIDIA、AMD、Intel) |
Microsoft.ML.OnnxRuntimeGenAI.QNN |
僅限 NPU(高通) |
Microsoft.ML.OnnxRuntimeGenAI |
僅限 CPU(跨平台) |
請勿在同一個專案中參考超過一個這些套件,因為它們會隨附互相衝突的 onnxruntime.dll 檔案。 生成式 AI API 程式碼(Model、Tokenizer、Generator)無論你用哪個套件都是一樣的。