Configurar RAG
Guia completo para configurar Retrieval-Augmented Generation. Vector store, embeddings, indexação e busca híbrida.
Visão Geral
RAG (Retrieval-Augmented Generation) combina busca semântica com geração de texto para produzir respostas fundamentadas em documentos específicos. Este guia mostra como configurar um pipeline RAG completo usando a Polar.
Arquitetura
Documentos → Chunking → Embeddings (urso-embed) → Vector Store
↓
Consulta → Embedding → Busca Híbrida (BM25 + Semântica) → Reranking → LLM (urso-mabe)Passo 1: Escolher Vector Store
A Polar suporta dois vector stores:
Qdrant (Recomendado)
Otimizado para busca vetorial de alta performance.
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
client = QdrantClient(host="localhost", port=6333)
# Criar coleção
client.create_collection(
collection_name="meus_documentos",
vectors_config=VectorParams(
size=1024, # Dimensão do urso-embed
distance=Distance.COSINE
)
)OpenSearch
Para quem já usa OpenSearch (ou Elasticsearch) ou precisa de busca por texto completo.
from opensearchpy import OpenSearch
es = OpenSearch("http://localhost:9200")
# Criar índice com mapeamento para vetores
es.indices.create(
index="meus_documentos",
body={
"mappings": {
"properties": {
"content": {"type": "text", "analyzer": "portuguese"},
"embedding": {
"type": "dense_vector",
"dims": 1024,
"index": True,
"similarity": "cosine"
},
"metadata": {"type": "object"}
}
}
}
)Passo 2: Criar Embeddings com urso-embed
from openai import OpenAI
client = OpenAI(
base_url="https://api.polarai.com.br/v1",
api_key="pk-sua-chave-aqui"
)
def get_embedding(text: str) -> list[float]:
response = client.embeddings.create(
model="urso-embed",
input=text
)
return response.data[0].embedding
# Exemplo
embedding = get_embedding("Direito do consumidor brasileiro")
print(f"Dimensões: {len(embedding)}") # 1024Embeddings em Lote
def get_embeddings_batch(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(
model="urso-embed",
input=texts
)
return [item.embedding for item in response.data]
# Processar em lotes de 100
texts = ["texto 1", "texto 2", ..., "texto 1000"]
batch_size = 100
all_embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
embeddings = get_embeddings_batch(batch)
all_embeddings.extend(embeddings)Passo 3: Indexar Documentos
Chunking
Divida documentos grandes em chunks gerenciáveis:
def chunk_text(text: str, chunk_size: int = 512, overlap: int = 50) -> list[str]:
"""Divide texto em chunks com sobreposição."""
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = " ".join(words[i:i + chunk_size])
if chunk:
chunks.append(chunk)
return chunksIndexar no Qdrant
from qdrant_client.models import PointStruct
import uuid
def index_document(doc_text: str, metadata: dict):
chunks = chunk_text(doc_text)
embeddings = get_embeddings_batch(chunks)
points = [
PointStruct(
id=str(uuid.uuid4()),
vector=embedding,
payload={
"content": chunk,
"metadata": metadata
}
)
for chunk, embedding in zip(chunks, embeddings)
]
client.upsert(
collection_name="meus_documentos",
points=points
)
# Indexar um documento
index_document(
doc_text="Conteúdo completo do documento aqui...",
metadata={"source": "manual.pdf", "category": "tributário"}
)Indexar no Elasticsearch
def index_document_es(doc_text: str, metadata: dict):
chunks = chunk_text(doc_text)
embeddings = get_embeddings_batch(chunks)
for chunk, embedding in zip(chunks, embeddings):
es.index(
index="meus_documentos",
body={
"content": chunk,
"embedding": embedding,
"metadata": metadata
}
)Passo 4: Busca Híbrida (BM25 + Semântica)
A busca híbrida combina busca por palavras-chave (BM25) com busca semântica vetorial para resultados superiores:
Com Qdrant
from qdrant_client.models import Filter, FieldCondition, MatchValue
def hybrid_search(query: str, limit: int = 10) -> list:
query_embedding = get_embedding(query)
# Busca semântica
results = client.search(
collection_name="meus_documentos",
query_vector=query_embedding,
limit=limit,
score_threshold=0.5
)
return [
{
"content": hit.payload["content"],
"score": hit.score,
"metadata": hit.payload["metadata"]
}
for hit in results
]Com Elasticsearch (Busca Híbrida Nativa)
def hybrid_search_es(query: str, limit: int = 10) -> list:
query_embedding = get_embedding(query)
results = es.search(
index="meus_documentos",
body={
"size": limit,
"query": {
"bool": {
"should": [
# BM25 (palavras-chave)
{
"match": {
"content": {
"query": query,
"boost": 0.3
}
}
},
# KNN (semântico)
{
"knn": {
"field": "embedding",
"query_vector": query_embedding,
"num_candidates": 50,
"boost": 0.7
}
}
]
}
}
}
)
return [
{
"content": hit["_source"]["content"],
"score": hit["_score"],
"metadata": hit["_source"]["metadata"]
}
for hit in results["hits"]["hits"]
]Passo 5: Reranking
Após a busca, aplique reranking para refinar a ordem dos resultados:
def rerank(query: str, documents: list, top_k: int = 5) -> list:
"""Rerank usando o modelo urso como cross-encoder."""
scored_docs = []
for doc in documents:
response = client.chat.completions.create(
model="urso-mabe",
messages=[
{
"role": "system",
"content": "Avalie a relevância do documento para a consulta. "
"Responda apenas com um número de 0 a 10."
},
{
"role": "user",
"content": f"Consulta: {query}\n\nDocumento: {doc['content']}"
}
],
max_tokens=5,
temperature=0
)
try:
score = float(response.choices[0].message.content.strip())
scored_docs.append({**doc, "rerank_score": score})
except ValueError:
scored_docs.append({**doc, "rerank_score": 0})
scored_docs.sort(key=lambda x: x["rerank_score"], reverse=True)
return scored_docs[:top_k]Pipeline Completo
def rag_query(question: str) -> str:
"""Pipeline RAG completo: busca → rerank → geração."""
# 1. Busca híbrida
results = hybrid_search(question, limit=20)
# 2. Reranking
top_docs = rerank(question, results, top_k=5)
# 3. Montar contexto
context = "\n\n---\n\n".join([
f"[Fonte: {doc['metadata'].get('source', 'desconhecida')}]\n{doc['content']}"
for doc in top_docs
])
# 4. Gerar resposta
response = client.chat.completions.create(
model="urso-mabe",
messages=[
{
"role": "system",
"content": (
"Responda a pergunta baseando-se APENAS no contexto fornecido. "
"Se a resposta não estiver no contexto, diga que não encontrou "
"informação relevante. Cite as fontes."
)
},
{
"role": "user",
"content": f"Contexto:\n{context}\n\nPergunta: {question}"
}
],
temperature=0.3,
max_tokens=2000
)
return response.choices[0].message.content
# Usar
answer = rag_query("Qual o prazo prescricional para danos morais?")
print(answer)Dicas de Otimização
- Chunk size: Use chunks de 256-512 tokens para melhor precisão na busca
- Overlap: 10-20% de sobreposição entre chunks para manter contexto
- Metadata: Adicione metadados ricos (fonte, data, categoria) para filtros
- Batch embeddings: Processe embeddings em lotes para melhor performance
- Cache: Armazene embeddings de consultas frequentes
- Avaliação: Teste com perguntas reais para calibrar pesos de busca híbrida