Blog Dla Programistów C#/.NET

Jak dodać AI do aplikacji .NET? Microsoft.Extensions.AI i IChatClient krok po kroku

niedziela, 27 września 2026 Tagi: C#/.NETProgramowanieAI

Weź zwykłą aplikację .NET, na przykład system zgłoszeń (helpdesk): formularz, lista, baza danych. Klient pisze ścianę tekstu na 15 zdań, z której wynika tylko tyle, że nie działa mu logowanie. Agent musi to wszystko przeczytać, sam ustawić kategorię, sam nadać priorytet i sam napisać odpowiedź. Każde zgłoszenie to kilka minut klikania.

A teraz ta sama aplikacja po 20 minutach pracy: sama podsumowuje zgłoszenia, sama nadaje im kategorię i priorytet i sama pisze szkic odpowiedzi do klienta, który pojawia się na ekranie słowo po słowie. Zero magii i zero doktoratu z machine learningu. Wystarczy 1 paczka NuGet, 1 interfejs i darmowy dostęp do modeli AI - bez podawania karty kredytowej.

W tym artykule pokażę Ci krok po kroku cały kod: konfigurację Microsoft.Extensions.AI z darmowym GitHub Models, a potem 3 funkcje AI - podsumowanie zgłoszenia, klasyfikację zwracaną jako typowany obiekt C# (structured output) i szkic odpowiedzi ze streamingiem. Na koniec policzymy, ile to realnie kosztuje na produkcji, i przejdziemy przez 4 pułapki, na których programiści wykładają się najczęściej.

Stan danych: październik 2026. Przykład działa na .NET 10, a paczki Microsoft.Extensions.AI i Microsoft.Extensions.AI.OpenAI są dostępne w stabilnych wersjach (linia 10.x). GitHub Models jest darmowy, z limitami requestów zależnymi od planu Copilota na Twoim koncie, a produkcyjna cena małego modelu gpt-4o-mini to ok. 0,15 USD za milion tokenów wejściowych. Biblioteka wciąż się rozwija, więc jeśli któraś sygnatura metody nie będzie się zgadzać, zajrzyj do aktualnej dokumentacji na learn.microsoft.com.

Jak dodać AI do aplikacji .NET? Microsoft.Extensions.AI i IChatClient krok po kroku

Plan: 3 funkcje AI w istniejącej aplikacji


Nie budujemy chatbota-zabawki, tylko coś, co realnie oszczędza czas w prawdziwej firmie. Do istniejącego systemu zgłoszeń dodamy 3 funkcje:

1. Podsumowanie zgłoszenia - 1 zdanie zamiast ściany tekstu od klienta.
2. Klasyfikacja - kategoria i priorytet zwracane jako typowany obiekt C#.
3. Szkic odpowiedzi dla agenta - generowany na żywo, ze streamingiem token po tokenie.

Szczególnie ważny jest punkt 2. Odpowiedź modelu nie będzie tekstem, który trzeba parsować regexem - AI zwróci nam gotowy obiekt z enumami. Za chwilę zobaczysz, jak to działa.

Aplikacja "przed". Punkt wyjścia jest celowo nudny: Blazor Web App na .NET 10, 2 widoki jako komponenty Razor (lista zgłoszeń i szczegóły zgłoszenia), dane przez EF Core i proste repozytorium. Nic odkrywczego - i dobrze, bo dokładnie tak wygląda większość aplikacji biznesowych, do których prędzej czy później ktoś zechce dodać AI.

Dlaczego helpdesk? Bo każdy rozumie go w 10 sekund, a AI daje tu natychmiastowy, widoczny efekt. Jeśli chcesz przejść ten przykład u siebie, przygotuj w bazie kilka zgłoszeń o różnym charakterze: jedno wściekłe (awaria, zdenerwowany klient), jedno pytanie o fakturę i jedną prośbę o nową funkcję. Przydadzą się przy testowaniu klasyfikacji.

Jak aplikacja .NET rozmawia z modelem AI?


To po prostu API. Zacznijmy od demistyfikacji: rozmowa z modelem AI (LLM) to zwykłe wywołanie HTTP. Wysyłasz tekst, czyli tak zwany prompt, i dostajesz tekst z powrotem:

Twoja aplikacja .NET → HTTP → model AI (LLM) → odpowiedź

Tyle. Cała "magia" siedzi po stronie modelu.

Problem: każdy dostawca ma inne API. OpenAI ma swoje SDK, Azure OpenAI swoje, modele uruchamiane lokalnie (np. przez Ollamę) swoje, Mistral swoje. Jeśli napiszesz aplikację pod konkretne SDK, a za rok zechcesz zmienić dostawcę, przepisujesz pół aplikacji.

Rozwiązanie: Microsoft.Extensions.AI i IChatClient. Microsoft rozwiązał ten problem paczką Microsoft.Extensions.AI. Dostajesz 1 wspólny interfejs - IChatClient - który działa dokładnie jak ILogger: kodujesz pod interfejs, a to, co siedzi pod spodem, jest szczegółem konfiguracji. W tym przykładzie pod spodem będzie GitHub Models, czyli darmowy dostęp do topowych modeli, do którego wystarczy konto na GitHubie. Idealne do nauki i developmentu.

Czego potrzebujesz. Lista jest krótka:

• .NET SDK (przykład jest na .NET 10),
• darmowe konto na GitHubie,
• token PAT z GitHuba (za chwilę pokażę, gdzie go wygenerować).

Czego nie potrzebujesz? Karty kredytowej ani doktoratu z machine learningu.

Konfiguracja: token, paczki NuGet i rejestracja IChatClient


Krok 1: token. Na GitHubie wejdź w Settings → Developer settings → Personal access tokens i wygeneruj nowy token. Jeśli wybierasz token typu fine-grained, nadaj mu uprawnienie Models w trybie tylko do odczytu (models:read) - to wystarczy do wywoływania modeli. Token trafia do user secrets, nigdy do kodu. To standard, a nie wyjątek:

dotnet user-secrets init
dotnet user-secrets set "AI:Token" "tu-wklej-swoj-token"

Polecenie init wykonujesz raz na projekt (dodaje UserSecretsId do pliku .csproj). Klucz AI:Token odczytamy za chwilę przez builder.Configuration["AI:Token"].

Krok 2: paczki. Potrzebujesz 2 paczek NuGet:

dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI

Pierwsza paczka to abstrakcje, czyli nasz IChatClient. Druga to implementacja zgodna z API OpenAI - a GitHub Models wystawia właśnie taki endpoint. Starsze poradniki każą dodawać drugą paczkę z flagą --prerelease, ale dziś obie są dostępne w stabilnych wersjach.

Krok 3: rejestracja w DI. W Program.cs tworzymy klienta OpenAI, który wskazuje na endpoint GitHub Models, i rejestrujemy go jako IChatClient:

/* Program.cs */
using System.ClientModel;
using Microsoft.Extensions.AI;
using OpenAI;

var builder = WebApplication.CreateBuilder(args);

/* GitHub Models wystawia endpoint zgodny z API OpenAI */
var openAiClient = new OpenAIClient(
new ApiKeyCredential(builder.Configuration["AI:Token"]!),
new OpenAIClientOptions
{
Endpoint = new Uri("https://models.github.ai/inference")
});

builder.Services.AddChatClient(
openAiClient.GetChatClient("openai/gpt-4o-mini").AsIChatClient());

I to jest cała konfiguracja. Zwróć uwagę na jedną rzecz: gdybyś jutro chciał przejść na Azure OpenAI, płatne API OpenAI albo uruchomić model lokalnie przez Ollamę, zmieniasz tylko te kilka linijek. Reszta aplikacji nawet tego nie zauważy, bo wszędzie indziej używasz wyłącznie IChatClient.

Krok 4: smoke test. Zanim zaczniesz budować funkcje, sprawdź, czy połączenie działa. Wystarczy prosty endpoint testowy:

app.MapGet("/ai-test", async (IChatClient chat) =>
{
var response = await chat.GetResponseAsync("Powiedz 'działa' po polsku");
return response.Text;
});

Uruchom aplikację i wejdź w przeglądarce na /ai-test. Jeśli zobaczysz odpowiedź modelu, właśnie wykonałeś swój pierwszy request do LLM-a z .NET-a. I to wszystko. Reszta to już tylko ubieranie tego w sensowne funkcje.

Funkcja 1: AI podsumowuje zgłoszenie w 1 zdaniu


Całą logikę AI zamkniemy w 1 serwisie - TicketAiService. IChatClient dostajemy przez konstruktor (tu w formie primary constructor), a pierwsza metoda wygląda tak:

public class TicketAiService(IChatClient chat)
{
public async Task<string> SummarizeAsync(Ticket ticket)
{
var prompt = $"""
Podsumuj poniższe zgłoszenie klienta w JEDNYM zdaniu po polsku.
Skup się na konkretnym problemie, pomiń uprzejmości i emocje.

Tytuł: {ticket.Title}
Treść: {ticket.Body}
""";

var response = await chat.GetResponseAsync(prompt);
return response.Text;
}
}

Serwis rejestrujemy w Program.cs:

builder.Services.AddScoped<TicketAiService>();

Prompt to 80% roboty. Sam kod jest banalny. Prawdziwa praca dzieje się w prompcie i warto się przy nim zatrzymać. Stosuję zawsze 3 zasady:

1. Mów dokładnie, czego chcesz. "W JEDNYM zdaniu", a nie "krótko". Model traktuje polecenia dosłownie, a "krótko" może znaczyć cokolwiek.

2. Mów, co ma pominąć. Bez instrukcji "pomiń uprzejmości i emocje" dostaniesz podsumowanie w stylu "klient grzecznie prosi o pomoc", które nic nie wnosi.

3. Używaj raw string literals. Potrójne cudzysłowy, dostępne od C# 11, to najlepszy przyjaciel promptów w C#: tekst jest czytelny, wielolinijkowy i obsługuje interpolację.

Podpięcie w UI. Metodę wołasz z handlera przycisku w komponencie szczegółów zgłoszenia, a wynik wyświetlasz obok oryginalnej treści:

@rendermode InteractiveServer
@inject TicketAiService Ai

<button class="btn btn-outline-primary" @onclick="SummarizeAsync">Podsumuj</button>
<p><b>@_summary</b></p>

@code {
private Ticket _ticket = default!; /* wczytane z repozytorium, np. w OnInitializedAsync */
private string? _summary;

private async Task SummarizeAsync()
=> _summary = await Ai.SummarizeAsync(_ticket);
}

Najlepiej przetestować to na najbardziej wściekłym, 15-zdaniowym zgłoszeniu. Obok ściany tekstu pojawia się 1 konkretne zdanie w stylu "Klient nie może zalogować się do systemu" - i ten kontrast robi całą robotę.

Funkcja 2: klasyfikacja jako typowany obiekt C# (structured output)


Teraz najlepsza część. Podsumowanie to tekst - w porządku. Ale AI może zwrócić Ci też gotowy, typowany obiekt C#. Bez parsowania, bez regexów i bez modlitwy, żeby format się zgadzał.

Najpierw opisujemy, co chcemy dostać:

public record TicketClassification(
TicketCategory Category,
TicketPriority Priority,
string Reasoning);

public enum TicketCategory { Bug, Billing, FeatureRequest, Question, Other }

public enum TicketPriority { Low, Medium, High, Critical }

A potem dodajemy metodę do serwisu:

public async Task<TicketClassification> ClassifyAsync(Ticket ticket)
{
var prompt = $"""
Sklasyfikuj zgłoszenie klienta.
Critical = awaria blokująca pracę lub utrata danych.
High = poważny problem z obejściem.

Tytuł: {ticket.Title}
Treść: {ticket.Body}
""";

var response = await chat.GetResponseAsync<TicketClassification>(prompt);
return response.Result;
}

Jak to działa? Kluczowa jest ta linijka: GetResponseAsync<TicketClassification>. Podajesz typ generyczny, a biblioteka pod spodem generuje z Twojego rekordu schemat JSON, wysyła go modelowi i wymusza odpowiedź w tym formacie. Z powrotem dostajesz obiekt w response.Result. Enum to enum, a nie string "bardzo pilne!!!".

Zauważ też, że w prompcie definiujemy, co znaczy Critical, a co High. To ta sama zasada co przy podsumowaniu: mów dokładnie, czego chcesz. Im precyzyjniej opiszesz poziomy, tym bardziej przewidywalne będą wyniki.

Trik: pole Reasoning. W rekordzie jest jeszcze pole Reasoning - prosimy w nim model, żeby uzasadnił swoją decyzję. Z 2 powodów. Po pierwsze, modele klasyfikują lepiej, kiedy muszą uzasadnić wybór. Po drugie, przy debugowaniu od razu widzisz, dlaczego zgłoszenie trafiło do złej kategorii. Mała wskazówka: model wypełnia pola w kolejności ze schematu, więc jeśli przeniesiesz Reasoning na początek rekordu, najpierw "pomyśli", a dopiero potem wybierze kategorię i priorytet.

Efekt. Na przygotowanych zgłoszeniach wygląda to tak: wściekły opis awarii dostaje Bug / Critical, pytanie o fakturę Billing / Low, a w Reasoning widać, czym model się kierował.

Na produkcję: TryGetResult. Model nie ma obowiązku trzymać się schematu w 100%. Jeśli jego odpowiedzi nie da się zamienić na obiekt, odczyt response.Result rzuci wyjątkiem. Bezpieczniej użyć TryGetResult i mieć gotowy plan B:

var response = await chat.GetResponseAsync<TicketClassification>(prompt);

if (!response.TryGetResult(out var classification))
{
/* model nie trzymał się schematu - zgłoszenie idzie do ręcznej weryfikacji */
return new TicketClassification(
TicketCategory.Other, TicketPriority.Medium, "Brak klasyfikacji AI");
}

return classification;


Funkcja 3: szkic odpowiedzi ze streamingiem


Ostatnia funkcja to szkic odpowiedzi do klienta. I tu ważna rzecz o UX: wygenerowanie pełnej odpowiedzi trwa kilka sekund. Jeśli użytkownik patrzy przez 5 sekund na kręcące się kółko, myśli, że aplikacja się zawiesiła. Dlatego streamujemy - tekst pojawia się słowo po słowie, tak jak w ChatGPT.

Metoda w serwisie zwraca strumień fragmentów zamiast gotowego tekstu:

public IAsyncEnumerable<ChatResponseUpdate> SuggestReplyAsync(Ticket ticket)
{
var prompt = $"""
Napisz profesjonalną, empatyczną odpowiedź do klienta po polsku.
Maksymalnie 4 zdania. Nie obiecuj terminów.

Zgłoszenie: {ticket.Body}
""";

return chat.GetStreamingResponseAsync(prompt);
}

Zwróć uwagę na zdanie "Nie obiecuj terminów". To typ zabezpieczenia, o którym ludzie zapominają, a potem AI obiecuje klientowi naprawę "do jutra".

Streaming w Blazorze. GetStreamingResponseAsync zwraca IAsyncEnumerable<ChatResponseUpdate>, więc iterujemy po nim przez await foreach i po każdym fragmencie wołamy StateHasChanged:

<button class="btn btn-primary" @onclick="SuggestReplyAsync">Zaproponuj odpowiedź</button>
<textarea class="form-control mt-3" rows="6" @bind="_reply"></textarea>

@code {
/* ten sam komponent szczegółów zgłoszenia co wcześniej */
private string _reply = "";

private async Task SuggestReplyAsync()
{
_reply = "";

await foreach (var update in Ai.SuggestReplyAsync(_ticket))
{
_reply += update.Text;
StateHasChanged();
}
}
}

Blazor w trybie Interactive Server sam dopycha zmiany do przeglądarki przez swoje połączenie SignalR, więc nie musisz ręcznie pisać żadnego SSE ani EventSource. Tekst płynie na żywo prosto do pola tekstowego, w którym agent może go poprawić przed wysłaniem.

Streaming w czystym API. Jeśli Twój front to osobna aplikacja (np. React albo Angular), a backend to Minimal API, fragmenty wypychasz do przeglądarki samodzielnie, jako Server-Sent Events:

app.MapGet("/tickets/{id}/suggest-reply", async (
int id, TicketRepository repo, TicketAiService ai, HttpContext ctx) =>
{
var ticket = await repo.GetAsync(id);
ctx.Response.ContentType = "text/event-stream";

await foreach (var update in ai.SuggestReplyAsync(ticket))
{
await ctx.Response.WriteAsync($"data: {update.Text}\n\n");
await ctx.Response.Body.FlushAsync();
}
});

Po stronie przeglądarki odbierasz je przez EventSource. W .NET 10 możesz też skorzystać z wbudowanego TypedResults.ServerSentEvents, który zajmie się formatowaniem strumienia za Ciebie.

Efekt końcowy: 30 sekund zamiast 5 minut


Złóżmy wszystko razem. Wpada nowe zgłoszenie z dramatycznym opisem awarii i od razu (np. zaraz po zapisaniu zgłoszenia wołasz SummarizeAsync i ClassifyAsync):

• pojawia się 1-zdaniowe podsumowanie,
• zgłoszenie dostaje kategorię Bug,
• i priorytet Critical.

Agent klika "Zaproponuj odpowiedź", szkic streamuje się na żywo, a agent tylko go edytuje i wysyła.

Porównaj to z wersją sprzed 20 minut. Z perspektywy firmy: agent zamiast 5 minut na zgłoszenie potrzebuje 30 sekund. Z perspektywy Twojego CV: właśnie nauczyłeś się rzeczy, którą w ogłoszeniach o pracę wpisuje się teraz praktycznie wszędzie.

Ile kosztuje AI w aplikacji .NET na produkcji?


To najczęstsze pytanie. Odpowiedź zwykle jest przyjemnym zaskoczeniem.

• Nauka i development: 0 zł. GitHub Models jest darmowy, ma tylko limity requestów. Do nauki i testów w zupełności wystarczą.

• Produkcja: grosze. Na produkcji przechodzisz na płatnego dostawcę (np. OpenAI albo Azure OpenAI) - i tu znowu przydaje się IChatClient, bo zmieniasz tylko konfigurację. Mały model, taki jak gpt-4o-mini, kosztuje ok. 0,15 USD za milion tokenów wejściowych (i 0,60 USD za milion tokenów wyjściowych). Dla systemu z 1000 zgłoszeń dziennie to rząd wielkości kilku-kilkunastu dolarów miesięcznie - zależnie od tego, jak długie są zgłoszenia i odpowiedzi.

Innymi słowy: miesięczny rachunek za AI w takim scenariuszu jest niższy niż koszt 1 godziny pracy programisty. Zawsze policz to pod swój ruch, ale nie zakładaj z góry, że AI = drogo.

4 pułapki przed wdrożeniem na produkcję


Działa? Świetnie. Ale zanim wrzucisz to na produkcję, sprawdź 4 rzeczy, na których ludzie wykładają się najczęściej.

1. Klucz API w kodzie. Nigdy. Klucz trafia do user secrets albo do zmiennych środowiskowych - zawsze. Wystarczy 1 commit z kluczem na GitHubie i masz problem, którego nie cofniesz.

2. Brak obsługi błędów. Model to zewnętrzne API. Czasem poleci timeout, czasem dostaniesz błąd, czasem przekroczysz limit. Dlatego potrzebujesz retry (najlepiej z Polly), fallbacku i sensownego komunikatu dla użytkownika - zamiast aplikacji, która sypie się przy pierwszym czknięciu sieci.

3. Wpychanie wszystkiego do prompta. Każdy token kosztuje. Nie wysyłaj modelowi całej bazy "na wszelki wypadek", tylko to, co naprawdę jest potrzebne do konkretnego zadania. Krócej znaczy taniej - i często celniej.

4. Ślepe zaufanie. Najważniejsza z pułapek. AI proponuje, człowiek zatwierdza. Dlatego w naszym helpdesku agent dostaje gotowy szkic i go edytuje, a odpowiedź nie trafia do klienta automatycznie. To jest dokładnie ta różnica między poważnym wdrożeniem AI a zabawką, która prędzej czy później obieca klientowi coś, czego nie powinna.

Ściąga: 3 funkcje AI i metody IChatClient


FunkcjaMetoda IChatClientCo dostajeszKluczowe w prompcie
Podsumowanie zgłoszeniaGetResponseAsync(prompt)tekst (response.Text)"w JEDNYM zdaniu", "pomiń uprzejmości i emocje"
KlasyfikacjaGetResponseAsync<T>(prompt)typowany obiekt C# (response.Result lub TryGetResult)definicje poziomów priorytetu + pole Reasoning
Szkic odpowiedziGetStreamingResponseAsync(prompt)strumień fragmentów (IAsyncEnumerable<ChatResponseUpdate>)"maksymalnie 4 zdania", "nie obiecuj terminów"


Co dalej: function calling, RAG i więcej


Te 3 funkcje to dopiero czubek góry lodowej. Kiedy opanujesz podstawy, naturalne kolejne kroki to:

• function calling - AI samo wywołuje Twoje metody w C#,
• RAG - AI odpowiada na podstawie Twojej własnej dokumentacji, a nie ogólnej wiedzy modelu,
• obraz i mowa - analiza zdjęć, rozpoznawanie i synteza mowy,
• ML.NET - trenowanie własnych modeli bezpośrednio w .NET.

Dobra wiadomość: IChatClient, którego właśnie użyłeś, obsługuje także function calling, więc nic z tego, czego się tu nauczyłeś, nie pójdzie do kosza.

Podsumowanie


Dodanie AI do aplikacji .NET nie wymaga rewolucji w architekturze ani wiedzy z machine learningu. Zbierzmy najważniejsze rzeczy:

• Rozmowa z modelem to zwykłe API: wysyłasz prompt, dostajesz odpowiedź.
• Microsoft.Extensions.AI + IChatClient: 1 interfejs dla wszystkich dostawców, a zmiana dostawcy to kilka linijek konfiguracji.
• GitHub Models: darmowy start, bez karty kredytowej.
• Prompt to 80% roboty: mów dokładnie, czego chcesz i co ma zostać pominięte.
• Structured output: GetResponseAsync<T> zwraca typowany obiekt C#, bez parsowania.
• Streaming: GetStreamingResponseAsync + await foreach, żeby użytkownik nie patrzył na spinner.
• Produkcja: koszty to grosze, ale klucz w sekretach, obsługa błędów, krótkie prompty i człowiek, który zatwierdza.

W tym artykule pokazałem 3 funkcje, ale pod spodem siedzi tego znacznie więcej: wymuszanie odpowiedzi w konkretnym formacie, function calling, RAG, obsługa obrazu i mowy czy trenowanie własnych modeli w ML.NET. Jeśli czujesz, że chcesz to ogarnąć po kolei, od podstaw i na realnych projektach - zamiast sklejać wiedzę z losowych tutoriali - mam dla Ciebie coś konkretnego.

Prowadzę Szkołę AI w C#/.NET - kompletne, praktyczne szkolenie online, w którym przez 10 modułów budujesz prawdziwe aplikacje z AI, m.in. chat, agenta AI, asystenta głosowego i asystenta kulinarno-dietetycznego, który rozpoznaje składniki ze zdjęcia. Wszystko w .NET, wszystko po polsku, krok po kroku. Przez cały czas masz mnie jako mentora: sprawdzam Twoje prace domowe i odpowiadam na pytania. Na koniec masz portfolio, które realnie pokażesz na rozmowie o pracę. Dostęp jest dożywotni, a jeśli szkolenie Ci nie podejdzie, masz 14 dni na zwrot pieniędzy, więc nic nie ryzykujesz.

Program, szczegóły i najbliższy termin zapisów znajdziesz tutaj: modestprogrammer.pl/szkola-ai-csharp-dotnet.

Powodzenia z pierwszą funkcją AI w Twojej aplikacji :)

Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Dodaj komentarz
© Copyright 2026 modestprogrammer.pl | Sztuczna Inteligencja | Regulamin | Polityka prywatności. Design by Kazimierz Szpin. Wszelkie prawa zastrzeżone.