agent·llm

Guide pédagogique · à partir d'un vrai code

Comment fonctionne un
agent LLM de codage

On entend partout « agent », « copilote », « IA qui code toute seule ». Derrière le mot, le mécanisme tient en une idée simple : un modèle de langage placé dans une boucle, avec des outils pour agir. Ce guide en démonte les rouages — à partir d'un agent réel écrit en Go, en ~1000 lignes et zéro dépendance.

01

Un agent, c'est quoi ?

Un modèle de langage (LLM) seul ne sait que produire du texte. Posez-lui une question, il répond — mais il ne peut rien faire : ni lire un fichier, ni lancer une commande, ni vérifier son propre travail. C'est un cerveau sans mains.

Un agent, c'est ce même modèle auquel on donne deux choses : des outils (des mains pour agir sur le monde) et une boucle (la possibilité d'agir plusieurs fois de suite, en observant le résultat de chaque action avant de décider de la suivante). Rien de plus. La « magie » des agents de codage tient entièrement dans ces deux ingrédients.

Chatbot

Vous écrivez → il répond. Une passe. Le texte est la fin du chemin.

message réponse
Agent

Vous écrivez → il agit, observe, recommence, jusqu'à ce que la tâche soit réellement faite.

message réflexion action observation

Tout ce guide s'appuie sur un agent réel et minimal. Il parle à n'importe quel serveur compatible OpenAI (LM Studio, Ollama, un modèle local, une API cloud). Pas de framework, pas de SDK : juste la bibliothèque standard de Go, pour que chaque mécanisme reste lisible et démystifié.

02

La boucle agentique

Le cœur de tout agent tient en une boucle. À chaque tour (un message de l'utilisateur), l'agent répète une étape tant qu'il a quelque chose à faire :

🧑 → 🤖 on envoie l'historiqueau modèle 💭 décide répondre ?appeler un outil ? 🔧 agit exécute l'outildemandé 📥 observe le résultat retournedans l'historique ↻ on recommence
Une étape = un aller-retour avec le modèle. Tant que le modèle demande un outil, on exécute et on reboucle. Quand il répond sans demander d'outil, le tour est fini.

Dans le code, c'est la fonction handleTurn. Lisez-la comme une recette : on demande au modèle, on regarde s'il veut un outil, si oui on l'exécute et on remet le résultat dans l'historique, puis on recommence.

for step := 0; step < a.maxSteps; step++ {
    a.compactContext(ctx)                       // garder le contexte sous contrôle

    msg, err := a.client.Chat(ctx, a.history, a.registry.Schemas())   // 1. demander au modèle
    if err != nil { /* on rend la main proprement */ return }
    a.history = append(a.history, msg)          // mémoriser sa réponse

    if len(msg.ToolCalls) > 0 {                 // 2. le modèle veut agir ?
        for _, call := range msg.ToolCalls {
            result := a.runTool(ctx, call.Function.Name, parseJSONArgs(call.Function.Arguments))
            a.history = append(a.history, Message{   // 3. l'observation retourne au modèle
                Role: "tool", ToolCallID: call.ID,
                Name: call.Function.Name, Content: result,
            })
        }
        continue                                // 4. on reboucle
    }
    return                                      // pas d'outil demandé → tour terminé
}
Pourquoi une limite d'étapes ? Un agent peut se tromper et répéter la même action indéfiniment. Une borne (maxSteps) garantit qu'un tour finit toujours. Le code ajoute même un détecteur de boucle : si l'agent redemande exactement la même action qu'à l'étape précédente, on s'arrête — il ne progresse plus.
03

Les outils : les mains de l'agent

Un outil, c'est simplement une fonction que le modèle peut décider d'appeler. Dans cet agent, un outil est une fonction pure (contexte, arguments) → (résultat, erreur). Il ne fait aucun affichage : il calcule un résultat, point. C'est ce qui le rend simple à écrire, à tester et à réutiliser.

type Tool struct {
    Name        string                  // ex. "read_file"
    Description string                  // lu par le modèle pour savoir quand l'utiliser
    Parameters  map[string]any          // le schéma JSON des arguments
    Confirm     func(args) (bool, string)  // garde-fou optionnel (actions risquées)
    Run         ToolFunc                // (ctx, args) → (résultat, erreur)
}

L'agent fournit trois outils de base — c'est tout ce qu'il faut pour coder :

📖

read_file

Lit le contenu d'un fichier texte.

✏️

write_file

Écrit ou écrase un fichier.

execute_shell

Lance une commande (build, tests, git…).

Comment le modèle sait-il quels outils existent ? On lui décrit chacun dans un schéma qu'on envoie à chaque requête. Le modèle lit le nom, la description et les paramètres, puis il choisit lequel appeler.

r.Register(Tool{
    Name:        "read_file",
    Description: "Lit le contenu d'un fichier texte.",
    Parameters: map[string]any{
        "type": "object",
        "properties": map[string]any{
            "path": map[string]any{"type": "string", "description": "Chemin du fichier à lire"},
        },
        "required": []string{"path"},
    },
    Run: toolReadFile,
})
Ajouter un outil = ajouter une capacité. Recherche web, requête base de données, appel d'API… il suffit d'enregistrer un nouveau Tool. Le reste de la boucle ne change pas. C'est là toute la puissance du modèle : le registre d'outils est extensible sans toucher au moteur.
04

Parler au modèle

Reste à connecter le modèle. La conversation est une simple liste de messages (système, utilisateur, assistant, résultats d'outils) qu'on renvoie en entier à chaque étape — le modèle n'a pas de mémoire propre, le contexte est la mémoire.

Le function calling : comment le modèle « appelle » un outil

Le modèle ne lance pas l'outil lui-même. Il renvoie une intention structurée — « je veux appeler read_file avec path=math.go » — et c'est notre code qui l'exécute, puis lui renvoie le résultat.

votre code le modèle historique + liste des outils tool_call: read_file(path="math.go") exécute l'outil résultat : "func Somme(a, b int)…"
Le modèle décide quoi appeler ; votre code décide comment l'exécuter. La séparation est nette.

Le streaming : voir la réponse se former

Plutôt que d'attendre la réponse complète, on lit un flux (SSE) et on affiche chaque fragment dès son arrivée. Les appels d'outils, eux, arrivent en morceaux qu'on réassemble par leur index.

for _, d := range delta.ToolCalls {
    tc := toolCalls[d.Index]                 // un fragment par index d'outil
    if d.Function.Name != "" { tc.Function.Name = d.Function.Name }
    tc.Function.Arguments += d.Function.Arguments   // les arguments arrivent par bouts
}

Le filet de sécurité : et si le modèle ne sait pas appeler d'outils ?

Tous les modèles ne supportent pas le function calling natif. L'agent prévoit donc un repli : si le modèle écrit son intention en texte (Action: read_file(path="…")), on la repère par une expression régulière. Subtilité : on n'accepte que le préfixe Action: en début de ligne — sinon, quand le modèle récapitule ses actions, on les ré-exécuterait en boucle.

// (?m) : ^ s'ancre en début de ligne — évite de ré-exécuter une action citée
//        dans un récapitulatif ("1. Action: write_file(...)").
pattern := `(?sm)^[ \t]*Action\s*:\s*(` + strings.Join(names, "|") + `)\s*\(\s*(.*)\)`
05

Les garde-fous

Donner à un modèle le droit de lancer des commandes shell, c'est puissant — et dangereux. Un bon agent n'est pas qu'une boucle : c'est une boucle prudente. Quatre protections, simples mais essentielles :

1

Confirmation des actions risquées

Une commande qui matche un motif dangereux (rm -rf, sudo, dd, fork bomb…) demande une validation humaine avant de s'exécuter.

2

Détection de boucle

Si l'agent répète exactement la même action, on arrête le tour : il ne progresse plus, inutile de brûler des tokens.

3

Timeouts

Chaque commande et chaque appel au modèle a une limite de temps. Une commande bloquée ne fige jamais l'agent.

4

Troncature des sorties

Le résultat d'un outil est plafonné avant d'être réinjecté : une sortie énorme ne sature pas le contexte.

var dangerousPatterns = []*regexp.Regexp{
    regexp.MustCompile(`\brm\s+-[a-zA-Z]*[rf]`),   // rm -rf
    regexp.MustCompile(`\bdd\s+if=`),
    regexp.MustCompile(`:\s*\(\)\s*\{`),           // fork bomb
    regexp.MustCompile(`\b(shutdown|reboot|halt)\b`),
    regexp.MustCompile(`\bsudo\b`),
    // …
}
Une erreur n'est pas une catastrophe. Quand un outil échoue, l'agent ne plante pas : il renvoie l'erreur au modèle comme une observation. Le modèle lit le message, comprend ce qui s'est passé, et corrige tout seul à l'étape suivante. L'auto-correction émerge de la boucle.
06

La mémoire & le contexte

À chaque étape, l'historique grossit : messages, appels d'outils, résultats. Or la fenêtre de contexte d'un modèle est limitée. Sans rien faire, on finit par la dépasser. La solution de l'agent : compacter.

Quand l'historique dépasse un budget de tokens, on garde intacts les messages récents (le travail en cours), et on demande au modèle de résumer les plus anciens en quelques puces. Le résumé remplace les vieux messages. La mémoire longue devient compacte, la mémoire courte reste précise.

avant après résumé 4 anciens messages récents gardés
Les anciens échanges sont condensés en un résumé ; les messages récents restent intacts.
if totalTokens(a.history) <= a.maxCtx { return }   // sous le budget : rien à faire

older  := rest[:keepFrom]                          // les anciens messages
recent := rest[keepFrom:]                           // les ~60% récents, gardés tels quels

summary, err := a.client.Summarize(ctx, older)      // le modèle résume les anciens
// → [système initial] + [résumé] + [messages récents]
07

Tout assembler

On a tous les morceaux. Le programme principal les relie : il crée le registre d'outils, le client modèle, et lance une boucle de lecture (un REPL). Chaque message de l'utilisateur déclenche un tour d'agent — la boucle qu'on a disséquée.

Et la « personnalité » de l'agent ? Elle tient dans un prompt système : quelques phrases qui lui rappellent sa mission, ses outils et ses règles.

`Tu es un agent de codage autonome.
Règles :
1. Décompose les missions en étapes et appelle les outils nécessaires.
2. Analyse le résultat de chaque outil ; en cas d'erreur, corrige puis réessaie.
3. Une action destructive peut demander une confirmation à l'utilisateur.
4. Quand la mission est accomplie, fais un court récapitulatif puis rends la main.`

En une phrase

Un agent de codage, c'est une boucle qui envoie l'historique à un modèle, exécute les outils qu'il demande, lui renvoie les résultats, et recommence — le tout entouré de garde-fous et d'une gestion du contexte. Aucune magie : de l'ingénierie lisible.

08

Questions fréquentes

Quelle est la différence entre un agent et un chatbot ?

Un chatbot répond en une seule passe : message puis réponse. Un agent agit, observe le résultat de chaque action et recommence dans une boucle, en utilisant des outils, jusqu'à ce que la tâche soit réellement accomplie.

Faut-il un framework pour construire un agent de codage ?

Non. L'agent de ce guide est écrit en Go avec la seule bibliothèque standard, en ~1000 lignes et zéro dépendance. Il fonctionne avec n'importe quel serveur compatible OpenAI (LM Studio, Ollama, vLLM, API cloud).

Comment un modèle de langage exécute-t-il des actions ?

Il ne les exécute pas lui-même. Via le function calling, le modèle renvoie une intention structurée (nom d'outil et arguments) ; c'est le code de l'agent qui exécute réellement l'outil, puis renvoie le résultat au modèle.

Comment éviter qu'un agent tourne en boucle ou exécute une action dangereuse ?

Avec des garde-fous : une limite d'étapes par tour, une détection de répétition d'action, une confirmation humaine pour les commandes risquées, des timeouts, et la troncature des sorties trop longues. Les erreurs sont réinjectées au modèle pour qu'il se corrige.

Comment gérer une fenêtre de contexte limitée ?

En compactant l'historique : quand il dépasse un budget de tokens, les messages récents sont gardés intacts tandis que les plus anciens sont résumés par le modèle et remplacés par ce résumé.

09

Le code & pour aller plus loin

Tout ce guide décrit un agent réel et complet. Le code est ouvert, commenté ligne par ligne, et tourne avec n'importe quel serveur compatible OpenAI. Le meilleur moyen de comprendre : le lire, le lancer, le modifier.

Nhilo94/comprendre-agent-llm

Agent de codage en Go — boucle LLM + outils, ~1000 lignes, zéro dépendance.

Go stdlib only compatible OpenAI

Lancer l'agent en 30 secondes

git clone https://github.com/Nhilo94/comprendre-agent-llm
cd comprendre-agent-llm
go run .            # puis choisissez votre modèle et discutez

Quelques pistes d'extension

  • Ajouter un outil de recherche web ou d'appel d'API.
  • Remplacer le résumé par une vraie mémoire vectorielle.
  • Donner à l'agent la possibilité de planifier avant d'agir.
  • Brancher un second agent qui relit le travail du premier.