YandexGPT-Integration in .NET: Ein praktischer Leitfaden für Entwickler
YandexGPT ist nicht nur ein Cloud-LLM-Dienst; es ist eine vollständig verwaltete API, die asynchrone Text- und Bildgenerierung unterstützt und sich nahtlos in die Unternehmensinfrastruktur von .NET integriert. Dieser Artikel bietet eine produktionsreife Implementierung der Integration und geht auf reale Herausforderungen wie Timeouts, Betriebsstatus, Base64-Behandlung und Markdown-Ausgabe-Sanitisierung ein. Wir konzentrieren uns auf das, was in Produktionsumgebungen am wichtigsten ist: Fehlertoleranz, Typsicherheit, Kontrolle über Prompt-Engineering und sichere Speicherung von Anmeldedaten.
Client-Architektur und sichere Initialisierung
Die Klasse MyGPTClient muss threadsicher sein und Ressourcenlecks vermeiden. Die Verwendung eines globalen HttpClient ist ein Anti-Pattern. Stattdessen nutzen wir IHttpClientFactory, registriert über Dependency Injection:
// 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"]);
});
Der Konstruktor von MyGPTClient akzeptiert nun IHttpClientFactory statt direkt einen HttpClient zu erstellen. Modell-URIs (gpt://.../yandexgpt, art://.../yandex-art/latest) werden in die Konfiguration ausgelagert, sodass ein einfacher Wechsel zwischen Umgebungen (Dev/Staging/Prod) ohne Neukompilierung möglich ist.
Wichtig: Die Rolle ai.languageModels.user erfordert eine explizite Zuweisung in der IAM-Richtlinie des Yandex Cloud-Katalogs. Für CI/CD-Umgebungen ist ein separates Dienstkonto mit minimalen Berechtigungen – nur ai.*.user – erforderlich. Keine editor oder admin Rollen.
Asynchrone Task-Handhabung: Vom Polling bis zur Retry-Logik
Die ursprüngliche Methode WaitForTaskCompletionAsync verwendet eine Endlosschleife mit einer festen Task.Delay(1000). Das ist in der Produktion inakzeptabel: Bei über 50 gleichzeitigen Anfragen belastet es die API-Limits und erschöpft die Threads. Wir implementieren adaptives Polling mit exponentieller Backoff-Strategie und Retry-Grenzen:
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($"API-Fehler: {result.error.message}");
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
// Behandle 429 – erhöhe die Verzögerung
delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.5, 10_000));
}
catch (JsonException)
{
throw new InvalidOperationException("Ungültiges JSON im Task-Status-Response");
}
await Task.Delay(delay);
attempt++;
delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.2, 5_000));
}
throw new TimeoutException($"Task {taskId} wurde innerhalb von {maxAttempts} Versuchen nicht abgeschlossen");
}
Diese Lösung adressiert zwei zentrale Probleme: Erstens verhindert sie API-Überlastung bei vorübergehender Nichtverfügbarkeit; zweitens löst sie bei einem Timeout eine klare Ausnahme aus – essentiell für korrektes Logging und Alarmierung.
Ausgabe-Sanitisierung: Von Markdown zu semantischem HTML
YandexGPT gibt Markdown mit inkonsistenter Formatierung zurück: #, ##, *, , sowie zufällige Zeichen (////, ----, ****). Einfache Regex-Ersetzungen reichen nicht aus. Wir implementieren einen Parser basierend auf Markdig, einer Bibliothek, die eine sichere Konvertierung im Whitelist-Modus unterstützt:
public string ConvertToHtml(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown)) return string.Empty;
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.UseEmojiAndSmiley()
.Build();
// Entferne Überschriften der ersten Ebene – sie duplizieren das Thema des Artikels
var cleaned = Regex.Replace(markdown, @"^#{1,6}\s+.*$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);
// Deaktiviere gefährliche Elemente
var html = Markdown.ToHtml(cleaned, pipeline);
return SanitizeHtml(html); // implementiert via 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);
}
Dies stellt sicher, dass die Ausgabe gültiges, sicheres HTML ohne <script>, <iframe> oder Inline-Stile ist.
Bildgenerierung: Base64-Validierung und Speicherung
Die Methode GenerateImageAsync erfordert strenge Validierung von MIME-Typ und Größe. Base64-Strings können beschädigt sein oder nicht-JPEG-Daten enthalten. Wir fügen Validierung hinzu:
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;
}
}
Das Speichern erfolgt in IWebHostEnvironment.WebRootPath/Resources/ArticleImg – dem Standardpfad für statische Dateien in ASP.NET Core. Dateinamen werden mit Path.GetRandomFileName() und .jpg-Erweiterung generiert, statt Guid.NewGuid(), um Kollisionen unter hoher Last zu vermeiden.
Wichtige Erkenntnisse
- Sicherheit: API-Schlüssel werden über
IConfigurationübergeben, nicht hardcodiert. Dienstkonten haben nurai.*.userRollen. - Zuverlässigkeit: Polling wird mit exponentiellem Backoff und Retry-Grenzen implementiert. Alle HTTP-Fehler werden explizit behandelt.
- Kompatibilität: Der Client nutzt
IHttpClientFactory, was Polly für Retries und Circuit Breakers ermöglicht. - Ausgabe-Reinheit: Markdown-zu-HTML-Konvertierung erfolgt über
MarkdigundHtmlSanitizer, nicht über Regex-Ersetzungen. - Performance: Bilder werden im
WebRootPathgespeichert, zugänglich über URL ohne Middleware.
Für Tests empfiehlt es sich, einen Mock-Server (z. B. WireMock.NET) zu verwenden, der /foundationModels/v1/completionAsync und /operations/{id} emuliert. Damit werden alle Zustände abgedeckt: done: true, done: false, error, 429 Too Many Requests.
Der vollständige Code, einschließlich DI-Registrierung, Unit-Tests und einem Controller-Nutzungsbeispiel, ist im dotnet-yandexgpt-sample-Repository verfügbar. In der Produktion sollte unbedingt Application Insights für das Tracing von Aufrufen an die Yandex-API konfiguriert werden – es ist entscheidend für die Diagnose von Verzögerungen und Fehlern.
— Editorial Team
Noch keine Kommentare.