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 faz que sim com a cabeça. Você fica com a tarefa de provisionar a infra. Antes de subir recurso, vale entender o que RAG realmente faz por dentro.
tl;dr: RAG é pipeline de indexação + retrieval + prompt. O que mais quebra em produção é chunk ruim, busca ruim e custo do LLM, não falta de fine-tuning.
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" \
--skip-authorization-header \
--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
import tiktoken
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
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-10-21"
)
TOKENIZER = tiktoken.get_encoding("cl100k_base")
def chunk_text(text, chunk_size=800, overlap=200):
"""Divide texto em chunks aproximados por token."""
if overlap >= chunk_size:
raise ValueError("overlap precisa ser menor que chunk_size")
token_ids = TOKENIZER.encode(text)
chunks = []
start = 0
step = chunk_size - overlap
while start < len(token_ids):
end = start + chunk_size
chunk = TOKENIZER.decode(token_ids[start:end])
chunks.append(chunk)
start += step
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", encoding="utf-8") as f:
content = f.read()
chunks = chunk_text(content)
documents = []
for i, chunk in enumerate(chunks):
documents.append(
{
"id": f"{os.path.basename(file_path)}-{i}",
"title": title,
"content": chunk,
"source_file": file_path,
"chunk_index": i,
"embedding": get_embedding(chunk),
}
)
search_client.upload_documents(documents=documents)
print(f"Indexado: {title} ({len(chunks)} chunks)")
Passo 4: Query com hybrid search
def rag_query(question, top_k=5):
"""Busca documentos relevantes e gera resposta."""
question_vector = get_embedding(question)
vector_query = VectorizedQuery(
vector=question_vector,
kind="vector",
fields="embedding",
k_nearest_neighbors=top_k,
)
# Hybrid search: vector + keyword
results = search_client.search(
search_text=question,
vector_queries=[vector_query],
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 com base 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).
Em pipeline mais caprichado, o top-K inicial ainda passa por reranking. Pode ser o semantic ranker do Azure AI Search ou um cross-encoder separado. A busca vetorial abre o leque. O reranker decide quais chunks realmente merecem entrar no prompt.
Custos em produção
| Componente | Como costuma pesar | Escala com |
|---|---|---|
| Azure AI Search (Standard S1) | Custo fixo por search unit | Número de documentos e queries |
| Embedding generation (indexação) | Custo pontual, normalmente baixo | Volume de documentos |
| Embedding generation (query) | Custo baixo por request | Número de queries |
| LLM | Geralmente domina a conta | Tokens de input e output |
| Storage (embeddings) | Cresce com o corpus, mas raro ser o vilão | Dimensão × quantidade |
Em muito projeto, indexar é a parte barata. A conta que cresce de verdade costuma ser o LLM respondendo consulta o dia inteiro.
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”
- Agrupe as chamadas de embedding (o limite costuma ser 2.048 inputs por request, além do teto total de tokens)
- Paralelize com cuidado por causa do rate limit
- Considere modelos 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 quase sempre ganha de vector-only. Especialmente com documentação técnica cheia de termos específicos.
- Monitore retrieval separado de generation. Se o modelo erra, primeiro veja se os chunks certos estão sendo recuperados.
- Custo escala com queries, não com documentos. Indexar costuma ser barato. Servir milhares de requests com GPT-4o é onde a conta cresce.
Se retrieval, reranking e prompt estão bem encaixados, você volta pra daily com um plano claro: indexar os runbooks, recuperar só o que importa e responder sem inventar. A buzzword some. Fica um pipeline operável.
Leitura complementar
- How RAG Works (Neo Kim, System Design Newsletter)
- Azure AI Search: Retrieval Augmented Generation
- OpenAI Cookbook: RAG best practices