Accueil / Articles

Entraîner un modèle personnalisé dans Azure ML et l’appeler depuis un agent Foundry

Formez, enregistrez et déployez un modèle de déshérence sur un point de terminaison géré par Azure ML, puis exposez-le à un agent Foundry en tant que FunctionTool, en incluant des informations relatives à l’authentification et aux conditions de production.

Éditeur : REACTAPP.TOP7 min de lecture1652 mots

Sources: Building and Serving a Custom Model with Azure ML, Then Wiring It Into a Foundry Agent

Le catalogue de modèles de Foundry est vaste, mais certains projets en sortent. Imaginez prédire le risque de désabonnement à partir des propres données clients d’une entreprise, ou évaluer la fraude en analysant son historique de transactions. Il s’agit là de tâches classiques d’apprentissage automatique sur des données tabulaires privées, et la solution de Microsoft pour celles-ci est Azure Machine Learning, un produit distinct doté d’un SDK propre, d’un espace de travail spécifique ainsi que de sa propre méthode de déploiement des modèles.

La procédure présentée ci-dessous couvre l’ensemble du processus : entraîner un modèle à l’aide de l’SDK Azure ML, le enregistrer, le servir depuis une extrémité gérée, puis exposer cette extrémité à un agent Foundry en tant qu’outil fonctionnel. Cet agent peut alors appeler le modèle personnalisé au cours d’une conversation, exactement comme il appellerait n’importe quel autre outil.

Où les deux plateformes se rencontrent

Ces deux produits fonctionnent avec des environnements d’exécution distincts mais appartiennent au même écosystème, et Microsoft s’est efforcé de réduire au minimum la rupture entre eux. Connaître précisément l’endroit où se situe cette rupture facilite la compréhension du processus de construction. Quatre éléments y sont impliqués :

  • Tâche de commande. C’est ici que se déroulent les exécutions d’entraînement. Le SDK est orienté vers un script, une cible de calcul et des données d’entrée, et il exécute le code sous forme de tâche traçable et reproductible.
  • Registre de modèles. Un modèle entraîné devient un artefact nommé et versionné qui existe indépendamment de la tâche qui l’a généré, ce qui permet de promouvoir une version en environnement de production sans avoir à répéter l’opération d’entraînement.
  • Point de terminaison en ligne géré. Il sert à fournir des prédictions issues d’un modèle enregistré. Il s’agit d’une infrastructure réelle, toujours active ou auto-échelonnée, disposant de son propre URL, de mécanismes d’authentification et de coûts, et il se trouve à l’intérieur de l’espace de travail Azure ML, et non dans Foundry.
  • Foundry FunctionTool. Il s’agit du pont de communication. Foundry ne sait ni ne se soucie du fait que la fonction derrière l’outil appelle une extrémité Azure ML. Pour l’agent, il s’agit simplement d’un nom, d’un schéma et d’un résultat.
  • Prérequis

    1. Un espace de travail Azure ML et un projet Foundry. Ils peuvent se trouver dans les mêmes groupes de ressources ou dans des groupes différents, car rien ici ne nécessite une infrastructure partagée.
    2. Python 3.9 ou une version ultérieure, accompagné des deux SDK :
    pip install azure-ai-ml azure-ai-projects azure-identity
    

    Un MLClient s’authentifie et cible l’espace de travail :

    from azure.ai.ml import MLClient
    from azure.identity import DefaultAzureCredential
    
    ml_client = MLClient(
        DefaultAzureCredential(),
        subscription_id="<subscription-id>",
        resource_group_name="<resource-group>",
        workspace_name="<aml-workspace-name>",
    )
    

    L’entraînement en tant que tâche de commande

    Étape d’entraînement : il s’agit d’une tâche de commande. Elle prend un script d’entraînement, dans ce cas un classifieur scikit-learn ordinaire, et l’exécute sur des ressources informatiques gérées :

    from azure.ai.ml import command, Input, Output
    
    train_job = command(
        code="./src",
        command="python train.py --data ${{inputs.training_data}} --model_output ${{outputs.model_output}}",
        inputs={"training_data": Input(type="uri_folder", path="azureml://datastores/workspaceblobstore/paths/churn-training/")},
        outputs={"model_output": Output(type="uri_folder")},
        environment="azureml://registries/azureml/environments/sklearn-1.5/labels/latest",
        compute="cpu-cluster",
        display_name="churn-model-training",
    )
    returned_job = ml_client.jobs.create_or_update(train_job)
    ml_client.jobs.stream(returned_job.name)
    

    L’appel à ml_client.jobs.stream attend et affiche des journaux jusqu’à ce que l’entraînement soit terminé. Préférez-le dans tout script qui sera réellement exécuté. Un job qui échoue silencieusement en arrière-plan est un moyen désagréable de découvrir qu’un pipeline est défectueux.

    Enregistrement du modèle

    L’enregistrement transforme la sortie du job en un artefact nommé et versionné qui peut être référencé de manière indépendante :

    from azure.ai.ml.entities import Model
    from azure.ai.ml.constants import AssetTypes
    
    model = ml_client.models.create_or_update(
        Model(
            path=f"azureml://jobs/{returned_job.name}/outputs/model_output",
            name="churn-classifier",
            type=AssetTypes.MLFLOW_MODEL,
            description="Customer churn classifier, trained on 18 months of account history.",
        )
    )
    

    Le choix de MLFLOW_MODEL comme type n’est pas fortuit. Si le script d’entraînement enregistre le modèle via l’autologging de MLflow, les points d’extrémité gérés peuvent le déployer en utilisant un conteneur d’évaluation intégré, sans script d’inférence personnalisé score.py. Cela permet d’économiser du temps réel, et le script d’entraînement doit être conçu en tenant compte de cela dès le début.

    Déploiement derrière un point d’extrémité

    Un point d’extrémité associé à un déploiement met le modèle en ligne :

    from azure.ai.ml.entities import ManagedOnlineEndpoint, ManagedOnlineDeployment
    
    endpoint = ManagedOnlineEndpoint(name="churn-endpoint", auth_mode="key")
    ml_client.online_endpoints.begin_create_or_update(endpoint).result()
    deployment = ManagedOnlineDeployment(
        name="blue",
        endpoint_name="churn-endpoint",
        model=model,
        instance_type="Standard_DS3_v2",
        instance_count=1,
    )
    ml_client.online_deployments.begin_create_or_update(deployment).result()
    endpoint.traffic = {"blue": 100}
    ml_client.online_endpoints.begin_create_or_update(endpoint).result()
    

    Le nom de déploiement blue n’est pas obligatoire, mais conservez-le, car il instaure une habitude utile dès la première nouvelle version du modèle. Créez un déploiement green à côté de blue, répartissez le trafic entre eux et passez complètement à la nouvelle version une fois qu’elle aura gagné la confiance. Écraser blue en espérant le meilleur est l’alternative risquée.

    Avant de connecter quoi que ce soit d’autre, obtenez une prévision réelle directement :

    import json
    
    test_input = {"input_data": {"columns": ["tenure_months", "monthly_spend", "support_tickets"], "data": [[14, 89.50, 3]]}}
    response = ml_client.online_endpoints.invoke(
        endpoint_name="churn-endpoint",
        request_file=None,
        deployment_name="blue",
        input_data=json.dumps(test_input),
    )
    print(response)
    

    Déboguer un point de terminaison défectueux à l’aide du cycle d’appel de fonctions d’un agent est bien plus difficile que de le déboguer directement, il faut donc d’abord s’assurer qu’il fonctionne.

    Connexion du point de terminaison à un agent Foundry

    L’agent ne fait jamais appel directement à Azure ML. C’est le code de l’application qui s’en charge, lorsque l’agent le demande. La configuration définit un FunctionTool, crée l’agent et définit la fonction qui envoie la requête HTTP au point de terminaison :

    import os
    import requests
    from azure.ai.projects import AIProjectClient
    from azure.ai.projects.models import PromptAgentDefinition, Tool, FunctionTool
    from azure.identity import DefaultAzureCredential
    
    def get_churn_score(tenure_months: int, monthly_spend: float, support_tickets: int) -> dict:
        payload = {"input_data": {"columns": ["tenure_months", "monthly_spend", "support_tickets"], "data": [[tenure_months, monthly_spend, support_tickets]]}}
        resp = requests.post(
            "https://churn-endpoint.<region>.inference.ml.azure.com/score",
            headers={"Authorization": f"Bearer {os.environ['AML_ENDPOINT_KEY']}", "Content-Type": "application/json"},
            json=payload,
            timeout=10,
        )
        resp.raise_for_status()
        return {"churn_probability": resp.json()[0]}
    func_tool = FunctionTool(
        name="get_churn_score",
        description="Predict churn probability for a customer given tenure, spend, and support ticket history.",
        parameters={
            "type": "object",
            "properties": {
                "tenure_months": {"type": "integer", "description": "How many months the customer has been active."},
                "monthly_spend": {"type": "number", "description": "Average monthly spend in dollars."},
                "support_tickets": {"type": "integer", "description": "Number of support tickets in the last 90 days."},
            },
            "required": ["tenure_months", "monthly_spend", "support_tickets"],
            "additionalProperties": False,
        },
        strict=True,
    )
    project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=DefaultAzureCredential())
    tools: list[Tool] = [func_tool]
    agent = project.agents.create_version(
        agent_name="retention-agent",
        definition=PromptAgentDefinition(
            model="gpt-4.1-mini",
            instructions="Help the team assess churn risk. Call get_churn_score whenever specific customer numbers are provided.",
            tools=tools,
        ),
    )
    

    Ensuite, une conversation a lieu, l’appel de fonction du modèle est intercepté et le résultat est retourné :

    import json
    from openai.types.responses.response_input_param import FunctionCallOutput
    
    openai_client = project.get_openai_client()
    conversation = openai_client.conversations.create()
    response = openai_client.responses.create(
        input="A customer's been with us 14 months, spends about $90/month, and filed 3 tickets recently. Churn risk?",
        conversation=conversation.id,
        extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}},
    )
    for item in response.output:
        if item.type == "function_call" and item.name == "get_churn_score":
            result = get_churn_score(**json.loads(item.arguments))
            follow_up = openai_client.responses.create(
                input=[FunctionCallOutput(type="function_call_output", call_id=item.call_id, output=json.dumps(result))],
                conversation=conversation.id,
                extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}},
            )
            print(follow_up.output_text)
    

    Le modèle décide si une question justifie l’appel à get_churn_score. Lorsque c’est le cas, le code de l’application effectue une requête HTTP vers Azure ML, et la réponse est renvoyée dans la même conversation afin que le modèle puisse terminer sa réponse. Rien dans la définition de l’agent ni dans les appels à l’API Responses ne dépend de ce que fait la fonction en arrière-plan. Elle peut interroger une base de données, invoquer une fonction Lambda ou accéder à un autre espace de travail Azure ML, comme c’est le cas ici.

    Deux ressources, deux limites d’authentification

    Un point reste caché dans la documentation du SDK. L’endpoint géré d’Azure ML et l’endpoint de projet de Foundry sont deux ressources protégées par deux mécanismes d’authentification distincts, et rien dans cette configuration ne les relie. Azure ML dispose de sa propre clé ou d’une identité gérée pour l’espace de travail, tandis que Foundry possède des identifiants différents. La déclaration d’un FunctionTool ne crée aucune confiance entre eux ; elle se contente de donner au modèle un nom et un schéma afin qu’il puisse demander quelque chose à votre code. Toute authentification vers Azure ML a lieu à l’intérieur de votre propre fonction, exactement comme lorsqu’on appelle n’importe quelle autre API externe.

    C’est un avantage : le côté Azure ML peut être géré, sécurisé et mis à jour de manière indépendante de Foundry, même par une autre équipe, et aucune des deux parties n’a besoin d’accès en écriture aux ressources de l’autre.

    Notes pour la production

    • Une appel de fonction expire après 10 minutes. Les résultats doivent être renvoyés dans la conversation avant que la fenêtre ne se ferme, sinon l’exécution échoue. Si l’appel HTTP vers Azure ML est lent, considérez cela comme une échéance stricte et prévoyez du temps en conséquence.
    • Passez à une identité gérée au-delà du prototype. Une clé stockée dans une variable d’environnement est acceptable en environnement local, mais représente un risque dans tout contexte non surveillé. Accordez à l’identité qui exécute le code de l’outil le rôle AzureML Data Scientist, ou un rôle personnalisé plus restreint limité à cet endpoint et non à l’ensemble de l’espace de travail.
    • Déployez progressivement de nouveaux modèles. Lancez le nouveau déploiement à côté du modèle existant et transférez progressivement le trafic vers lui. C’est le seul moyen de détecter un mauvais réentraînement avant que chaque demande réelle ne dépende de lui.
  • Gardez strict=True activé. Les schémas stricts empêchent le modèle d’envoyer des arguments incomplets ou mal formatés à get_churn_score, ce qui, sinon, provoquerait une erreur à l’intérieur de votre fonction et non avant l’appel.
  • Les ressources inutilisées coûtent de l’argent. Un Managed Online Endpoint exécutant une instance toute la journée est facturé même en l’absence de trafic, contrairement aux appels à un modèle serverless. Pour un volume faible ou irrégulier, envisagez des endpoints avec fonction de mise à l’échelle à zéro ou des endpoints par lots.
  • Le décalage entre données réelles et données d’entraînement est silencieux. Rien ici ne se réentraîne lorsque les données du monde réel s’éloignent de la distribution d’entraînement. Enregistrez les entrées et sorties de chaque appel à get_churn_score dans un endroit accessible, et réexaminez régulièrement les données d’entraînement, et non seulement lorsque les prédictions se détériorent visiblement.
  • Choisir la bonne plateforme

    Pour la plupart des travaux d’IA générative, le catalogue de Foundry constitue l’outil idéal, et les équipes ne disposant pas de données d’entraînement propriétaires n’en auront probablement jamais besoin davantage. Lorsque la question devient une prédiction concernant vos propres clients à partir de votre propre historique, Azure ML est le choix préférable. Sinon, un modèle de langage est utilisé à des fins pour lesquelles il n’a pas été conçu, ou bien l’infrastructure de service doit être développée manuellement. L’élément clé reste simple : une seule tâche d’entraînement, un seul modèle enregistré, un seul point de terminaison géré et une seule FunctionTool. Les plateformes restent distinctes, chacune faisant ce pour quoi elle est optimisée.