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.
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.
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ę:
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 roleai.*.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
MarkdigiHtmlSanitizer, 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
Brak komentarzy.