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>
This commit is contained in:
pr-daaif
2026-07-17 21:24:33 +01:00
parent 28eecc1fb9
commit b69c54ed0f
15 changed files with 213 additions and 23 deletions

View File

@@ -10,10 +10,23 @@
## Décision
1. **Embeddings : locaux, sur CPU**`fastembed` (ONNX, sans PyTorch) avec
`sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` (384 dimensions, multilingue, ~120 Mo, 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.
`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.