O VP de produto chega na daily: “Quero que o chatbot responda perguntas sobre nossa documentação interna. Tem 2000 páginas de runbooks, políticas, e procedimentos. O ChatGPT não sabe nada disso.”
O time de ML responde: “Vamos implementar RAG.”
Todo mundo concorda. Você fica com a tarefa de provisionar a infra. Mas antes de subir recursos, vale entender o que RAG realmente faz por dentro.
O mapa pro profissional de infra
| Conceito RAG | O que faz | Equivalente em infra |
|---|---|---|
| Retrieval | Buscar documentos relevantes | Query no search engine |
| Augmentation | Adicionar docs ao prompt do LLM | Montar o payload do request |
| Generation | LLM gera resposta usando o contexto | O response do modelo |
| Chunking | Dividir documentos em pedaços menores | Partition de dados, sharding |
| Indexing pipeline | Processar docs e gerar embeddings | ETL/data pipeline |
| Hybrid search | Combinar busca semântica + keyword | Usar CDN + origin server |
O problema que RAG resolve
LLMs têm duas limitações fundamentais:
- Knowledge cutoff: o modelo só sabe o que viu durante treinamento. Seus runbooks internos não estão lá.
- Context window finito: mesmo que você pudesse colar 2000 páginas no prompt, não caberia (e seria absurdamente caro em tokens).
RAG resolve ambas: busca apenas os trechos relevantes e injeta no prompt. O modelo “vê” a informação necessária sem precisar ter sido treinado nela.
Sem RAG:
Usuário: "Qual o procedimento pra failover do banco?"
LLM: "Em geral, failover envolve..." (resposta genérica, pode estar errada)
Com RAG:
Usuário: "Qual o procedimento pra failover do banco?"
[Sistema busca nos runbooks → encontra o doc "DR-003: Failover PostgreSQL"]
LLM recebe: prompt + conteúdo do doc DR-003
LLM: "Segundo o procedimento DR-003, execute: 1. Verificar replicação..." (resposta específica)
O pipeline completo
RAG tem duas fases: indexação (offline, periódica) e query (online, a cada pergunta).
Fase 1: Indexação (offline)
Documentos: PDFs, wikis, runbooks, tickets, código. Qualquer coisa com texto.
Chunking: dividir documentos em pedaços que cabem no context window. Tipicamente 500-1000 tokens por chunk. Com overlap de 100-200 tokens entre chunks pra não perder contexto na fronteira.
Embedding: cada chunk vira um vetor via modelo de embedding (text-embedding-3-small, por exemplo).
Vector DB: vetores são armazenados no índice pra busca posterior.
Fase 2: Query (online)
- Pergunta do usuário é transformada em embedding
- Vector DB busca os K chunks mais similares (tipicamente 3-10)
- Chunks são inseridos no prompt junto com a pergunta
- LLM gera resposta baseada no contexto fornecido
Implementação prática com Azure AI Search
Vamos montar um pipeline RAG básico. Azure AI Search é a opção managed mais completa porque oferece hybrid search (vector + keyword) que melhora significativamente a qualidade dos resultados.
Passo 1: Criar os recursos
# Criar resource group
az group create --name rg-rag-demo --location eastus2
# Criar Azure AI Search
az search service create \
--name rag-demo-search \
--resource-group rg-rag-demo \
--sku standard \
--partition-count 1 \
--replica-count 1
# Criar Azure OpenAI (pra embeddings e chat)
az cognitiveservices account create \
--name rag-demo-openai \
--resource-group rg-rag-demo \
--kind OpenAI \
--sku S0 \
--location eastus2
# Deploy do modelo de embedding
az cognitiveservices account deployment create \
--name rag-demo-openai \
--resource-group rg-rag-demo \
--deployment-name text-embedding-3-small \
--model-name text-embedding-3-small \
--model-version "1" \
--model-format OpenAI \
--sku-capacity 1 \
--sku-name Standard
# Deploy do modelo de chat
az cognitiveservices account deployment create \
--name rag-demo-openai \
--resource-group rg-rag-demo \
--deployment-name gpt-4o \
--model-name gpt-4o \
--model-version "2024-08-06" \
--model-format OpenAI \
--sku-capacity 1 \
--sku-name Standard
Passo 2: Criar o índice com suporte a vector + text
# Criar índice via REST API
az rest --method PUT \
--url "https://rag-demo-search.search.windows.net/indexes/runbooks?api-version=2024-07-01" \
--headers "Content-Type=application/json" "api-key=<admin-key>" \
--body '{
"name": "runbooks",
"fields": [
{"name": "id", "type": "Edm.String", "key": true, "filterable": true},
{"name": "title", "type": "Edm.String", "searchable": true},
{"name": "content", "type": "Edm.String", "searchable": true},
{"name": "source_file", "type": "Edm.String", "filterable": true},
{"name": "chunk_index", "type": "Edm.Int32", "filterable": true},
{"name": "embedding", "type": "Collection(Edm.Single)",
"searchable": true, "retrievable": false, "stored": false,
"dimensions": 1536, "vectorSearchProfile": "rag-profile"}
],
"vectorSearch": {
"algorithms": [{"name": "hnsw-config", "kind": "hnsw",
"hnswParameters": {"m": 4, "efConstruction": 400, "efSearch": 500, "metric": "cosine"}}],
"profiles": [{"name": "rag-profile", "algorithm": "hnsw-config"}]
}
}'
Passo 3: Chunking e indexação (Python)
import os
from azure.search.documents import SearchClient
from azure.core.credentials import AzureKeyCredential
from openai import AzureOpenAI
# Configuração
search_client = SearchClient(
endpoint="https://rag-demo-search.search.windows.net",
index_name="runbooks",
credential=AzureKeyCredential(os.environ["SEARCH_KEY"])
)
openai_client = AzureOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_key=os.environ["AZURE_OPENAI_KEY"],
api_version="2024-06-01"
)
def chunk_text(text, chunk_size=800, overlap=200):
"""Divide texto em chunks com overlap."""
words = text.split()
chunks = []
start = 0
while start < len(words):
end = start + chunk_size
chunk = " ".join(words[start:end])
chunks.append(chunk)
start = end - overlap
return chunks
def get_embedding(text):
"""Gera embedding via Azure OpenAI."""
response = openai_client.embeddings.create(
input=text,
model="text-embedding-3-small"
)
return response.data[0].embedding
def index_document(file_path, title):
"""Processa e indexa um documento."""
with open(file_path, "r") as f:
content = f.read()
chunks = chunk_text(content)
documents = []
for i, chunk in enumerate(chunks):
doc = {
"id": f"{os.path.basename(file_path)}-{i}",
"title": title,
"content": chunk,
"source_file": file_path,
"chunk_index": i,
"embedding": get_embedding(chunk)
}
documents.append(doc)
search_client.upload_documents(documents=documents)
print(f"Indexado: {title} ({len(chunks)} chunks)")
Passo 4: Query com hybrid search
from azure.search.documents.models import VectorizedQuery
def rag_query(question, top_k=5):
"""Busca documentos relevantes e gera resposta."""
question_vector = get_embedding(question)
# Hybrid search: vector + keyword
results = search_client.search(
search_text=question, # keyword search
vector_queries=[
VectorizedQuery(
vector=question_vector,
k_nearest_neighbors=top_k,
fields="embedding",
kind="vector"
)
],
top=top_k
)
# Montar contexto com os chunks encontrados
context_parts = []
for result in results:
context_parts.append(f"[{result['title']}]\n{result['content']}")
context = "\n\n---\n\n".join(context_parts)
# Gerar resposta com o LLM
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content":
"Responda a pergunta usando APENAS o contexto fornecido. "
"Se a informação não estiver no contexto, diga que não encontrou."},
{"role": "user", "content":
f"Contexto:\n{context}\n\nPergunta: {question}"}
],
temperature=0.1
)
return response.choices[0].message.content
Chunking: a decisão mais subestimada
Chunking parece simples (“divide o texto em pedaços”), mas a estratégia de chunking afeta diretamente a qualidade das respostas.
| Estratégia | Como funciona | Prós | Contras |
|---|---|---|---|
| Fixed size | Divide a cada N tokens | Simples, previsível | Pode cortar no meio de uma frase |
| Sentence-based | Divide em sentenças completas | Mantém coerência | Chunks de tamanho variável |
| Semantic | Agrupa por tópico/seção | Melhor contexto | Mais complexo, precisa de modelo |
| Document structure | Usa headers/sections do doc | Respeita estrutura original | Depende de docs bem formatados |
| Overlap | Chunks compartilham N tokens nas bordas | Não perde contexto na fronteira | Mais storage, mais tokens indexados |
Regra prática: comece com fixed size (800 tokens) + overlap (200 tokens). Refine depois baseado nos resultados.
Hybrid search: por que keyword + vector é melhor que vector sozinho
Busca puramente vetorial tem um problema: termos técnicos específicos (nomes de serviços, códigos de erro, IDs) às vezes não são bem capturados por embeddings. “ERR_AKS_NODEPOOL_SCALE_FAILED” pode ter embedding parecido com qualquer erro de AKS, mas você quer o documento que menciona exatamente esse código.
Hybrid search combina:
- Vector search: encontra documentos semanticamente relacionados
- Keyword search (BM25): encontra documentos com termos exatos
Azure AI Search faz isso nativamente e combina os scores com Reciprocal Rank Fusion (RRF).
Custos em produção
| Componente | Custo aproximado | Escala com |
|---|---|---|
| Azure AI Search (Standard S1) | ~$250/mês por search unit | Número de documentos e queries |
| Embedding generation (indexação) | ~$0.02 por 1M tokens de input | Volume de documentos |
| Embedding generation (query) | Negligível | Queries são curtas |
| LLM (GPT-4o Global Standard) | ~$2.50/1M input, ~$7.50/1M output | Número de queries |
| Storage (embeddings) | Incluído no Search | Dimensão × quantidade |
Pra 10.000 documentos (~50MB de texto), indexar custa ~$5 em embeddings. Servir 1000 queries/dia com 5 chunks cada, ~$15/dia em tokens de LLM.
Problemas comuns e como resolver
“O modelo está alucinando mesmo com RAG”
- Chunks recuperados não são relevantes (problema de retrieval, não de generation)
- Temperature muito alta (baixe pra 0-0.2 pra tarefas factuais)
- System prompt fraco (instrua explicitamente: “responda APENAS com base no contexto”)
“As respostas são genéricas demais”
- Chunks muito grandes (perde especificidade)
- Top-K muito alto (muitos chunks irrelevantes diluem o sinal)
- Falta de metadata filtering (não está filtrando por categoria/data)
“Indexação demora muito”
- Batch as chamadas de embedding (Azure OpenAI aceita até 2048 inputs por request)
- Paralelizar com cuidado no rate limit
- Considere embedding models menores pra prototipação (text-embedding-3-small vs large)
O que levar pra segunda-feira
- RAG não é mágica. É search + LLM. Se o search retorna lixo, o LLM responde com lixo contextualizado.
- Chunking importa mais do que parece. Invista tempo testando estratégias diferentes pro seu tipo de documento.
- Hybrid search > vector-only. Sempre. Especialmente com documentação técnica cheia de termos específicos.
- Monitore retrieval separado de generation. Se o modelo erra, primeiro verifique se os chunks corretos estão sendo recuperados.
- Custo escala com queries, não com documentos. Indexar é barato. Servir milhares de requests com GPT-4o é onde o custo vive.
No próximo post, vamos falar de Context Engineering. Agora que você sabe como buscar informação (RAG), vamos aprender como montar o prompt ideal pra extrair o melhor do modelo.
Leitura complementar
- How RAG Works (Neo Kim, System Design Newsletter)
- Azure AI Search: Retrieval Augmented Generation
- OpenAI Cookbook: RAG best practices