f61fc5c61f
- 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
7.3 KiB
7.3 KiB
Architecture Phase 2 — Couche tactique LLM
Document consolidé depuis
phase-2-llm-architecture.md(historique complet conservé dans l'original à la racine dedocs/)
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_repairstracée parNLPolicyStats - 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èvePromptPackErrorsi invalideprompt_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 »FakeLLMClientpour 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ègeEncounterReport,DangerMetrics,NLPolicyStats,ClientStats,MatchupBenchmark,DangerCell,SensitivityEntrybuild_report_context(): fonction pure, calculeDangerMetricsdepuisRunOutcomesi fournibuild_report_prompt(ctx, template): rend rapport factuel + danger + robustesse + observabilité LLMLLMReportPolicy: distinct deNLPolicy(sortie texte libre, retry simple,ReportStats)ReportBudget: plafonds vitrine (max_calls=1, ajusté pour retries viafor_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_DIRenv
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
PromptPackcommenl_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_contexttest_report_prompt_pack.py(3) — prompt pack version/rendertest_llm_report_policy.py(4) —LLMReportPolicy+ retry/fallbacktest_format_llm_report.py(4) —format_llm_reportempty/with narrativetest_cli_llm_report.py(4) — flags mutuellement exclusifs, env reqtest_report_budget.py(8) —ReportBudget,CostBudget, retries
Gate complète
uv run pytest -q
uv run ruff check src tests
uv run basedpyright src