#
Que sont les outils ?

Un aspect crucial des agents est leur capacité à prendre des actions. Comme nous l’avons vu, cela se fait par l’utilisation d’outils.

Dans cette section, nous verrons ce que sont les outils, comment les concevoir efficacement, et comment les intégrer à votre agent via le message système.

En fournissant à votre agent les bons outils — et en décrivant clairement le fonctionnement de ces outils — vous pouvez augmenter de manière spectaculaire ce que votre IA peut accomplir. Plongeons-nous dedans !

##
Que sont les outils d’IA ?

Un outil est une fonction fournie au LLM. Cette fonction doit remplir un objectif clair.

Voici quelques outils couramment utilisés dans les agents :

Outil | Description
--- | ---
Recherche Web | Permet à l’agent de récupérer des informations à jour depuis Internet.
Génération d’images | Crée des images à partir de descriptions textuelles.
Recherche | Récupère des informations à partir d’une source externe.
Interface API | Interagit avec une API externe (GitHub, YouTube, Spotify, etc.).

Ce ne sont que des exemples, car en réalité, vous pouvez créer un outil pour n’importe quel cas d’utilisation !

Un bon outil doit être quelque chose qui complémente la puissance d’un LLM.

Par exemple, si vous devez effectuer des opérations arithmétiques, fournir une calculatrice à votre LLM donnera de meilleurs résultats que de se fier aux capacités natives du modèle.

De plus, les LLM prédisent la complétion du prompt en se basant sur leurs données d’entraînement, ce qui signifie que leur connaissance interne n’inclut que les événements antérieurs à leur entraînement. Par conséquent, si votre agent a besoin de données à jour, vous devez les fournir via un outil.

Par exemple, si vous demandez directement à un LLM (sans outil de recherche) la météo d’aujourd’hui, le LLM pourrait inventer une météo aléatoire.

- Un outil doit contenir :

  - Une description textuelle de ce que fait la fonction.
  - Un appeleur (quelque chose pour effectuer une action).
  - Des arguments avec typage.
  - (Optionnel) Des sorties avec typage.

##
Comment fonctionnent les outils ?

Comme nous l’avons vu, les LLM ne peuvent recevoir que des entrées textuelles et générer des sorties textuelles. Ils ne peuvent pas appeler des outils par eux-mêmes. Lorsque nous parlons de fournir des outils à un agent, nous entendons enseigner au LLM l’existence de ces outils et lui demander de générer des invocations textuelles en cas de besoin.

Par exemple, si nous fournissons un outil pour vérifier le temps qu’il fait à un endroit donné à partir d’internet et que nous demandons ensuite au LLM le temps qu’il fait à Paris, le LLM reconnaîtra qu’il s’agit d’une occasion d’utiliser l’outil « météo ». Au lieu de récupérer les données météorologiques elles-mêmes, le LLM générera un texte pour appeller l’outil, tel que ``call weather_tool("Paris")`.

L’agent lit alors cette réponse, identifie qu’un appel d’outil est nécessaire, exécute l’outil au nom du LLM et récupère les données météorologiques réelles.

Les étapes de l’appel d’outil ne sont généralement pas montrées à l’utilisateur : l’agent les ajoute à un nouveau message avant de transmettre à nouveau la conversation mise à jour au LLM. Le LLM traite alors ce contexte supplémentaire et génère une réponse naturelle pour l’utilisateur. Du point de vue de l’utilisateur, il semble que le LLM interagisse directement avec l’outil, mais en réalité, c’est l’agent qui gère l’ensemble du processus d’exécution en arrière-plan.

Nous reviendrons plus en détail sur ce processus dans les prochaines sessions.

##
Comment fournir des outils à un LLM ?

La réponse complète peut sembler complexe, mais nous utilisons essentiellement le prompt système pour fournir au modèle des descriptions textuelles des outils disponibles :

Pour que cela fonctionne, nous devons être très précis et rigoureux concernant :

1. Ce que fait l’outil
2. Les entrées exactes qu’il attend

C’est la raison pour laquelle les descriptions d’outils sont généralement fournies en utilisant des structures expressives mais précises, telles que des langages informatiques ou du JSON. Il n’est pas nécessaire de procéder ainsi, tout format précis et cohérent fonctionnerait.

Si cela semble trop théorique, voyons cela à travers un exemple concret.

Nous allons implémenter un outil simplifié calculatrice qui se contentera de multiplier deux entiers. Voici une implémentation en Python :

    def calculator(a: int, b: int) -> int:
        """Multiplie deux entiers."""
        return a * b

    print(calculator.to_string())

Ainsi, notre outil s’appelle `calculator`, il multiplie deux entiers, et il requiert les entrées suivantes :

- `a` (int): Un entier.
- `b` (int): Un entier.

La sortie de l’outil est un autre nombre entier que nous pouvons décrire ainsi :

- (int): Le produit de `a` et `b`.

Tous ces détails sont importants. Rassemblons-les dans une chaîne de texte qui décrit notre outil pour que le LLM puisse le comprendre.

    Nom de l'outil: calculator, Description: Multiplie deux entiers., Arguments: a: int, b: int, Sorties: int

> Rappel : Cette description textuelle est ce que nous voulons que le LLM sache à propos de l’outil.

Lorsque nous passons la chaîne précédente dans l’entrée du LLM, le modèle la reconnaîtra comme un outil et saura quelles entrées fournir et ce qu’il doit attendre en sortie.

Si nous souhaitons fournir des outils supplémentaires, nous devons rester cohérents et utiliser toujours le même format. Ce processus peut être fragile, et nous pourrions accidentellement négliger certains détails.

Existe-t-il une meilleure méthode ?

###
Sections d’auto-formatage des outils

Notre outil a été écrit en Python, et l’implémentation fournit déjà tout ce dont nous avons besoin :

- Un nom descriptif de ce qu’il fait : `calculator`
- Une description plus détaillée, fournie par le commentaire docstring de la fonction : `Multiplie deux entiers.`
- Les entrées et leur type : la fonction attend clairement deux `int`.
- Le type de la sortie.

Il y a une raison pour laquelle on utilise des langages de programmation : ils sont expressifs, concis et précis.

Nous pourrions fournir le code source Python comme spécification de l’outil pour le LLM, mais la manière dont l’outil est implémenté n’a pas d’importance. Tout ce qui compte, c’est son nom, ce qu’il fait, les entrées qu’il attend et la sortie qu’il fournit.

Nous tirerons parti des fonctionnalités d’introspection de Python pour exploiter le code source et construire automatiquement une description de l’outil. Tout ce dont nous avons besoin, c’est que l’implémentation de l’outil utilise des annotations de types, des docstrings et des noms de fonction pertinents. Nous écrirons un peu de code pour extraire les parties pertinentes du code source.

Une fois cela fait, il nous suffira d’utiliser un décorateur Python pour indiquer que la fonction `calculator` est un outil :

    @tool
    def calculator(a: int, b: int) -> int:
        """Multiplie deux entiers."""
        return a * b

    print(calculator.to_string())

Notez le décorateur `@tool` avant la définition de la fonction.

Avec l’implémentation que nous verrons ensuite, nous serons capables d’extraire automatiquement le texte suivant à partir du code source :

    Nom de l'outil: calculator, Description: Multiplie deux entiers., Arguments: a: int, b: int, Sorties: int

Comme vous pouvez le constater, c’est la même chose que nous avons écrit manuellement précédemment !

###
Implémentation générique d’un outil

Nous créons une classe générique `Tool` que nous pouvons réutiliser chaque fois que nous avons besoin d’utiliser un outil.

> Avertissement : Cette implémentation à titre d’exemple est fictive mais ressemble de près aux implémentations réelles dans la plupart des bibliothèques.

    class Tool:
        """
        Une classe représentant un morceau de code réutilisable (Outil).

        Attributs:
            name (str): Nom de l'outil.
            description (str): Une description textuelle de ce que fait l'outil.
            func (callable): La fonction que cet outil encapsule.
            arguments (list): Une liste d'arguments.
            outputs (str ou list): Le(s) type(s) de retour de la fonction encapsulée.
        """
        def __init__(self,
                     name: str,
                     description: str,
                     func: callable,
                     arguments: list,
                     outputs: str):
            self.name = name
            self.description = description
            self.func = func
            self.arguments = arguments
            self.outputs = outputs

        def to_string(self) -> str:
            """
            Retourne une représentation sous forme de chaîne de l'outil,
            incluant son nom, sa description, ses arguments, et ses sorties.
            """
            args_str = ", ".join([
                f"{arg_name}: {arg_type}" for arg_name, arg_type in self.arguments
            ])

            return (
                f"Tool Name: {self.name},"
                f" Description: {self.description},"
                f" Arguments: {args_str},"
                f" Outputs: {self.outputs}"
            )

        def __call__(self, *args, **kwargs):
            """
            Invoque la fonction sous-jacente (callable) avec les arguments fournis.
            """
            return self.func(*args, **kwargs)

Cela peut sembler compliqué, mais en y allant pas à pas, nous pouvons voir ce qu’elle fait. Nous définissons une classe `Tool` qui inclut :

- `name` (str): Le nom de l’outil.
- `description` (str): Une brève description de ce que fait l’outil.
- `function` (callable): La fonction que l’outil exécute.
- `arguments` (list): Les paramètres d’entrée attendus.
- `outputs` (str ou list): Les sorties attendues de l’outil.
- `__call__()` : Appelle la fonction lorsque l’instance de l’outil est invoquée.
- `to_string()` : Convertit les attributs de l’outil en une représentation textuelle.

Nous pourrions créer un outil avec cette classe en utilisant le code suivant :

    calculator_tool = Tool(
        "calculator",                   # nom
        "Multiplie deux entiers.",      # description
        calculator,                     # fonction à appeler
        [("a", "int"), ("b", "int")],   # entrées (noms et types)
        "int",                          # sortie
    )

Mais nous pouvons également utiliser le module `inspect` de Python pour récupérer toutes les informations pour nous ! C’est ce que fait le décorateur `@tool`.

> Si cela vous intéresse, vous pouvez afficher la section suivante pour voir l’implémentation du décorateur.

code du décorateur

    def tool(func):
        """
        Un décorateur qui crée une instance de Tool à partir de la fonction donnée.
        """
        # Récupérer la signature de la fonction
        signature = inspect.signature(func)

        # Extraire les paires (nom_param, annotation_param) pour les entrées
        arguments = []
        for param in signature.parameters.values():
            annotation_name = (
                param.annotation.__name__
                if hasattr(param.annotation, '__name__')
                else str(param.annotation)
            )
            arguments.append((param.name, annotation_name))

        # Déterminer l'annotation de retour
        return_annotation = signature.return_annotation
        if return_annotation is inspect._empty:
            outputs = "Pas d'annotation de retour"
        else:
            outputs = (
                return_annotation.__name__
                if hasattr(return_annotation, '__name__')
                else str(return_annotation)
            )

        # Utiliser la docstring de la fonction comme description (par défaut si vide)
        description = func.__doc__ or "No description provided."

        # Le nom de la fonction devient le nom de l'outil
        name = func.__name__

        # Retourner une nouvelle instance de Tool
        return Tool(
            name=name,
            description=description,
            func=func,
            arguments=arguments,
            outputs=outputs
        )

Pour réitérer, avec ce décorateur en place, nous pouvons implémenter notre outil comme ceci :

    @tool
    def calculator(a: int, b: int) -> int:
        """Multiplie deux entiers."""
        return a * b

    print(calculator.to_string())

Et nous pouvons utiliser la méthode `to_string` de `Tool` pour récupérer automatiquement un texte adapté à être utilisé comme description d’un outil pour un LLM :

    Nom de l'outil: calculator, Description: Multiplie deux entiers., Arguments: a: int, b: int, Sorties: int

La description est injectée dans le prompt système. En reprenant l’exemple avec lequel nous avons commencé cette section, voici à quoi cela ressemblerait après avoir remplacé le `tools_description` :

Dans la section sur les actions, nous en apprendrons davantage sur la façon dont un agent peut appeler cet outil que nous venons de créer.

Les outils jouent un rôle crucial dans l’amélioration des capacités des agents.

###
Model Context Protocol (MCP) : une interface d’outils unifiée

Model Context Protocol (MCP) est un protocole ouvert qui standardise la manière dont les applications fournissent des outils aux LLM. MCP offre :

- Une liste croissante d’intégrations pré-construites que votre LLM peut directement utiliser
- La flexibilité de changer entre fournisseurs et vendeurs de LLM
- Les meilleures pratiques pour sécuriser vos données dans votre infrastructure

Cela signifie que tout framework intégrant MCP peut utiliser les outils définis dans le protocole, éliminant le besoin de réimplémenter la même interface d’outils pour chaque framework.

Si vous voulez approfondir MCP, vous pouvez consulter notre cours gratuit sur MCP.

Les outils jouent un rôle crucial dans l’amélioration des capacités des agents.

Pour résumer, nous avons appris :

- Ce que sont les outils : des fonctions qui offrent des capacités supplémentaires aux LLM, comme effectuer des calculs ou accéder à des données externes.

- Comment définir un outil : en fournissant une description textuelle claire, des entrées, des sorties, et une fonction exécutable.

- Pourquoi les outils sont essentiels : ils permettent aux agents de surmonter les limites de l’entraînement statique des modèles, de gérer des tâches en temps réel, et d’effectuer des actions spécialisées.

Maintenant, nous pouvons passer au workflow de l’agent où vous verrez comment un agent observe, réfléchit et agit. Cela rassemble tout ce que nous avons vu jusqu’à présent et prépare le terrain pour créer votre propre agent entièrement fonctionnel.

Mais d’abord, il est temps pour un autre court quiz !

Update on GitHub