Il y a un moment précis où j’ai compris que les fondamentaux n’étaient pas du legacy.
J’écrivais les spécifications d’un pipeline multi-agents pour une bibliothèque C. Je cherchais comment structurer le cycle de vie du contexte de chaque agent — ce qu’il devait recevoir au démarrage, ce qu’il devait produire en sortie, et surtout : quand et comment libérer ce qu’il avait consommé pour que l’agent suivant parte avec un contexte propre.
Et je me suis retrouvé à dessiner un schéma que je connaissais déjà. Que tout ingénieur qui a sérieusement travaillé en C connaît déjà. La gestion de durée de vie d’une ressource. L’ownership. La frontière entre ce qui est partagé et ce qui est local. L’inévitable question : qui libère ?
Ce n’était pas de la nostalgie. C’était de la reconnaissance.
Ce que C t’apprend sur les systèmes agentiques
Quand tu écris du C et que tu alloues de la mémoire, tu prends une décision explicite : je demande au système une ressource, je l’utilise, je la rends. Pas de garbage collector. Pas de borrow checker. Toi et le heap, à mains nues.
void *ctx = malloc(CTX_SIZE);
if (!ctx) {
/* gestion d'erreur — elle n'est pas optionnelle */
return NULL;
}
/* utilisation */
free(ctx);
ctx = NULL; /* discipline : éviter le dangling pointer */
Trois lignes d’intention. Mais derrière chacune se cachent des décisions d’architecture : Qui est propriétaire de ctx ? Combien de temps vit-il ? Qui a le droit de le modifier ? Que se passe-t-il si la fonction retourne prématurément ?
Ces questions ne disparaissent pas quand tu montes d’abstraction. Elles se rebaptisent.
Dans un système multi-agents, le “contexte” qu’un LLM reçoit à chaque appel joue exactement le même rôle qu’un bloc alloué : il occupe une ressource limitée (la fenêtre de contexte, mesurée en tokens), il a une durée de vie, et quelqu’un doit décider quand et comment le libérer — ou le compresser, ou le transmettre.
La terminologie change. Le problème reste.
Trois patterns de mémoire C, trois patterns d’architecture IA
malloc/free → Context management des agents
malloc te donne un bloc. Tu l’utilises. Tu le libères. C’est l’allocation dynamique de base : tu ne sais pas à l’avance de quoi tu auras besoin, donc tu demandes au moment où le besoin émerge.
Un agent IA reçoit un contexte construit dynamiquement à chaque invocation : l’historique de la conversation (ou un résumé), les résultats des outils précédents, les instructions systèmes. Ce contexte n’existe que pendant l’exécution de l’agent. Quand l’agent a terminé et produit son rapport, ce contexte est “libéré” — il n’est plus transmis aux étapes suivantes, sauf la partie délibérément retenue.
Le bug classique en C : oublier de libérer, ou libérer deux fois. Le bug classique en architecture agentique : laisser s’accumuler dans le contexte des informations qui ne servent plus (context pollution), ou tronquer sans raison des informations encore pertinentes.
Dans les deux cas, la conséquence est la même : un système qui dérive. En C, le programme consomme de la mémoire jusqu’à l’OOM. En agentique, le raisonnement se dégrade au fil des tours jusqu’à produire des réponses incohérentes.
Allocation C : [malloc] → [utilisation] → [free]
Contexte agent: [build] → [inférence] → [compress/drop]
La discipline est identique. La visibilité sur ce qui se passe est différente — en C, Valgrind te dit exactement ce qui fuit. En agentique, le signal est indirect : la qualité des réponses.
Pool allocator → Agent pool
Le pool allocator est une technique simple et efficace : au lieu d’appeler malloc pour chaque objet, tu alloues un bloc de N objets d’avance et tu en distribues un à la demande. Quand un objet est “libéré”, il retourne dans le pool.
typedef struct {
object_t slots[POOL_SIZE];
bool in_use[POOL_SIZE];
} pool_t;
object_t *pool_acquire(pool_t *p) {
for (int i = 0; i < POOL_SIZE; i++) {
if (!p->in_use[i]) {
p->in_use[i] = true;
return &p->slots[i];
}
}
return NULL; /* pool épuisé */
}
Pourquoi ? Parce que malloc a un coût. Et si tu crées et détruis des milliers d’objets identiques par seconde, ce coût s’accumule. Le pool élimine la fragmentation, prévisibilise les latences, et rend le comportement du système déterministe.
Dans un système multi-agents en production, tu retrouves le même pattern sous le nom d’agent pool ou worker pool : un ensemble d’instances d’agents pré-initialisées, prêtes à être assignées à une tâche. Pas de cold start à chaque requête. Pas de réinitialisation du contexte système depuis zéro. L’instance est disponible, elle reçoit le contexte spécifique à la tâche, elle exécute, elle retourne au pool.
Le raisonnement est identique : le coût d’initialisation est non nul, le débit prévisible est une contrainte, les instances sont interchangeables.
Si tu as compris pourquoi un pool allocator existe — pas comme recette, mais comme réponse à un problème de coût et de prévisibilité — alors tu comprends déjà pourquoi un agent pool existe. Ce n’est pas un pattern cloud-native. C’est de la gestion de ressources.
Arena allocator → Conversation scope
L’arena allocator (ou region allocator) pousse l’idée encore plus loin. Au lieu de libérer les objets un par un, tu alloues dans une zone linéaire et tu libères tout d’un coup quand la tâche est terminée.
typedef struct {
uint8_t buf[ARENA_SIZE];
size_t offset;
} arena_t;
void *arena_alloc(arena_t *a, size_t size) {
if (a->offset + size > ARENA_SIZE) return NULL;
void *ptr = a->buf + a->offset;
a->offset += size;
return ptr;
}
void arena_reset(arena_t *a) {
a->offset = 0; /* libération totale en O(1) */
}
Aucune libération individuelle. Aucune fragmentation. À la fin de la tâche, on remet le compteur à zéro. Tout ce qui a été alloué dans l’arena est invalidé en une seule opération.
Ce pattern s’applique directement à ce que les systèmes agentiques appellent le scope de conversation — ou parfois session context. Pendant une session, l’agent accumule des informations : ce que l’utilisateur a dit, ce que les outils ont retourné, les décisions intermédiaires. À la fin de la session, ce contexte devient inutile. Il ne doit pas polluer les sessions suivantes.
L’arena agentique est la frontière de session. Elle est délibérément opaque vers l’extérieur. Ce qui se passe dans une session ne “fuit” pas dans la suivante, sauf ce qu’on choisit explicitement de persister dans la mémoire longue terme.
Arena C : [alloc × N] → [utilisation] → [reset O(1)]
Session agent: [tour × N] → [utilisation] → [flush/persist]
Le flush de session, c’est un arena_reset. La persistance sélective — résumés, faits clés, décisions — c’est la mémoire long terme qu’on copie hors de l’arena avant de la réinitialiser.
Ownership et passage de contexte
Il y a un troisième concept que C rend impossible à ignorer : l’ownership. À chaque pointeur, il faut pouvoir répondre : qui possède cette mémoire ? Qui a le droit de la libérer ?
En C, ne pas répondre à cette question produit des bugs : double-free, use-after-free, fuites. Le compilateur ne t’aide pas (en C standard). La discipline vient de la conception.
En architecture agentique, la même question se pose pour chaque document, chaque rapport, chaque état intermédiaire qui transite entre les agents. Dans le pipeline que j’ai construit :
PM (brief) → Analyste (rapport) → Planner (plan) → Programmer (code + tests) → Tester (validation) → Auditor
À chaque étape, un agent produit un artefact et le passe à l’étape suivante. Qui “possède” le brief du PM une fois que l’Analyste l’a lu ? Est-ce que le Planner doit le relire ou se fier au rapport de l’Analyste ? Est-ce que le Programmer a accès à l’historique complet ou seulement au plan ?
Ces questions ne sont pas triviales. Un Programmer qui reçoit trop de contexte va parfois l’ignorer, parfois se laisser influencer par des informations obsolètes. Un Programmer qui reçoit trop peu va manquer de contraintes importantes.
C’est du design d’API. Pas de l’API HTTP, du design d’interface entre composants. Le même travail qu’en C : définir des contrats clairs entre les fonctions, minimiser le couplage, éviter que chaque composant sache trop de choses sur ses voisins.
[ownership explicite]
PM ──► brief ──► Analyste ──► rapport ──► Planner ──► plan ──► Programmer
(lit brief, (lit rapport, (lit plan,
ne le modifie pas) ne lit pas brief) ignores brief)
Le brief du PM n’est jamais retransmis directement au Programmer. Ce n’est pas un oubli — c’est une décision d’architecture. Le Programmer ne doit pas interpréter le besoin initial. Il doit exécuter le plan.
Séparation des responsabilités. Ownership clair. Interfaces minimales.
Pourquoi les fondamentaux C font de meilleurs architectes IA
Il ne s’agit pas de prétendre que tout architecte IA doit écrire du C. Ce serait une mauvaise lecture.
Ce que le C enseigne, c’est une façon de raisonner sur les ressources, les durées de vie et les frontières de responsabilité. Ces concepts se retrouvent partout — dans les protocoles réseau, dans la conception de bases de données, dans l’architecture des systèmes distribués, et maintenant dans la conception des systèmes agentiques.
Le développeur qui a sérieusement travaillé sur la gestion mémoire en C a développé une intuition que d’autres n’ont pas : il voit naturellement les questions de durée de vie et d’ownership là où les autres voient des “détails d’implémentation”. Quand il conçoit un pipeline d’agents, il se pose spontanément les bonnes questions :
- Ce contexte, qui le possède ?
- Quand est-il libéré ?
- Que se passe-t-il si l’agent échoue en cours d’exécution ?
- Est-ce que la sortie de cet agent peut contaminer l’entrée du suivant ?
Ce ne sont pas des questions qu’on pose en ayant lu la documentation d’un framework d’orchestration. Ce sont des questions qu’on se pose après avoir debuggé des fuites mémoire à 2h du matin.
Ce qui a changé
Un élément est réellement différent entre la gestion mémoire C et l’ingénierie de contexte agentique : la visibilité.
En C, tu peux instrumenter. Valgrind te dit exactement quel bloc n’a pas été libéré, à quelle ligne il a été alloué, quelle fonction appelante est responsable. L’outil est parfois verbeux, parfois lent, mais il est précis.
En agentique, le signal est flou. Tu observes le comportement en sortie — la qualité du raisonnement, la cohérence des décisions — pour inférer l’état du contexte. C’est comme debugguer une fuite mémoire sans Valgrind, en observant uniquement la consommation RAM globale du processus.
C’est pour ça que l’architecture agentique demande plus de rigueur a priori que la programmation C classique. En C, tu peux corriger après observation. En agentique, un mauvais design de contexte est difficile à observer directement. Il faut l’anticiper.
Ce qui signifie qu’investir dans la compréhension des problèmes fondamentaux de gestion de ressources — durée de vie, ownership, isolation — paie doublement en architecture agentique : non seulement parce que les patterns se retrouvent, mais parce que l’outillage d’observation est moins mature.
Ce que je retiens
Les fondamentaux ne sont pas des connaissances qui “servent à comprendre les choses complexes plus tard”. Ils sont les choses complexes, vues d’un angle différent.
malloc et le context management d’un agent posent le même problème : gérer une ressource limitée avec une durée de vie explicite. Le pool allocator et l’agent pool répondent au même impératif : prévisibiliser le coût d’initialisation. L’arena et le scope de session partagent la même logique de frontière : tout ce qui naît dans cette zone meurt ensemble.
L’article sur le pipeline montrait le comment — les agents, les règles, les scripts. Cet article est le pourquoi : ce pipeline ressemble à ce qu’il est parce que les problèmes qu’il résout ne sont pas nouveaux.
Ce qui est nouveau, c’est le medium. Le contexte n’est pas un bloc de bytes — c’est du texte structuré. L’agent n’est pas une fonction — c’est un LLM avec des instructions. La libération n’est pas un free() — c’est une décision sur ce qu’on conserve entre les tours.
Mais le raisonnement est le même. Et ce raisonnement s’apprend plus vite quand on a déjà passé du temps à comprendre pourquoi ctx = NULL après free(ctx) n’est pas une superstition mais une discipline.
Lis. Comprends. Maîtrise. Recommence.