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.0Cosseno 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].textO 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çãoConcatenar 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 baixosOs <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 crashArmadilhas
⚠️ 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}'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.