Files
pf1e-simulator/docs/phase-2-llm-architecture.md
T
ctan be64451ba4 docs(phase2): architecture and implementation plan for the LLM decision layer
Design-only document (no code): target architecture (compact versioned
observation view, legal-actions API, LLM client + cache, NL policy with
parse/validate/repair/fallback, strategy sweep, replay/eval harness), the
concrete gaps in the current runner/CLI wiring, the decisions to settle and a
phased implementation plan (P0 foundations, P1 client+policy, P2 strategy
sweep, P3 provider+report) with exit criteria and risks.
2026-09-21 09:51:05 +02:00

12 KiB
Raw Blame History

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

  1. 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.
  2. 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.
  3. Pas de schema_version dans to_dict() → prompts non versionnables.
  4. Pas de vue compacte du snapshot pour l'LLM (les grilles terrain, elevation, threatened, ranged_threatened peuvent être volumineuses).
  5. 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…).
  6. Pas d'historique/mémoire : LLMPolicy(observation, state) ne reçoit pas de mémoire inter-tours (seul last_turn_results).
  7. Pas de journal de replay/évaluation : seuls les transcripts sont capturés et mis en forme ; pas de log (observation, actions, résultats) exploitable.
  8. Signature LLMPolicy(observation, state) : le README affirme que le snapshot est la seule source, mais state (vivant) est transmis → à trancher.

3. Architecture cible

                        ┌──────────────────────────────────────────┐
                        │              Moteur (inchangé)           │
                        │  CombatEngine : Observation, Action,     │
                        │  ActionResult, RNG seedé, règles SRD     │
                        └───────────────┬──────────────────────────┘
                                        │ observation (snapshot figé)
                                        ▼
        ┌──────────────────────────────────────────────────────────────┐
        │                    Couche Phase 2 (nouvelle)                 │
        │                                                              │
        │  view.py        ObservationView : projection compacte,       │
        │                 déterministe, versionnée (schema_version)    │
        │                                                              │
        │  legality.py    legal_actions(view) : actions légales         │
        │                 (réutilise les helpers du moteur)            │
        │                                                              │
        │  prompt.py      PromptBuilder : système + stratégie + vue +  │
        │                 vocabulaire d'actions + version de prompt    │
        │                                                              │
        │  client.py      LLMClient (protocol) : complete(prompt)->str │
        │                 + ResponseCache(prompt_hash)                 │
        │                 + FakeLLMClient (CI, déterministe)           │
        │                                                              │
        │  policy.py      NLPolicy : build → call → parse → validate   │
        │                 → repair (N essais) → fallback sûr           │
        │                 + PolicyMemory (notes inter-tours, opt-in)   │
        │                                                              │
        │  strategy.py    StrategySpec (texte NL + leviers) → politique│
        │  sweep.py       SweepRunner : stratégies × cartes ×          │
        │                 déploiements → métriques agrégées            │
        │  replay.py      ReplayLog (JSONL) + replay déterministe      │
        └──────────────────────────────────────────────────────────────┘

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

  1. CombatEngine._take_turn construit Observation et appelle NLPolicy.
  2. 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.
  3. En cas d'actions invalides : boucle de réparation (renvoyer le motif de rejet) ; au-delà de N essais : repli sûr.
  4. 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)

  1. Fournisseur/modèle : API distante ou modèle local ? (impacte P3, coût, latence, confidentialité)
  2. Budget : plafond de tokens/coût par sweep ?
  3. Exigence de reproductibilité : un run LLM doit-il être rejouable bit-à-bit (cache obligatoire) ?
  4. Périmètre tactique v1 : quelles dimensions en premier (offensive, positionnement, contrôle) ?
  5. Emplacement des prompts : dans le dépôt (versionnés) ou externes ?
  6. Entrée de la politique : valide-t-on D1 (observation seule) ?