Guide technique

Décisions IA Défendables : Auditabilité, Traçabilité et Preuve en Production

Comment construire des systèmes IA avec une traçabilité complète des décisions. Événements d'audit structurés, reçus HMAC, chaînes de décisions par session, enregistrements d'approbation humaine et architecture de rétention.

5 mars 202618 min de lectureÉquipe d'Ingénierie Oronts

"Qu'est-ce que l'IA a fait, et peux-tu le prouver ?"

Cette question revient dans chaque déploiement IA en entreprise. Elle ne vient pas des ingénieurs. Elle vient du juridique, de la conformité, des achats et du conseil d'administration. La réponse qu'ils attendent, ce n'est pas "on a utilisé GPT-4" ou "le modèle a été fine-tuné sur nos données." Ils veulent des précisions : quelles données sont entrées, quel modèle les a traitées, quels outils ont été appelés, quel humain a validé l'action, et si l'enregistrement est vérifiable après coup.

La plupart des systèmes IA ne savent pas répondre à cette question. Ils loguent les prompts et les réponses (quand ils loguent quelque chose), mais ces logs ne te disent pas la chaîne de décision. Ils ne te disent pas pourquoi le système a choisi l'option A plutôt que l'option B. Ils ne te disent pas qui a approuvé une action à forte valeur. Et ils ne fournissent certainement pas de preuve inaltérable que l'enregistrement n'a pas été modifié après la prise de décision.

Nous avons intégré la traçabilité des décisions dans plusieurs systèmes IA en production. Cet article couvre les patterns d'architecture qui rendent les décisions IA défendables. Pas défendables en théorie. Défendables de manière prouvable, avec des reçus cryptographiques et des enregistrements immuables.

Pour le contexte sur notre approche de la gouvernance IA au sens large et des systèmes humain-dans-la-boucle en particulier, ces guides couvrent des patterns connexes. Cet article se concentre sur la couche de preuve : quoi loguer, comment le structurer, et comment le rendre vérifiable.

Ce que la Traçabilité des Décisions Signifie Réellement

La traçabilité des décisions, ce n'est pas du logging. Le logging te dit ce qui s'est passé. La traçabilité te dit pourquoi c'est arrivé, qui l'a autorisé, et si l'enregistrement est fiable.

CapacitéLogging StandardTraçabilité des Décisions
Ce qui s'est passéTexte prompt et réponseÉvénement de décision structuré avec champs typés
Quel modèlePeut-être dans les en-têtesExplicite : model ID, version, fournisseur, temperature
Quelles données utiliséesPrompt brut (contient des DCP)Token IDs référençant un mapping de session (pas de DCP)
Quels outils appelésPeut-être dans les logs de debugChaîne d'appels d'outils structurée avec entrées et sorties
Qui a approuvéNon suiviEnregistrement d'approbation : qui, quand, ce qu'il a vu, ce qu'il a décidé
Vérifiable ?Non (les logs sont modifiables)Reçu HMAC : inaltérable, signé cryptographiquement
RétentionCe que ton agrégateur de logs conserveBasée sur des politiques : 90 jours opérationnel, 7 ans archive

La différence compte quand un client conteste une recommandation générée par l'IA, quand un régulateur demande comment une décision a été prise, ou quand un audit interne doit vérifier que le système IA a respecté la politique.

Le Schéma d'Événement de Décision

Chaque décision IA génère un événement structuré. Pas une ligne de log. Un enregistrement typé avec des champs explicites pour chaque dimension de la décision.

interface AiDecisionEvent {
    // Identité
    event_id: string;              // UUID, unique par événement
    event_type: string;            // "transform", "rehydrate", "tool_call", "agent_action", "approval"
    timestamp: string;             // ISO 8601 UTC

    // Acteur
    actor_type: string;            // "agent" | "human" | "system" | "scheduler"
    actor_id: string;              // ID de thread agent, ID utilisateur, ou nom du composant système

    // Contexte
    tenant_id: string;             // cadrage multi-tenant
    session_id: string;            // regroupe les événements au sein d'une session
    correlation_id: string;        // lie les événements liés entre services
    channel_id?: string;           // quel canal (web, api, widget)

    // Modèle
    model_provider?: string;       // "openai" | "anthropic" | "local"
    model_id?: string;             // "gpt-4o" | "claude-sonnet-4-20250514"
    model_version?: string;        // version de déploiement ou checkpoint

    // Décision
    action: string;                // ce qui a été fait : "generate_response", "call_tool", "approve_order"
    input_summary: object;         // résumé structuré (PAS de DCP brutes, seulement token IDs et types)
    output_summary: object;        // résumé structuré du résultat
    decision_rationale?: string;   // pourquoi cette action a été choisie (raisonnement de l'agent)

    // Politique
    policy_id?: string;            // quelle politique a été évaluée
    policy_result?: string;        // "allowed" | "denied" | "escalated"
    policy_conditions?: object;    // quelles conditions ont été vérifiées

    // Approbation (si HITL)
    approval_required: boolean;
    approval_status?: string;      // "pending" | "approved" | "rejected"
    approved_by?: string;          // ID utilisateur de l'approbateur
    approved_at?: string;          // quand l'approbation a été donnée
    approval_context?: object;     // ce que l'approbateur a vu au moment de décider

    // Intégrité
    receipt_hmac?: string;         // HMAC-SHA256 du payload de l'événement
    previous_event_id?: string;    // maillon de chaîne vers l'événement précédent de la session
}

Les choix de conception clés :

Pas de DCP brutes dans les événements. Le champ input_summary contient des token IDs (p_001, e_001) et des types d'entités, jamais de valeurs brutes. Cela signifie que ton stockage d'audit ne devient pas un système régulé par le RGPD. Consulte notre guide de conformité RGPD pour l'architecture complète.

Identification explicite du modèle. Pas simplement "on a utilisé un LLM." Le fournisseur spécifique, l'ID du modèle et la version sont enregistrés. Quand un modèle est mis à jour ou remplacé, tu peux tracer quelles décisions ont utilisé quelle version.

Chaînage. Le champ previous_event_id crée une chaîne liée d'événements au sein d'une session. L'événement 3 pointe vers l'événement 2, qui pointe vers l'événement 1. La chaîne prouve la séquence des décisions et qu'aucun événement n'a été inséré ou supprimé après coup.

Chaînes de Décisions par Session

Une seule interaction IA implique souvent plusieurs décisions. Un agent de support client pourrait : lire le ticket (événement 1), chercher les infos client (événement 2), vérifier la facturation (événement 3), rédiger une réponse (événement 4), et envoyer l'email (événement 5). Chaque étape est un événement de décision. Ensemble, ils forment une chaîne de décisions.

┌──────────────────────────────────────────────────────┐
│                  SESSION: sess_abc123                  │
│                                                       │
│  Event 1         Event 2         Event 3              │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐         │
│  │ READ     │──▶│ LOOKUP   │──▶│ CHECK    │         │
│  │ TICKET   │   │ CUSTOMER │   │ BILLING  │         │
│  │          │   │          │   │          │         │
│  │ model:   │   │ tool:    │   │ tool:    │         │
│  │ claude   │   │ crm_api  │   │ billing  │         │
│  │          │   │          │   │ _api     │         │
│  │ tokens:  │   │ tokens:  │   │ tokens:  │         │
│  │ p_001    │   │ cid_001  │   │ o_001    │         │
│  └──────────┘   └──────────┘   └──────────┘         │
│       │              │              │                 │
│       ▼              ▼              ▼                 │
│  Event 4         Event 5                              │
│  ┌──────────┐   ┌──────────┐                         │
│  │ DRAFT    │──▶│ SEND     │                         │
│  │ RESPONSE │   │ EMAIL    │                         │
│  │          │   │          │                         │
│  │ model:   │   │ channel: │                         │
│  │ claude   │   │ email    │                         │
│  │          │   │          │                         │
│  │ policy:  │   │ restore: │                         │
│  │ support  │   │ formatted│                         │
│  └──────────┘   └──────────┘                         │
│                                                       │
└──────────────────────────────────────────────────────┘

Chaque événement référence le précédent. La chaîne est vérifiable : si quelqu'un supprime l'événement 3, la chaîne de l'événement 4 vers l'événement 2 a un trou. Si quelqu'un insère un faux événement entre le 2 et le 3, les maillons de la chaîne ne correspondent pas.

Interroger la Chaîne

Quand un auditeur demande "que s'est-il passé avec la session X ?", tu interroges par session_id et tu reconstitues la chaîne :

async function getDecisionChain(sessionId: string): Promise<AiDecisionEvent[]> {
    const events = await this.eventStore.findBySessionId(sessionId, {
        orderBy: 'timestamp',
        direction: 'ASC',
    });

    // Vérifier l'intégrité de la chaîne
    for (let i = 1; i < events.length; i++) {
        if (events[i].previous_event_id !== events[i - 1].event_id) {
            throw new ChainIntegrityError(
                `Chain broken at event ${events[i].event_id}: ` +
                `expected previous ${events[i - 1].event_id}, ` +
                `got ${events[i].previous_event_id}`
            );
        }
    }

    return events;
}

Pour voir comment nous gérons une vérification de chaîne similaire dans les transactions commerce, consulte notre guide du protocole commerce agentique qui utilise les reçus HMAC dans le même but.

Reçus HMAC : Preuve Inaltérable

Les événements de décision stockés dans une base de données peuvent être modifiés. Un reçu HMAC prouve que les données de l'événement n'ont pas changé depuis leur création.

function signDecisionEvent(event: AiDecisionEvent, tenantSecret: string): string {
    // Forme canonique : clés triées, JSON déterministe
    const canonical = JSON.stringify(event, Object.keys(event).sort());
    return crypto.createHmac('sha256', tenantSecret).update(canonical).digest('hex');
}

function verifyDecisionEvent(event: AiDecisionEvent, storedHmac: string, tenantSecret: string): boolean {
    const recomputed = signDecisionEvent(event, tenantSecret);
    return crypto.timingSafeEqual(
        Buffer.from(recomputed, 'hex'),
        Buffer.from(storedHmac, 'hex')
    );
}

Chaque événement de décision est signé au moment de sa création. Le HMAC est stocké à côté de l'événement. Pour vérifier, tu recalcules le HMAC à partir des données actuelles de l'événement et tu compares. Si un seul champ a été modifié après la signature, le HMAC ne correspondra pas.

PropriétéValeur
AlgorithmeHMAC-SHA256
CléSecret par tenant (rotation annuelle)
CanonicalisationJSON.stringify(payload, Object.keys(payload).sort())
SortieChaîne encodée en hexadécimal (64 caractères)
ComparaisonTiming-safe (crypto.timingSafeEqual)

Le secret par tenant signifie que les reçus d'un tenant ne peuvent pas être vérifiés avec la clé d'un autre tenant. La rotation de clé inclut une période de chevauchement de 24 heures pendant laquelle l'ancienne et la nouvelle clé sont acceptées pour la vérification.

Enregistrements d'Approbation Humaine

Quand une décision nécessite une approbation humaine (transactions à forte valeur, accès à des données sensibles, exceptions de politique), l'approbation elle-même est un événement de décision avec des champs spécifiques :

interface ApprovalEvent extends AiDecisionEvent {
    event_type: 'approval';
    approval_required: true;

    // Ce que l'humain a vu au moment de décider
    approval_context: {
        original_request: string;      // résumé de ce qui a été demandé
        estimated_impact: string;      // "Order for 2,500 EUR from supplier Alpha"
        policy_triggered: string;      // "require_human_approval_above: 500"
        agent_recommendation: string;  // ce que l'agent a suggéré
        risk_flags: string[];          // alertes surfacées pour l'approbateur
    };

    // Ce que l'humain a décidé
    approved_by: string;               // ID utilisateur
    approved_at: string;               // ISO 8601
    approval_status: 'approved' | 'rejected';
    rejection_reason?: string;         // si rejeté, pourquoi
    approval_duration_ms: number;      // combien de temps l'humain a mis pour décider
}

Le champ approval_context est critique. Il enregistre quelles informations ont été présentées à l'humain au moment de sa décision. Cela empêche l'argument "j'ai approuvé mais je ne savais pas X." L'enregistrement montre exactement ce que l'approbateur a vu.

approval_duration_ms est aussi utile pour l'audit. Si un approbateur approuve systématiquement en moins de 2 secondes, cela suggère du tampon automatique plutôt qu'une véritable revue. Les équipes de conformité utilisent cette métrique pour évaluer si la supervision humaine est réelle.

Ce qu'il Ne Faut PAS Loguer

La traçabilité des décisions exige de la discipline sur ce qui entre dans la piste d'audit.

A loguer :

  • Token IDs et types d'entités (ex. "entity p_001 of type person was detected")
  • Identifiants et versions de modèles
  • Noms d'appels d'outils et paramètres structurés
  • Résultats d'évaluation de politiques
  • Enregistrements d'approbation avec contexte
  • Informations de timing (latences, durées)
  • Codes d'erreur et raisons d'échec

A NE PAS loguer :

  • DCP brutes (noms, emails, numéros de téléphone, adresses)
  • Texte complet du prompt (contient des DCP et est volumineux)
  • Réponses complètes du modèle (mêmes problèmes)
  • Identifiants d'authentification ou clés API
  • Mots de passe internes ou chaînes de connexion
// Bon : structuré, sans DCP
{
    event_type: "transform",
    action: "detect_and_tokenize",
    input_summary: {
        entities_detected: 3,
        entity_types: ["person", "email", "customer_id"],
        token_ids: ["p_001", "e_001", "cid_001"],
        detection_confidence: [0.95, 1.0, 0.99],
    },
    policy_id: "german-support",
    model_id: "ner-spacy-de",
    duration_ms: 12,
}

// Mauvais : contient des DCP, inutile pour des requêtes structurées
{
    event_type: "transform",
    action: "process_input",
    input: "Hallo, ich bin Sara Mustermann, meine Kundennummer ist 948221...",
    output: "Hallo, ich bin {{person:p_001}}...",
}

Le bon exemple est interrogeable ("montre-moi tous les événements où la confiance de détection était inférieure à 0.8"), filtrable ("montre-moi tous les événements pour la politique german-support"), et sans DCP. Le mauvais exemple est un bloc de texte qui devient une responsabilité RGPD.

Pour l'architecture complète du logging sécurisé en termes de DCP dans les systèmes IA, consulte notre guide d'observabilité IA.

Architecture de Rétention

Les événements de décision ont des exigences de rétention différentes selon leur contexte réglementaire :

NiveauStockageRétentionInterrogeableCas d'usage
HotBase de données (PostgreSQL / DynamoDB)90 joursSQL/requête complèteDébogage, tableaux de bord ops, monitoring temps réel
WarmStockage objet (S3)2 ansPar session_id, plage de datesAudits internes, litiges clients, revues de conformité
ColdStockage objet avec verrous write-once7 ansPar session_id uniquementAudits réglementaires, gels juridiques, conformité financière

Le niveau cold utilise du stockage objet avec des verrous en mode conformité. Une fois écrits, les enregistrements ne peuvent être ni modifiés ni supprimés avant la fin de la période de rétention. Ce n'est pas juste du contrôle d'accès. Le système de stockage empêche physiquement la suppression, même par les administrateurs.

Événement de Décision Créé
  │
  ├──▶ Niveau Hot (base de données) : écriture immédiate, interrogeable
  │
  ├──▶ Niveau Warm (stockage objet) : export quotidien par lots
  │
  └──▶ Niveau Cold (stockage objet verrouillé) : flux depuis la base
       via change data capture, write-once, verrou 7 ans

Le streaming de la base de données vers le stockage cold se fait par change data capture (flux de base de données ou WAL shipping). Les événements sont écrits dans l'archive immuable en quelques minutes après leur création. Il n'y a pas de job batch qui tourne quotidiennement et pourrait manquer des événements. Le flux est continu.

Corrélation entre Services

Dans un système IA distribué, une seule requête utilisateur peut traverser plusieurs services : une API gateway, un runtime de protection des données, un fournisseur LLM, un serveur d'outils et un service d'audit. Le correlation_id relie tous les événements de décision de tous les services.

// L'API gateway génère le correlation_id
const correlationId = generateUUID();

// Chaque service en aval le reçoit
const response = await dataProtection.transform(input, {
    headers: { 'X-Correlation-Id': correlationId },
});

// Chaque événement de décision l'inclut
const event: AiDecisionEvent = {
    correlation_id: correlationId,
    // ...
};

Pour le débogage ou l'audit, tu interroges par correlation_id pour obtenir le tableau complet de tous les services. C'est le même pattern utilisé dans le tracing distribué, mais appliqué spécifiquement aux événements de décision plutôt qu'aux traces de performance.

Implémentation Pratique

Choix de Stockage

ExigencePostgreSQLDynamoDBEvent Store (ex. EventStoreDB)
Requêtes structuréesExcellentLimité (clé-valeur)Limité (basé sur les flux)
Débit en écritureBon (avec connection pooling)Excellent (auto-scaling)Excellent
Intégrité de chaîneNiveau applicatifNiveau applicatifIntégré (flux append-only)
Politiques de rétentionNiveau applicatifTTL sur les itemsIntégré
Coût à l'échelleFixe (serveur)Paiement à la requêteFixe

Pour la plupart des implémentations, PostgreSQL est le bon choix pour le niveau hot. C'est interrogeable, transactionnel, et ton équipe le connaît déjà. DynamoDB fonctionne bien si tu es sur AWS et que tu as besoin d'un débit en écriture auto-scalable. Un event store dédié est disproportionné sauf si tu as des milliers d'événements de décision par seconde.

Patterns de Requêtes

Les requêtes les plus courantes sur le store d'événements de décision :

-- Toutes les décisions d'une session (reconstituer la chaîne)
SELECT * FROM ai_decision_events
WHERE session_id = $1
ORDER BY timestamp ASC;

-- Toutes les décisions d'un agent spécifique dans les dernières 24 heures
SELECT * FROM ai_decision_events
WHERE actor_type = 'agent' AND actor_id = $1
AND timestamp > NOW() - INTERVAL '24 hours'
ORDER BY timestamp DESC;

-- Toutes les évaluations de politique refusées (trouver les politiques mal configurées)
SELECT * FROM ai_decision_events
WHERE policy_result = 'denied'
AND timestamp > NOW() - INTERVAL '7 days'
ORDER BY timestamp DESC;

-- Toutes les approbations humaines avec un temps de revue court (détection de tampon automatique)
SELECT * FROM ai_decision_events
WHERE event_type = 'approval'
AND approval_status = 'approved'
AND approval_duration_ms < 3000
AND timestamp > NOW() - INTERVAL '30 days';

-- Vérifier l'intégrité de la chaîne pour une session
SELECT e1.event_id, e1.previous_event_id,
       CASE WHEN e2.event_id IS NULL AND e1.previous_event_id IS NOT NULL
            THEN 'BROKEN' ELSE 'OK' END as chain_status
FROM ai_decision_events e1
LEFT JOIN ai_decision_events e2 ON e1.previous_event_id = e2.event_id
WHERE e1.session_id = $1;

Indexation

CREATE INDEX idx_session ON ai_decision_events (session_id, timestamp);
CREATE INDEX idx_actor ON ai_decision_events (actor_type, actor_id, timestamp);
CREATE INDEX idx_correlation ON ai_decision_events (correlation_id);
CREATE INDEX idx_policy_result ON ai_decision_events (policy_result, timestamp);
CREATE INDEX idx_approval ON ai_decision_events (event_type, approval_status, timestamp)
    WHERE event_type = 'approval';

Pièges Courants

  1. Loguer les prompts bruts comme piste d'audit. Les prompts contiennent des DCP. Ton stockage d'audit devient régulé par le RGPD. Utilise des événements structurés avec des token IDs à la place.

  2. Pas de chaînage entre les événements. Sans previous_event_id, tu ne peux pas prouver la séquence des décisions. Des événements peuvent être insérés, supprimés ou réordonnés sans détection.

  3. Pas de signature HMAC. Les enregistrements en base de données peuvent être modifiés. Sans reçus cryptographiques, la piste d'audit n'est pas inaltérable. "Faites-nous confiance, on n'a pas modifié les logs" n'est pas défendable.

  4. Même rétention pour tout. Les données de débogage ont besoin de 90 jours. Les données de conformité ont besoin de 7 ans. Mélanger les deux gaspille de l'argent (garder les données de débogage trop longtemps) ou crée du risque (supprimer les données de conformité trop tôt).

  5. Pas de contexte d'approbation. Enregistrer que "l'utilisateur X a approuvé l'action Y" ne suffit pas. Enregistre quelles informations l'approbateur a vues au moment de décider. Sans contexte, l'approbation est inutile pour l'audit.

  6. Détection de tampon automatique absente. Si la supervision humaine est une exigence de conformité, tu dois vérifier que les humains font vraiment une revue, pas qu'ils cliquent "approuver" de manière réflexe. Suis approval_duration_ms.

  7. Pas de corrélation entre services. Si ton système IA couvre plusieurs services, les événements de chaque service sont isolés. Sans correlation_id, tu ne peux pas reconstituer la chaîne de décision complète.

  8. Stockage cold modifiable. Si ton archive long terme peut être éditée ou supprimée par les administrateurs, ce n'est pas une piste d'audit. Utilise du stockage write-once avec des verrous en mode conformité.

Points Clés

  • "On a utilisé GPT-4" n'est pas une réponse défendable. Enregistre le modèle spécifique, la version, le fournisseur, les tokens d'entrée, les tokens de sortie, les outils appelés, les politiques évaluées, et les humains qui ont approuvé. Chaque dimension de la décision.

  • Des événements structurés, pas des lignes de log. Les champs typés permettent des requêtes structurées, des tableaux de bord, la détection d'anomalies et des rapports de conformité. Les logs en texte libre ne permettent rien d'autre que grep.

  • Pas de DCP dans les événements de décision. Utilise des token IDs provenant de ta couche de protection des données. La piste d'audit ne doit pas elle-même devenir un passif en matière de protection des données.

  • Le chaînage prouve la séquence. Chaque événement pointe vers son prédécesseur. Les trous et les insertions sont détectables. Combiné avec la signature HMAC, la chaîne est inaltérable.

  • Les reçus HMAC fournissent une preuve cryptographique. Clés de signature par tenant, sérialisation JSON canonique, comparaison timing-safe. Toute modification de n'importe quel champ invalide le reçu.

  • Les enregistrements d'approbation humaine doivent inclure le contexte. Qu'est-ce que l'approbateur a vu ? Combien de temps a-t-il mis ? Sans cela, la "supervision humaine" est une case à cocher, pas un contrôle.

  • La rétention à trois niveaux correspond à la réalité réglementaire. Hot pour le débogage (90 jours), warm pour les audits (2 ans), cold avec verrous write-once pour les régulateurs (7 ans).

Nous appliquons ces patterns dans tous nos systèmes IA, des runtimes de protection des données aux plateformes commerce agentiques. Si tu construis des systèmes IA qui doivent satisfaire des exigences de conformité entreprise, parle à notre équipe ou demande un devis. Tu peux explorer nos services IA, notre approche de la confiance et de la conformité, et nos guides sur l'architecture des systèmes IA et les modes de défaillance IA pour plus de contexte.

Sujets couverts

auditabilité IAtraçabilité IAjournalisation décisions IAaudit conformité IApreuve décision IAgouvernance IA productionpiste daudit LLMresponsabilité IA

Prêt à construire des systèmes IA prêts pour la production ?

Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.

Démarrer une conversation