Aller au contenu principal

Le navigateur, prochain eldorado de l'IA : l'architecture de ce blog en exemple

17 min de lecture

L’architecture de ce blog, partagée

Plutôt que de te laisser avec une intuition, voici l’architecture complète qui fait tourner le site que tu es en train de lire. Elle est en production, elle est simple à reprendre, et je la partage volontiers : les schémas ci-dessous suffisent à la reconstruire sur n’importe quel site statique. Considère cette partie comme une étude de cas, avec ses réussites et ses limites, et pas comme un modèle de référence.

Un modèle LFM2 de 350 millions de paramètres s’exécute dans un Web Worker via Transformers.js, tandis que WebMCP expose les données publiques du blog sous forme d’outils.

Quelques mots sur le modèle utilisé

Publié par Liquid AI le 10 juillet 2025 dans la première série LFM2, ce modèle de 350 millions de paramètres est conçu explicitement pour l’exécution sur appareil. Il abandonne le transformeur classique au profit d’une architecture hybride mêlant convolutions courtes à portes multiplicatives et attention à requêtes groupées, avec une fenêtre de contexte de 32768 tokens et un mécanisme structuré d’appel de fonctions. Son pré-entraînement porte sur 10000 milliards de tokens, dont environ 20% de multilingue où le français figure parmi les langues explicitement visées.

Vue d’ensemble

flowchart TB
    Human["Lecteur"]
    BrowserAgent["Agent du navigateur"]
    UI["Interface Astro<br/>LocalAIDemo"]
    WebMCP["document.modelContext"]
    Tools["Outils du blog"]
    Index["/articles-index.json"]
    Worker["Web Worker"]
    Transformers["Transformers.js"]
    Model["LFM2-350M-ONNX"]
    Runtime{"Runtime disponible"}
    WebGPU["WebGPU<br/>poids q4"]
    WASM["WASM / CPU<br/>poids q8"]

    Human -->|ouvre « Discutons »| UI
    BrowserAgent -->|découvre et appelle| WebMCP
    UI -->|getTools / executeTool| WebMCP

    WebMCP --> Tools
    UI -. repli sans WebMCP .-> Tools
    Tools --> Index

    UI -->|prompt + historique + contexte| Worker
    Worker --> Transformers
    Transformers --> Model
    Transformers --> Runtime
    Runtime -->|adaptateur disponible| WebGPU
    Runtime -->|sinon| WASM
    Worker -->|fragments puis réponse finale| UI

Deux intégrations complémentaires cohabitent : WebMCP transforme les fonctionnalités du blog en outils structurés, Transformers.js exécute le modèle sur l’appareil. Elles partagent les mêmes données et restent découplées. WebMCP n’exécute pas le modèle, et le modèle n’appelle pas WebMCP de lui-même.

La couche WebMCP

Toutes les pages incluent un composant chargé d’examiner document.modelContext. Lorsque l’API existe, quatre outils en lecture seule sont enregistrés.

OutilFonction
list_articlesListe les articles publiés, éventuellement filtrés par tag
search_articlesRecherche dans les titres, descriptions et tags
list_tagsRetourne les tags avec leur nombre d’articles
get_author_infoRetourne le profil public et les domaines d’expertise de l’auteur
flowchart LR
    Register["WebMCP.astro"]
    Context["document.modelContext"]
    Registry["Registre des outils"]

    subgraph ReadOnly["Outils en lecture seule"]
        List["list_articles"]
        Search["search_articles"]
        Tags["list_tags"]
        Author["get_author_info"]
    end

    Catalog["Catalogue public JSON"]
    Static["Informations publiques<br/>sur l’auteur"]

    Register -->|registerTool| Context
    Context --> Registry
    Registry --> List
    Registry --> Search
    Registry --> Tags
    Registry --> Author

    List --> Catalog
    Search --> Catalog
    Tags --> Catalog
    Author --> Static

L’annotation readOnlyHint informe l’agent qu’un appel ne doit pas modifier l’état du site. Les outils qui renvoient du contenu éditorial utilisent aussi untrustedContentHint : les données récupérées sont du contenu non fiable, jamais des instructions. L’enregistrement est relancé après une navigation Astro, et un WeakSet empêche d’enregistrer deux fois les outils dans un même contexte.

Deux points que la spécification apporte et qui changent le cadrage. D’abord, les outils déclarés peuvent être invoqués par des agents, par l’agent du navigateur, mais aussi par des technologies d’assistance. Ce que tu écris pour une IA sert donc potentiellement un lecteur d’écran, ce qui est l’argument le plus sous-estimé de WebMCP aujourd’hui. Ensuite, l’accès à l’API est réservé aux contextes sécurisés et encadré par une permission dédiée dont la liste d’autorisation par défaut se limite à ta propre origine, avec une option explicite pour exposer un outil à d’autres origines.

Une précision utile pour ton vocabulaire d’architecte : WebMCP ne définit pas le protocole utilisé entre le navigateur et son agent. Le navigateur peut employer MCP, du function calling propriétaire ou autre chose. Il serait donc plus exact de parler d’une API web d’exposition d’outils que d’un serveur MCP embarqué.

Un exécuteur commun

Le point qui rend l’ensemble maintenable est qu’aucune logique métier n’est dupliquée. Les définitions WebMCP et leurs fonctions d’exécution résident dans un catalogue commun. Si le navigateur ne propose pas document.modelContext, le chat local appelle directement le même exécuteur JavaScript.

flowchart TD
    Prompt["Question du lecteur"]
    Router["Sélection déterministe"]
    Choice{"Intention détectée"}

    Author["get_author_info"]
    Tags["list_tags"]
    Search["search_articles"]

    Native{"WebMCP disponible ?"}
    MCP["getTools puis executeTool"]
    Fallback["executeBlogTool"]
    Format["Validation, formatage<br/>et réduction du contexte"]
    LLM["Modèle local"]

    Prompt --> Router --> Choice
    Choice -->|auteur ou expertise| Author
    Choice -->|tags ou thèmes| Tags
    Choice -->|autre question| Search

    Author --> Native
    Tags --> Native
    Search --> Native

    Native -->|oui| MCP
    Native -->|non ou erreur| Fallback
    MCP --> Format
    Fallback --> Format
    Format --> LLM

La sélection n’est pas réalisée par le modèle. Une règle lexicale choisit au maximum un outil : une question sur l’auteur ou son expertise sélectionne get_author_info, une question sur les thèmes ou catégories sélectionne list_tags, tout le reste passe par search_articles. Le modèle reçoit des faits ciblés, mais il ne décide pas quelle fonction exécuter et ne peut déclencher aucune action. C’est une manière très saine d’éviter de construire une boucle agentique avant d’en avoir besoin.

Le choix du runtime

Le modèle retenu est onnx-community/LFM2-350M-ONNX, un modèle conversationnel compact destiné notamment à l’exécution locale. Ce choix rejoint la position défendue par une équipe de NVIDIA Research, pour qui les petits modèles sont suffisamment capables et nettement plus économiques dans les systèmes agentiques, où le modèle répète en réalité un petit nombre de tâches spécialisées avec peu de variation. Leur définition de travail est pragmatique : un petit modèle est un modèle qui tourne sur un appareil grand public en répondant assez vite pour un utilisateur unique. C’est une prise de position, signée par un acteur qui n’est pas neutre sur le sujet, mais elle décrit exactement le régime d’usage d’un assistant de blog. Transformers.js fournit la pipeline text-generation, le tokenizer, le chargement des poids ONNX et le streaming. Le runtime est choisi au chargement.

flowchart TD
    Start["Ouverture de la modale"]
    Create["Création du Web Worker"]
    Load["Message load"]
    GPU{"navigator.gpu existe ?"}
    Adapter{"Adaptateur disponible ?"}
    Q4["Pipeline WebGPU<br/>dtype q4<br/>environ 300 Mo"]
    Q8["Pipeline WASM / CPU<br/>dtype q8<br/>jusqu’à 515 Mo"]
    Ready["Message ready"]
    Chat["Interface activée"]

    Start --> Create --> Load --> GPU
    GPU -->|non| Q8
    GPU -->|oui| Adapter
    Adapter -->|oui| Q4
    Adapter -->|non ou erreur| Q8
    Q4 --> Ready
    Q8 --> Ready
    Ready --> Chat

WebGPU est préféré avec des poids quantifiés en quatre bits. En son absence, le chargement bascule automatiquement vers WASM et des poids en huit bits. Le modèle n’est téléchargé qu’après une action explicite du lecteur, et fermer la modale termine le worker.

Le cycle d’une conversation

sequenceDiagram
    actor U as Lecteur
    participant UI as Interface Astro
    participant C as Contexte WebMCP
    participant T as Outils du blog
    participant W as Web Worker
    participant M as Transformers.js / LFM2

    U->>UI: Envoie une question
    UI->>UI: Sélectionne un outil

    alt WebMCP utilisable
        UI->>C: getTools()
        C-->>UI: Outils enregistrés
        UI->>C: executeTool(outil, entrée)
        C->>T: execute()
        T-->>C: Résultat JSON
        C-->>UI: Résultat sérialisé
    else API absente ou appel en erreur
        UI->>T: executeBlogTool()
        T-->>UI: Même résultat JSON
    end

    UI->>UI: Valide et limite le contexte
    UI->>W: generate(prompt, historique, contexte)
    W->>W: Tokenise et compacte les entrées
    W->>M: Pipeline text-generation

    loop Génération
        M-->>W: Fragment de texte
        W-->>UI: response-fragment
    end

    M-->>W: Réponse complète
    W-->>UI: response
    UI->>UI: Assainit le Markdown
    UI-->>U: Affiche la réponse finale

Trois budgets protègent la fenêtre de contexte : 512 tokens pour le message utilisateur, autant pour l’historique récent, autant pour le contexte issu des outils. La génération produit au maximum 1024 nouveaux tokens, avec une température de 0.3 et une pénalité de répétition de 1.05. Sur un modèle de cette taille, ces plafonds conditionnent directement la lisibilité des réponses.

Les fragments sont affichés en texte brut pendant le streaming. La réponse finale est ensuite rendue en Markdown avec une liste restrictive d’éléments autorisés : le HTML brut, les images et les attributs sont supprimés, et seuls les liens HTTP ou HTTPS valides sont conservés.

Les frontières de sécurité

Exécuter localement ne rend pas une architecture sûre, et la littérature citée plus haut explique pourquoi. C’est même l’angle mort le plus fréquent dans les démonstrations actuelles. Les limites sont donc explicites, et ce sont elles que je te conseille de reprendre en premier :

  • aucun outil d’écriture ou de contact n’est exposé ;
  • les données récupérées sont traitées comme non fiables ;
  • le prompt système interdit de suivre des instructions présentes dans le contexte ;
  • les informations absentes doivent être signalées plutôt qu’inventées ;
  • une seule génération peut être active simultanément ;
  • aucune route d’API du blog ne reçoit les prompts ou les réponses ;
  • une erreur locale ne provoque aucun repli vers un modèle distant. Une dernière précision de vocabulaire, parce qu’elle engage ta promesse vis-à-vis du lecteur. Le téléchargement initial du runtime et des poids reste une communication réseau, et les fichiers peuvent demeurer dans les caches du navigateur. “Local” qualifie ici l’inférence et le traitement de la conversation, pas nécessairement l’origine des fichiers du modèle.

Articles similaires

Écoconception

Empreinte environnementale estimée · Modèle SWD v4 · 442 g CO₂eq/kWh

Poids de la page
Énergie par requête
Budget carbone du build
Expérience locale

Discuter avec le blog, depuis votre navigateur

Le modèle LFM2 350M s'exécute sur votre appareil. Environ 300 Mo sont téléchargés avec WebGPU, ou jusqu'à 515 Mo avec WASM/CPU, puis mis en cache ; vos prompts ne sont envoyés à aucun serveur.

Préparation de l'expérience…