⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 6.3

🧠 Padrões avançados de skills

Skills que chamam skills, máquinas de estado com checkpoints, RAG embutido com vector store local, routers que escolhem o LLM ideal por subtarefa e tratamento sério de side-effects. Saia do hello world e veja como skills crescem em produção sem virar monólitos.

6
Tópicos
~55
Minutos
Avançado
Nível
Eng. skills
Tipo
1

🪆 Composition — Skill A invoca skill B

Skills bem desenhadas são unidades pequenas e focadas. Quando uma tarefa precisa de várias capacidades, você compõe: a skill orquestradora chama outras skills via hermes.invoke(), recebe o resultado e segue. O resultado é um grafo de skills, não um monólito.

💡 Por que compor em vez de inlinar tudo

  • Reuso real: a mesma summarize-pdf serve para skills de email, jurídico e research.
  • Eval isolado: testa fetch-prices sozinha sem rodar a skill pai inteira.
  • Substituição barata: troca a implementação de send-email sem mexer em quem chama.

Exemplo real: skill briefing-diario que compõe outras três skills já existentes:

# ~/.hermes/skills/briefing-diario/skill.py
from hermes import skill, invoke
from datetime import date

@skill(
    name="briefing-diario",
    description="Use when the user asks for the daily briefing, morning summary, or 'what's new today'.",
)
def run(ctx):
    # Cada invoke roda outra skill registrada e retorna structured output
    calendar = invoke("calendar-today", {"user": ctx.user})
    inbox    = invoke("inbox-triage",   {"since": "08:00", "max": 20})
    news     = invoke("news-digest",    {"topics": ctx.prefs["topics"], "n": 5})

    return ctx.render(
        "briefing.md.j2",
        date=date.today().isoformat(),
        calendar=calendar,
        inbox=inbox,
        news=news,
    )

Grafo resultante visualizado pelo CLI:

$ hermes skill graph briefing-diario

briefing-diario
├── calendar-today        (deps: mcp/google-calendar)
├── inbox-triage          (deps: mcp/gmail, skill/summarize-thread)
│   └── summarize-thread  (deps: llm/haiku)
└── news-digest           (deps: tool/rss-fetch, skill/dedupe-articles)
    └── dedupe-articles   (deps: llm/haiku)

5 skills · 4 MCP/tool deps · depth=2 · no cycles ✓

💡 Dica

Rode hermes skill graph <name> antes de publicar. Ele detecta ciclos, mede profundidade e mostra dependências externas. Profundidade > 4 quase sempre indica que você precisa repensar a separação.

⚠️ Cuidado: loop infinito de composition

Se A chama B e B chama A (mesmo indiretamente), você cria um ciclo. O Hermes detecta ciclos diretos via skill graph, mas ciclos condicionais (só sob certo input) escapam. Defina max_depth no invoke() e tenha kill switch: invoke("b", args, max_depth=3).

2

🤖 State machines — Workflows de N passos com estado

Quando a skill precisa de vários passos sequenciais com estado persistente (cada passo lê o output do anterior, pode falhar e retomar), você sai do "uma chamada e responde" e entra em state machine. O Hermes oferece @hermes.step com checkpointing automático.

# ~/.hermes/skills/onboard-customer/skill.py
from hermes import skill, step, invoke

@skill(name="onboard-customer",
       description="Use when onboarding a new B2B customer end-to-end.")
class Onboard:

    @step(order=1)
    def validate_input(self, ctx, state):
        assert state["email"] and "@" in state["email"]
        state["domain"] = state["email"].split("@")[1]
        return state

    @step(order=2, retries=3, backoff=2.0)
    def create_account(self, ctx, state):
        resp = ctx.tools.crm.create_account(
            email=state["email"], domain=state["domain"]
        )
        state["account_id"] = resp["id"]
        return state

    @step(order=3, depends_on=["create_account"])
    def send_welcome(self, ctx, state):
        invoke("send-email", {
            "to": state["email"],
            "template": "welcome-b2b",
            "vars": {"account_id": state["account_id"]},
        })
        state["welcomed_at"] = ctx.now().isoformat()
        return state

    @step(order=4, depends_on=["create_account"], on_error="skip")
    def schedule_kickoff(self, ctx, state):
        slot = invoke("calendar-find-slot", {"duration_min": 30})
        state["kickoff"] = slot
        return state

Em execução, o estado é serializado entre passos. Se send_welcome falhar no passo 3, ao retomar o Hermes lê o checkpoint e pula direto para o passo 3 — account_id já está no estado.

📊 Quando vale state machine

  • 3+ passos com dependências entre si
  • Pelo menos um passo com chamada externa instável (HTTP, fila, banco remoto)
  • Necessidade de retomar após crash sem refazer trabalho
  • Auditoria: quem perguntar "onde está o cliente X?" deve ver o passo atual

💡 Dica

Inspecione o estado de uma run com hermes run inspect <run_id>. Para retomar: hermes run resume <run_id>. O checkpoint fica em ~/.hermes/state/runs/<run_id>.json.

3

📚 RAG dentro de skill — Embedding + vector store local

Skills frequentemente precisam consultar um corpus específico (docs internos, base de produtos, jurisprudência). Embutir RAG na skill — em vez de depender de um MCP externo — dá controle total sobre o chunking, embedding e relevância.

# ~/.hermes/skills/policy-qa/skill.py
import chromadb
from chromadb.utils import embedding_functions
from hermes import skill

EMB = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="all-MiniLM-L6-v2"
)
_client = chromadb.PersistentClient(path=".hermes_data/policy")
_coll = _client.get_or_create_collection(
    name="policies", embedding_function=EMB
)

@skill(
    name="policy-qa",
    description=("Use when the user asks about internal HR, security, "
                 "travel or expense policies."),
)
def run(ctx, question: str):
    hits = _coll.query(query_texts=[question], n_results=5)
    snippets = [
        f"[{m['source']}] {d}"
        for d, m in zip(hits["documents"][0], hits["metadatas"][0])
    ]

    prompt = (
        "Answer ONLY from the policy snippets below. "
        "If unsure, say you don't know.\n\n"
        + "\n---\n".join(snippets)
        + f"\n\nQuestion: {question}"
    )
    return ctx.llm.complete(prompt, model="hermes-3-llama-3.1-70b")


def ingest(path):
    """Run once via:  hermes skill exec policy-qa:ingest ./policies/"""
    import pathlib, uuid
    for f in pathlib.Path(path).glob("*.md"):
        text = f.read_text()
        chunks = [text[i:i+800] for i in range(0, len(text), 600)]
        _coll.add(
            ids=[str(uuid.uuid4()) for _ in chunks],
            documents=chunks,
            metadatas=[{"source": f.name} for _ in chunks],
        )

✓ FAZER em RAG embutido

  • Versionar o índice junto da skill (path relativo)
  • Expor função ingest() separada do run()
  • Retornar fontes (metadados) junto da resposta
  • Setar n_results entre 3 e 8 — mais é ruído

✗ NÃO fazer

  • Re-embedar todo o corpus em cada run()
  • Mandar 50 chunks pro modelo — context blow
  • Chunks de 50 palavras (perde contexto) ou 5000 (perde precisão)
  • Esconder que a resposta veio de RAG — usuário precisa saber
4

🔀 Multi-LLM skills — Router escolhendo modelo por subtarefa

Nem toda subtarefa merece o modelo grande. Classificar intenção, extrair JSON ou reescrever uma linha são tarefas que o modelo pequeno faz por 10% do custo. Skills sérias roteiam internamente: Haiku para triagem, Hermes 70B para o trabalho pesado, GPT-4o para visão.

# ~/.hermes/skills/smart-reply/skill.py
from hermes import skill

MODELS = {
    "classify":   "claude-haiku-4",
    "extract":    "claude-haiku-4",
    "draft":      "hermes-3-llama-3.1-70b",
    "vision":     "gpt-4o-mini",
    "long_form":  "hermes-3-llama-3.1-405b",
}

def pick(task: str, payload: dict) -> str:
    if payload.get("has_image"):     return MODELS["vision"]
    if task == "classify":           return MODELS["classify"]
    if task == "extract":            return MODELS["extract"]
    if payload.get("tokens", 0) > 8000: return MODELS["long_form"]
    return MODELS["draft"]

@skill(name="smart-reply",
       description="Use when drafting a reply to an incoming customer email.")
def run(ctx, email: dict):
    # 1. classify intent — cheap model
    intent = ctx.llm.complete(
        f"Intent (one of: question, complaint, sales, spam)?\n{email['body']}",
        model=pick("classify", email),
        max_tokens=10,
    ).strip().lower()

    if intent == "spam":
        return {"action": "discard"}

    # 2. extract structured facts — cheap model
    facts = ctx.llm.json(
        f"Extract: order_id, sentiment(-1..1), urgency(low/med/high)\n{email['body']}",
        model=pick("extract", email),
    )

    # 3. draft reply — big model, has full context
    draft = ctx.llm.complete(
        ctx.render("reply.j2", email=email, intent=intent, facts=facts),
        model=pick("draft", {**email, "tokens": len(email["body"]) // 4}),
    )

    return {"intent": intent, "facts": facts, "draft": draft}

Tabela de roteamento típica de uma skill madura:

SubtarefaModeloCusto rel.Por quê
Classificar intent (1 label)Haiku 41xOutput curto, decisão simples
Extrair JSON estruturadoHaiku 41xSchema guia o modelo
Redigir resposta naturalHermes 70B8xTom e nuance importam
Síntese de 50k tokensHermes 405B25xContexto longo + raciocínio
Descrever screenshotGPT-4o-mini3xVisão multimodal nativa

📊 Economia real medida

  • 72% de redução de custo em skills smart-reply que rotearam classify/extract para Haiku
  • 3x mais rápido em p50 (Haiku responde em ~400ms vs Hermes 70B ~1.2s)
  • ~0% de perda de qualidade — porque classify não precisava do modelo grande
5

🧪 Skill com side-effects — Idempotência, retries, locks

Toda skill que muda o mundo (manda email, cria registro, debita conta) precisa tratar três coisas: o que acontece se rodar duas vezes (idempotência), o que fazer quando falha no meio (retries) e como evitar duas execuções concorrentes (lock).

# ~/.hermes/skills/charge-customer/skill.py
import hashlib
from hermes import skill, lock, idempotent

@skill(name="charge-customer",
       description="Use to charge a customer for an order. Idempotent by order_id.")
@lock(key="charge:{order_id}", timeout=30)         # 1 execução por vez
@idempotent(key="charge:{order_id}", ttl_days=30)  # mesma chave → mesmo resultado
def run(ctx, order_id: str, amount_cents: int, currency: str = "BRL"):
    # idempotency_key derivado e estável
    ikey = hashlib.sha256(
        f"{order_id}:{amount_cents}:{currency}".encode()
    ).hexdigest()

    return ctx.tools.stripe.charge(
        amount=amount_cents,
        currency=currency,
        metadata={"order_id": order_id},
        idempotency_key=ikey,   # propaga ao provider também
        # retries da própria SDK Stripe lidam com 5xx transitórios
    )

Os 3 decoradores cobrem cenários distintos. Tabela de padrões clássicos:

PadrãoResolveQuando usar
SagaCompensação de passos parcialmente aplicadosWorkflows multi-passo sem transação distribuída
Chain of responsibilityRoteamento entre handlersSkill que tenta N estratégias até uma resolver
MediatorAcoplamento entre N skillsVários produtores/consumidores via event bus
OutboxGarantia at-least-once em side-effectsSempre que perda silenciosa é inaceitável
Circuit breakerCascata de falhasDependência externa flutuando

⚠️ Atenção

Modelos retomam silenciosamente em alguns runtimes — se a skill já criou o registro e o modelo "decide" tentar de novo, sem idempotência você cobra duas vezes. @idempotent + idempotency_key propagado ao provider são obrigatórios em qualquer skill financeira ou que dispare comunicação externa.

6

🎯 Anti-patterns — Skills que viram monólitos

O sintoma é sempre o mesmo: SKILL.md com 500 linhas, run() com 12 ifs aninhados, description tentando descrever 8 capacidades diferentes. O roteador para de conseguir escolher, o eval vira impossível, ninguém quer mexer.

✓ Sinais de skill saudável

  • 1 skill = 1 responsabilidade verbalizável em 1 frase
  • Description começa com "Use when..." e tem < 200 chars
  • SKILL.md cabe em uma tela (~150 linhas)
  • Eval com 5-15 casos cobre 90% do uso
  • Compõe outras skills via invoke()

✗ Sinais de monólito

  • Description com "Use when X, OR Y, OR Z..." (3+ ORs)
  • Mega-skill com 500 linhas e 8 funções privadas
  • run() com switch/if no início roteando "modos"
  • Eval com 50+ casos porque a skill faz tudo
  • Nome genérico tipo helper, utils, do-stuff

Refactor típico de skill monolítica em 3 skills focadas:

1

Antes: email-assistant (520 linhas)

"Use when user wants to triage, summarize, draft a reply, schedule from email, OR archive."

Roteador escolhia a skill em 41% dos casos relevantes. Eval com 87 casos, 22 falhando. Ninguém mais querida tocar.

2

Quebra em 3 skills + 1 orquestradora

inbox-triage · summarize-thread · draft-reply (+ briefing-diario as composer)

Cada uma com description focada, SKILL.md de 80-120 linhas, eval próprio de 8-12 casos.

3

Depois: trigger rate 89%, eval verde

3 semanas após o refactor

Skills passaram a ser reutilizadas por outras orquestradoras (briefing-diario, weekly-review). Custo de manutenção caiu ~60%.

💡 Dica

Regra prática: se você não consegue descrever a skill em uma frase sem usar "ou", ela já é dois skills. Quebre antes de o monólito enraizar.

Resumo do Módulo

Composition: skills pequenas chamando outras via invoke(), profundidade controlada, skill graph antes de publicar.
State machines: @step + checkpointing automático; retoma onde parou; obrigatório quando há 3+ passos com chamadas externas.
RAG embutido: ChromaDB local versionado junto da skill; ingest separado do run; 3-8 chunks; sempre retorne fontes.
Multi-LLM: router por subtarefa economiza ~70% mantendo qualidade — Haiku triagem/extração, Hermes 70B redação, 405B contexto longo.
Side-effects: idempotência por chave estável, lock para concorrência, retries no provider; saga/outbox para garantias fortes.
Anti-monolito: 1 skill = 1 responsabilidade descritível em 1 frase sem "ou"; quebre cedo.

Próximo módulo:

6.4 — 🏗️ Construindo um MCP server próprio. Vamos do zero, em Python e Node, expor uma API interna da empresa para o Hermes. É o caminho mais comum de adoção corporativa.