The execution tiers answer 'how to run a tactic', not 'which tactic is best' nor 'where is the danger'. Adds two decision layers: an optimizer over StrategySpec (objectives incl. tail risk, coarse-to-fine search, LLM as candidate seeder) and an adversarial cartography (AdversarySpec + alternating min-max + scenario sweeps + danger frontier + sensitivity), giving the danger view for BOTH sides. Reframes the LLM as hypothesis generator / red team / explicator / reporter, never the optimizer or per-turn executor in sweeps. Adds D12-D14 and phases P3 (optimizer) / P4 (robustness).
12 KiB
Phase 2 — Architecture et plan d'implémentation de la couche de décision LLM
Document de conception (révisé). Aucun code n'est encore écrit. Révision intégrant deux demandes : optimiser la meilleure stratégie de survie et cartographier tous les cas où un camp est en difficulté.
1. Objectif et périmètre
Le simulateur doit répondre à trois questions distinctes, souvent confondues :
| Question | Nature | Réponse apportée par |
|---|---|---|
| « Comment exécuter une tactique donnée ? » | Exécution | Étage A (compilé) / Étage B (LLM par tour) |
| « Quelle est la meilleure stratégie de survie ? » | Optimisation | Couche Optimiseur (recherche sur StrategySpec) |
| « Où les PJ (ou les monstres) sont-ils en difficulté ? » | Robustesse adversariale | Couche Robustesse (min-max + cartographie de scénarios) |
Hors périmètre : la rédaction finale du rapport d'équilibrage (phase 3).
Principe directeur : les règles du moteur restent inchangées ; la couche LLM et les couches d'analyse sont additives. La LLM n'est ni l'optimiseur ni l'exécutant des sweeps.
2. Exécution — les deux étages (rappel)
Mettre la LLM à chaque tour dans une boucle Monte Carlo est prohibitif
(runs × rounds × combattants appels, caches non mutualisables). D'où :
- Étage A — stratégie compilée (quantitatif). La LLM traduit une stratégie NL
en un objet de paramètres (
StrategySpec) ; un compilateur déterministe en fait unePolicyPython exécutée sans appel LLM. → bandes valides, coût maîtrisé, déterminisme bit-à-bit. - Étage B — LLM par tour (qualitatif, vitrine).
NLPolicy(observation)à chaque tour, sur ≤ 20 runs. → démonstration du raisonnement, transcripts exploitables ; pas de conclusion statistique.
default_policy devient un cas particulier : compile_strategy(SPEC_PAR_DEFAUT).
3. Décider — les deux nouvelles couches
L'étage A fournit une fonction d'évaluation déterministe : c'est le bon substrat pour chercher et pour éprouver.
3.1 Optimiseur — « meilleure stratégie de survie »
- Espace : les leviers de
StrategySpec(par PJ ou par rôle). - Oracle :
evaluate(spec, scenario, n, seeds) → métriques(runs parallèles). - Objectifs :
P(survivre tous),P(TPK),P(≥1 mort), espérance de PV perdus, ressources consommées, rounds, risque de queue (CVaR / pire décile). - Algorithmes : grille grossière / échantillonnage latin → affinage
(descente coordonnée, CMA-ES, optimisation bayésienne) ; la LLM peut
amorcer avec des stratégies candidates (NL →
StrategySpec). - Sortie : un playbook classé (« si groupés : Buff → Contrôle → Focus lanceur ; reculer sous 30 % PV ») avec incertitude.
3.2 Robustesse et cartographie — « où est le danger »
Un danger n'existe que contre un adversaire qui cherche à gagner : évaluer les PJ contre la politique par défaut sous-estime le risque. Il faut :
AdversarySpec(symétrique deStrategySpec) : priorité de cible, focus fire, usage des capacités, embuscade, positionnement.- Min-max : pour chaque scénario,
max_politique_PJ · min_politique_monstres(optimisation alternée = meilleures réponses) → pire cas réaliste. La même mesure donne les deux sens : difficulté pour les PJ et pour les monstres (rencontre trop facile). - Cartographie (au lieu d'une énumération impossible) :
- grille grossière sur
ScenarioSpec→ cellules d'échec ; - affinage près de la frontière (bissection / apprentissage actif) ;
- frontière de danger : pour chaque mix, le seuil (ex. « < 45 % PV
cumulés et sans soigneur à portée ⇒
P(TPK) > 20 %») ; - analyse de sensibilité : facteurs dominants (surprise, terrain, épuisement des ressources, initiative).
- grille grossière sur
4. État des lieux (socle vérifié)
| Élément | Emplacement | Vérifié |
|---|---|---|
Observation figée, to_dict() JSON, sans engine.* |
combat.py:1262 / 1310 |
oui |
Policy / LLMPolicy / ReactionPolicy |
combat.py:10573/10575/10577 |
oui |
Câblage policy/llm_policy exclusifs |
combat.py:1615-1623 |
oui |
ActionResult + ACTION_RESULT_REASONS + last_turn_results |
combat.py:1483, 1528 |
oui |
| Démo « observation seule » | tests/test_llm_engine_capabilities.py |
oui |
Déterminisme SeededRng(seed+i) + métriques 3σ + attrition |
runner.py, metrics.py |
oui |
| Base de règles conforme | docs/conformance-srd.md (gate 1981) |
oui |
Manques confirmés : run_encounter sans llm_policy (runner.py:177) ;
CLI sans politique ; pas de schema_version ; pas de vue compacte ; pas d'API
d'actions légales ; pas de mémoire inter-tours ; pas de journal de replay ;
LLMPolicy(obs, state) transmet l'état vivant ; aucune couche d'objectifs,
de recherche, d'adversaire ou de scénarios.
5. Architecture cible
Moteur (règles inchangées ; frontière additive)
|
|-- EXÉCUTION
| |- Étage A : compile_strategy(StrategySpec) -> Policy (sweeps)
| \-- Étage B : NLPolicy(observation) (vitrine <=20 runs)
|
|-- DÉCISION / ANALYSE
| |- objectives.py : métriques de survie/danger (dont queue)
| |- scenarios.py : ScenarioSpec + échantillonnage
| |- search.py : optimiseur (objectif = objectives), oracle = simulateur
| |- adversary.py : AdversarySpec + politiques de monstres fortes (red team)
| \-- robustness.py : min-max, cartographie de danger, frontière, sensibilité
|
\-- SUPPORT
|- view.py : ObservationView (objet structuré, versionné) + render()
|- candidates.py : actions plausibles dérivées des pairs
|- prompt.py : prompt versionné
|- client.py : LLMClient + cache composite + FakeLLMClient scripté + coût
|- replay.py : JSONL clé call_seq + replay()
\-- sweep.py : exécution parallèle des runs (substrat commun)
Interfaces clés : ObservationView (objet + render()), candidates()
(dérivées des pairs, pas de 2ᵉ moteur de légalité), ResponseCache (clé
composite), ReplayRecord (clé call_seq), PolicyFactory (par run),
evaluate(...) (oracle d'optimisation), danger_map(...) (cartographie).
6. Espaces de recherche
| Espace | Contenu | Utilisé par |
|---|---|---|
StrategySpec |
priorité de cible, style d'engagement, distance, esquive d'AoO, seuil de repli, buffs d'ouverture, politique de sorts, ressources, focus fire | Optimiseur / Étage A |
AdversarySpec |
mêmes leviers côté monstres (focus, ciblage du soigneur/lanceur, contrôle, embuscade) | Robustesse |
ScenarioSpec |
composition/niveaux, PV et ressources initiaux, déploiement/positionnement, carte/terrain, surprise, distance, mix et nombre de monstres, CR | Cartographie |
7. Objectifs et métriques
| Métrique | Pourquoi |
|---|---|
P(survivre tous) / P(TPK) / P(≥ 1 mort) |
lecture directe de la survie |
| Espérance de PV perdus (groupe / PJ) | gravité |
| Ressources consommées (sorts, potions, canal) | soutenabilité |
| Rounds | tempo |
| CVaR / pire décile | risque de queue — la moyenne cache les TPK |
| Bandes 3σ (n suffisant) | incertitude — étage A uniquement |
8. Rôle de la LLM (recadré)
| Usage | Pourquoi la LLM |
|---|---|
NL → StrategySpec / AdversarySpec |
traduction structurée (petit, robuste) |
| Générateur d'hypothèses (candidats variés à amorcer) | créativité, pas de gradient |
| Red Team (tactiques de monstres fortes) | révéler les faiblesses |
| Explicateur (lire les transcripts dangereux) | causalité en langage naturel |
| Rapporteur (playbook + carte de risque) | rédaction (phase 3) |
Non-usage : optimisation numérique (faite par search.py) et exécution par
tour en sweep (faite par l'étage A).
9. Décisions
| # | Décision | Recommandation |
|---|---|---|
| D1 | Entrée : (observation, state) ou (observation) ? |
(observation) + adaptateur. À trancher en P0.0. |
| D2 | Vue dédiée vs to_dict() brut |
Vue compacte, structurée, versionnée |
| D3 | Mémoire inter-tours | Retirée de la v1 |
| D4 | Sortie d'actions | JSON strict, schéma généré depuis Action, swift/free/full_round exclus |
| D5 | Légalité | Pas de 2ᵉ moteur : candidats dérivés des pairs |
| D6 | Reproductibilité | Cache composite (model, params, prompt_version, schema_version, hash) |
| D7 | Runner/CLI | llm_policy + policy_factory sur run_encounter ; entrée de sweep dédiée |
| D8 | Périmètre v1 | Sous-ensemble curé ; ReactionPolicy par défaut conservée |
| D9 | Économie | Deux étages (exécution) |
| D10 | Parallélisme | Runs indépendants → pool de threads, cache shardé |
| D11 | Réactions | v1 par défaut ; LLM via ready |
| D12 | Danger = adversarial | Mesurer contre des monstres optimisés, sinon sous-estimation |
| D13 | Objectifs | Inclure le risque de queue, pas seulement la moyenne |
| D14 | Cartographie | Non exhaustive : coarse-to-fine + frontière + sensibilité |
10. Plan d'implémentation
P0 — Frontière durcie (sans LLM ; débloque tout)
P0.0 trancher D1 (obs seule + adaptateur) · P0.1 schema_version + clés pinnées ·
P0.2 ObservationView + mesure de taille · P0.3 candidates() (depuis pairs) ·
P0.4 run_encounter(..., llm_policy=, policy_factory=) · P0.5 ReplayLog
(call_seq) + replay() identique.
Sortie : gate verte ; un run enregistré se rejoue à l'identique.
P1 — Client et politiques
P1.1 LLMClient + FakeLLMClient scripté + cache composite · P1.2 garde-fous
coût/tokens + retry/backoff · P1.3 NLPolicy (étage B : parse, valide, 1
re-essai, repli) · P1.4 PromptBuilder versionné + schéma d'action généré.
Sortie : bataille complète pilotée par le Fake, hors ligne.
P2 — Étage A : compilation, objectifs, sweep
P2.1 compilateur NL → Policy paramétrée · P2.2 objectives.py · P2.3
SweepRunner parallèle.
Sortie : sweep ≥ 2×2 avec métriques, borné en coût.
P3 — Optimiseur (« meilleure stratégie »)
P3.1 scenarios.py (ScenarioSpec + échantillonnage) · P3.2 search.py
(grille → affinage) · P3.3 amorçage par candidats LLM.
Sortie : playbook classé avec incertitude pour ≥ 1 rencontre type.
P4 — Adversaire et cartographie (« où est le danger »)
P4.1 AdversarySpec + politiques de monstres fortes · P4.2 robustness.py
(min-max alterné) · P4.3 danger_map (coarse-to-fine, frontière, sensibilité).
Sortie : carte de danger (PJ et monstres) + frontière seuils.
P5 — Fournisseur réel et rapport (frontière phase 3)
P5.1 ProviderClient (clé env, coût/observabilité) · P5.2 few-shot + réparation
complète · P5.3 rapport FR (phase 3).
11. Risques
- Économie per-tour → résolue par les deux étages.
- Adversaire trop faible : danger sous-estimé → monstres optimisés (D12).
- Replay sous-spécifié (
delay, invocations) → clécall_seq. - Cache sans identité de modèle → clé composite.
- Divergence de légalité → candidats dérivés des
pairs. - Bascule tardive de D1 → à trancher en P0.0.
- Bandes 3σ trompeuses à petit n → étage A pour le quantitatif.
- Explosion combinatoire → coarse-to-fine, pas d'énumération (D14).
- Événements rares (TPK) : n énorme en Monte Carlo naïf → ciblage par scénarios, ou annoncer la limite.
12. Première action recommandée
Un spike (sans LLM) qui :
(a) tranche D1 + schema_version + test de clés pinné ;
(b) mesure taille de prompt et appels par cellule de sweep (décide per-tour
vs compilé) ;
(c) esquisse ScenarioSpec + objectives.py et valide la chaîne de
cartographie sur une rencontre type, en comparant default_policy contre une
politique de monstres renforcée — ce dernier point prouve immédiatement la valeur
« prédire où sont les difficultés », avant toute LLM.
13. Questions ouvertes
- Fournisseur/modèle (API distante ou local) ?
- Budget (tokens/coût) par sweep / par optimisation ?
- Reproductibilité : cache obligatoire (runs rejouables) ?
- Priorité v1 : optimiseur (meilleure stratégie) ou cartographie (où est le danger) ?
- Prompts dans le dépôt ou externes ?
- Valides-tu D1 (observation seule), D9 (deux étages), D12 (danger mesuré en adversaire) et D14 (cartographie non exhaustive) ?