mozak.tech Engenharia de IA Corporativa 33%

Parte I — Operações e Infraestrutura Potencializadas por IA

1.10 — Runbooks Inteligentes: Busca Semântica e Execução Guiada (RAG Runbooks)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar RAG sobre runbooks com embedding de frequência de palavras

Implementar executar_runbook() com substituição de variáveis passo a passo

Construir o pipeline completo: query → busca semântica → contexto → resposta LLM

Comparar embeddings reais (OpenAI/Anthropic) vs embedding simples por frequência

Completar o TODO de executar_runbook no starter

Por que isso importa

Runbooks são o conhecimento operacional da empresa. Um time SRE que trabalha há anos desenvolve procedimentos detalhados para resolver os problemas mais comuns — OOMKilled, banco lento, certificado expirado, disco cheio. Mas esse conhecimento fica preso em wikis que ninguém lembra de consultar durante um incidente às 3h da manhã.

RAG sobre runbooks transforma isso: o SRE pergunta “meu pod está dando OOMKilled” e recebe o procedimento exato da empresa, não uma resposta genérica do LLM que não conhece sua infraestrutura.

Conceitos Fundamentais

Estrutura dos Runbooks

RUNBOOKS = [

    {

        'id': 'RB-001',

        'titulo': 'Resolver OOMKilled em Pods Kubernetes',

        'tags': ['kubernetes', 'oom', 'memoria', 'pod'],

        'passos': [

            '1. Verificar logs: kubectl logs <pod> --previous',

            '2. Checar limits: kubectl describe pod <pod> | grep -A3 Limits',

            '3. Verificar uso de memória: kubectl top pods -n <namespace>',

            '4. Aumentar memory limit no deployment: resources.limits.memory',

        ],

        'causas_comuns': ['memory leak', 'carga inesperada', 'limits muito baixos'],

        'tempo_resolucao_estimado': '15-30 minutos',

    },

]

O titulo + tags + causas_comuns + passos formam o corpus para embedding. Consultas como “pod matando por OOM” devem matchear RB-001.

Embedding por Frequência de Palavras

def embedding_simples(texto: str) -> dict[str, int]:

    palavras = re.findall(r'\b\w+\b', texto.lower())

    freq = {}

    for p in palavras:

        if len(p) > 3:  # filtrar stopwords curtas

            freq[p] = freq.get(p, 0) + 1

    return freq



# 'kubernetes oom memoria pod' → {'kubernetes': 1, 'memoria': 1, 'pod': 1}

Similaridade Cosseno

def similaridade(v1: dict, v2: dict) -> float:

    comum = set(v1.keys()) & set(v2.keys())

    if not comum:

        return 0.0

    dot = sum(v1[k] * v2[k] for k in comum)

    norm1 = sum(v ** 2 for v in v1.values()) ** 0.5

    norm2 = sum(v ** 2 for v in v2.values()) ** 0.5

    return dot / (norm1 * norm2) if norm1 * norm2 > 0 else 0.0

Cosseno mede o ângulo entre vetores de frequência. Duas consultas são similares se usam as mesmas palavras na mesma proporção — independente do tamanho dos textos.

TODO: executar_runbook(runbook_id, contexto)

def executar_runbook(runbook_id: str, contexto: dict) -> str:

    rb = next((r for r in RUNBOOKS if r['id'] == runbook_id), None)

    if not rb:

        return f'Runbook {runbook_id} não encontrado'

    

    from datetime import datetime

    

    resultado = [f'Executando: {rb["titulo"]}']

    resultado.append(f'Início: {datetime.utcnow().isoformat()}')

    resultado.append(f'Contexto: {contexto}')

    resultado.append('')

    

    for i, passo in enumerate(rb['passos'], 1):

        # Substituição de variáveis do contexto

        passo_formatado = passo

        for chave, valor in contexto.items():

            passo_formatado = passo_formatado.replace(f'<{chave}>', str(valor))

            passo_formatado = passo_formatado.replace(f'{{{chave}}}', str(valor))

        

        # Simular execução

        resultado.append(f'[{datetime.utcnow().strftime("%H:%M:%S")}] Passo {i}: {passo_formatado}')

        resultado.append(f'  → [SIMULADO] Executado com sucesso')

    

    resultado.append(f'')

    resultado.append(f'Conclusão: {rb["tempo_resolucao_estimado"]} estimado')

    resultado.append(f'Causas comuns: {", ".join(rb["causas_comuns"])}')

    

    return '\n'.join(resultado)

Substituição de variáveis: <pod> → auth-service-abc12, <namespace> → prod. O contexto dict mapeia nomes de variáveis para valores reais do incidente.

Aprofundamento Técnico

Pipeline RAG Completo

def responder_query(query: str) -> str:

    print(f'\n[RAG] Query: {query}')

    

    # 1. Busca semântica

    runbooks_relevantes = buscar_runbooks(query, top_k=2)

    

    # 2. Construir contexto

    if not runbooks_relevantes:

        contexto = 'Nenhum runbook relevante encontrado.'

    else:

        contexto = '\n\n---\n\n'.join(

            f'Runbook: {r.runbook["titulo"]}\n{r.contexto}'

            for r in runbooks_relevantes

        )

        print(f'Runbooks: {[r.runbook["id"] for r in runbooks_relevantes]}')

    

    # 3. LLM responde com contexto

    response = client.messages.create(

        model='claude-haiku-4-5-20251001',

        max_tokens=500,

        system='Você é um SRE assistente. Use os runbooks fornecidos para responder. Seja direto e específico.',

        messages=[{

            'role': 'user',

            'content': f'RUNBOOKS DISPONÍVEIS:\n{contexto}\n\nPERGUNTA: {query}',

        }],

    )

    return response.content[0].text

O LLM não “conhece” seus runbooks — ele os recebe no contexto a cada query. É isso que diferencia RAG de fine-tuning.

Embedding por Frequência vs Embedding Real

Aspecto

Frequência de palavras

OpenAI/Anthropic embeddings

Custo

Zero

$0.0001 / 1K tokens

Velocidade

Instantâneo

~100ms por chamada

Qualidade semântica

Média

Alta

“OOM” vs “Out of Memory”

Não relaciona

Relaciona semanticamente

Sinônimos

Não detecta

Detecta

Para produção, use embeddings reais. Para lab sem API calls extras, frequência de palavras é suficiente.

Indexação dos Runbooks

def indexar_runbooks(runbooks: list[dict]) -> list[tuple[dict, dict]]:

    return [

        (

            rb,

            embedding_simples(

                rb['titulo'] + ' ' +           # título tem peso alto

                ' '.join(rb['tags']) + ' ' +   # tags repetidas = peso maior

                ' '.join(rb['causas_comuns']) + ' ' +

                ' '.join(rb['passos'])          # passos são densos em termos técnicos

            ),

        )

        for rb in runbooks

    ]



INDICE = indexar_runbooks(RUNBOOKS)  # pré-computado na inicialização

Concatenar título + tags + causas + passos no embedding dá mais peso aos termos que aparecem em múltiplos campos.

Exemplos Anotados

Exemplo 1: Busca com Diferentes Queries

# Query direta

buscar_runbooks('OOMKilled no pod', top_k=1)

# → RB-001 (score alto: 'oom', 'pod', 'kubernetes' na query e no runbook)



buscar_runbooks('banco de dados lento', top_k=1)

# → RB-002 (score alto: 'banco', 'dados', 'lento')



buscar_runbooks('disco cheio servidor produção', top_k=1)

# → RB-004 (score alto: 'disco', 'servidor')



buscar_runbooks('circuit breaker microservice timeout', top_k=1)

# → RB-005 (score alto: 'circuit', 'breaker', 'timeout')

Exemplo 2: TODO executar_runbook com Contexto

# Incidente: auth-service com OOMKilled no namespace prod

contexto = {

    'pod': 'auth-service-abc12',

    'namespace': 'prod',

    'service': 'auth-service',

}



resultado = executar_runbook('RB-001', contexto)

print(resultado)

# → Executando: Resolver OOMKilled em Pods Kubernetes

# → Início: 2024-01-15T14:32:00

# → Contexto: {'pod': 'auth-service-abc12', 'namespace': 'prod'}

# →

# → [14:32:01] Passo 1: Verificar logs: kubectl logs auth-service-abc12 --previous

# →   → [SIMULADO] Executado com sucesso

# → [14:32:01] Passo 2: Checar limits: kubectl describe pod auth-service-abc12 | grep -A3 Limits

# →   → [SIMULADO] Executado com sucesso

# → ...

# → Causas comuns: memory leak, carga inesperada, limits muito baixos

Os <pod> e <namespace> no runbook são substituídos pelos valores reais do contexto.

Padrões e Armadilhas

Padrões

Padrão 1: Índice pré-computado na inicialização

INDICE = indexar_runbooks(RUNBOOKS)  # calculado uma vez ao carregar o módulo

# Na busca:

for rb, emb_rb in INDICE:  # sem recalcular embeddings a cada query

    score = similaridade(embedding_query, emb_rb)

Padrão 2: top_k conservador — menos é mais

runbooks_relevantes = buscar_runbooks(query, top_k=2)

Mais runbooks = contexto maior = LLM pode se confundir. 2-3 runbooks é suficiente para a maioria das queries.

Padrão 3: Substituição de variáveis com fallback

# Substituição não quebra se variável não existe no contexto

passo_formatado = passo.replace(f'<{chave}>', str(valor))

# Passo sem a variável: fica com <namespace> literal — melhor do que crash

Armadilhas

⚠️ Armadilha 1: Embedding sem filtrar stopwords adequadas

if len(p) > 3:  # filtra 'a', 'de', 'em', 'por' mas não 'para', 'como'

# Stopwords aparecem em todos os runbooks → não discriminam

# Melhor: usar lista de stopwords pt-br

⚠️ Armadilha 2: Score zero para todos quando query usa sinônimos

buscar_runbooks('memória esgotada no contêiner')  # 'esgotada' não está nos runbooks

# → score zero para todos → contexto vazio → LLM responde sem runbooks

# Solução: embeddings reais ou expandir sinônimos nos runbooks

⚠️ Armadilha 3: executar_runbook sem validação do contexto

# Se contexto não tem 'pod', o passo fica com <pod> literal

# Adicionar validação:

variaveis_necessarias = re.findall(r'<(\w+)>', ' '.join(rb['passos']))

faltando = [v for v in variaveis_necessarias if v not in contexto]

if faltando:

    return f'Contexto incompleto. Variáveis necessárias: {faltando}'
⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

1 TODO principal no starter.py:

TODO — executar_runbook(runbook_id, contexto):

def executar_runbook(runbook_id: str, contexto: dict) -> str:

    rb = next((r for r in RUNBOOKS if r['id'] == runbook_id), None)

    if not rb:

        return f'Runbook {runbook_id} não encontrado'

    

    linhas = [f'=== Executando: {rb["titulo"]} ===']

    for i, passo in enumerate(rb['passos'], 1):

        passo_fmt = passo

        for k, v in contexto.items():

            passo_fmt = passo_fmt.replace(f'<{k}>', str(v))

        linhas.append(f'[Passo {i}] {passo_fmt}')

        linhas.append(f'  → OK (simulado)')

    

    return '\n'.join(linhas)

Para testar, chame executar_runbook('RB-001', {'pod': 'meu-pod', 'namespace': 'prod'}) e observe a substituição de variáveis.

Rode python starter.py e observe o RAG respondendo às 4 queries de demo.

Agora você está pronto para o lab.