mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,43 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-17 — Pr. Daaif (+ Claude) — Recette R5 (sans clé API) + durcissement production `siop2-ai`
|
||||
|
||||
**Actions**
|
||||
|
||||
- **Recette rejouée au vrai modèle, mode extractif (aucune clé API)** sur un corpus mis en scène (notice
|
||||
Otis Gen2 générée, 2 pages). Elle a **invalidé le modèle d'embeddings choisi en R5.1** : sur la question
|
||||
type du plan (« quel couple de serrage pour les guides du Gen2 ? »), MiniLM-384 classait la page-réponse
|
||||
DERRIÈRE des passages sans rapport (0,24 contre 0,41). Banc comparatif mesuré : `mpnet-base-v2` multilingue
|
||||
(768 d) rétablit le classement et une marge signal/bruit exploitable (pertinent ≥ 0,46, hors-corpus ≤ 0,42) ;
|
||||
`multilingual-e5-large` (1 024 d, 2,2 Go) classait bien aussi mais scores compressés (0,73-0,90) et poids
|
||||
rédhibitoires. **Bascule vers mpnet** (ADR-004 amendé, 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).
|
||||
- **Recette validée après bascule** : réponse sourcée p. 2 en tête ✓, refus honnête chiffré sur question hors
|
||||
corpus ✓ (« routeur wifi » → refus ; charabia → refus), suggestions étagées ✓, D1-D5 tenues, le tout SANS
|
||||
clé. 16/16 Playwright (l'embeddeur déterministe suit les 768 dims sans recalibrage), 78 tests API, 23 pytest.
|
||||
- **Revue pixel** : captures réelles des 6 écrans (web ×4, liseré « suggéré », mobile) face aux écrans de
|
||||
maquette-r5 — artefact publié pour le référent avec 3 points d'arbitrage (« Appliquer » à écriture directe,
|
||||
vignettes vs tableau du corpus, refus « voisin de domaine » dépendant du calibrage client).
|
||||
- **Durcissement production** : `apps/ai/Dockerfile` (uv, venv non éditable, **modèle ONNX téléchargé au
|
||||
build** — ADR-004 §1, non-root, healthcheck) ; compose Dokploy : service `siop2-ai` interne (jamais sur
|
||||
`dokploy-network`, secret `AI_SERVICE_TOKEN` requis, génération opt-in par variables, seuils calibrables),
|
||||
`siop2-api` branché (`AI_SERVICE_URL`) ; runbook enrichi (§2 variables, §5 service IA, §6 calibrage des
|
||||
seuils sur corpus client, réindexation post-déploiement).
|
||||
|
||||
**Décisions**
|
||||
|
||||
- Le refus « voisin de domaine » (question ascenseur absente du corpus) reste dépendant du calibrage : jamais
|
||||
d'invention (extraits réels cités), mais pas toujours un refus. Les seuils sont des variables d'environnement
|
||||
pour être calibrés sur le corpus SPELEV réel — procédure au runbook §6.
|
||||
- Le tag `release/r5` attend la validation de la revue pixel par le référent.
|
||||
|
||||
**Prochaine étape** : validation du référent (revue pixel + arbitrages) → tag `release/r5`. Restes : recette
|
||||
R4 sur téléphone (Expo Go), redéploiement Dokploy (`release/r3` puis r5), secret `DOKPLOY_WEBHOOK_URL`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-17 — Pr. Daaif (+ Claude) — R5.3 : les écrans de l'IA (web + mobile) et le corpus administrable
|
||||
|
||||
**Actions**
|
||||
|
||||
Reference in New Issue
Block a user