# LLM como coach de estratégia

> Entregue a um LLM as decisões "para o que economizar, onde colocar trabalhadores, atacar ou segurar neste minuto" e deixe a camada de regras apenas executar e vetar. O cérebro de referência já funciona assim; esta página explica o padrão e as armadilhas.

Fonte: https://war3ai.com/pt/docs/llm-coach/

Quando seu Bot passa de certo tamanho, você percebe que as regras de economia vão sendo empilhadas umas sobre as outras: uma regra para quantos lenhadores, uma para 5 trabalhadores por mina, uma para cortar a madeira pela metade quando sobra, uma para mandar mais gente ao ouro quando falta ouro e sobra madeira… Cada regra está certa isoladamente, mas juntas produzem situações como "a mina está sem trabalhadores enquanto todos os camponeses cortam árvores" — situações pelas quais **nenhuma regra é responsável**.

Julgamentos do tipo "olhar o quadro geral e definir prioridades" nunca se encaixaram bem em `if / else`, mas são exatamente o que LLMs fazem bem. O cérebro de referência (`brains/xwar3/strategy/brain/coach.py`) usa as camadas abaixo.

## Camadas

```text
LLM (conselheiro)       Uma vez a cada 20 segundos de jogo, assíncrono, nunca bloqueia um tick
  Entrada: um snapshot de uma página da partida (recursos, comida, distribuição de camponeses, minas, tipos de unidade, tecnologia, heróis, informações do inimigo, eventos recentes)
  Saída: JSON estrito — um diagnóstico de uma frase + divisão de trabalhadores + o que produzir primeiro + a postura deste minuto + o que evitar
        │
        ▼  lista de permissões + limites mín./máx. + veto
Camada de regras (Bot, a cada tick)  Traduz o conselho em "vieses" sobre capacidades existentes: divisão de trabalhadores, prioridade de construção / treino, postura de ataque
        │
        ▼
Camada de execução (SDK / camada reflexa)  Dá ordens, lê recibos, faz micro
```

## Contrato de saída

Faça o modelo produzir JSON com um conjunto fixo de campos — sem acrescentar nem remover nenhum:

```json
{
  "diagnosis": "Uma frase: o maior problema da partida, que precisa ter base nos dados de entrada",
  "workers":   { "gold": 10, "lumber": 6 },
  "priority":  ["hpea", "hhou", "hbar"],
  "posture":   "creep",
  "avoid":     ["Não pesquise Placas de Ferro primeiro quando faltar madeira"]
}
```

| Campo | Como a camada de regras usa | Limites do cérebro de referência |
|---|---|---|
| `workers` | Número alvo de trabalhadores no ouro e na madeira | Ouro 2 ~ 25, madeira 1 ~ 20; a soma não pode passar do total de camponeses |
| `priority` | Ordem de prioridade de treino / construção / pesquisa | No máximo 4; só aceita códigos de quatro caracteres que aparecem na tabela de "códigos permitidos" |
| `posture` | A postura deste minuto | Precisa ser um de `attack` `defend` `creep` `expand` `recover` `hold` |
| `avoid` | O que não fazer neste minuto | No máximo 2 |
| `diagnosis` | Usado só para logs e para exibição no console | — |

Use um prompt por raça, cobrindo apenas as escolhas específicas daquela raça (a construção cooperativa e a Milícia dos Humanos, as Tocas dos Orcs, a Mina de Ouro Assombrada dos Mortos-vivos, a Mina de Ouro Enredada dos Elfos Noturnos…). Coloque as regras comuns em uma parte compartilhada — não copie tudo quatro vezes.

## Quatro restrições rígidas

O cérebro de referência aprendeu cada uma delas do jeito difícil:

1. **O conselheiro nunca dá ordens diretas a unidades.** Ele não enxerga o que acontece na escala de 150 ms e alucina. Ele só muda metas e prioridades; quem vai para onde e quem ataca o quê continua sendo decidido pela camada de regras e pela camada reflexa — o comando só pode ter um dono.
2. **Assíncrono.** Uma consulta ao conselheiro leva cerca de 1 segundo e roda em uma thread em segundo plano; vale o resultado mais recente, e ela **nunca bloqueia um tick**. Se o modelo não subiu, estourou o tempo ou respondeu lixo, aja como se essa camada não existisse e volte às regras puras. Conselhos antigos demais (mais de 3 intervalos) também não são usados.
3. **Lista de permissões + limites.** Todo campo precisa corresponder a uma capacidade existente, e os valores numéricos são limitados a uma faixa razoável. Conteúdo não reconhecido é **contado e depois descartado**, não ignorado em silêncio.
4. **Conte tudo.** Quantas vezes você perguntou, quantas deram certo, quantas estouraram o tempo, quantas foram limitadas, quantas vezes cada campo foi adotado — publique tudo isso junto com a última entrada enviada ao modelo. Caso contrário, "essa camada serve para alguma coisa?" vira uma pergunta sem resposta.

> **O que degrada com segurança é o que mais degrada em silêncio**
>
> O conselheiro foi projetado para que "falha = agir como se essa camada não existisse"; por isso, quando o serviço do modelo não está no ar, o Bot se comporta exatamente como as regras puras e nada parece errado visto de fora. O cérebro de referência já passou um dia inteiro com o conselheiro sem conseguir se conectar em todas as 6 instâncias, sem que ninguém percebesse. Sempre publique "horário da última chamada bem-sucedida" e "motivo da última falha" — é exatamente para isso que existe a página "coach de estratégia" no [console Farsight](https://war3ai.com/pt/docs/console/).

## Implementando no seu próprio Bot

Abaixo está um esqueleto mínimo que funciona com qualquer API compatível com OpenAI (LM Studio, Ollama ou uma API na nuvem) e usa só a biblioteca padrão:

```python title="coached_bot.py"
import collections, json, threading, urllib.request
from openwar3 import Bot

BASE = "http://127.0.0.1:1234/v1"            # LM Studio / Ollama / qualquer serviço compatível com OpenAI
MODEL = "your-model"
POSTURES = {"attack", "defend", "creep", "expand", "recover", "hold"}
SYSTEM = """Você é um coach de macro de Warcraft III. Cuida só de economia e estratégia, não de micro.
Produza apenas JSON, com campos fixos: {"diagnosis": uma frase, "workers": {"gold": inteiro, "lumber": inteiro},
"priority": [códigos de quatro caracteres, no máximo 4, só os de allowed], "posture": um dos seis, "avoid": [no máximo 2]}
Baseie tudo nos dados da partida que você recebeu; não invente nada que não esteja nos dados."""

def ask(state: dict) -> dict:
    body = {"model": MODEL, "temperature": 0.3, "max_tokens": 260,
            "messages": [{"role": "system", "content": SYSTEM},
                         {"role": "user", "content": json.dumps(state, ensure_ascii=False)}]}
    req = urllib.request.Request(f"{BASE}/chat/completions", json.dumps(body).encode(),
                                 {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=8) as r:
        text = json.load(r)["choices"][0]["message"]["content"]
    return json.loads(text[text.index("{"): text.rindex("}") + 1])

class CoachedBot(Bot):
    EVERY = 20.0                              # segundos de jogo: decisões de macro acontecem na escala de minutos, não precisa perguntar a cada tick
    allowed = {"hpea", "hfoo", "hrif", "hkni", "hhou", "hbar", "hbla", "Rhme", "Rhar"}

    def on_start(self, g):
        self.plan, self.plan_at, self.asked_at, self.busy = {}, -1e9, -1e9, False
        self.stats = collections.Counter()

    def summary(self, g) -> dict:             # lê o snapshot na thread principal; a thread em segundo plano nunca toca em g
        res = g.resources() or {}
        return {"clock": round(g.clock() or 0), "gold": res.get("gold"), "lumber": res.get("lumber"),
                "food": [res.get("food_used"), res.get("food_cap")],
                "workers": len(g.my_workers()), "idle_workers": len(g.idle_workers()),
                "army": collections.Counter(u.type for u in g.my_army()),
                "enemy_seen": collections.Counter(u.type for u, _t, _age in g.last_seen(max_age=90)),
                "night": g.is_night(), "allowed": sorted(self.allowed)}

    def consult(self, state, now):
        try:
            p = ask(state)
            self.stats["ok"] += 1
            posture = p.get("posture")
            if posture not in POSTURES:
                self.stats["bad_posture"] += 1                       # conta e descarta — nunca em silêncio
                posture = "hold"
            self.plan = {"gold": min(25, max(2, int(p["workers"]["gold"]))),      # limita
                         "lumber": min(20, max(1, int(p["workers"]["lumber"]))),
                         "priority": [c for c in p.get("priority", []) if c in self.allowed][:4],
                         "posture": posture}
            self.plan_at = now
        except Exception as e:                                       # timeout / resposta lixo: age como se a camada não existisse
            self.stats[f"error:{type(e).__name__}"] += 1
        finally:
            self.busy = False

    def on_tick(self, g):
        now = g.clock() or 0.0
        if not self.busy and now - self.asked_at >= self.EVERY:
            self.busy, self.asked_at = True, now
            threading.Thread(target=self.consult, args=(self.summary(g), now), daemon=True).start()
        plan = self.plan if now - self.plan_at <= 3 * self.EVERY else {}   # não usa conselhos antigos demais
        # ↓ camada de regras: com plan vazio, segue as regras padrão; com plan, só ajusta divisão, prioridades e postura — as ordens em si continuam decididas pelas regras
        ...
```

## Como escolher o modelo

| Situação | Recomendação |
|---|---|
| Local, precisa ser rápido | Modelos MoE (que ativam só uma pequena parte dos parâmetros por chamada) são muito mais rápidos que modelos densos do mesmo tamanho. O cérebro de referência usa Qwen3.6-35B-A3B (LM Studio, Q4): mediana de **1.09 s**, a mais lenta 1.45 s, e 5/5 saídas passam direto por `json.loads` |
| Modelos locais que "pensam" | **Você precisa desligar a seção de raciocínio**, senão todos os tokens vão para o raciocínio e nenhum JSON sai. O LM Studio ignora `/no_think`; o cérebro de referência passou a usar `/v1/completions`, monta o ChatML por conta própria e preenche antecipadamente um `<think></think>` vazio seguido de um `{` |
| Modelos na nuvem | A latência costuma ser maior, mas essas camadas já são assíncronas por design; decisões de macro se medem em minutos, então alguns segundos de latência são aceitáveis |

> **Nota**
>
> O mesmo modelo também pode dar voz às suas unidades: veja [Balões de fala e modelos locais](https://war3ai.com/pt/docs/speech/). Se você quer que o modelo dê ordens diretamente a cada tick (em vez de atuar como conselheiro), aguarde o gateway JSON da [Arena](https://war3ai.com/pt/arena/).
