MirakaiGED — Stack MAF et création d'agents : le code qui fait tourner les équipes

MirakaiGED — Stack MAF et création d'agents : le code qui fait tourner les équipes

Cet article s'adresse aux développeurs .NET et architectes logiciels qui construisent ou envisagent de construire des systèmes agentiques.

Les deux articles précédents de cette série ont couvert le quoi : ce que font les agents au quotidien, et les principes architecturaux des équipes autonomes.

Cet article couvre le comment. Du code. De vrais snippets du projet. Le choix de stack, les patterns d'implémentation, ce que ça donne à l'exécution dans le visualisateur.


La décision de stack — quatre quadrants, quatre outils

La tentation dans un système multi-agents est d'utiliser un seul framework pour tout. C'est une erreur. Les besoins sont fondamentalement différents selon le type de traitement.

┌─────────────────────────────────────────────────────┐
│  PIPELINES TECHNIQUES (non-IA)                       │
│  Upload → AV Scan → OCR → Embed → Index              │
│  → MassTransit Sagas (fiabilité, retry, dead-letter) │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  ÉQUIPES D'AGENTS PRÉDÉFINIES                        │
│  Due Diligence / AO / Audit / Revue Contractuelle    │
│  → MAF WorkflowBuilder C# (type-safe, checkpointing) │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  AGENTS CONVERSATIONNELS + BACKGROUND JOBS           │
│  Chat doc, Sentinelle, Archiviste, Morning Pulse     │
│  → MAF Agent (session persistante, pas de workflow)  │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  AGENTS DYNAMIQUES / EXPÉRIMENTAUX                   │
│  ReAct loops, research agents, graphes libres        │
│  → LangGraph Python (via Python FastAPI Docker)      │
└─────────────────────────────────────────────────────┘

MassTransit Sagas gèrent le pipeline documentaire technique — OCR, embedding, indexation. Ces étapes n'impliquent pas de LLM. Elles doivent être fiables, retryables, dead-letterable. MassTransit est conçu exactement pour ça.

MAF WorkflowBuilder (Microsoft Agent Framework) orchestre les équipes d'agents spécialisés. MAF est le successeur officiel de Semantic Kernel — il fusionne SK et AutoGen en une API unifiée, avec support natif du protocole A2A v1.

LangGraph Python couvre les cas où le graphe d'exécution est dynamique — un agent qui décide lui-même de son prochain nœud. Il n'existe pas de port C# officiel de LangGraph ; les agents Python tournent dans un container FastAPI dédié.

Trois stores, trois rôles distincts :

  • SQL Server 2025 : métadonnées transactionnelles, traces d'exécution, vecteurs natifs pour les requêtes hybrides simples
  • Weaviate : recherche hybride BM25 + vectorielle à l'échelle — le RAG du Chat Documentaire
  • Memgraph : traversée de graphe en Cypher pour les questions relationnelles complexes — "quels contrats impliquent cette entité juridique et ses filiales ?"

Stratégie LLM hybride :

Tâche Modèle Raison
Classification, extraction mistral:7b (Ollama) Rapide, volume élevé, faible coût
Synthèse, résumés phi-4 (Ollama) Meilleur en synthèse que Mistral
Analyse contractuelle complexe Claude Opus Raisonnement profond, fidélité
Fallback cloud GPT-4o Redondance
Mode souverain complet Ollama uniquement Clients secteurs réglementés

Construire un agent spécialisé

La base : interface et contrat

Chaque agent spécialisé implémente IStaticAgent. Le contrat est simple : une requête entre, une réponse sort. La magie est dans le prompt système.

// Domain/Agents/Interfaces/IStaticAgent.cs
public interface IStaticAgent
{
    string AgentId { get; }
    Task<AgentResponse> ProcessAsync(AgentRequest req, CancellationToken ct);
}

L'Archiviste — un agent complet

L'Archiviste est un bon exemple d'agent simple : une seule responsabilité, un modèle léger (Mistral 7B via Ollama), un output JSON structuré.

// Application/Agents/Static/ArchivistAgent.cs
public sealed class ArchivistAgent(IOllamaService ollama, IUserProfileService profiles)
    : IStaticAgent
{
    public string AgentId => "archivist-v1";

    public async Task<AgentResponse> ClassifyAsync(AgentRequest req, CancellationToken ct)
    {
        var prompt = $"""
            Classify this document. Return ONLY valid JSON:
            {{
              "category": "CONTRACT|INVOICE|REPORT|MEMO|OTHER",
              "subcategory": "...",
              "confidence": 0.0-1.0,
              "keywords": ["...", "..."],
              "sensitivity": "PUBLIC|INTERNAL|CONFIDENTIAL|SECRET"
            }}
            User context: {req.UserContext}
            Document (first 2000 chars): {req.Content[..Math.Min(2000, req.Content.Length)]}
            """;

        var response = await ollama.ChatAsync(new OllamaChatRequest(
            Model: "mistral:7b",
            Messages: [
                new("system", "Tu es exclusivement responsable de la classification documentaire. " +
                              "Tu ne résumes jamais, tu n'analyses jamais le fond. " +
                              "Tu renvoies uniquement du JSON valide."),
                new("user", prompt)
            ]
        ), ct);

        return ParseClassificationResponse(response.Content);
    }
}

La ligne clé du prompt système : "Tu es exclusivement responsable de X. Tu ne fais jamais Y." Cette contrainte explicite dans le prompt est aussi importante que la contrainte dans le code.

Le pattern ProfessionalAgent

Les agents métier (Juriste, Financier, Commercial…) partagent une base commune — seule la perspective change.

// Application/Agents/Static/Professional/ProfessionalAgentBase.cs
public abstract class ProfessionalAgentBase(IOllamaService ollama) : IStaticAgent
{
    protected abstract string Expertise      { get; }
    protected abstract string AnalysisFocus  { get; }

    public async Task<AgentResponse> AnalyzeAsync(AgentRequest req, CancellationToken ct)
    {
        var systemPrompt = $"""
            Tu es un expert en {Expertise}.
            Concentre ton analyse sur : {AnalysisFocus}
            Adapte ta réponse au profil suivant : {req.UserContext}
            Ne répète jamais ce qu'un autre expert aurait déjà couvert.
            """;
        // ...
    }
}

public sealed class JuristeAgent(IOllamaService ollama) : ProfessionalAgentBase(ollama)
{
    public override string AgentId      => "juriste-v1";
    protected override string Expertise => "droit des contrats et conformité réglementaire";
    protected override string AnalysisFocus =>
        "obligations contractuelles, clauses de résiliation, pénalités, conformité sectorielle";
}

public sealed class FinancierAgent(IOllamaService ollama) : ProfessionalAgentBase(ollama)
{
    public override string AgentId      => "financier-v1";
    protected override string Expertise => "analyse financière";
    protected override string AnalysisFocus =>
        "montants, échéances, risques de trésorerie, provisions, coûts cachés";
}

Le UserProfile middleware — injecter le contexte dans chaque agent

Le profil utilisateur est injecté automatiquement dans chaque invocation d'agent via un middleware MAF. Aucun agent n'a besoin de le récupérer lui-même.

// Infrastructure/AI/Middleware/UserProfileMiddleware.cs
public class UserProfileMiddleware(IUserProfileService profileService) : IAgentMiddleware
{
    public async Task InvokeAsync(AgentContext ctx, AgentMiddlewareDelegate next)
    {
        var profile = await profileService.GetAgentContextAsync(ctx.UserId);

        // Injecté comme suffix du system prompt de chaque agent
        ctx.SystemPromptSuffix = profile.AgentContextPrompt;
        // Exemple : "Utilisateur : Directeur Financier, Finance.
        //            Mission en cours : Audit Q3 2026.
        //            Niveau de détail souhaité : Executive.
        //            Style de réponse : concis, bullet points, chiffres en premier."

        await next(ctx);
    }
}

L'impact sur les réponses est mesurable :

Profil Comportement agent
Executive + Concis Max 5 lignes, décision en premier, chiffres clés
Expert + Détaillé Analyse en profondeur, jurisprudence, références
Manager + Visuel Tableaux, bullet points, pas de jargon
Opérationnel Actions étape par étape, délais, contacts

Assembler une équipe séquentielle — MAF WorkflowBuilder

Le WorkflowBuilder de MAF construit des graphes d'exécution typés en C#. Chaque AddEdge définit quel message passe d'un agent au suivant — avec le type du message comme contrainte à la compilation.

Due Diligence — graphe séquentiel

// Application/AgentTeams/Workflows/DueDiligenceWorkflow.cs
public sealed class DueDiligenceWorkflow
{
    public Workflow<DocumentAnalysisState> Build(
        MAFAgentFactory factory,
        IUserProfileContext userCtx,
        ICheckpointStore checkpointStore)
    {
        return new WorkflowBuilder<DocumentAnalysisState>()
            .AddExecutor("archiviste", new SpecializedAgentExecutor(
                factory.Create(AgentSpecialization.Archiviste, userCtx)))
            .AddExecutor("juriste",    new SpecializedAgentExecutor(
                factory.Create(AgentSpecialization.Juriste, userCtx)))
            .AddExecutor("financier",  new SpecializedAgentExecutor(
                factory.Create(AgentSpecialization.Financier, userCtx)))
            .AddExecutor("conformite", new SpecializedAgentExecutor(
                factory.Create(AgentSpecialization.Compliance, userCtx)))
            .AddExecutor("directeur",  new SpecializedAgentExecutor(
                factory.Create(AgentSpecialization.Directeur, userCtx)))

            // Graphe séquentiel : chaque résultat est le contexte du suivant
            .AddEdge<ArchiveResultMessage>  ("archiviste", "juriste")
            .AddEdge<LegalAnalysisMessage>  ("juriste",    "financier")
            .AddEdge<FinancialAnalysisMessage>("financier", "conformite")
            .AddEdge<ComplianceReportMessage>("conformite", "directeur")

            // Persistence : le run peut être repris après un crash
            .WithCheckpointing(checkpointStore)

            // Human-in-the-loop : le Directeur valide avant que le rapport soit émis
            .WithHumanInTheLoop<DirectorDecisionMessage>("directeur")

            .Build();
    }
}

📸 Capture à venir : Visualisateur — workflow Due Diligence en cours d'exécution, nœuds colorés par statut : vert (complété), bleu (en cours), gris (en attente)


Le visualisateur — voir les agents travailler

Le visualisateur est une interface dédiée aux administrateurs et équipes techniques. Il reçoit les traces d'exécution en temps réel via SignalR et affiche le graphe d'agents avec l'état de chaque nœud.

Chaque run génère une trace structurée complète :

{
  "runId": "run_7f3a2b91",
  "teamId": "due-diligence",
  "topology": "cascade",
  "documentId": "doc_4e8c1f",
  "startedAt": "2026-06-25T09:14:00Z",
  "completedAt": "2026-06-25T09:17:42Z",
  "budget": {
    "tokensUsed": 87543,
    "agentsInstantiated": 5,
    "costUsd": 0.12
  },
  "stages": [
    {
      "agent": "ArchivistAgent",
      "status": "completed",
      "durationMs": 1240,
      "model": "mistral:7b",
      "promptEffective": "...",
      "result": { "category": "CONTRACT", "confidence": 0.94 }
    },
    {
      "agent": "JuristeAgent",
      "status": "completed",
      "durationMs": 8350,
      "model": "claude-opus-4-5",
      "result": { "clauses_risk": 3, "summary": "..." }
    }
  ]
}

Cette trace est stockée en SQL Server pour audit, et diffusée en live via SignalR au visualisateur pendant l'exécution.

📸 Capture à venir : Visualisateur en mode live — graphe en cours d'exécution, nœuds animés, logs en temps réel sur le panneau latéral, métriques budget (tokens utilisés / max, durée estimée)

📸 Capture à venir : Vue post-run — graphe complété, timeline par agent, coût total, bouton "Rejouer" pour relancer avec les mêmes paramètres


Assembler une équipe en parallèle — fan-out / fan-in

Certaines analyses gagnent à être conduites en parallèle. MAF supporte le pattern fan-out / fan-in via AddParallelEdges et AddJoinEdge.

Appel d'offre — fan-out / fan-in

// Application/AgentTeams/Workflows/AOWorkflow.cs
new WorkflowBuilder<AOAnalysisState>()
    .AddExecutor("router",     new RouterExecutor())
    .AddExecutor("commercial", new SpecializedAgentExecutor(...Commercial...))
    .AddExecutor("juriste",    new SpecializedAgentExecutor(...Juriste...))
    .AddExecutor("financier",  new SpecializedAgentExecutor(...Financier...))
    .AddExecutor("directeur",  new SpecializedAgentExecutor(...Directeur...))

    // Fan-out : router déclenche Commercial ET Juriste simultanément
    .AddParallelEdges<AOStartMessage>("router", ["commercial", "juriste"])

    // Fan-in : Financier attend les outputs des deux — ils arrivent quand ils arrivent
    .AddJoinEdge<CommercialAnalysis, LegalAnalysis>("commercial", "juriste", "financier")

    // Suite séquentielle classique
    .AddEdge<FinancialAnalysis>("financier", "directeur")

    .Build();

Le AddJoinEdge est critique : MAF attend que les deux messages (CommercialAnalysis ET LegalAnalysis) soient disponibles avant de déclencher financier. Si Commercial termine en 8 secondes et Juriste en 23, Financier attend 23 secondes — mais les deux analyses arrivent complètes.

🎬 Vidéo à venir : Lancement d'une équipe Due Diligence sur un contrat de prestation — l'interface utilisateur, le déclenchement, la progression dans le visualisateur jusqu'au rapport final


La grammaire de topologie — définir les équipes en YAML

Les workflows MAF C# ci-dessus sont puissants, mais nécessitent une release pour chaque nouvelle équipe. Pour les cas courants, MirakaiGED supporte une grammaire de topologie déclarative en YAML — le chef d'équipe lit le descripteur au runtime et instancie le workflow correspondant.

Topology Cascade (séquentielle)

# config/teams/due-diligence.yaml
topology: cascade
name: Due Diligence
budget:
  maxAgents: 6
  maxTokens: 200000
  maxDurationMinutes: 10
stages:
  - agent: ArchivistAgent
    description: Classification + extraction structure
  - agent: JuristeAgent
    description: Analyse clauses juridiques
  - agent: FinancierAgent
    description: Analyse implications financières
  - agent: ComplianceAgent
    description: Vérification conformité réglementaire
  - agent: SynthesisAgent
    description: Rapport final consolidé

Topology Football (coordinateur + joueurs)

# config/teams/appel-offre.yaml
topology: football
name: Analyse Appel d'Offre
budget:
  maxAgents: 5
  maxTokens: 150000
coordinator: OfferCoordinatorAgent
players:
  - JuristeAgent       # Exigences contractuelles
  - FinancierAgent     # Analyse financière
  - InformaticienAgent # Exigences techniques
  - CommercialAgent    # Positionnement marché

Dans la topologie football, le coordinateur distribue les chunks du document aux joueurs, collecte leurs analyses partielles, les synthétise, et itère si nécessaire. Contrairement à la cascade où chaque agent voit le travail du précédent, chaque joueur travaille de manière indépendante sur sa perspective.

Topology Boucle d'amélioration

# config/teams/analyse-strategique.yaml
topology: improvement-loop
name: Analyse Stratégique
maxIterations: 3
convergenceThreshold: 0.85
writer: StrategistAgent
critic: DevilsAdvocateAgent

Le rédacteur produit une analyse. Le critique l'attaque. Le rédacteur révise. Jusqu'à convergence ou maxIterations. Utile pour les analyses de risque où on veut challenger les conclusions.

📸 Capture à venir : Éditeur de topologie dans l'interface d'administration — liste des équipes disponibles avec leur type de topologie, budget configuré, et statut actif/inactif


Le régime dynamique — agents éphémères instanciés au runtime

Pour les documents volumineux (150 pages, 10 Mo), l'Extracteur d'Entités n'appelle pas un seul agent Juriste. Il en instancie plusieurs — un par chunk — qui travaillent en parallèle et ne persistent jamais.

Chef d'équipe reçoit document (150 pages)
    │
    ├─ Découpe en chunks (2000 tokens, overlap 200)
    │
    ├─ Task.WhenAll() — 5 DynamicJuristeAgent instanciés
    │   ├─ DynamicJuristeAgent(chunk=pages 1-30)
    │   ├─ DynamicJuristeAgent(chunk=pages 31-60)
    │   ├─ DynamicJuristeAgent(chunk=pages 61-90)
    │   ├─ DynamicJuristeAgent(chunk=pages 91-120)
    │   └─ DynamicJuristeAgent(chunk=pages 121-150)
    │
    ├─ await Task.WhenAll(agents)  ← résultats partiels collectés
    │
    ├─ SynthesisAgent consolide les 5 résultats partiels
    └─ MORT des 5 agents dynamiques ← zéro persistance directe

La règle absolue : un agent dynamique ne persiste jamais directement. Il remonte ses résultats au chef d'équipe. Si un agent dynamique échoue, le chef gère l'exception — rien n'a été écrit dans la base de connaissance, l'état reste cohérent.

// Implémentation du chef d'équipe
public async Task<SynthesisResult> ProcessLargeDocumentAsync(
    Document document, CancellationToken ct)
{
    var chunks = _chunker.Split(document.Content, chunkSize: 2000, overlap: 200);

    // Instanciation et exécution parallèle des agents dynamiques
    var partialResults = await Task.WhenAll(
        chunks.Select(chunk => ProcessChunkAsync(chunk, ct))
    );

    // Synthèse — seul ce résultat sera persisté via un command MediatR
    return await _synthesisAgent.ConsolidateAsync(partialResults, ct);
}

private async Task<ChunkAnalysisResult> ProcessChunkAsync(Chunk chunk, CancellationToken ct)
{
    // Agent éphémère : pas de DI container, pas d'état persistant
    var agent = new DynamicJuristeAgent(_ollamaService);
    return await agent.AnalyzeAsync(chunk, ct);
    // L'agent est garbage-collected après cette ligne
}

🎬 Vidéo à venir : Lancement d'une équipe dynamique sur un appel d'offre volumineux — les agents dynamiques apparaissent en temps réel dans le visualisateur, s'exécutent en parallèle, et disparaissent après synthèse


Le pont LangGraph Python

Pour les agents qui nécessitent des boucles de raisonnement auto-modifiables (ReAct pattern), MAF WorkflowBuilder n'est pas le bon outil — il nécessite un graphe connu à la compilation.

LangGraph Python tourne dans un container FastAPI dédié. Le .NET l'appelle via HTTP (ou via le protocole A2A v1 pour l'interopérabilité complète).

# docker/python-runtime/agents/langgraph_agents.py
from langgraph.graph import StateGraph, END
from langchain_ollama import ChatOllama

def build_research_agent():
    graph = StateGraph(AgentState)
    graph.add_node("think", think_node)
    graph.add_node("act",   act_node)
    
    # L'agent décide lui-même de continuer ou de terminer
    graph.add_conditional_edges(
        "think",
        lambda s: "act" if s.needs_action else END
    )
    graph.add_edge("act", "think")  # boucle think → act → think → ...
    return graph.compile()
// Infrastructure/AI/LangGraphBridgeService.cs
public sealed class LangGraphBridgeService(HttpClient http) : ILangGraphBridgeService
{
    public async Task<AgentResponse> RunAsync(
        string agentId, AgentRequest req, CancellationToken ct)
    {
        var payload = new {
            agent_id  = agentId,
            input     = req.Content,
            config    = new {
                user_context = req.UserContext,
                tenant_id    = req.TenantId
            }
        };
        var response = await http.PostAsJsonAsync(
            "http://python-runtime:8500/langgraph/run", payload, ct);
        return await response.Content
            .ReadFromJsonAsync<AgentResponse>(ct);
    }
}

Quand utiliser LangGraph Python plutôt que MAF WorkflowBuilder :

Critère MAF WorkflowBuilder LangGraph Python
Graphe connu à l'avance
Graphe auto-modifiable
Boucles ReAct Possible mais verbeux Natif
Type-safety C# ❌ (Python)
Checkpointing intégré ✅ (LangGraph Checkpointer)
Expérimentation rapide Non

Dans MirakaiGED, LangGraph couvre environ 10% des cas — les agents de recherche exploratoire qui ne savent pas à l'avance combien d'étapes ils vont effectuer. Les équipes prédéfinies (90%) restent en MAF WorkflowBuilder.


Ce que ce projet valide — et ce qu'il reste ouvert

Validé en construction :

MAF WorkflowBuilder est mature pour les workflows prédéfinis. Le pattern AddParallelEdges + AddJoinEdge fonctionne de manière fiable pour le fan-out / fan-in. Le checkpointing permet de reprendre un workflow long après un crash sans ré-exécuter les étapes complétées.

Encore ouvert :

La dérive sémantique sur les agents persistants-évolutifs — introduite dans l'article architecture — n'est pas résolue proprement au niveau des métriques de détection. Le versionning et le rollback sont implémentés, mais évaluer quand un agent a dérivé reste heuristique.

La gestion des erreurs silencieuses dans les sous-agents dynamiques (section Régime dynamique ci-dessus) est plus difficile qu'elle n'y paraît. Une exception attrapée localement et transformée en résultat partiel vide remonte sans déclencher d'alerte — le rapport final semble complet mais manque d'une dimension. Les tests sur les chemins d'erreur sont devenus une priorité non négociable.


Ce projet est en développement actif. Si vous construisez un système agentique similaire et que vous voulez comparer notes sur les patterns MAF, les topologies de coordination ou la gestion des états distribués, je suis disponible.

Série MirakaiGED : Fonctionnalités métierArchitecture agentique — Stack technique (cet article)

Un projet d'agents IA en tête ?