Standards pour une IA distribuée et auditable par design

Ce billet est une synthèse du preprint "Toward a Transparent, Auditable, and Distributed Architecture for LLM Tasks Using W3C Linked Data Notifications and Remote uv Scripts" accessible sur Zenodo https://doi.org/10.5281/zenodo.17367758

Le but de ce papier est de proposer une approche exploratoire afin de formaliser traçabilité et archivage des chaînes d’inférence et des workflows pilotés par LLM, en documentant chaque étape d’exécution d’une tâche d’IA par des notifications sémantiques enrichies de métadonnées de provenance. Ce cadre vise à assurer transparence, auditabilité et reproductibilité, étant entendu qu'ici la reproductibilité des résultats ne signifie pas la reproduction exacte des sorties d’un modèle (les LLMs étant intrinsèquement non déterministes) mais la capacité à rejouer intégralement les chaînes d’inférence et leurs conditions d’exécution (modèles, paramètres, ...).


Pour ce qui concerne l'explicabilité des calculs au sein des réseaux de neurones à proprement parler, on peut se reporter aux méthodes dites XAI pour eXplainable AI qui se concentrent sur l'interprétabilité des résultats, par exemple en analysant le fonctionnement interne des architectures transformer ou en développant des outils de visualisation des poids et des attentions.


Première brique : scripts Python distants et package manager uv

uv est un gestionnaire de packages Python cross plateforme (écrit en Rust) qui se positionne en alternative à pip ou poetry pour la gestion des dépendances et des environnements virtuels. Et en effet il suffit d’utiliser une fois uv init et uv pip install ou encore de configurer les dépendances de son projet dans un fichier pyproject.toml pour constater deux choses : d’une part la grande rapidité d’installation des packages, et d’autre part une bien meilleure gestion des conflits (ce point pouvant tourner au cauchemar avec pip). uv est donc d’abord un packager manager qui gère les dépendances, installe les paquets, résout les conflits et crée des environnements virtuels isolés et spécifiques à chaque projet, tout ceci avec rapidité et fiabilité.

En prime de ces fonctions de packaging, uv peut se transformer en package runner grâce à la commande uv run qui permet aux utilisateurs d’exécuter localement des scripts Python (uv run myscript.py) , y compris des scripts hébergés à distance (uv run https://<server_name>/myscript.py). Enfin, last but not least, utilisé conjointement avec le schéma PEP-723 qui normalise l’ajout des dépendances dans l’en-tête des scripts python (le script indique en tête de fichier les informations et dépendances nécessaires à sa propre exécution), on dispose donc d’un outil qui permet de lancer des fichiers auto-descriptifs sans aucune installation manuelle ni configuration d’environnement en suivant les étapes suivantes :

  • lecture des dépendances et de la version de Python requise dans les métadonnées embarquées en PEP ;
  • création automatique d'un environnement d’exécution (venv) isolé ;
  • installation des dépendances nécessaires uniquement pour ce script ;
  • exécution locale du code :
  • puis nettoyage de l’environnement virtuel, tout ceci sans jamais modifier la machine hôte.

Prenons l'exemple simple d’un script générique avec métadonnées PEP hébergé sur Github qui retourne le résultat Json d’une requête sur un web service Idref :

#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "requests"
# ]
# ///

import requests
import argparse
import json
import sys

idref_ws_url = "https://www.idref.fr/services/"
json_suffix = "&format=text/json"

def get_abes_data(web_service: str, id: str):
    url = f"{idref_ws_url}{web_service}/{id}{json_suffix}"
    response = requests.get(url)
    return response.json()

def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Idref webservice basic."
    )
    parser.add_argument("--web-service", required=True, help="biblio |references | merged| merged_inv | idref2id | id2idref | iln2rcr | rcr2ilnata | idref2rnsr | iln2td3")
    parser.add_argument("--id", required=True, help="single id or list of id separated by comma")
    parser.add_argument("--json-output", required=False, action="store_true", help="Print raw JSON.")
    return parser.parse_args(argv)

if __name__ == "__main__":
    args = parse_args(sys.argv[1:])
    if args.json_output:
        print(json.dumps({"text": get_abes_data(args.web_service, args.id)}, ensure_ascii=False, indent=2))
    else:
        print(get_abes_data(args.web_service, args.id))

En local, il suffit de lancer une commande

uv run https://raw.githubusercontent.com/gegedenice/uv-scripts/main/idref-webservice-basic.py --web-service idref2id --id 240229061 --json-output

Pour obtenir :

{
  "text": {
    "sudoc": [
      {
        "query": {
          "ppn": "240229061",
          "result": {
            "source": "VIAF",
            "identifiant": "http://viaf.org/viaf/96157340820609922651"
          }
        }
      },
      {
        "query": {
          "ppn": "240229061",
          "result": {
            "source": "HAL",
            "identifiant": "geraldine-geoffroy"
          }
        }
      }
    ]
  }
}

Stocker les scripts python sur un entrepôt Git est intéressant car cela permet de gérer le versionning des fichiers et de développer une infra de confiance en pinnant les urls des scripts avec le SHA du commit comme fingerprint (à la place de la branche "main" ou autre dans l'url) afin d’identifier et d’utiliser en confiance la version souhaitée du script indépendamment des éventuelles mises à jour dans les branches du dépôt.


En étendant ce principe de scripts distants (donc partageables), modularisables et auto-exécutables localement sans maintenance ni installation préalable ni état persistant, on comprend rapidement comment il peut s’appliquer :

  • 1) à l’inférence sur des LLMs via des scripts “zéro-install” mutualisés qui, plutôt que de voir chaque équipe recoder les mêmes fonctions utilitaires dans leur projet, permettent de partager des scripts versionnés, signés et directement invoquables, avec garanties de reproductibilité et d’isolation. Ce fichier python peut par exemple être exécuté par tous avec une commande du type :
GH_RAW="https://raw.githubusercontent.com/gegedenice/uv-scripts/main/llms-openai-inference.py"
groq_api_key = "..."
uv run "{GH_RAW}" \
  --provider groq \
  --model moonshotai/kimi-k2-instruct-0905 \
  --api-key "{groq_api_key}" \
  -s "You are an AI expert." \
  -u "Give 3 bullet points about RAG pitfalls."
  --temperature 0.0 \
  --max-tokens 16384

Voir le README du repo pour la documentation

  • 2) Voire à tout système d’IA (pipeline RAG, agents, …) en modélisant une architecture distribuée de scripts en tant que briques fonctionnelles indépendantes, chacune responsable d’une tâche ou d’un process donné : inférence, vectorisation, calcul de similarités, classifier, etc...

De ce point de vue, la logique classique de déploiement s'en trouve transformée, puisqu'au lieu d’un serveur monolithique hébergeant tous les modules, on peut imaginer un réseau de compétences codées (des sortes de "script-skills") mutualisées, reproductibles et activables à la demande, une telle architecture devenant plus facilement auditable grâce au standard LDN.

Deuxième brique : le standard W3C des Linked Data Notifications (LDN)

Le Linked Data Notifications (LDN) est un standard du W3C (dont la première spécification date de 2017) conçu pour l’échange de messages sous forme de notifications exprimées en Json-LD. Cette infrastructure générique et extensible d’échange d’informations sur le Web s’appuie sur trois composantes :

  • L'Inbox (receiver) : le point d’entrée HTTP, c’est-à-dire l’endpoint qui reçoit les notifications (via des requêtes POST).
  • Le Sender : l’émetteur qui envoie les notifications en POST à l’Inbox
  • Le Consumer : le ou les agents qui lisent les notifications (via des requêtes GET) et y réagissent éventuellement.

Les flux LDN fondés sur ce protocole présentent plusieurs propriétés fondamentales :

  • les échanges de notifications circulent via le protocole HTTP avec de simples requêtes GET et POST, sans dépendance à un middleware particulier
  • la compatibilité native avec le Web sémantique est assurée par l’encodage des notifications en Json-LD et repose sur le vocabulaire standard ActivityStreams ("https://www.w3.org/ns/activitystream”)
  • le protocole est générique donc agnostique quant à la nature du message. Pour le dire simplement avec une formule qui rappellera des souvenirs aux plus vieux, avec le LDN on peut notifier n’importe quoi (la mise à disposition d’un dataset, une requête d’inférence IA, un flag de conformité, la génération d’un résumé… ) à n’importe qui
  • enfin Le LDN peut également transporter des informations de provenance en intégrant le schéma PROV-O (http://www.w3.org/ns/prov#) dans son contexte JSON-LD, ce qui permet ainsi d’enrichir les notifications avec des informations structurées d'origine, de contexte, d'agent responsable de l’exécution…. Par exemple :
"prov:wasGeneratedBy": {
  "type": "Activity",
  "prov:wasAssociatedWith": "splitter.py@<SHA>"
}

A noter que le standard LDN n’est pas que théorique puisqu'il sert déjà de fondement à des initiatives concrètes comme par exemple le protocole COAR Notify, utilisé pour orchestrer les échanges de notifications entre dépôts institutionnels, éditeurs et services de peer review.


Pour illustrer un exemple de flux typique de messages sémantiques pour orchestrer une tâche d'inférence, on peut séquencer les étapes suivantes :

  • Un client (agent, application, repository…) qui souhaite déclencher une inférence envoie en POST une notification Offer ou Request vers une inbox de service qui stocke le message
{ 
  "@context": "https://www.w3.org/ns/activitystreams",
  "id": "urn:uuid:bde3e011-8769-4ff5-926e-562476a96c9e",
  "type": "Create",
  "actor": "https://smartbiblia.fr/agents/cli-user-inference",
  "object": {
    "provider": "groq",
    "model": "moonshotai/kimi-k2-instruct-0905",
    "user_prompt": "Explain reranking in RAG.",
    "system_prompt": "You are an expert in IA systems.",
    "temperature": 0.1,
    "max_tokens": 500
    },
  "instrument": {
    "type": "Service",
    "action": "infer"
  }
}
  • Un Consumer (script déporté) qui écoute l’Inbox de service en continu intercepte cette notification, exécute la tâche d’inférence selon les paramètres de l’objet notifié :
uv run https://raw.githubusercontent.com/<user>/LLM-notify/<SHA>/inference-notify-demo/inference.py \
--provider groq \
--model moonshotai/kimi-k2-instruct-0905 \
--user-prompt "Explain reranking in RAG." \
--system-prompt "You are an expert in IA systems." \
--temperature 0.1 \
--max-tokens 500
  • le consumer publie une notification de sortie Announce ou Create sur l'Inbox décrivant le nouvel artefact produit assorti de liens vers les sources.
{
  "@context": ["https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/prov#"],
  "type": "Announce",
  "actor": "https://smartbiblia.fr/actors/inference-runner",
  "object": {
    "type": "Document",
    "id": "urn:smartbiblia:state:inference-result-abc123.txt",
    "name": "inference-result-abc123.txt",
    "url": "http://localhost:8080/state/inference-result-abc123.txt"
    },
  "prov:wasGeneratedBy": "urn:uuid:original-create-notification-id"
}
  • Le client récupère ainsi la réponse depuis l'Inbox
Orchestrator started. Polling inbox at http://localhost:8080/inbox...
Received new inference job: 576c84413a0e1346a6a664008edaccd704bef558
Running command: uv run https://raw.githubusercontent.com/<user>/LLM-notify/<SHA>/inference-notify-demoReranking in RAG is a second-stage reordering of the documents that the first-stage retriever returned.
Its goal is simple: move the chunks that are actually useful for answering the question to the top of thWhy we need it
The initial retriever (dense, sparse, or hybrid) is optimized for recall, not precision.
The generator is sensitive to order: the first 1-3 passages dominate what it writes.
The context window is small and expensive; we want to fill it with the best material.
How it works
Retrieve: Top-k (e.g., 30-100) passages are fetched quickly.
Rerank: A slower but smarter model scores each passage on the query-passage pair.
Truncate: Keep the top-n (e.g., 3-5) passages and feed them to the generator.
Typical rerankers
Cross-encoder BERT: Concatenate query and passage, output relevance score
...

A noter que l’exemple ci-dessus se situe dans le cadre d’une architecture ouverte et totalement transparente basée sur un LDN en mode Inbox fédérée qui sert de canal partagé entre les acteurs du flux (tous les participants peuvent à la fois publier et consommer des notifications sur et depuis une Inbox commune), mais que le LDN peut aussi s’opérer en mode bilatéral asymétrique où chaque participant dispose de son Inbox et reçoit les notifications de sortie du provider via un replyTo, les deux circuits pouvant également cohabiter.


Combinazione

En combinant d'une part des scripts distants auto-descriptifs indépendants et d'autre part une interconnexion par un protocole de notifications sémantiques, on forme les bases d’une infrastructure distribuée par notifications pour les LLM, traçable et reproductible by design, où chaque notification est archivée sous forme de reçu LDN avec provenance dans une ou plusieurs Inbox qui deviennent des registres de preuves. Grâce à ce système de preuves notifiées et archivées dans un format standardisé, chaque requête peut être inspectée donc auditée, et rejouée donc reproductible.

Si l'on imagine par exemple un système (basique) de RAG décentralisé et piloté par notifications, on pourrait avoir 1. Une notification Create pour l'ajout de document

{
  "@context": ["https://www.w3.org/ns/activitystreams"],
  "type": "Create",
  "actor": "https://smartbiblia.fr/actors/publisher",
  "object": {
    "type": "Document",
    "id": "https://example.org/docs/sample.txt",
    "mediaType": "text/plain",
    "name": "Sample Document",
    "url": "https://example.org/docs/sample.txt"
    },
  "target": "https://pipeline.smartbiblia.fr/inbox",
  "instrument": {
  "type": "Service",
  "name": "RAG Indexing Pipeline",
  "action": "index"
  }
}
  1. Un consumer splitter.py qui capte la notification, exécute le traitement, et poste une nouvelle notification
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://www.w3.org/ns/prov#",
    "https://schema.org"
  ],
  "type": "Announce",
  "actor": "https://smartbiblia.fr/agents/splitter",
  "object": {
    "type": "schema:Dataset",
    "id": "urn:smartbiblia:state:chunks:sample",
    "name": "Text Chunks from Sample.txt",
    "url": "file://./state/chunks.jsonl",
    "mediaType": "application/jsonl"
    },
  "prov:wasGeneratedBy": {
    "type": "prov:Activity",
    "prov:used": "https://example.org/docs/sample.txt",
    "prov:wasAssociatedWith": "splitter.py@2e9fc12",
    "prov:startedAtTime": "2025-10-01T09:00:12Z",
    "prov:endedAtTime": "2025-10-01T09:00:13Z",
    "prov:parameters": {
    "chunk_size": 900
    },
  "prov:generated": {
    "outputHash": "sha256:3adf...1a9",
    "format": "jsonl",
    "executionEnv": "uv@0.1.30"
    }
  }
}
  1. Un consumer embeddings.py qui intercepte la notification, genère les embeddings des chunks et poste le résultat
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://www.w3.org/ns/prov#",
    "https://schema.org"
  ],
  "type": "Announce",
  "actor": "https://smartbiblia.fr/agents/embedder",
  "object": {
    "type": "schema:Dataset",
    "id": "urn:smartbiblia:state:embeddings:sample",
    "name": "Embeddings for Sample.txt",
    "url": "file://./state/embeddings.jsonl",
    "mediaType": "application/jsonl"
    },
  "prov:wasGeneratedBy": {
    "type": "prov:Activity",
    "prov:used": "file://./state/chunks.jsonl",
    "prov:wasAssociatedWith": "embedder.py@1baf37d",
    "prov:startedAtTime": "2025-10-01T09:00:14Z",
    "prov:endedAtTime": "2025-10-01T09:00:17Z",
    "prov:parameters": {
      "model": "sentence-transformers/all-MiniLM-L6-v2",
      "normalize_embeddings": true
    },
  "prov:usedSoftware": {
    "type": "schema:SoftwareApplication",
    "name": "sentence-transformers",
    "version": "3.0.0"
    },
  "prov:generated": {
    "outputHash": "sha256:fb79...ce4",
    "format": "jsonl",
    "executionEnv": "uv@0.1.30"
    }
  }
}
  1. Un consumer indexer.py qui prend le relai pour créer/mettre à jour l'index des embeddings
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://www.w3.org/ns/prov#",
    "https://schema.org"
    ],
  "type": "Announce",
  "actor": "https://smartbiblia.fr/agents/indexer",
  "object": {
    "type": "schema:Dataset",
    "id": "urn:smartbiblia:state:index:sample",
    "name": "Vector Index for Sample.txt",
    "url": "file://./state/index.jsonl",
    "mediaType": "application/json"
    },
  "prov:wasGeneratedBy": {
    "type": "prov:Activity",
    "prov:used": "file://./state/embeddings.jsonl",
    "prov:wasAssociatedWith": "indexer.py@e93ba1f",
    "prov:startedAtTime": "2025-10-01T09:00:18Z",
    "prov:endedAtTime": "2025-10-01T09:00:19Z",
    "prov:parameters": {
      "index_format": "jsonl",
      "dimension": 384
        },
    "prov:generated": {
      "outputHash": "sha256:9c2f...d41",
      "format": "json",
      "executionEnv": "uv@0.1.30"
      }
  }
}
  1. Un sender peut alors poster une requête sur l'Inbox, donnant lieu à cette notification qui archive les chunks utilisés pour alimenter le prompt au LLM
{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://www.w3.org/ns/prov#",
    "https://schema.org"
    ],
  "type": "Announce",
  "actor": "https://smartbiblia.fr/agents/query-runner",
  "object": {
    "type": "schema:CreativeWork",
    "id": "urn:smartbiblia:outputs:response:query123",
    "name": "LLM Response to Query",
    "url": "file://./outputs/response_query123.json",
    "text": "The document discusses the environmental impacts of urbanization...",
    "mediaType": "text/plain"
    },
  "prov:wasGeneratedBy": {
    "type": "prov:Activity",
    "prov:used": [
      "file://./state/index.jsonl",
      "query:text:What are the main themes in the document?"
      ],
    "prov:wasAssociatedWith": "query.py@da8b12e",
    "prov:parameters": {
      "model": "gpt-4o-mini",
      "top_k": 2,
      "embedding_model": "sentence-transformers/all-MiniLM-L6-v2",
      "prompt_template": "context-injection-rag-v1"
      },
    "prov:qualifiedUsage": [
      {
        "prov:entity": "chunk-003",
        "prov:role": "retrieved-context",
        "prov:location": "file://./state/chunks.jsonl",
        "prov:value": "Urbanization affects biodiversity through habitat loss..."
        },
        {
        "prov:entity": "chunk-007",
        "prov:role": "retrieved-context",
        "prov:location": "file://./state/chunks.jsonl",
        "prov:value": "The report emphasizes sustainable development goals (SDGs)..."
        }
      ],
  "prov:startedAtTime": "2025-10-01T09:05:01Z",
  "prov:endedAtTime": "2025-10-01T09:05:02Z",
  "prov:generated": {
    "outputHash": "sha256:fa88...b4c",
    "format": "plain/text",
    "executionEnv": "uv@0.1.30"
    }
  }
}

Opportunités

Outre les avantages induits par cette approche en terme de transparence et de traçabilité, son caractère fédéré introduit également des opportunités de mutualisation et d'extension à l’échelle d’un réseau de structures (laboratoires, bibliothèques, agences, ... ?) de workflows modulaires interconnectés, en abonnant simplement des agents à des flux de notifications thématiques ou orientés tâches.

  • Reproductibilité : chaque inférence étant consignée, versionnée et documentée sous forme de métadonnées (lisibles par machine), les journaux d’exécution et les traces d’audit garantissent la possibilité de rejouer, vérifier ou comparer les traitements dans le temps.
  • FAIR Compliance : puisque les métadonnées relatives aux modèles, scripts, paramètres et environnements d’exécution sont persistées et interrogeables, elles assurent une forme de conformité avec les principes FAIR (Findable, Accessible, Interoperable, Reusable).
  • Collaboration : la nature distribuée des composants permet de formaliser des workflows modulaires interconnectés au sein desquels :
  • peut s'opérer une mutualisation et une complémentarité des compétences et des ressources
  • la synchronisation entre agents (scripts) s'organise par une logique d'abonnement à des flux de notifications thématiques ou orientés tâches.
  • Évolutivité : dans la mesure où chaque agent peut être mis à jour, remplacé ou désactivé indépendamment des autres, cette modularité permet d’intégrer rapidement des update (par exemple un nouveau modèle) sans perturber le reste du système.
"> ');