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.
This commit is contained in:
@@ -0,0 +1,183 @@
|
|||||||
|
# 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) ?
|
||||||
Reference in New Issue
Block a user