Powrót do strony głównej

Integracja YandexGPT z .NET — przewodnik

Przewodnik po integracji YandexGPT w projekty .NET. Omówiono bezpieczną inicjalizację klienta, asynchroniczne przetwarzanie zadań z wykładniczym backoff, walidację i zapis obrazów, konwersję Markdown na bezpieczny HTML. Cel — production-ready rozwiązanie dla programistów.

Jak poprawnie podłączyć YandexGPT do projektu .NET
Advertisement 728x90

Integracja YandexGPT z .NET: praktyczny przewodnik dla programistów

YandexGPT to nie tylko chmurowy serwis LLM, ale pełnowartościowe API z obsługą asynchronicznej generacji tekstu i obrazów, kompatybilne z infrastrukturą enterprise .NET. W tym artykule przedstawiamy praktyczną implementację integracji bez abstrakcji, uwzględniającą realne problemy: timeouty, statusy operacji, przetwarzanie base64 oraz oczyszczanie wyjścia w formacie Markdown. Skupiamy się na tym, co jest krytyczne w środowisku produkcyjnym: odporność na awarie, typobezpieczność, kontrola nad inżynierią promptów oraz bezpieczne przechowywanie danych uwierzytelniających.

Architektura klienta i bezpieczna inicjalizacja

Klasa MyGPTClient musi być bezpieczna dla wielowątkowości i unikać wycieków zasobów. Używanie globalnego HttpClient to antypatrzyn. Zamiast tego stosujemy IHttpClientFactory, zarejestrowany za pomocą DI:

// Program.cs
builder.Services.AddHttpClient<MyGPTClient>((sp, client) =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    client.BaseAddress = new Uri("https://llm.api.cloud.yandex.net/");
    client.DefaultRequestHeaders.Authorization = 
        new System.Net.Http.Headers.AuthenticationHeaderValue(
            "Api-Key", config["Yandex:ApiKey"]);
});

Konstruktor MyGPTClient teraz przyjmuje IHttpClientFactory, a nie tworzy HttpClient bezpośrednio. URI modeli (gpt://.../yandexgpt, art://.../yandex-art/latest) są przenoszone do konfiguracji — dzięki temu można łatwo przełączać się między środowiskami (dev/staging/prod) bez konieczności rekompilacji.

Google AdInline article slot

Ważne: rola ai.languageModels.user wymaga jawnego przyznania w polityce IAM katalogu Yandex Cloud. Dla środowisk CI/CD potrzebny jest osobny konto serwisowe z minimalnymi uprawnieniami — tylko ai.*.user. Żadnych editor ani admin.

Asynchroniczne przetwarzanie zadań: od pollingu do logiki retry

Metoda WaitForTaskCompletionAsync w oryginalnym rozwiązaniu używa nieskończonego pętli z stałym Task.Delay(1000). To niedopuszczalne w produkcji: przy 50+ jednoczesnych żądaniach powstaje obciążenie limitów API i zużywane są wszystkie potoki. Implementujemy adaptacyjny polling z eksponencjalną opóźnieniem i ograniczeniem liczby prób:

private async Task<dynamic> WaitForTaskCompletionAsync(string taskId, int maxAttempts = 30)
{
    int attempt = 0;
    TimeSpan delay = TimeSpan.FromMilliseconds(500);

    while (attempt < maxAttempts)
    {
        try
        {
            var response = await _client.GetAsync($"operations/{taskId}");
            response.EnsureSuccessStatusCode();

            var responseBody = await response.Content.ReadAsStringAsync();
            var result = JsonConvert.DeserializeObject<dynamic>(responseBody);

            if (result?.done == true && result?.response != null)
                return result.response;

            if (result?.error != null)
                throw new InvalidOperationException($"Błąd API: {result.error.message}");
        }
        catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
        {
            // Obsługa 429 — zwiększamy opóźnienie
            delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.5, 10_000));
        }
        catch (JsonException)
        {
            throw new InvalidOperationException("Nieprawidłowy JSON w odpowiedzi o statusie zadania");
        }

        await Task.Delay(delay);
        attempt++;
        delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.2, 5_000));
    }

    throw new TimeoutException($"Zadanie {taskId} nie zostało zakończone w ciągu {maxAttempts} prób");
}

To rozwiązuje dwa problemy: po pierwsze, zapobiega fludowi API w przypadku czasowej niedostępności; po drugie, dostarcza jasny wyjątek w przypadku timeoutu — bez tego niemożliwe jest poprawne logowanie i wysyłanie alertów.

Google AdInline article slot

Oczyszczanie wyjścia: od Markdown do semantycznego HTML

YandexGPT zwraca Markdown z niekonsekwentnym formatowaniem: #, ##, *, , ale także losowe znaki (////, ----, ****). Prosta zamiana za pomocą regularnych wyrażeń jest niewystarczająca. Realizujemy parser oparty na Markdig — bibliotece wspierającej bezpieczną konwersję w trybie whitelist:

public string ConvertToHtml(string markdown)
{
    if (string.IsNullOrWhiteSpace(markdown)) return string.Empty;

    var pipeline = new MarkdownPipelineBuilder()
        .UseAdvancedExtensions()
        .UseEmojiAndSmiley()
        .Build();

    // Usuwamy nagłówki pierwszego stopnia — duplicują temat artykułu
    var cleaned = Regex.Replace(markdown, @"^#{1,6}\s+.*$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);

    // Wyłączamy niebezpieczne elementy
    var html = Markdown.ToHtml(cleaned, pipeline);
    return SanitizeHtml(html); // realizacja za pomocą HtmlSanitizer
}

private string SanitizeHtml(string html)
{
    var sanitizer = new HtmlSanitizer();
    sanitizer.AllowedTags.Clear();
    sanitizer.AllowedTags.UnionWith(new[] { "p", "br", "strong", "em", "ul", "ol", "li", "h2", "h3" });
    sanitizer.AllowedAttributes.Clear();
    return sanitizer.Sanitize(html);
}

To gwarantuje, że na wyjściu otrzymujemy weryfikowany, bezpieczny HTML bez <script>, <iframe> czy inline-stylów.

Generacja obrazów: weryfikacja base64 i zapis

Metoda GenerateImageAsync wymaga ścisłej weryfikacji typu MIME i rozmiaru. String base64 może być uszkodzony lub zawierać coś innego niż JPEG. Dodajemy weryfikację:

Google AdInline article slot
private bool IsValidBase64Image(string base64String)
{
    if (!base64String.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) ||
        !base64String.Contains(";base64,"))
        return false;

    var data = base64String.Substring(base64String.IndexOf(',') + 1);
    if (!IsValidBase64String(data)) return false;

    try
    {
        var bytes = Convert.FromBase64String(data);
        using var ms = new MemoryStream(bytes);
        using var img = Image.Load(ms);
        return img.Metadata.DecodedImageOrientation == ExifOrientation.Undefined ||
               img.Width > 100 && img.Height > 100;
    }
    catch
    {
        return false;
    }
}

Zapis odbywa się w IWebHostEnvironment.WebRootPath/Resources/ArticleImg — standardowym miejscu dla plików statycznych w ASP.NET Core. Nazwa pliku generowana jest przez Path.GetRandomFileName() z rozszerzeniem .jpg, a nie Guid.NewGuid(), aby uniknąć kolizji przy dużym obciążeniu.

Co jest ważne

  • Bezpieczeństwo: klucze API przekazywane są przez IConfiguration, a nie hardcode. Konto serwisowe posiada tylko role ai.*.user.
  • Niezawodność: polling realizowany jest z eksponencjalnym opóźnieniem i limitem prób. Wszystkie błędy HTTP są jasno traktowane.
  • Kompatybilność: klient korzysta z IHttpClientFactory, co umożliwia stosowanie Polly do retry i circuit breaker.
  • Czystość wyjścia: konwersja Markdown → HTML przebiega przez Markdig i HtmlSanitizer, a nie przez regex-zamiany.
  • Wydajność: obrazy zapisywane są w WebRootPath, dostępne pod URL bez middleware.

Do testowania zaleca się użycie mock-servera (np. WireMock.NET), emulującego /foundationModels/v1/completionAsync i /operations/{id}. Pozwala to na pokrycie wszystkich stanów: done: true, done: false, error, 429 Too Many Requests.

Pełny kod z rejestracją DI, testami unit oraz przykładem użycia w kontrolerze dostępny jest w repozytorium dotnet-yandexgpt-sample. W produkcji koniecznie należy skonfigurować Application Insights do śledzenia wywołań do Yandex API — to krytyczne dla diagnostyki opóźnień i błędów.

— Editorial Team

Advertisement 728x90

Czytaj dalej