.NETC#AI EngineeringDevOpsCI/CD

Évitez qu’un refactoring C# casse les appels d’outils IA : noms explicites, schéma JSON testé et compatibilité contrôlée dans GitHub Actions.

Yva Hajatiana
24 octobre 2026
8 min de lecture
Partager :X / TwitterLinkedIn
Composition éditoriale abstraite sur la qualité et les déploiements logiciels

Stabiliser les contrats d’outils IA .NET en CI

Renommer une méthode C# est normalement une opération sûre. Pour un agent, elle peut devenir une rupture d’API : le nom de l’outil et de ses paramètres fait partie du schéma JSON présenté au modèle. Un renommage tel que GetReleaseStatusAsync vers GetStatusAsync, ou targetEnvironment vers environment, peut donc modifier un contrat que les prompts, les traces, les clients et les jeux de tests attendent déjà.

Depuis la version 10.8 de Microsoft.Extensions.AI, les attributs AIFunctionNameAttribute et AIParameterNameAttribute permettent de séparer ces identifiants publics de la forme du code C#. Ce tutoriel les utilise pour construire un contrat d’outil de déploiement stable, vérifier son schéma sans appeler de modèle, puis empêcher une régression dans GitHub Actions.

État de l’API. Ces attributs sont marqués expérimentaux (MEAI001) dans les versions actuelles de Microsoft.Extensions.AI. Épinglez la version NuGet, gardez l’avertissement visible et exécutez les tests de contrat avant chaque mise à jour. « Expérimental » ne veut pas dire « sans test » ; c’est précisément une raison supplémentaire de tester.

Le contrat que la CI doit protéger

Le contrat exposé au modèle ne se limite pas à la signature C#. Il rassemble le nom de l’outil, ses paramètres visibles, leur caractère requis, leurs types et la description qui aide le modèle à faire un choix. Les paramètres techniques comme CancellationToken ne doivent pas apparaître dans ce contrat.

Illustration d’un code C# qui produit un document de schéma, vérifié par une chaîne CI matérialisée par un bouclier
Le code peut évoluer ; le schéma qui décrit l’outil au modèle doit évoluer de manière intentionnelle et revue.

Une stratégie simple consiste à distinguer trois couches :

CoucheExempleRègle de compatibilité
Implémentation C#GetReleaseStatusAsyncPeut être renommée ou déplacée.
Contrat d’outilget_release_status, environment, release_idStable tant qu’il existe des consommateurs.
Politique d’exécutionidentité de lecture seule, validation d’entréeNe dépend jamais d’une description fournie au modèle.

Ce découpage complète les contrôles d’autorisation : un nom stable aide le modèle à appeler le bon outil, mais il ne rend pas l’opération autorisée. Une fonction qui modifie un environnement doit conserver une identité restreinte, une validation métier et, si nécessaire, une approbation humaine.

Prérequis

Il faut le SDK .NET 10 ou ultérieur et une solution de test xUnit. La génération et l’inspection du schéma ne contactent aucun fournisseur IA : elles sont donc adaptées à chaque pull request, y compris celles provenant d’un fork.

Créez une solution minimale :

dotnet new sln --name ReleaseAssistant
dotnet new classlib --framework net10.0 --name ReleaseAssistant.Tools
dotnet new xunit --framework net10.0 --name ReleaseAssistant.Tools.Tests
dotnet sln add ReleaseAssistant.Tools/ReleaseAssistant.Tools.csproj ReleaseAssistant.Tools.Tests/ReleaseAssistant.Tools.Tests.csproj
dotnet add ReleaseAssistant.Tools/ReleaseAssistant.Tools.csproj package Microsoft.Extensions.AI --version 10.9.0
dotnet add ReleaseAssistant.Tools.Tests/ReleaseAssistant.Tools.Tests.csproj reference ReleaseAssistant.Tools/ReleaseAssistant.Tools.csproj
dotnet add ReleaseAssistant.Tools.Tests/ReleaseAssistant.Tools.Tests.csproj package Microsoft.Extensions.AI --version 10.9.0

La version est un exemple cohérent avec la documentation de l’API. Dans une application, utilisez une propriété centrale telle que MicrosoftExtensionsAiVersion et mettez à jour les deux projets dans la même pull request. Ne mélangez pas arbitrairement les versions de Microsoft.Extensions.AI et Microsoft.Extensions.AI.Abstractions.

1. Donner des noms publics explicites aux outils

Dans ReleaseAssistant.Tools/ReleaseTools.cs, les noms C# restent descriptifs pour l’équipe. Les attributs définissent en revanche le vocabulaire stable vu par le modèle. Le #pragma est local : il documente que l’équipe a accepté l’API expérimentale à cet endroit, sans masquer tous les avertissements du projet.

using System.ComponentModel;
using Microsoft.Extensions.AI;

namespace ReleaseAssistant.Tools;

public sealed record ReleaseStatus(string Environment, string ReleaseId, string State);

public sealed class ReleaseTools
{
#pragma warning disable MEAI001
    [AIFunctionName("get_release_status")]
    [Description("Retourne l’état en lecture seule d’une release précise dans un environnement non-production.")]
    public ReleaseStatus GetReleaseStatusAsync(
        [AIParameterName("environment")] string targetEnvironment,
        [AIParameterName("release_id")] string releaseId,
        CancellationToken cancellationToken)
#pragma warning restore MEAI001
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(targetEnvironment);
        ArgumentException.ThrowIfNullOrWhiteSpace(releaseId);
        cancellationToken.ThrowIfCancellationRequested();

        // Remplacez cet exemple par un adaptateur à votre système de livraison.
        // L’adaptateur doit employer une identité de lecture seule.
        return new ReleaseStatus(targetEnvironment, releaseId, "healthy");
    }
}

AIFunctionFactory.Create inspecte la méthode et produit un AIFunction. Les paramètres ordinaires sont inclus dans son JsonSchema; CancellationToken est automatiquement lié à l’exécution et en est exclu. Ainsi, renommer targetEnvironment en deploymentTarget ne change pas le paramètre public environment.

Exposez ensuite la fonction au client de chat de votre choix :

using System.Reflection;
using Microsoft.Extensions.AI;
using ReleaseAssistant.Tools;

var tools = new ReleaseTools();
MethodInfo method = typeof(ReleaseTools).GetMethod(
    nameof(ReleaseTools.GetReleaseStatusAsync))!;

AIFunction releaseStatus = AIFunctionFactory.Create(method, tools, options: null);

Console.WriteLine(releaseStatus.Name); // get_release_status
Console.WriteLine(releaseStatus.JsonSchema.GetRawText());

Ne transmettez pas directement une valeur proposée par le modèle à une API d’infrastructure. La méthode doit convertir environment vers une liste fermée d’environnements permis, vérifier le tenant et appeler un adaptateur qui ne dispose que des droits de lecture nécessaires.

2. Tester les invariants du schéma

Un snapshot brut est utile pour relire un changement de schéma, mais un test d’invariants donne un diagnostic plus clair. Créez ReleaseAssistant.Tools.Tests/ReleaseToolContractTests.cs :

using System.Reflection;
using Microsoft.Extensions.AI;
using ReleaseAssistant.Tools;

namespace ReleaseAssistant.Tools.Tests;

public sealed class ReleaseToolContractTests
{
    private static AIFunction CreateFunction()
    {
        var target = new ReleaseTools();
        MethodInfo method = typeof(ReleaseTools).GetMethod(
            nameof(ReleaseTools.GetReleaseStatusAsync))!;

        return AIFunctionFactory.Create(method, target, options: null);
    }

    [Fact]
    public void Keeps_the_public_tool_name()
    {
        AIFunction function = CreateFunction();

        Assert.Equal("get_release_status", function.Name);
    }

    [Fact]
    public void Exposes_only_the_expected_arguments()
    {
        AIFunction function = CreateFunction();
        var properties = function.JsonSchema.GetProperty("properties");

        string[] names = properties.EnumerateObject()
            .Select(property => property.Name)
            .Order(StringComparer.Ordinal)
            .ToArray();

        Assert.Equal(new[] { "environment", "release_id" }, names);

        string[] required = function.JsonSchema.GetProperty("required")
            .EnumerateArray()
            .Select(value => value.GetString()!)
            .ToArray();

        Assert.Equal(new[] { "environment", "release_id" }, required.Order());
    }
}

Exécutez ces tests localement :

dotnet test --configuration Release

Ce test échoue si une personne supprime un attribut, introduit une collision de noms ou rend un argument inattendu visible au modèle. La fabrique refuse d’ailleurs les deux paramètres mappés au même identifiant : laissez ce défaut faire échouer le build plutôt que de choisir arbitrairement un argument à l’exécution.

Pour un outil critique, ajoutez un snapshot versionné du JSON canonique à côté de ces assertions. Revoyez le diff comme une évolution d’API : il doit expliquer pourquoi un paramètre, une description ou une contrainte change. Ne snapshotez pas seulement la chaîne brute si votre serializer ou votre version de package peut réordonner des propriétés ; triez récursivement le JSON avant la comparaison.

3. Choisir une évolution compatible

Un contrat d’outil est plus proche d’une API publique que d’une méthode interne. Les changements suivants n’ont pas le même risque :

ChangementDécision recommandée
Renommer la méthode C#Conserver les attributs : le contrat reste inchangé.
Préciser une descriptionRevue du diff de schéma ; vérifier l’effet sur le choix de l’outil.
Ajouter un paramètre facultatifAutoriser après test et documentation du comportement par défaut.
Ajouter un paramètre requisCréer plutôt une nouvelle version d’outil ou coordonner tous les consommateurs.
Changer la signification d’un argumentNe réutilisez pas le nom existant ; créez un nom explicite et migrez.
Donner un pouvoir d’écritureSéparer l’outil, l’identité et la politique d’approbation.

Les descriptions sont aussi des données de contrat. Elles influencent le routage par le modèle, mais ne constituent ni une autorisation ni une validation. Écrivez-les sans promettre une capacité que l’identité de l’outil ne possède pas.

4. Verrouiller le contrat dans GitHub Actions

Placez ce workflow dans .github/workflows/tool-contract.yml. Il ne contient aucun secret ni id-token: write : le schéma est construit localement, et la pull request n’a pas besoin de contacter Azure ou un modèle.

name: Validate AI tool contracts

on:
  pull_request:
    paths:
      - "ReleaseAssistant.Tools/**"
      - "ReleaseAssistant.Tools.Tests/**"
      - ".github/workflows/tool-contract.yml"
  push:
    branches: [master]

permissions:
  contents: read

jobs:
  test-contracts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v5
        with:
          dotnet-version: 10.0.x
      - run: dotnet restore ReleaseAssistant.slnx
      - run: dotnet test ReleaseAssistant.slnx --configuration Release --no-restore

Si vous exécutez aussi une campagne d’intégration avec un vrai modèle, conservez-la dans un autre job protégé par un environnement GitHub et une identité fédérée limitée. Les tests de contrat doivent rester déterministes, rapides et exécutables sur les forks ; le smoke test fournisseur teste une autre propriété et ne doit pas devenir la condition nécessaire pour vérifier un renommage C#.

Points de vigilance

  • Ne figez pas une erreur métier. La stabilité du schéma ne dispense pas de corriger une sémantique dangereuse ; introduisez une nouvelle fonction et dépréciez l’ancienne de manière planifiée.
  • Validez après le schéma. Le JSON schema guide l’appel, mais chaque valeur reçue reste non fiable. Contrôlez l’environnement, le tenant, les formats et les limites côté C#.
  • N’utilisez pas un outil d’écriture pour une lecture. Des outils séparés rendent les droits minimaux et les règles d’approbation vérifiables.
  • Gardez les versions alignées. Le contrat peut changer lors d’une mise à niveau de Microsoft.Extensions.AI, même sans modification de votre méthode ; les tests doivent donc passer dans la pull request de dépendances.
  • Tracez sans exfiltrer. Journalisez le nom de l’outil, l’issue et l’identifiant de corrélation, mais anonymisez les arguments et les sorties qui pourraient contenir des données métier.

Sources officielles

Un agent fiable ne doit pas dépendre du nom accidentel d’une méthode. En déclarant les identifiants vus par le modèle et en traitant le schéma comme un contrat testé, une équipe .NET peut faire évoluer son code sans transformer un refactoring banal en incident de production.

Mots-clés :.NETC#AI EngineeringDevOpsCI/CD
Y

Yva Hajatiana

Lead développeur .NET à Paris et architecte logiciel, 10 ans d'expérience sur des SI critiques bancaires (.NET, Angular, systèmes distribués, IAM, DevOps).