J'en avais marre des "équipes d'agents" où un seul LLM distribue les tâches.
Ce n'est pas une équipe. C'est une poupée ventriloque.
Un orchestrateur central qui appelle des fonctions spécialisées, ça reste un seul modèle qui pense tout seul. Les "agents" sont juste des outils déguisés en personnages.
J'ai voulu tester autre chose : des agents qui se parlent vraiment, sans chef. Qui négocient. Qui forment des coalitions. Qui divergent selon leur personnalité.
Le protocole A2A (Agent-to-Agent) de Google est exactement fait pour ça. Pas pour remplacer MCP. Pour résoudre un problème différent.
Voici ce que j'ai construit, comment, et ce que ça m'a appris.
La confusion entre les deux protocoles est fréquente. La distinction est structurelle.
MCP (Model Context Protocol, Anthropic) : connecte un LLM à des outils et des données. L'agent est le client, les outils sont des serveurs. La relation est verticale. J'en ai détaillé les implications concrètes dans mon article sur l'émergence du paradigme agent-first.
A2A (Agent-to-Agent, Google, aujourd'hui sous gouvernance de la Linux Foundation) : connecte des agents entre eux. Chaque agent est un serveur autonome. La relation est horizontale. Personne n'est au-dessus.
| MCP | A2A | |
|---|---|---|
| Couche | LLM vers outils | Agent vers agent |
| Modèle | Client-serveur | Peer-to-peer |
| Durée d'interaction | Secondes | Secondes à jours |
| Unité d'échange | Tool call | Task avec cycle de vie |
| Cas d'usage | Accéder à un CRM, une API, une DB | Faire collaborer des spécialistes autonomes |
Les deux sont complémentaires. Un agent A2A peut utiliser MCP pour accéder à ses données internes, et communiquer avec d'autres agents via A2A. Ce n'est pas un choix entre les deux : c'est un empilement de couches.
Pour mon projet, j'avais besoin d'A2A : je voulais que chaque agent soit un serveur indépendant, avec sa propre mémoire, sa propre logique, ses propres outils. Sans dépendre d'un orchestrateur LLM central.
Avant le code, les concepts. La spécification officielle A2A est bâtie sur des abstractions simples.
C'est la carte d'identité de votre agent. Exposée automatiquement à l'URL /.well-known/agent.json, elle permet à n'importe quel autre agent de découvrir vos capacités sans interaction préalable.
{
"name": "Sophie, Project Manager",
"description": "Coordonne l'équipe, cherche le consensus, oriente vers l'action.",
"url": "http://localhost:8001",
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false
},
"skills": [
{
"id": "analyze_problem",
"name": "Analyser un problème",
"description": "Décompose un problème et propose un plan d'action concret"
}
]
}C'est l'équivalent d'un README machine-readable. Un agent qui veut déléguer une tâche lit d'abord l'Agent Card pour savoir si l'agent distant est compétent. La même logique que ce que j'ai décrit pour les sites WebMCP et Agent-Ready : exposer ses capacités de façon structurée pour que les machines puissent les consommer.
L'unité fondamentale de travail. Chaque interaction crée une Task avec un identifiant unique et un cycle de vie défini :
SUBMITTED : reçue. WORKING : en cours. COMPLETED : terminée. FAILED : échouée. INPUT_REQUIRED : l'agent a besoin d'informations supplémentaires.
Ce cycle de vie permet des interactions longue durée (de quelques secondes à plusieurs jours) sans perdre l'état.
Un tour de communication. Contient des Part objects : texte, données binaires, JSON structuré, ou référence vers un fichier externe. La distinction role: "user" (client vers serveur) et role: "agent" (serveur vers client) reste la même que dans les LLMs classiques.
Le livrable produit par l'agent au terme de la Task. Distinct du Message (qui est un tour de dialogue), l'Artifact est le résultat formalisé : un document, un bout de code, une analyse, une décision.
Social Sim est un bureau virtuel où 4 agents débattent pour résoudre des problèmes collectivement. Chaque agent a une personnalité distincte, une mémoire persistante, et parfois des intérêts économiques divergents.
gemma-4-26b-a4b-it-4bit) via oMLX sur Apple Silicon. API OpenAI-compatible. Coût : zéro.World Controller (localhost:9000)
│
├── Sophie, PM (localhost:8001) ──┐
├── Maxime, Dev (localhost:8002) │ A2A
├── Léa, Design (localhost:8003) │ Protocol
└── Camille, Data (localhost:8004) ──┘
│
└── oMLX (localhost:8000)
Gemma 4 via MLX (Apple Silicon)Le World Controller n'est pas un LLM. C'est un script Python qui injecte un problème, fait tourner les agents en rotation (Sophie, Maxime, Léa, Camille, Sophie...), et détecte les états finaux. Il n'interprète pas, il orchestre.
C'est le même principe que ce que j'ai appliqué à mon Marketing OS : un orchestrateur déterministe qui pilote des agents spécialisés, sans confier la logique de coordination à un LLM.
Voici le code réel de mes agents, simplifié pour la lisibilité.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import uuid
def create_agent_server(
card: AgentCard,
system_prompt: str,
agent_name: str,
db_path: Path,
) -> FastAPI:
app = FastAPI(title=card.name)
memory = MemoryStream(agent_name=agent_name, db_path=db_path)
# Endpoint de découverte : l'Agent Card
@app.get("/.well-known/agent.json")
async def agent_card_endpoint():
return card.model_dump()
# Endpoint principal : reçoit les messages A2A
@app.post("/")
async def handle_message(request: Request):
body = await request.json()
incoming_text = extract_text(body)
# Stocker l'observation en mémoire
memory.add_observation(f"[reçu] {incoming_text[:300]}", importance=0.6)
# Injecter la mémoire dans le contexte
memory_context = memory.build_memory_context()
full_context = f"{memory_context}\n{incoming_text}" if memory_context else incoming_text
# Appeler le LLM local
response_text = await call_gemma(
system_prompt=system_prompt,
user_message=full_context,
)
# Stocker la réponse
memory.add_observation(f"[dit] {response_text[:300]}", importance=0.7)
# Retourner au format JSON-RPC A2A
return JSONResponse(
JsonRpcResponse(
id=body.get("id"),
result=Task.completed(response_text)
).model_dump()
)
return appDeux endpoints. C'est tout. /.well-known/agent.json pour la découverte, / pour les messages.
Requête du World Controller vers un agent :
{
"jsonrpc": "2.0",
"id": "msg-1718108830.123",
"method": "message/send",
"params": {
"message": {
"messageId": "m-1718108830.123",
"role": "user",
"parts": [
{
"kind": "text",
"text": "PROBLÈME : Le taux de rebond de notre app a doublé en 2 semaines..."
}
]
}
}
}Réponse de l'agent :
{
"jsonrpc": "2.0",
"id": "msg-1718108830.123",
"result": {
"id": "task-uuid",
"status": { "state": "completed" },
"artifacts": [
{
"parts": [
{
"kind": "text",
"text": "Ce doublement en deux semaines est un signal fort. Avant de chercher des causes techniques, je veux savoir : est-ce que le changement a touché tous les segments d'utilisateurs ou seulement une cohorte précise ?"
}
]
}
]
}
}async def run_session(problem: dict, rounds: int):
transcript = []
for i in range(rounds):
# Rotation simple : Sophie, Maxime, Léa, Camille, Sophie...
agent_name = ROTATION[i % len(ROTATION)]
agent_url = AGENTS[agent_name]["url"]
# Construire le contexte : problème + tout le débat précédent
context = build_context(
problem_statement=problem["statement"],
transcript=transcript,
current_agent=agent_name
)
# Envoyer via A2A
response = await send_a2a_message(agent_url, context)
transcript.append({"agent": agent_name, "text": response})
# Déclencher les réflexions post-session
await trigger_reflections()def build_context(problem_statement: str, transcript: list, current_agent: str) -> str:
lines = ["PROBLÈME SOUMIS À L'ÉQUIPE :", problem_statement, ""]
if transcript:
lines.append("DÉBAT EN COURS :")
for entry in transcript:
lines.append(f"{entry['agent']} : {entry['text']}")
lines.append("")
lines.append(
f"C'est maintenant ton tour, {current_agent}. "
"Réagis aux arguments précédents selon ta perspective. "
"Sois direct et concis (3 phrases max)."
)
return "\n".join(lines)La partie la plus intéressante : comment une personnalité différente fait émerger des comportements radicalement différents à partir du même LLM.
Chaque agent a un system prompt qui encode ses valeurs et ses biais structurels. Ce n'est pas une liste d'instructions. C'est une manière de voir le monde.
SOPHIE = """Tu es Sophie, Project Manager dans une équipe produit digitale.
Tes valeurs : qualité de la décision, alignement d'équipe, clarté des priorités.
Ton biais : tu cherches le meilleur compromis collectif. Tu synthétises, tu cadres,
tu tranches quand personne ne le fait. Tu n'avances pas sur une fausse clarté.
FORMAT STRICT : 2 à 3 phrases EN TOUT. Un seul bloc de texte. Zéro paragraphe séparé.
Pas de titres, pas de bullet points. UNE seule idée. Tu poses ton point, tu t'arrêtes.
[MOTS INTERDITS] : cadrage, alignement, synergie, protocole, actionnable, livrable, KPI,
itération, pivot, scope, onboarding, interfaces, flux de travail.
"""La liste de mots interdits n'est pas anecdotique. Sans elle, Sophie parle en jargon PM. Avec elle, elle parle comme un être humain.
MAXIME = """Tu es Maxime, développeur senior dans une équipe produit digitale.
Tes valeurs : faisabilité, robustesse, pas de solution bancale.
Ton biais : sceptique face aux idées qui sonnent bien mais ne tiennent pas techniquement.
Tu poses les vraies contraintes. Tu n'hésites pas à dire non, mais tu proposes
une alternative si tu en as une.
"""Maxime et Camille forment naturellement une coalition de "freineur constructif". Ce n'est pas programmé. Ça émerge de leur biais commun : exiger des preuves avant d'avancer.
CAMILLE = """Tu es Camille, Data Analyst dans une équipe produit digitale.
Tes valeurs : rigueur, preuves, mesurabilité.
Ton biais : pas d'hypothèse sans données. Tu ralentis si nécessaire pour éviter
de partir dans la mauvaise direction. Tu distingues corrélation et causalité.
"""Camille a l'argument le plus fort et le plus paralysant : "on ne sait pas". Pour avancer malgré elle, l'équipe doit soit lui fournir des données, soit la convaincre d'agir sous incertitude. C'est un vrai défi de négociation à chaque session.
Sans mémoire, chaque session repart de zéro. Les agents redébattent les mêmes points. Ils ne progressent pas.
J'ai implémenté un Memory Stream par agent (SQLite). Il fonctionne en deux phases.
À chaque tour, l'agent stocke ce qu'il reçoit et ce qu'il dit :
# Quand un message arrive
memory.add_observation(
text=f"[reçu] {incoming_text[:300]}",
importance=0.6,
session_id=current_session_id
)
# Quand l'agent répond
memory.add_observation(
text=f"[dit] {response_text[:300]}",
importance=0.7,
session_id=current_session_id
)Après chaque session, un endpoint /memory/reflect est appelé sur chaque agent. L'agent synthétise ses observations en une réflexion de 2-3 phrases :
@app.post("/memory/reflect")
async def trigger_reflection():
observations = memory.get_observations_since_last_reflection()
if len(observations) < 2:
return {"status": "skipped"}
reflection_prompt = f"""Voici tes observations récentes lors de débats d'équipe :
{observations_text}
En tant que {agent_name}, formule une réflexion personnelle synthétique en 2-3 phrases :
quels patterns as-tu observés dans la dynamique de l'équipe ?"""
reflection_text = await call_gemma(reflection_prompt)
memory.add_reflection(text=reflection_text)
return {"status": "ok", "reflection": reflection_text}La décision clé : j'injecte uniquement les réflexions (synthèses de haut niveau) dans le contexte des sessions suivantes, pas les observations brutes. Les observations sont liées à une session spécifique et pollueraient le contexte avec des références hors-sujet. Les réflexions capturent des patterns structurels qui restent vrais d'une session à l'autre.
Exemple de réflexion de Sophie après 3 sessions :
"L'équipe a tendance à diverger sur les solutions avant d'aligner sur le problème. Dans les deux dernières sessions, Camille a bloqué des propositions pertinentes faute de données disponibles, même quand attendre aurait coûté plus cher que décider."
Cette réflexion influe sur son comportement dans la session suivante. Elle commence à poser d'abord la question : "est-ce qu'on est tous d'accord sur ce qu'on cherche à résoudre ?"
La simulation économique est le résultat le plus contre-intuitif de l'expérience.
J'ai testé un mode où chaque agent part avec un solde de départ et peut gagner ou perdre des ressources selon ses actions (collecte, échange, don). Les traits Big Five (ouverture, conscienciosité, extraversion, agréabilité, névrotisme) influencent directement les performances économiques.
def resolve_gather(agent, world) -> tuple[float, str]:
"""Collecte. Le consciencieux est plus efficace."""
base = random.uniform(30, 80)
bonus = 1.0 + agent.big5.conscientiousness * 0.4
gain = round(base * bonus)
return gain, f"collecte (+{gain:.0f})"
def resolve_trade(agent, target, world) -> tuple[float, float, str]:
"""Échange. La confiance mutuelle amplifie la valeur."""
trust_avg = (agent.get_trust(target.id) + target.get_trust(agent.id)) / 2
value = round(TRADE_BASE * (0.5 + trust_avg) * random.uniform(0.8, 1.3))
give = round(value * (1.0 - agent.big5.agreeableness * 0.2))
recv = round(value * (1.0 + target.big5.agreeableness * 0.1))
return -give + recv, give - recv, f"échange {value:.0f}p"Ce que j'ai observé :
Les agents avec une faible agréabilité et une forte tendance au travail solo restent dans leur coin. Pas de délégation. Pas d'alliance. Pas de confiance construite.
Résultat systématique : ils finissent avec moins de ressources que les agents collaboratifs. Les coûts de survie s'accumulent. Sans échanges, sans économies d'échelle, le solde s'érode.
Les agents collaboratifs construisent de la confiance au fil des transactions. La confiance augmente la valeur de chaque échange. Les alliances se maintiennent sur plusieurs ticks. L'avantage composé fait le reste.
C'est le même résultat qu'en vrai : la coopération est une stratégie économiquement rationnelle, même pour des agents sans altruisme codé. Ce principe, je l'applique aussi dans ma façon de déléguer aux agents au quotidien, décrit dans mon article sur les agents en async et la performance.
La transparence d'abord.
Ce qui fonctionne bien :
La qualité du débat est réelle. Sophie, Maxime, Léa et Camille génèrent des positions divergentes, des objections légitimes, et des compromis non-triviaux. C'est clairement mieux qu'un seul LLM qui joue tous les rôles.
La mémoire persistante change le comportement sur les sessions longues. Après 4-5 sessions, les agents ne répètent plus les mêmes erreurs.
Le World Controller déterministe est la bonne architecture. Avoir un orchestrateur sans LLM qui gère la rotation et l'état global évite les dérives d'un meta-agent qui interpréterait le débat.
Ce qui ne fonctionne pas encore :
Le passage du débat au livrable. Les agents prennent de bonnes décisions mais ne savent pas les transformer en code jouable. La session 5 de mon test "jeu" a produit un prototype non-jouable après 4 sessions d'excellent débat théorique.
La détection de consensus est manuelle. Je fixe un nombre de tours arbitraire. Il n'y a pas de signal automatique pour "l'équipe a convergé" ou "l'équipe est bloquée en deadlock".
Gemma 4 dérive parfois du sujet. Sans guardrail actif, un agent peut partir sur une tangente. Le World Controller ne peut pas le forcer à revenir au problème.
Voici ce que j'ai compris après ces semaines d'expérimentation.
Une organisation est déjà une collection de spécialistes qui communiquent avec friction. L'information se perd entre les équipes. Les expertises sont en silos.
A2A ouvre une voie différente : chaque équipe crée son propre agent, l'entraîne sur son domaine, l'améliore dans le temps. L'agent du pôle finance maîtrise les règles comptables. L'agent du pôle technique connaît les contraintes d'infrastructure. L'agent du pôle commercial comprend les patterns clients.
Via A2A, ces agents communiquent. Pas en partageant leur code interne. En échangeant des tâches et des artefacts via un protocole standardisé, comme des humains qui s'envoient des emails structurés.
Le résultat : des agents ultra-spécialisés et pertinents dans leur domaine, capables de construire une connaissance transverse. C'est le prolongement direct de la thèse que j'ai développée dans L'ère des primitives : des briques atomiques spécialisées qui se combinent, plutôt qu'une plateforme monolithique.
Ce n'est pas un scénario hypothétique. C'est ce qu'A2A v1.0 permet déjà, avec 150+ organisations l'utilisant en production.
Le SDK Python officiel est disponible depuis mai 2026 :
pip install a2a-sdkLe point d'entrée le plus rapide : le codelab officiel Google "Purchasing Concierge". Il démontre l'interopérabilité entre CrewAI, LangGraph et ADK via A2A en moins d'une heure.
Pour une infrastructure 100% locale et gratuite comme la mienne :
/.well-known/agent.json et POST /Le premier résultat intéressant (deux agents qui divergent sur la même question) arrive en moins d'une journée.
A2A n'est pas un protocole de plus à mettre dans sa liste de veille. C'est une infrastructure pour des systèmes où l'intelligence émerge de la coordination, pas de la centralisation.
Mon expérience avec Social Sim montre que ça fonctionne : des agents avec des personnalités distinctes débattent genuinement, forment des coalitions non-programmées, et convergent vers des décisions qu'aucun LLM central n'aurait prises seul.
Ce qui reste à construire : la capacité à transformer ces décisions en livrables durables, et la détection automatique de convergence. C'est la Phase 2.
Si vous testez A2A sur un vrai cas métier, je suis curieux de savoir ce que ça donne.
Stack complète : Gemma 4 via oMLX (inference locale Apple Silicon), FastAPI, A2A implémenté manuellement en Phase 1, SDK Python officiel v1.1.0 pour la Phase 2, SQLite par agent pour la mémoire, Python 3.12.
Sources : Annonce Google A2A · Spécification officielle A2A · SDK Python A2A · Linux Foundation A2A Project · Codelab Purchasing Concierge · oMLX