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

@@ -19,7 +19,8 @@ domaine (Traefik/Dokploy) ──▶ siop2-web :80 (nginx, statique)
siop2-api :3000 (NestJS)
│── siop2-postgres :5432 (pgvector + PostGIS)
│── siop2-redis :6379
── siop2-minio :9000
── siop2-minio :9000
└── siop2-ai :8000 (FastAPI — R5, interne)
```
- **Convention `siop2-`** (leçon v1) : le réseau Dokploy est partagé entre projets ;
@@ -42,6 +43,11 @@ domaine (Traefik/Dokploy) ──▶ siop2-web :80 (nginx, statique)
| `DEMO_MODE_I_KNOW` | — | double verrou ADR-002 : requis si `DEMO_MODE=true` en production, sinon l'API **refuse de démarrer** |
| `SEED_ON_START` | — | `true` sur l'instance de démonstration : rôles, matrice et comptes démo au boot (idempotent) |
| `SEED_DEMO_PASSWORD` | — | mot de passe commun des comptes démo (défaut `Demo!2026`) |
| `AI_SERVICE_TOKEN` | ✅ (R5) | secret partagé api ↔ service IA (générer : `openssl rand -hex 32`) — le service IA refuse tout appel sans lui |
| `AI_GENERATION` | — | `off` (défaut, mode extractif — la recette R5 passe ainsi) ou `api` (rédaction LLM, exige `AI_API_KEY`) |
| `AI_API_KEY` | — | clé API Anthropic — **uniquement** si `AI_GENERATION=api` ; le service refuse de démarrer si elle manque en mode api |
| `AI_MODEL` | — | défaut `claude-opus-4-8` |
| `AI_SEUIL_PERTINENCE` / `AI_SEUIL_SUGGESTION` / `AI_SEUIL_CONFIANCE_FORTE` | — | défauts 0,45 / 0,40 / 0,55 — **à calibrer sur le corpus client** (voir §7) |
Profils types :
@@ -64,6 +70,7 @@ Dans le service **Compose** du projet Dokploy (déjà créé pour l'instance ENS
POSTGRES_PASSWORD=<openssl rand -hex 24>
MINIO_ROOT_PASSWORD=<openssl rand -hex 24>
JWT_SECRET=<openssl rand -hex 32>
AI_SERVICE_TOKEN=<openssl rand -hex 32>
DEMO_MODE=true
DEMO_MODE_I_KNOW=true
SEED_ON_START=true
@@ -103,7 +110,36 @@ Les migrations de la release s'appliquent au démarrage du conteneur API ; en ca
d'échec de migration, le conteneur s'arrête **sans** servir de trafic (l'ancienne
version reste visible côté web).
## 5. Incidents & retours arrière
## 5. Le service IA en production (R5)
- `siop2-ai` **ne rejoint jamais** `dokploy-network` : il n'a pas de domaine, pas de
port publié — seul `siop2-api` le contacte, avec `AI_SERVICE_TOKEN`. S'il est
éteint, l'assistant répond « indisponible » (503 propre) et **tout le reste de
l'application fonctionne**.
- Le modèle d'embeddings (mpnet multilingue, ~1 Go) est **dans l'image** : premier
build long (téléchargement au build), démarrages rapides ensuite, aucun accès à
Hugging Face requis en production.
- **Après chaque déploiement qui change le modèle ou le découpage** (ex. migration
`r5_embeddings_mpnet`) : l'index est vide — cliquer **« Réindexer tout »** dans
Bibliothèque (ou `POST /assistant/reindex`) pour reconstruire le corpus.
- Mode génératif : ajouter `AI_GENERATION=api` + `AI_API_KEY` dans l'environnement
Dokploy puis redéployer le service `siop2-ai` seul. La clé ne transite jamais par
le dépôt ni par les journaux (`/healthz` n'expose que le mode).
## 6. Calibrage des seuils sur le corpus client
Les seuils par défaut ont été mesurés sur un banc synthétique (journal 17/07/2026).
Sur le vrai corpus SPELEV (notices réelles), rejouer une dizaine de questions métier
et quelques questions hors corpus via l'écran Assistant, puis ajuster :
- trop de refus → baisser `AI_SEUIL_PERTINENCE` par pas de 0,03 ;
- réponses « à côté » sur des questions hors corpus → le monter ;
- suggestions de bilan trop rares/trop bruyantes → jouer sur `AI_SEUIL_SUGGESTION`.
Chaque ajustement = variable d'environnement Dokploy + redéploiement de `siop2-ai`
(pas de rebuild, pas de réindexation).
## 7. Incidents & retours arrière
- **Rollback applicatif** : Dokploy → Deployments → redéployer le commit précédent.
Les migrations Prisma étant additives (convention projet : jamais de `DROP` sans
@@ -115,7 +151,7 @@ version reste visible côté web).
Sauvegarde PostgreSQL planifiée côté Dokploy (onglet Backups) — à configurer
à la première mise en production réelle.
## 6. Répétition locale (sans Dokploy)
## 8. Répétition locale (sans Dokploy)
```bash
# depuis la racine — construit et lance les 5 services comme en production