Files
pf1e-simulator/docs/architecture/phase-2-llm-architecture.md
ctan f61fc5c61f docs: reorganize documentation structure
- Clean README.md with installation, usage, CLI, examples
- Move data formats to docs/data-formats/ (map.md, monster.md, pc-sheets.md, index.md)
- Move architecture to docs/architecture/ (index.md, technical-architecture.md, phase-2-llm-architecture.md)
- Add contributing guidelines in docs/contributing.md
- Clean README.md: install, usage, CLI, data-formats/rules/architecture links, contributing link
2026-09-26 20:12:05 +02:00

7.3 KiB
Raw Permalink Blame History

Architecture Phase 2 — Couche tactique LLM

Document consolidé depuis phase-2-llm-architecture.md (historique complet conservé dans l'original à la racine de docs/)


1. Vision générale

La Phase 2 ajoute une couche tactique LLM au simulateur déterministe Phase 0/1 :

  • P5.1 ✅ Fournisseur réel (ProviderClient) — client OpenAI-compatible HTTP, cache, budget, observabilité
  • P5.2 ✅ Few-shot + réparation bornée — prompts externes versionnés, boucle max_repairs tracée par NLPolicyStats
  • P5.3 ✅ Rapporteur LLM local — agrégation stats + rédaction FR (report.py, --llm-report)

Le contrat d'observation (Observation + wrapper LLMPolicy) est en place ; la couche LLM elle-même est désormais implémentée.


2. Contraintes & décisions arbitrées (D-table)

ID Domaine Décision
D1 Économie d'action Résolue par deux étages (budget général + filtres spécialisés)
D2 Cartographie danger Non exhaustive : région d'échec + frontière + sensibilité
D3 Priorité v1 Cartographie d'abord, puis optimiseur (mêmes briques)
D4 Fournisseur/modèle Client agnostique ; implémentation distante d'abord, locale ensuite ; mode JSON/schéma requis
D5 Budget Plafonds petits (≤ 50 appels de compilation/campagne, ≤ 20 combats en vitrine) + coupe-circuit dans le client
D6 Reproductibilité Cache obligatoire ; clé composite incluant le modèle ; replay call_seq
D7 Prompts Externes / dépôt séparé (voir §14)

3. P5.1 — ProviderClient (fournisseur réel)

Fichier : src/pf1e_simulator/provider.py — OpenAILLMClient

  • Speaks OpenAI chat-completions wire format over HTTPS (stdlib urllib.request)
  • Base URL, model, API key résolus depuis l'env (config.py : PF1E_LLM_API_KEY, PF1E_LLM_BASE_URL, PF1E_LLM_MODEL)
  • Aucune dépendance HTTP externe
  • Error taxonomy : ProviderError (transient 429/5xx), ProviderRejectedError (4xx permanent), ProviderResponseError (2xx body invalide)

Safeguards intégrés (dans LLMClient base class)

  • Cache : ResponseCache (clé composite = modèle|params|prompt_version|schema|prompt_hash), sharded, thread-safe
  • Budget : CostBudget (max_calls, max_total_tokens, max_cost_usd) — appliqué avant chaque appel
  • Retry/Backoff : retries (défaut 2), backoff_base_s (0.25s), exponentiel
  • Circuit Breaker : CircuitBreaker (seuil échecs consécutifs, probes après refus)

Interface

class LLMClient(ABC):
    def __init__(self, *, model: str, prompt_version: str, ...)
    def complete(self, prompt: str, params: ClientParams | None = None) -> str

prompt_version = version:name:hash12 (pack version + nom prompt + hash contenu) — requis pour validation cache D6.


4. P5.2 — Few-shot + réparation bornée

PromptPack externe (§14)

  • Répertoire pointé par PF1E_PROMPT_PACK_DIR (hors repo)
  • Layout : version.txt + <name>.md + <name>.examples.json (optionnel)
  • PromptPack.load(root) : strict, pas de fallback, lève PromptPackError si invalide
  • prompt_version = version:name:hash12 (hash couvre prompt + exemples)

NLPolicy

@dataclass(frozen=True)
class NLPolicy:
    client: LLMClient
    builder: PromptBuilder
    prompt_name: str = "nl_default"
    fallback: Fallback = fallback_default
    max_repairs: int = 1
    stats: NLPolicyStats = field(default_factory=NLPolicyStats)

def __call__(self, observation: Observation) -> tuple[Action, ...]:
    # 1. build prompt via PromptBuilder
    # 2. client.complete(prompt)
    # 3. parse_actions() → tuple[Action]
    # 4. si échec → retry jusqu'à max_repairs (injecte erreur + instruction)
    # 5. fallback scripté si épuisé
  • NLPolicyStats : calls, repairs_attempted, repairs_succeeded, fallbacks — « matière des rapports de P5.3 »
  • FakeLLMClient pour tests 100% offline (script de réponses, zéro réseau)

5. P5.3 — Rapporteur LLM local (Phase 3)

Module : src/pf1e_simulator/report.py

Structures

  • ReportContext : agrège EncounterReport, DangerMetrics, NLPolicyStats, ClientStats, MatchupBenchmark, DangerCell, SensitivityEntry
  • build_report_context() : fonction pure, calcule DangerMetrics depuis RunOutcome si fourni
  • build_report_prompt(ctx, template) : rend rapport factuel + danger + robustesse + observabilité LLM
  • LLMReportPolicy : distinct de NLPolicy (sortie texte libre, retry simple, ReportStats)
  • ReportBudget : plafonds vitrine (max_calls=1, ajusté pour retries via for_retries())
  • format_llm_report(ctx, llm_text) : préfixe factuel + === Récit d'équilibrage (LLM) === + narratif

CLI

  • --llm-report : active mode LLM (exclusif avec --transcript)
  • --output FILE.md : écrit rapport complet (factuel + LLM) dans fichier
  • --llm-model, --llm-base-url, --llm-api-key : overrides CLI
  • Exige PF1E_LLM_API_KEY + PF1E_PROMPT_PACK_DIR env

Pack FR

  • tests/fixtures/prompt_pack_fs/report.md : narrateur FR, structure 4 sections (Bilan, Danger, Robustesse, Observabilité)
  • report.examples.json : 2 exemples few-shot
  • Découvert via PromptPack comme nl_default

6. Risques & mitigations

Risque Impact Mitigation
Bandes 3σ trompeuses à petit n Mauvaises conclusions quantitatives Étage A pour le quantitatif (formule fermée)
Événements rares (TPK) Monte Carlo naïf inefficace Ciblage par scénarios, cartographie danger
Explosion combinatoire Énumération impossible Coarse-to-fine (danger_map, frontier, sensitivity)
Prompt trop long Coût/tokens élevés Vue compacte ~4k tokens, budget ≤ 50 appels
Hallucination chiffres Rapport non fiable Injection stats verbatim, pas d'invention
Tests flaky (texte libre) CI instable FakeLLMClient scripté + assertions structure

7. Intégration CLI (P5.3)

# Variables d'env requises
export PF1E_LLM_API_KEY=sk-...
export PF1E_PROMPT_PACK_DIR=/chemin/vers/prompt-pack

# Rapport LLM + sortie fichier
uv run pf1e-sim \
  --map data/maps/sample_arena.yaml \
  --side players data/monsters/goblin.json \
  --side monsters data/monsters/orc.json \
  --runs 100 --seed 1 \
  --llm-report --output rapport.md

Options ajoutées :

  • --llm-report : active LLM (exclusif --transcript)
  • --output FILE.md : écrit rapport complet (factuel + LLM)
  • --llm-model, --llm-base-url, --llm-api-key : overrides

8. Tests & Gate

Nouveaux tests (27)

  • test_report_context.py (4) — ReportContext + build_report_context
  • test_report_prompt_pack.py (3) — prompt pack version/render
  • test_llm_report_policy.py (4) — LLMReportPolicy + retry/fallback
  • test_format_llm_report.py (4) — format_llm_report empty/with narrative
  • test_cli_llm_report.py (4) — flags mutuellement exclusifs, env req
  • test_report_budget.py (8) — ReportBudget, CostBudget, retries

Gate complète

uv run pytest -q
uv run ruff check src tests
uv run basedpyright src

9. Liens transverses