Files
siop2/docs/03-architecture/adr/ADR-004-modeles-ia.md
pr-daaif b69c54ed0f feat(r5): recette sans clé API + durcissement production siop2-ai
Recette (mode extractif, aucune clé) — elle a invalidé le modèle R5.1 :
- MiniLM-384 classait la page-réponse DERRIÈRE des passages sans rapport
  (0,24 vs 0,41 sur la question type du plan) → bascule mesurée vers
  paraphrase-multilingual-mpnet-base-v2 (768 d, local/CPU), ADR-004 amendé
  avec le banc comparatif (e5-large écarté : 2,2 Go, scores compressés).
- Migration r5_embeddings_mpnet : pgvector 384 → 768, index vidé
  (re-dérivable par « Réindexer tout »).
- Découpage affiné (~350 caractères) : la phrase-réponse ne se noie plus,
  l'extrait cité est lisible ; seuils par défaut recalés 0,45/0,40/0,55.
- Rejouée après bascule : réponse sourcée p. 2 en tête, refus honnête
  chiffré, suggestions étagées — 16/16 Playwright, 78 API, 23 pytest.
- Revue pixel publiée (6 écrans réels vs maquettes, 3 arbitrages).

Durcissement :
- apps/ai/Dockerfile : uv, modèle ONNX téléchargé AU BUILD (ADR-004 §1),
  non-root, healthcheck ; répétition locale conteneurisée validée
  (healthz, reindex via MinIO/pgvector, 401 sans jeton, refus de boot
  api-sans-clé, réponse sourcée depuis le conteneur).
- Compose Dokploy : siop2-ai interne (jamais sur dokploy-network,
  AI_SERVICE_TOKEN requis, génération opt-in, seuils par env) ;
  siop2-api branché (AI_SERVICE_URL).
- Runbook §5-6 : service IA en production, calibrage des seuils sur le
  corpus client, réindexation post-déploiement.

Le tag release/r5 attend la validation de la revue pixel par le référent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 21:24:33 +01:00

4.4 KiB

ADR-004 — Modèles et topologie du service IA (R5)

  • Statut : acceptée (R5.1, 17 juillet 2026)
  • Contexte : les maquettes R5 et les décisions D1-D5 sont validées (l'IA propose/l'humain valide, sourcé ou silencieux, corpus fermé, anonymisation 09-08, voix purgée). Reste à trancher COMMENT : quels modèles, où tournent-ils, qui parle à qui. Contraintes : serveur Dokploy du partenaire (CPU, pas de GPU garanti), corpus en français, données d'un client marocain (loi 09-08), CI sans secret, démo qui marche sans dépendance externe.

Décision

  1. Embeddings : locaux, sur CPUfastembed (ONNX, sans PyTorch) avec sentence-transformers/paraphrase-multilingual-mpnet-base-v2 (768 dimensions, multilingue, ~1 Go, registre fastembed). Les textes du client ne quittent JAMAIS le serveur pour l'indexation ni pour la recherche. Le modèle est téléchargé au build de l'image (pas au démarrage). En tests/CI : embeddeur déterministe par hachage (pas de téléchargement, pas de flottement) derrière la même interface.

    Amendé en recette R5 (17/07/2026) : le choix initial, MiniLM-L12-v2 (384 d, ~220 Mo), a été invalidé par la mesure — sur un banc français question→passage, il classait la page contenant la réponse DERRIÈRE des passages sans rapport (0,24 contre 0,41) et ne laissait aucune marge pour le seuil de refus D2. mpnet-base-v2 rétablit le classement et une marge signal/bruit exploitable (pertinent ≥ 0,46 ; hors-corpus ≤ 0,42) pour un coût CPU encore raisonnable. multilingual-e5-large (1024 d, 2,2 Go) classait aussi correctement mais ses scores compressés (0,73-0,90) et son poids l'écartent — à réévaluer au calibrage sur le corpus client si la marge de mpnet s'avère insuffisante. Migration r5_embeddings_mpnet : colonne pgvector 384 → 768, index vidé (re-dérivable par réindexation). Seuils par défaut recalés : pertinence 0,45, suggestion 0,40, confiance forte 0,55.

  2. Stockage vectoriel : pgvector dans le PostgreSQL existant (extension déjà installée depuis R0) — table RagChunk gérée par la migration Prisma (le schéma reste la propriété d'apps/api, source unique). Pas de base vectorielle de plus à opérer.

  3. Génération (réponses rédigées de l'assistant) : opt-in par configuration.

    • Sans clé (AI_GENERATION=off, défaut) : l'assistant fonctionne en mode extractif — il montre les meilleurs extraits sourcés et une synthèse templatée, sans LLM. La démo, la CI et un déploiement sans budget API restent pleinement fonctionnels et honnêtes.
    • Avec clé (AI_GENERATION=api + AI_API_KEY, modèle configurable par AI_MODEL, défaut claude-opus-4-8) : rédaction par le SDK officiel Anthropic, sur textes DÉJÀ anonymisés (D4), avec l'obligation de citer les extraits fournis — jamais au-delà. La config est validée au boot (api sans clé = refus de démarrer) ; la clé n'apparaît ni dans les journaux ni au /healthz (qui n'expose que le mode). Tout échec du LLM (refus, quota, réseau) retombe silencieusement sur le mode extractif.
    • La suggestion de codes de bilan n'utilise PAS de LLM : similarité sémantique (embeddings locaux) entre la description et les libellés des référentiels + les bilans historiques du parc. Déterministe, testable, explicable (« 9 bilans similaires »).
  4. Topologie : apps/ai (FastAPI/uv, conteneur siop2-ai) n'est jamais exposé — réseau interne Dokploy uniquement, comme MinIO. Les utilisateurs passent par l'API NestJS (auth JWT + matrice de permissions réutilisées) qui proxifie vers siop2-ai (AI_SERVICE_URL + secret partagé AI_SERVICE_TOKEN). apps/ai lit Postgres (métadonnées, bilans, écriture des chunks) et MinIO (octets des PDF) en direct sur le réseau privé — la règle d'or ESLint d'ADR-001 concerne le code TypeScript de l'API, la topologie « MinIO jamais exposé » du runbook reste respectée.

Conséquences

  • L'anonymisation (D4) s'applique à l'ingestion — les index ne contiennent jamais d'identités ; le mode génératif n'envoie donc que des textes déjà nettoyés.
  • Le mode extractif est le contrat de base : toute recette R5 doit passer SANS clé API.
  • Si le partenaire veut un jour une génération 100 % locale (llama.cpp…), seul le point 3 change — interface Generateur prévue pour ça.