Accueil / Articles

Créer un assistant IA miniature de zéro en utilisant des agents, des outils et des compétences.

Un petit projet Python qui dirige une requête vers des agents, appelle un outil de calcul et de conversion de unités, exécute une compétence de résumé et envoie la requête restructurée à un LLM.

Éditeur : REACTAPP.TOP12 min de lecture2649 mots

Sources: Agents, Tools, and Skills for a Working Mini AI Assistant

Demandez à un chatbot quel était le temps à Chicago la semaine dernière et il devinera. Donnez à ce même chatbot un outil qui vérifie réellement, et il saura. La différence entre un modèle qui devine et un système qui connaît, c’est ce que les agents, outils et compétences viennent combler, ce qui explique en grande partie pourquoi l’IA générative est devenue un sujet si souvent abordé dans le domaine informatique.

La barrière à l’entrée est plus basse qu’il n’y paraît. Si vous savez effectuer une recherche correcte sur Google, vous pouvez rédiger un prompt convenable. Cependant, un seul prompt a ses limites. La partie intéressante commence lorsque vous cessez d’attendre que le modèle sache tout et que vous lui fournissez des agents, outils et compétences auxquels il peut s’appuyer. Ci-dessous, vous trouverez une explication des trois éléments, accompagnée de codes en Python que vous pouvez exécuter vous-même, basés sur un petit exemple de planification de voyage du début à la fin.

Définition des outils, compétences et agents

Les outils proviennent généralement d’une source externe, telle qu’un serveur MCP (Model Context Protocol). En appelant ce serveur, on obtient généralement une liste d’outils permettant de traiter la requête. Supposons que vous souhaitiez des informations météorologiques dans une requête : un serveur MCP météorologique pourrait exposer des outils tels que get_forecast, qui renvoie la prévision actuelle. Si votre requête demande dans quelle mesure le temps à Chicago a changé au cours de la semaine écoulée, un tel outil ajouterait l’instruction appropriée pour que la recherche réussisse.

get_current_conditions
get_forecast
get_alerts
get_historical_weather
get_air_quality
get_marine_conditions
get_river_conditions
get_wildfire_info
search_location
check_service_status

Cette liste n’est qu’un échantillon de ce qu’un serveur d’outils peut offrir. L’exemple pratique présenté plus bas remplace ceux-ci par deux outils personnalisés plus simples : une calculatrice et un convertisseur de distances.

Les compétences ressemblent davantage à des recettes. Si vous écrivez pour Medium, l’une de vos compétences pourrait être un guide de style. Dans de nombreux projets d’ingénierie des données, il s’agit de listes de calculs ou de valeurs comparables, ce qui en fait quelque chose comme des tables de référence. Si une demande demandait les prix de tous les vols en partance de la région de Chicago, une compétence pourrait indiquer ORD (O’Hare), MDW (Midway) ou CHI, le code couvrant tous les aéroports de la ville. Voici les familles de mesures qu’une compétence pourrait contenir :

Temperature = Celsius, Fahrenheit, Kelvin

Length / Distance = Inches, Feet, Yards, Miles, Millimeters, Centimeters, Meters, Kilometers

Mass / Weight = Ounces, Pounds, Stone, Grams, Kilograms, Metric Tons

Volume / Capacity = Fluid Ounces, Cups, Pints, Quarts, Gallons, Milliliters, Liters, Cubic Meters

Area =  Square Feet, Square Meters, Acres, Hectares

Speed =  Miles per Hour (mph), Kilometers per Hour (km/h), Knots, Meters per Second (m/s)

Time Zones =  UTC/GMT offsets, Daylight Saving Time (DST) transitions, Unix timestamps to human-readable dates

Storage =  Bytes, Kilobytes (KB), Megabytes (MB), Gigabytes (GB), Terabytes (TB)

Ce n’est là qu’une illustration générale de ce que peut contenir une table de recherche pour une compétence. L’exemple présenté ici n’a besoin que des lignes relatives aux distances, permettant de convertir entre miles, kilomètres, mètres et pieds.

C’est grâce aux agents que le traitement par une IA donne l’impression de raisonnement. En pratique, votre requête passe par plusieurs boucles, chacune s’appuyant sur la précédente. La première requête fournit au système suffisamment d’indications pour effectuer une première étape. Ensuite, le système décide quelle doit être l’étape suivante et continue tant que votre code le permet. Pensez à un livre de type « Choisissez votre propre aventure » : chaque itération peut prendre une direction différente pour aboutir à une réponse appropriée.

Le construire en code

L’architecture est délibérément simple. Vous pouvez écrire le code dans un notebook Jupyter ou Google Colab : c’est gratuit et facile à modifier. La présentation commence par les parties du code, dans l’ordre inverse de leur exécution, puis la section suivante montre comment tout fonctionne lorsqu’une requête réelle est envoyée.

Étape 1 : imports

Les bibliothèques requests, getpass et os récupèrent la clé nécessaire et appellent le modèle Groq LLM. La bibliothèque re aide à détecter des motifs de caractères dans la requête, et chaque outil ainsi que chaque compétence s’appuient sur elle. Vous avez besoin de Python 3.10 ou une version plus récente. Si requests n’est pas installé, exécutez d’abord !pip install requests.

import re
import os
import getpass
import requests

Étape 2 : le calculateur

Une expression régulière extrait chaque montant en dollars présent dans l’entrée, et la fonction les additionne. Elle cherche simplement un $ suivi de chiffres, puis additionne ces derniers. Si vous travaillez en livres sterling, il faudra modifier le modèle.

def calculator_tool(prompt: str) -> str:
    print("    [Tool 1: Calculator] scanning prompt for dollar amounts...")
    dollar_amounts = re.findall(r'\$\s?(\d+(?:\.\d{1,2})?)', prompt)

    if not dollar_amounts:
        result = "no dollar amounts found"
        print(f"    [Tool 1: Calculator] {result}")
        return result

    values = [float(a) for a in dollar_amounts]
    total = sum(values)
    breakdown = " + ".join(f"${v:g}" for v in values)
    result = f"{breakdown} = ${total:.2f}"
    print(f"    [Tool 1: Calculator] found {len(values)} amount(s) {values} -> {result}")
    return result

Étape 3 : un outil de conversion d’unités

Cet outil convertit les unités de distance courantes. Il normalise d’abord l’unité utilisée par l’utilisateur à l’aide du dictionnaire UNIT_ALIASES, puis convertit chaque distance en miles grâce au dictionnaire DISTANCE_TO_MILES, ce qui facilite le calcul du total. Selon votre usage, vous pouvez également ajouter des conversions pour les kilomètres ou le comptage de pas.

UNIT_ALIASES = {
    "kilometers": "km", "kilometer": "km", "km": "km",
    "mile": "miles", "miles": "miles",
    "m": "meters", "meter": "meters", "meters": "meters",
    "ft": "feet", "foot": "feet", "feet": "feet",
}

DISTANCE_TO_MILES = {"km": 0.621371, "miles": 1.0, "meters": 0.000621371, "feet": 0.000189394}

def unit_converter_tool(prompt: str) -> str:
    print("    [Tool 2: Unit Converter] scanning prompt for distance legs...")
    legs = re.findall(r'(\d+(?:\.\d+)?)\s*(kilometers?|km|miles?|meters?|feet|ft)\b',
                       prompt, re.IGNORECASE)

    if not legs:
        result = "no distances found"
        print(f"    [Tool 2: Unit Converter] {result}")
        return result

    total_miles = 0.0
    breakdown = []
    for value, unit in legs:
        value = float(value)
        unit_norm = UNIT_ALIASES.get(unit.lower(), unit.lower())
        total_miles += value * DISTANCE_TO_MILES.get(unit_norm, 1.0)
        breakdown.append(f"{value:g} {unit_norm}")
    result = f"{' + '.join(breakdown)} = {round(total_miles, 2)} miles total"
    print(f"    [Tool 2: Unit Converter] found {len(legs)} leg(s) -> {result}")
    return result

Étape 4 : une compétence de résumé

Un résumateur léger. Il évalue chaque phrase en supprimant les mots de stoplistés dans l’ensemble stopwords et classe ce qui reste en fonction du nombre de mots significatifs restants. L’objectif est d’éliminer les phrases qui n’apportent rien à la demande et de réduire l’utilisation inutile de tokens.

def summarizer_skill(text: str, max_sentences: int = 2) -> str:
    print("    [Skill: Summarizer] scanning message for key sentence(s)...")
    sentences = [s for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s]

    if len(sentences) <= max_sentences:
        print(f"    [Skill: Summarizer] only {len(sentences)} sentence(s) -- returning as-is")
        return text.strip()

    stopwords = {"the","a","an","is","are","was","were","in","on","at","to","of","and",
                 "or","for","it","this","that","i","you","he","she","they","we","really"}
    words = re.findall(r'\b\w+\b', text.lower())
    freq = {}
    for w in words:
        if w not in stopwords:
            freq[w] = freq.get(w, 0) + 1

    scored = []
    for idx, sentence in enumerate(sentences):
        s_words = re.findall(r'\b\w+\b', sentence.lower())
        score = sum(freq.get(w, 0) for w in s_words)
        scored.append((score, idx, sentence))

    top = sorted(scored, key=lambda x: x[0], reverse=True)[:max_sentences]
    top_in_order = sorted(top, key=lambda x: x[1])
    summary = " ".join(s for _, _, s in top_in_order)
    print(f"    [Skill: Summarizer] kept {len(top_in_order)} of {len(sentences)} sentence(s) -> {summary}")
    return summary

Étape 5 : connexion à un LLM

Ici, le code communique avec un modèle réel. Groq est utilisé car il dispose d’un plan gratuit, mais vous avez besoin de votre propre clé API depuis la console Groq pour que cela fonctionne.

GROQ_API_KEY = os.environ.get("GROQ_API_KEY") or getpass.getpass(
    "Enter your free Groq API key (from https://console.groq.com/keys), "
    "or press Enter to skip: "
)

GROQ_MODEL = "openai/gpt-oss-20b"
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"

def call_llm(augmented_prompt: str,
             system_prompt: str = "You are a helpful, concise assistant.") -> str:
    if not GROQ_API_KEY:
        return ("[No LLM reply -- no Groq API key was provided. Get a free one at "
                 "https://console.groq.com/keys, then re-run the setup cell above.]\n"
                 f"Here is the augmented prompt that would have been sent:\n\"\"\"\n{augmented_prompt}\n\"\"\"")
    try:
        response = requests.post(
            GROQ_ENDPOINT,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {GROQ_API_KEY}",
            },
            json={
                "model": GROQ_MODEL,
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": augmented_prompt},
                ],
                "temperature": 0.7,
                "max_tokens": 400,
            },
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()
        return data["choices"][0]["message"]["content"].strip()
    except requests.exceptions.RequestException as e:
        return f"[LLM request failed -- {e}]"
    except (KeyError, IndexError, ValueError):
        return "[LLM returned an unexpected response format.]"

print("LLM configured." if GROQ_API_KEY else "No key entered -- running in fallback mode.")

Étape 6 : les agents

C’est ici que les agents sont configurés. Ils ajoutent du contexte à votre demande, et vous verrez qu’ils utilisent des outils et des compétences pour obtenir des données précises.

def show_step(step_num: int, label: str, content: str) -> None:
    """Small helper so every agent prints its pipeline the same, readable way."""
    print(f"\n  STEP {step_num} - {label}:")
    for line in str(content).splitlines() or [""]:
        print(f"    {line}")


def trip_planner_agent(prompt: str) -> str:
    """Agent 1. Condition: 2+ dollar costs AND 2+ distances -- a multi-stop itinerary."""
    print("[Router] -> Agent 1: Trip Planner Agent activated (detected an itinerary: multiple costs + multiple distances)")
    show_step(1, "Original prompt", prompt)

    cost_result = calculator_tool(prompt)
    show_step(2, "Tool result (Calculator -- total cost)", cost_result)

    distance_result = unit_converter_tool(prompt)
    show_step(3, "Tool result (Unit Converter -- total distance)", distance_result)

    augmented_prompt = (
        f"The user asked: \"{prompt}\"\n\n"
        f"A calculator tool already computed the total cost: {cost_result}\n"
        f"A distance tool already computed the total distance traveled: {distance_result}\n\n"
        "Using those two verified totals (don't redo either calculation yourself), give the "
        "user a short, friendly trip summary that reports both totals clearly."
    )
    show_step(4, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

    reply = call_llm(augmented_prompt)
    show_step(5, "Final response from Groq", reply)

    return f"🧳 Agent 1: Trip Planner Agent:\n{reply}"


def text_agent(prompt: str) -> str:
    """Agent 2. Condition: default for any prompt, OR chained after Agent 1
    when the itinerary prompt also has an extra narrative sentence."""
    print("[Router] -> Agent 2: Text Analysis Agent activated")
    show_step(1, "Original prompt", prompt)

    summary = summarizer_skill(prompt)
    show_step(2, "Skill result (Summarizer)", summary)

    augmented_prompt = (
        f"The user wrote: \"{prompt}\"\n\n"
        f"Automatic summary of their message: {summary}\n\n"
        "Write a short, thoughtful, natural-sounding reply to the user that responds "
        "to what they actually said, informed by (but not just repeating) this summary."
    )
    show_step(3, "Prompt has changed -- now the augmented prompt sent to the LLM", augmented_prompt)

    reply = call_llm(augmented_prompt)
    show_step(4, "Final response from Groq", reply)

    return f"📝 Agent 2: Text Analysis Agent:\n{reply}"

Étape 7 : le routeur

Le routeur est le cerveau de l’opération. Il cherche des caractéristiques spécifiques dans une demande et l’envoie à l’agent ou aux agents appropriés. Un détail : has_extra_narrative est une condition vérifiée uniquement après que trip_planner_agent ait déjà trouvé une correspondance. Le text_agent s’exécute soit par défaut pour toute demande, soit en parallèle du planificateur de voyages lorsque cette condition est vraie.

def looks_like_itinerary(prompt: str) -> bool:
    dollar_amounts = re.findall(r'\$\s?\d+(?:\.\d{1,2})?', prompt)
    distance_legs = re.findall(r'\d+(?:\.\d+)?\s*(?:kilometers?|km|miles?|meters?|feet|ft)\b',
                                prompt, re.IGNORECASE)
    return len(dollar_amounts) >= 2 and len(distance_legs) >= 2

def has_extra_narrative(prompt: str) -> bool:
    sentences = [s for s in re.split(r'(?<=[.!?])\s+', prompt.strip()) if s]
    extra = [
        s for s in sentences
        if "quot; not in s
        and not re.search(r'\b(?:miles?|km|kilometers?|feet|ft)\b', s, re.IGNORECASE)
        and not s.strip().endswith("?")
    ]
    return len(extra) >= 1

def route_prompt(prompt: str) -> str:
    if looks_like_itinerary(prompt):
        reply = trip_planner_agent(prompt)
        if has_extra_narrative(prompt):
            print("[Router] -> also routing to Agent 2: Text Analysis Agent (extra narrative sentence detected)")
            reply += "\n\n" + text_agent(prompt)
        return reply
    else:
        return text_agent(prompt)

Étape 8 : essai

C’est le moment de tester tout le système.

prompt = input("Ask me anything: ")

print("=" * 60)
print(f"PROMPT: {prompt}")
print("=" * 60)
final_answer = route_prompt(prompt)
print(f"\n  >>> RETURNED: {final_answer}")
print("-" * 60 + "\n")

Suivre une demande à travers le système

Prenons cette demande et suivons chaque étape :

« Une partie des Cubs au Wrigley Field (billet à 55 $) a marqué le début de la journée. Ensuite, un trajet de 5,5 miles jusqu’à l’Art Institute of Chicago (entrée à 32 $), puis 1,5 mile jusqu’au Shedd Aquarium (20 $), et enfin 2,3 miles pour un dîner de type deep-dish à 30 $ chez Giordano’s à River North. Quel a été le coût total de la journée, et quelle était la longueur du parcours ? Le baseball, l’art, la faune marine et la pizza ont rendu cette journée très chargée ; un bref résumé pour le journal de voyage serait apprécié. »

Itinéraire

Tout commence par le routeur de requêtes. S’il détecte quelque chose ressemblant à une monnaie, comme un $, accompagné d’une distance en miles ou en km, il dirige la requête vers le planificateur de voyages, Agent 1. Ensuite, il évalue has_extra_narrative, qui ne s’exécute que lorsque le planificateur de voyages a trouvé une correspondance, et cherche une phrase supplémentaire sans rapport avec le coût ou la distance. S’il en existe une, le routeur fait également appel à l’agent de texte, Agent 2. Lorsque ni le modèle d’itinéraire ni une phrase supplémentaire n’apparaissent, l’agent de texte gère la requête seul. Ici, les deux conditions sont remplies, donc les deux agents s’exécutent. Mettre en surbrillance la requête en couleur permet de savoir quelles parties ont déclenché chacun d’eux.

Le planificateur de voyages appelle ses outils

L’agent du planificateur de voyages fait appel à la fois au calculateur et au convertisseur d’unités.

Le calculateur trouve toutes les sommes en dollars et renvoie une seule chaîne de caractères :

$55 + $32 + $20 + $30 = $137.00

Le convertisseur de unités calcule les distances et affiche :

5,5 miles, 1,5 mile et 2,3 mile font au total 9,3 miles

La requête est reconstruite

Ces deux résultats sont renvoyés au planificateur de voyage, qui les utilise pour reconstruire la requête en y ajoutant les totaux calculés au texte initial. Cette nouvelle requête est envoyée à Groq, le LLM utilisé pour la démonstration, qui pourrait répondre approximativement ainsi :

Quelle belle journée à Chicago.

  • Dépenses : $137
  • Distance : 9,3 miles

Gardez en mémoire ces souvenirs, ainsi que le deep-dish.

L’agent de texte et le résumé

Lorsque le premier agent a terminé, has_extra_narrative est redevenu vrai, ce qui a activé l’agent chargé du texte et fait passer la demande initiale à la compétence de résumé. Comme la demande contenait plus de phrases que la limite de deux phrases imposée par cette compétence, celle-ci a conservé les deux phrases ayant le meilleur score et a complètement supprimé la question du milieu. En comparant la demande originale avec celle qui en ressort, on constate que la question concernant le coût et les kilomètres a disparu, tandis que seules les phrases narratives sont restées.

Cette demande ne contenant que du texte narrative est envoyée une seconde fois à Groq, et le modèle produit une réflexion journalière approfondie sans avoir à recalculer quoi que ce soit. Les chiffres concernant le coût en dollars et les kilomètres parcourus figurant dans sa réponse proviennent de l’Agent 1, intervenu plus tôt dans le processus, et non de cette étape ; c’est là tout l’intérêt de diviser le travail entre les agents :

Votre journée a coûté au total 137 $ et a couvert 9,3 miles.

Résumé du journal :

  • Match au Wrigley Field : 55 $
  • Institut d’art, à 5,5 miles : 32 $
  • Aquarium Shedd, à 1,5 mile : 20 $
  • Pizza deep-dish chez Giordano’s, à 2,3 mile : 30 $

Le baseball, l’art, les créatures marines et la pizza de Chicago pour une journée bien remplie.

Pourquoi ce modèle vaut la peine d’être utilisé

Cet exemple montre que des agents, des compétences et des outils permettent à un système de reconstituer votre demande en arrière-plan, sans que vous ne voyiez les étapes supplémentaires. Un routeur et des agents collaborent pour déterminer quels outils ou compétences sont nécessaires, rassembler les informations pertinentes, remodeler la demande en fonction d’elles et transmettre le résultat au LLM. Dans des déploiements réels, on enchaînerait plusieurs tours, permettant au système d’enrichir et de perfectionner la demande à chaque fois qu’il apprend quelque chose de nouveau.

Les outils présentés ici ne sont que de simples fonctions. En environnement de production, ils seraient généralement hébergés sur un serveur MCP développé par quelqu’un d’autre, prêt à être appelé. Les compétences requises différeraient également légèrement, car les développeurs écrivent généralement ces directives dans un fichier Markdown plutôt que dans une fonction Python comme celle ci-dessus.

Vous n’avez pas besoin d’une équipe importante ni d’un framework lourd pour commencer. Un routeur accompagné de deux ou trois petits outils, ainsi qu’un modèle capable de s’appuyer sur eux plutôt que d’improviser, vous permettront de démarrer. Remplacez-les par vos propres outils, compétences ou agents pour les adapter à vos besoins, puis créez ceux nécessaires pour résoudre le problème qui se pose à vous.