Phase 2 — Architecture et plan d'implémentation de la couche de décision LLM
Document de conception. Aucun code n'est encore écrit. Ce document fige
l'architecture cible, les décisions à trancher et un plan d'implémentation
vérifiable par étapes.
1. Objectif et périmètre
Objectif phase 2 (cf. README, feuille de route) : traduire des stratégies
exprimées en langage naturel en politiques tactiques, puis balayer des matrices
de positionnement / de stratégie, en s'appuyant sur le moteur déterministe.
Hors périmètre phase 2 : la rédaction du rapport d'équilibrage par la LLM
(phase 3).
Principe directeur : le moteur reste déterministe et ignorant de la LLM.
Toute la partie LLM vit dans une couche séparée, branchée par une politique
(llm_policy) ; le snapshot d'observation est la seule entrée autorisée.
2. État des lieux (socle déjà en place)
| Élément |
Emplacement |
État |
Snapshot Observation (figé, to_dict() JSON-sérialisable, sans engine.*) |
combat.py:1262 / to_dict 1310 |
en place, documenté, audité |
Types de politique Policy, LLMPolicy, ReactionPolicy |
combat.py:10573-10577 |
en place |
Câblage moteur policy / llm_policy (mutuellement exclusifs) |
combat.py:1615-1622 |
en place |
Retour d'action ActionResult + last_turn_results + vocabulaire des raisons |
combat.py:1483 |
en place |
| Démo « observation seule » prouvant les capacités tactiques |
tests/test_llm_engine_capabilities.py, tests/test_observation.py::test_llm_policy_wrapper_e2e |
en place |
Déterminisme (SeededRng(seed+i), ScriptedRng) |
rng.py, runner.py |
en place |
| Base de règles conforme SRD |
docs/conformance-srd.md |
15 passes d'audit, gate 1981 tests |
Écarts / manques identifiés pour la phase 2
run_encounter n'accepte pas llm_policy (runner.py:172 : seul
policy est paramétrable) → impossible de lancer un sweep LLM via le runner
sans modifier le code.
- La CLI n'a aucune option de politique (
cli.py : politique par défaut
implicite) → pas de point d'entrée pour un run LLM.
- Pas de
schema_version dans to_dict() → prompts non versionnables.
- Pas de vue compacte du snapshot pour l'LLM (les grilles
terrain,
elevation, threatened, ranged_threatened peuvent être volumineuses).
- Pas d'API « actions légales » : la LLM doit inférer la légalité depuis
les helpers de paire (
weapon_for, can_charge, movement_cost_ft…).
- Pas d'historique/mémoire :
LLMPolicy(observation, state) ne reçoit pas
de mémoire inter-tours (seul last_turn_results).
- Pas de journal de replay/évaluation : seuls les transcripts sont capturés
et mis en forme ; pas de log
(observation, actions, résultats) exploitable.
- Signature
LLMPolicy(observation, state) : le README affirme que le
snapshot est la seule source, mais state (vivant) est transmis → à trancher.
3. Architecture cible
Interfaces clés
LLMClient (protocol) : complete(prompt: Prompt) -> str ; implémentations
FakeLLMClient (tests/CI), ProviderClient (réseau, phase 3).
ObservationView.render(observation) -> str : rendu compact et stable
(clés triées, grilles sparsifiées : listes de cases walls/difficult).
legal_actions(view) -> tuple[Action, ...] : sous-ensemble légal (facultatif
mais recommandé pour réduire les rejets).
NLPolicy(observation) -> tuple[Action, ...] : politique branchée au moteur ;
en interne : vue → prompt → client → parse JSON strict → validation →
réparation → repli (default_policy ou wait).
PolicyMemory : notes par combattant portées entre les tours (l'observation
étant sans état), opt-in via configuration.
ReplayRecord : {round, actor, schema_version, prompt_version, view, actions, results} sérialisé en JSONL.
Flux d'un tour
CombatEngine._take_turn construit Observation et appelle NLPolicy.
NLPolicy projette la vue, assemble le prompt (système + stratégie + vue +
rappel des actions légales + derniers résultats), interroge le client
(cache d'abord), parse et valide la séquence d'Action.
- En cas d'actions invalides : boucle de réparation (renvoyer le motif de
rejet) ; au-delà de N essais : repli sûr.
- Le moteur exécute, produit
ActionResult + transcript ; le ReplayLog
enregistre tout.
4. Décisions de conception à trancher
| # |
Décision |
Recommandation |
| D1 |
Entrée de la politique : (observation, state) ou (observation) seule ? |
Passer à (observation) (le state viole le contrat « snapshot seule source ») ; garder un adaptateur de compatibilité. |
| D2 |
Alimenter la LLM avec to_dict() brut ou une vue dédiée ? |
Vue dédiée compacte + versionnée (tokens, stabilité). |
| D3 |
Mémoire inter-tours |
PolicyMemory opt-in (scratchpad borné), sinon politique sans état. |
| D4 |
Format de sortie des actions |
JSON strict (liste d'objets {kind, params}) + validation schéma ; repair loop. |
| D5 |
Exposer legal_actions ? |
Oui : réduit les rejets et sert de garde-fou déterministe. |
| D6 |
Reproductibilité |
Température 0 + ResponseCache (hash de prompt) + log de replay. |
| D7 |
Intégration runner/CLI |
Ajouter llm_policy à run_encounter + un point d'entrée de sweep dédié (module/commande), sans toucher à la CLI par défaut. |
| D8 |
Périmètre tactique v1 |
Sous-ensemble curé d'actions (attaques, sorts, déplacement/destination, 5-ft, défense totale, ready/delay) avant le vocabulaire complet. |
5. Plan d'implémentation
P0 — Fondations (sans LLM) — prérequis
| Tâche |
Livrable |
Critère de sortie |
| P0.1 |
schema_version dans Observation.to_dict() + test du jeu de clés |
gate verte ; version pinnée |
| P0.2 |
ObservationView compacte et déterministe (llm_view.py) + mesure de taille |
test : rendu stable ; taille documentée |
| P0.3 |
legal_actions(view) (réutilise les helpers existants) |
tests unitaires par kind |
| P0.4 |
Paramètre llm_policy sur run_encounter |
test : une politique LLM factice tourne via le runner |
| P0.5 |
ReplayLog JSONL + fonction replay(record) |
test : un run enregistré se rejoue à l'identique |
P1 — Client LLM et politique
| Tâche |
Livrable |
Critère de sortie |
| P1.1 |
LLMClient protocol + FakeLLMClient + ResponseCache |
tests hors ligne |
| P1.2 |
NLPolicy : prompt, parse JSON strict, validation, repair loop, repli |
une bataille complète pilotée par le Fake ; rejets gérés et rejoués |
| P1.3 |
PromptBuilder versionné (système + stratégie + vue + actions) |
golden de prompt (version pinnée) |
| P1.4 |
Tests de robustesse : réponse malformée, action illégale, timeout |
repli sûr, aucun crash |
P2 — Stratégie et balayage
| Tâche |
Livrable |
Critère de sortie |
| P2.1 |
StrategySpec (texte NL + leviers structurés) → politique |
compilation testée |
| P2.2 |
SweepRunner : stratégies × cartes × déploiements |
sweep ≥ 2×2 bout en bout |
| P2.3 |
Métriques : taux de victoire ±3σ, taux d'actions invalides, essais de réparation, tokens/latence/coût |
rapport de sweep produit |
P3 — Intégration fournisseur et rapport (frontière phase 3)
| Tâche |
Livrable |
Critère de sortie |
| P3.1 |
ProviderClient réel (clé via environnement), garde-fous de coût |
run réseau reproductible via cache |
| P3.2 |
Few-shot, versionnement de prompt, validation de schéma renforcée |
taux d'invalidité mesuré et stable |
| P3.3 |
Rapport d'équilibrage (phase 3) alimenté par les métriques |
sortie FR style README |
6. Risques et mitigations
| Risque |
Mitigation |
| Non-déterminisme / coût de la LLM |
température 0, ResponseCache, FakeLLMClient en CI, petits sweeps |
| Dérive des prompts |
schema_version + prompt_version + goldens de prompt |
| Actions invalides fréquentes |
legal_actions + repair loop + repli default_policy |
| Explosion de tokens |
vue compacte + grilles sparsifiées + bornage des listes |
| Évolution du modèle fournisseur |
cache + replays enregistrés + version pinnée |
| Fuite d'état vivant dans la politique |
entrée (observation) seule (D1) + tests d'isolation |
7. Questions ouvertes (à trancher avant P1/P3)
- Fournisseur/modèle : API distante ou modèle local ? (impacte P3, coût, latence, confidentialité)
- Budget : plafond de tokens/coût par sweep ?
- Exigence de reproductibilité : un run LLM doit-il être rejouable bit-à-bit (cache obligatoire) ?
- Périmètre tactique v1 : quelles dimensions en premier (offensive, positionnement, contrôle) ?
- Emplacement des prompts : dans le dépôt (versionnés) ou externes ?
- Entrée de la politique : valide-t-on D1 (
observation seule) ?