Intégration de YandexGPT dans .NET : Guide pratique pour les développeurs
YandexGPT n’est pas seulement un service LLM cloud ; c’est une API entièrement gérée qui prend en charge la génération asynchrone de texte et d’images et s’intègre parfaitement à l’infrastructure .NET des entreprises. Cet article propose une implémentation d’intégration prête pour la production, en abordant des défis réels tels que les timeouts, les statuts d’opération, la gestion du Base64 et la purification des sorties Markdown. Nous nous concentrons sur ce qui compte le plus dans les environnements de production : la tolérance aux pannes, la sécurité des types, le contrôle de l’ingénierie des prompts et le stockage sécurisé des identifiants.
Architecture client et initialisation sécurisée
La classe MyGPTClient doit être thread-safe et éviter les fuites de ressources. Utiliser un HttpClient global est une anti-pattern. À la place, nous exploitons IHttpClientFactory, enregistré via l’injection de dépendances :
// 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"]);
});
Le constructeur de MyGPTClient accepte désormais IHttpClientFactory au lieu de créer directement un HttpClient. Les URI des modèles (gpt://.../yandexgpt, art://.../yandex-art/latest) sont déplacés vers la configuration, ce qui permet de basculer facilement entre les environnements (dev/staging/prod) sans recompilation.
Important : Le rôle ai.languageModels.user nécessite une attribution explicite dans la politique IAM du catalogue Yandex Cloud. Pour les environnements CI/CD, il faut utiliser un compte de service séparé avec des permissions minimales — uniquement ai.*.user » — et aucun rôle editor ou admin`.
Gestion des tâches asynchrones : du polling à la logique de retry
La méthode originale WaitForTaskCompletionAsync utilise une boucle infinie avec un Task.Delay(1000) fixe. Cela est inacceptable en production : avec plus de 50 requêtes simultanées, cela sollicite les limites de l’API et épuise les threads. Nous mettons en œuvre un polling adaptatif avec un backoff exponentiel et des limites de retry :
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($"Erreur API : {result.error.message}");
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
// Gérer le 429 — augmenter le délai
delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.5, 10_000));
}
catch (JsonException)
{
throw new InvalidOperationException("JSON invalide dans la réponse du statut de la tâche");
}
await Task.Delay(delay);
attempt++;
delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.2, 5_000));
}
throw new TimeoutException($"La tâche {taskId} n’a pas été complétée après {maxAttempts} tentatives");
}
Cette solution résout deux problèmes clés : premièrement, elle prévient la saturation de l’API lors d’une indisponibilité temporaire ; deuxièmement, elle lance une exception claire en cas de timeout — essentielle pour une journalisation et une alerte appropriées.
Purification des sorties : de Markdown à HTML sémantique
YandexGPT renvoie du Markdown avec une mise en forme incohérente : #, ##, *, , ainsi que des caractères aléatoires (////, ----, ****). Les remplacements simples par regex ne suffisent pas. Nous implémentons un parseur basé sur Markdig, une bibliothèque qui supporte une conversion sûre en mode whitelist :
public string ConvertToHtml(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown)) return string.Empty;
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.UseEmojiAndSmiley()
.Build();
// Supprimer les titres de premier niveau — ils dupliquent le sujet de l’article
var cleaned = Regex.Replace(markdown, @"^#{1,6}\s+.*$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);
// Désactiver les éléments dangereux
var html = Markdown.ToHtml(cleaned, pipeline);
return SanitizeHtml(html); // implémenté 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);
}
Cela garantit que la sortie est un HTML valide et sûr, sans <script>, <iframe> ni styles inline.
Génération d’images : validation et stockage du Base64
La méthode GenerateImageAsync nécessite une validation stricte du type MIME et de la taille. Les chaînes Base64 peuvent être corrompues ou contenir des données non-JPEG. Nous ajoutons une validation :
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;
}
}
L’enregistrement se fait dans IWebHostEnvironment.WebRootPath/Resources/ArticleImg — le chemin standard pour les fichiers statiques dans ASP.NET Core. Les noms de fichiers sont générés à l’aide de Path.GetRandomFileName() avec une extension .jpg, plutôt que Guid.NewGuid(), afin d’éviter les collisions sous forte charge.
Points clés à retenir
- Sécurité : les clés API sont passées via
IConfiguration, pas codées en dur. Les comptes de service n’ont que les rôlesai.*.user. - Fiabilité : le polling est mis en œuvre avec un backoff exponentiel et des limites de retry. Toutes les erreurs HTTP sont traitées explicitement.
- Compatibilité : le client utilise
IHttpClientFactory, ce qui permet d’utiliser Polly pour les retries et les circuit breakers. - Propreté des sorties : la conversion Markdown-to-HTML passe par
MarkdigetHtmlSanitizer, pas par des remplacements par regex. - Performance : les images sont enregistrées dans
WebRootPath, accessibles par URL sans middleware.
Pour les tests, il est recommandé d’utiliser un serveur mock (par exemple WireMock.NET) émulant /foundationModels/v1/completionAsync et /operations/{id}. Cela couvre tous les états : done: true, done: false, error, 429 Too Many Requests.
Le code complet, y compris l’enregistrement DI, les tests unitaires et un exemple d’utilisation du contrôleur, est disponible dans le dépôt dotnet-yandexgpt-sample. En production, veillez à configurer Application Insights pour tracer les appels à l’API Yandex — c’est crucial pour diagnostiquer les retards et les erreurs.
— Editorial Team
Aucun commentaire pour le moment.