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 RAGO que fazEquivalente em infra
RetrievalBuscar documentos relevantesQuery no search engine
AugmentationAdicionar docs ao prompt do LLMMontar o payload do request
GenerationLLM gera resposta usando o contextoO response do modelo
ChunkingDividir documentos em pedaços menoresPartition de dados, sharding
Indexing pipelineProcessar docs e gerar embeddingsETL/data pipeline
Hybrid searchCombinar busca semântica + keywordUsar CDN + origin server

O problema que RAG resolve

LLMs têm duas limitações fundamentais:

  1. Knowledge cutoff: o modelo só sabe o que viu durante treinamento. Seus runbooks internos não estão lá.
  2. 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)

Pipeline de indexação do RAGDocumentos(source)Chunking(split)Embedding(model)Vector DB(index)Pedaços de500-1000 tokensVetor pracada pedaço

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)

Pipeline de query do RAGPerguntado userEmbedding(query)Vector DB(search)Top KchunksPrompt:sistema +chunks +perguntaLLMResposta
  1. Pergunta do usuário é transformada em embedding
  2. Vector DB busca os K chunks mais similares (tipicamente 3-10)
  3. Chunks são inseridos no prompt junto com a pergunta
  4. LLM gera resposta baseada no contexto fornecido

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)")
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égiaComo funcionaPrósContras
Fixed sizeDivide a cada N tokensSimples, previsívelPode cortar no meio de uma frase
Sentence-basedDivide em sentenças completasMantém coerênciaChunks de tamanho variável
SemanticAgrupa por tópico/seçãoMelhor contextoMais complexo, precisa de modelo
Document structureUsa headers/sections do docRespeita estrutura originalDepende de docs bem formatados
OverlapChunks compartilham N tokens nas bordasNão perde contexto na fronteiraMais 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

ComponenteComo costuma pesarEscala com
Azure AI Search (Standard S1)Custo fixo por search unitNúmero de documentos e queries
Embedding generation (indexação)Custo pontual, normalmente baixoVolume de documentos
Embedding generation (query)Custo baixo por requestNúmero de queries
LLMGeralmente domina a contaTokens de input e output
Storage (embeddings)Cresce com o corpus, mas raro ser o vilãoDimensã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