Ollama: Pipeline RAG

RAG dá à IA um cofre de conhecimento — em vez de adivinhar respostas, ela recupera a verdade dos documentos.

💡 Dica: O princípio central do RAG é "recuperar primeiro, depois gerar" — o modelo responde apenas com base em fragmentos de documentos recuperados, não da memória. Isso reduz grandemente as "alucinações" (fabricar informações), mas a qualidade da recuperação determina diretamente a qualidade da resposta. A estratégia de fragmentação e a seleção do modelo de Embedding são fundamentais.

📋 Pré-requisitos: Você deve dominar o seguinte primeiro

1. O Que Você Vai Aprender


2. Uma História Real de Uma Empreendedora SaaS

⚠️ Aviso: RAG não pode eliminar alucinações — pode apenas reduzir significativamente sua probabilidade. Se os fragmentos de documentos recuperados forem irrelevantes ou insuficientes, o modelo ainda pode "fabricar" respostas. Estratégia-chave: constranger o modelo a responder apenas dentro do escopo dos resultados de recuperação, e adicionar ao System Prompt: "Se a informação relevante não estiver nos documentos, responda com 'Não posso confirmar isso'."

(1) O Problema: O Modelo Não Conhece os Detalhes dos Produtos

O SupportBot da Alice consegue lidar com atendimento ao cliente geral, mas não conhece políticas específicas de devolução de produtos, termos de garantia ou detalhes de preços. O modelo frequentemente "fabrica" informações, levando a constantes reclamações de clientes.

(2) A Solução: RAG Permite ao Modelo Responder Baseado em Documentos Reais

Importar manuais de produtos, FAQs e documentos de políticas para um banco de dados vetorial, recuperar fragmentos relevantes e deixar o modelo gerar respostas baseadas em fatos:

PYTHON
# RAG: Retrieve relevant docs, then generate answer
docs = retriever.invoke("What is the warranty for headphones?")
answer = llm.invoke(f"Based on: {docs}\nAnswer: What is the warranty?")

3. Arquitetura RAG em Detalhe

💡 Dica: O princípio central do RAG é "recuperar primeiro, depois gerar" — o modelo responde apenas com base em fragmentos de documentos recuperados, não da memória. Isso reduz grandemente as "alucinações" (fabricar informações), mas a qualidade da recuperação determina diretamente a qualidade da resposta. A estratégia de fragmentação e a seleção do modelo de Embedding são fundamentais.

(1) Fluxo do Pipeline de Ponta a Ponta

100%
flowchart LR
    A[Documentos] --> B[Loader]
    B --> C[Splitter]
    C --> D[Modelo Embedding<br/>nomic-embed-text]
    D --> E[Armazenamento Vetorial<br/>Chroma/FAISS]
    F[Consulta do Usuário] --> G[Embedding da Consulta]
    G --> E
    E --> H[Fragmentos Top-K]
    H --> I[Geração LLM<br/>qwen2.5]
    I --> J[Resposta]

(2) Detalhamento Estágio por Estágio

Estágio Ferramenta Entrada Saída
Carregar PyPDFLoader / TextLoader PDF/MD/TXT Lista de documentos
Fragmentar RecursiveCharacterTextSplitter Documento Lista de fragmentos
Embed OllamaEmbeddings Texto do fragmento Vetor (768d)
Armazenar Chroma / FAISS Vetor + texto Índice
Recuperar Similarity Search Vetor da consulta Fragmentos Top-K
Gerar ChatOllama Consulta + Fragmentos Resposta

4. Ollama Embeddings

⚠️ Nota: A seleção do modelo de Embedding afeta diretamente a qualidade da recuperação — nomic-embed-text (768 dimensões) tem bom desempenho em inglês mas é mediano em chinês, mxbai-embed-large (1024 dimensões) tem precisão mais alta mas é o dobro do tamanho. Para cenários em chinês, teste múltiplos modelos e escolha o melhor — não assuma que "maior é sempre melhor."

(1) Comparação de Modelos de Embedding

Modelo Dimensões Tamanho Velocidade Caso de Uso
nomic-embed-text 768 274 MB Rápido Inglês geral
mxbai-embed-large 1024 670 MB Médio Alta precisão
all-minilm 384 46 MB Muito rápido Leve

(2) Guia de Seleção de Modelo de Embedding

Cenário Modelo Recomendado Motivo
RAG geral nomic-embed-text Melhor custo-benefício
Recuperação de alta precisão mxbai-embed-large Dimensões maiores, melhor discriminação
Recursos limitados all-minilm Menor e mais rápido
Chinês dominante nomic-embed-text Desempenho aceitável em chinês

▶ Exemplo 1: Noções Básicas de Ollama Embeddings

PYTHON
from langchain_ollama import OllamaEmbeddings

# Initialize embeddings model
embeddings = OllamaEmbeddings(model="nomic-embed-text")

# Embed a single text
vector = embeddings.embed_query("What is the return policy?")
print(f"Dimension: {len(vector)}")  # 768

# Embed multiple texts
vectors = embeddings.embed_documents([
    "Free shipping on orders over $50",
    "30-day return policy for all products",
    "Warranty covers manufacturing defects for 1 year"
])
print(f"Embedded {len(vectors)} documents")

Saída:

TEXT
# Execution successful

5. Carregamento e Fragmentação de Documentos

(1) Carregadores de Documentos

Loader Formato Instalação
PyPDFLoader PDF pip install pypdf
TextLoader TXT Built-in
UnstructuredMarkdownLoader Markdown pip install unstructured
CSVLoader CSV Built-in
UnstructuredHTMLLoader HTML/URL pip install unstructured

(2) Comparação de Estratégias de Fragmentação

Estratégia Parâmetros Documentos Adequados Prós/Contras
Tamanho fixo chunk_size=500, overlap=50 Geral Simples mas pode quebrar semântica
Caractere recursivo chunk_size=500, overlap=50, separators Documentos estruturados Preserva limites de parágrafo
Fragmentação semântica similarity_threshold Textos longos Melhor qualidade mas computacionalmente cara
💡 Dica: RecursiveCharacterTextSplitter é a escolha preferida. chunk_size=500-1000 e overlap=50-100 são valores empiricamente ótimos.

▶ Exemplo 2: Carregamento e Fragmentação de Documentos

PYTHON
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Load text document
loader = TextLoader("product_faq.txt")
docs = loader.load()
print(f"Loaded {len(docs)} document(s)")

# Split into chunks
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)
print(f"Split into {len(chunks)} chunks")
print(f"First chunk preview: {chunks[0].page_content[:100]}...")

Saída:

TEXT
# Execution successful

6. Seleção de Banco de Dados Vetorial

(1) Comparação dos Três Principais Armazéns Vetoriais

Dimensão Chroma FAISS Qdrant
Tipo Embarcado Em memória Cliente/Servidor
Persistência ✅ Arquivos locais ❌ Apenas memória ✅ Disco/Nuvem
Instalação pip install pip install + faiss-cpu Docker / pip
Melhor para Dev/pequena escala Recuperação de alta velocidade Produção
Filtragem ✅ Filtragem de metadados ✅ Filtragem avançada
Distribuído

▶ Exemplo 3: Armazenamento e Recuperação Vetorial com Chroma

PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter

# Prepare documents
documents = [
    "Return policy: 30 days from purchase, items must be in original condition.",
    "Shipping: Free for orders over $50. Standard delivery takes 3-5 business days.",
    "Warranty: All electronics have a 1-year manufacturer warranty.",
    "International shipping: Available to 50+ countries. Import duties may apply.",
    "Refund process: Full refund within 5-7 business days after we receive the return."
]

# Split documents
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
chunks = splitter.create_documents(documents)

# Create vector store
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

# Similarity search
results = vectorstore.similarity_search("How do I get a refund?", k=2)
for doc in results:
    print(f"- {doc.page_content}")

Saída:

TEXT
# Execution successful

▶ Exemplo 4: Armazenamento Vetorial com FAISS

PYTHON
from langchain_ollama import OllamaEmbeddings
from langchain_community.vectorstores import FAISS

embeddings = OllamaEmbeddings(model="nomic-embed-text")

# Create FAISS index
texts = [
    "Product A: Wireless headphones, $89.99, Bluetooth 5.3",
    "Product B: Laptop stand, $34.99, Adjustable height",
    "Product C: USB-C cable, $12.99, 2-meter length"
]
vectorstore = FAISS.from_texts(texts, embeddings)

# Save and load
vectorstore.save_local("./faiss_index")
loaded_vs = FAISS.load_local("./faiss_index", embeddings,
                              allow_dangerous_deserialization=True)

# Search
results = loaded_vs.similarity_search("headphones", k=2)
for doc in results:
    print(doc.page_content)

Saída:

TEXT
# Execution successful

7. Pipeline RAG Completo e SupportBot V3

▶ Exemplo 5: Pipeline RAG Completo

PYTHON
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# Build retriever
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# Build RAG chain
llm = ChatOllama(model="qwen2.5", temperature=0.3)

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are SupportBot. Answer based ONLY on the following context. "
               "If the answer is not in the context, say 'I don't have that information. "
               "Let me connect you with a human agent.'\n\nContext:\n{context}"),
    ("human", "{question}")
])

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# Ask questions
answer = rag_chain.invoke("What is the return policy?")
print(answer)

Saída:

TEXT
# Function defined successfully

8. Exemplo Abrangente: Sistema de Atendimento ao Cliente RAG SupportBot V3

PYTHON
# ============================================
# Comprehensive: SupportBot V3 with RAG
# Product knowledge base powered customer service
# ============================================

from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from pathlib import Path

class SupportBotV3:
    def __init__(self, kb_path: str = "./knowledge_base",
                 model: str = "qwen2.5",
                 embed_model: str = "nomic-embed-text"):
        self.llm = ChatOllama(model=model, temperature=0.3)
        self.embeddings = OllamaEmbeddings(model=embed_model)
        self.kb_path = kb_path
        self.vectorstore = None
        self.rag_chain = None

    def build_knowledge_base(self, doc_dir: str):
        """Load documents and build vector store."""
        docs = []
        for f in Path(doc_dir).glob("*.txt"):
            loader = TextLoader(str(f))
            docs.extend(loader.load())
        for f in Path(doc_dir).glob("*.md"):
            loader = TextLoader(str(f))
            docs.extend(loader.load())

        splitter = RecursiveCharacterTextSplitter(
            chunk_size=500, chunk_overlap=50
        )
        chunks = splitter.split_documents(docs)

        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            persist_directory=self.kb_path
        )
        print(f"Indexed {len(chunks)} chunks from {len(docs)} documents")

    def setup_rag_chain(self):
        """Build the RAG chain."""
        if not self.vectorstore:
            self.vectorstore = Chroma(
                persist_directory=self.kb_path,
                embedding_function=self.embeddings
            )

        retriever = self.vectorstore.as_retriever(search_kwargs={"k": 3})

        prompt = ChatPromptTemplate.from_messages([
            ("system", (
                "You are SupportBot for GlobalShop e-commerce. "
                "Answer based ONLY on the provided context. "
                "If not in context, say: 'Let me connect you with a human agent.' "
                "Be concise (2-3 sentences).\n\nContext:\n{context}"
            )),
            ("human", "{question}")
        ])

        def format_docs(docs):
            return "\n\n".join(d.page_content for d in docs)

        self.rag_chain = (
            {"context": retriever | format_docs,
             "question": RunnablePassthrough()}
            | prompt | self.llm | StrOutputParser()
        )

    def ask(self, question: str) -> str:
        """Ask a question using RAG."""
        if not self.rag_chain:
            self.setup_rag_chain()
        return self.rag_chain.invoke(question)

# Usage
if __name__ == "__main__":
    bot = SupportBotV3()

    # First time: build knowledge base
    # bot.build_knowledge_base("./docs")

    # Ask questions
    questions = [
        "What is the return policy for electronics?",
        "How long does international shipping take?",
        "Do you offer free returns?",
    ]
    for q in questions:
        answer = bot.ask(q)
        print(f"Q: {q}")
        print(f"A: {answer}\n")

❓ Perguntas Frequentes

P: Devo escolher RAG ou Fine-tuning? R: RAG é adequado para cenários onde o conhecimento é frequentemente atualizado (FAQs de produtos, documentos de políticas) sem retreinamento. Fine-tuning é para mudar padrões de comportamento do modelo. 90% dos cenários empresariais são melhor servidos por RAG.

P: Como escolher chunk_size? R: 500-1000 caracteres é um bom ponto de partida. Documentos curtos (FAQs) usam 200-500; documentos longos (manuais) usam 500-1000. Defina overlap para 10-20% do chunk_size.

P: O que devo fazer se os resultados de recuperação forem imprecisos? R: Tente: 1) Mudar para um modelo de embedding de dimensões maiores (mxbai-embed-large); 2) Aumentar o valor k (3→5); 3) Usar busca MMR para mais diversidade; 4) Melhorar a qualidade da fragmentação de documentos.

P: Como escolher entre Chroma e FAISS? R: Use Chroma durante desenvolvimento (auto-persistência, suporte a filtragem). Use FAISS quando precisar de velocidade extrema e todos os dados couberem na memória. Use Qdrant para produção (distribuído, filtragem avançada).

P: Como estimar o consumo de tokens do RAG? R: Cada consulta consome = tokens da consulta + tokens dos fragmentos top-k + tokens da resposta gerada. Com k=3 e chunk_size=500, a entrada é aproximadamente 1500-2000 tokens. Inferência local não tem custo de token.

P: Como avaliar a qualidade do RAG? R: Use o framework RAGAS para avaliar 4 métricas: Faithfulness (a resposta é fiel ao contexto), Answer Relevancy (a resposta está no tema), Context Precision (precisão da recuperação), e Context Recall.


📖 Resumo


📝 Exercícios

  1. Básico (⭐): Use OllamaEmbeddings para armazenar 5 textos de FAQ de produtos no Chroma, e realize uma busca de similaridade.
  2. Intermediário (⭐⭐): Construa um pipeline RAG completo — do carregamento de arquivo de texto, fragmentação, embedding, armazenamento até recuperação-geração, e teste com 3 perguntas.
  3. Avançado (⭐⭐⭐): Construa uma base de conhecimento para o SupportBot V3 — prepare 10+ documentos de produtos, construa um sistema RAG, e compare a qualidade das respostas com e sem RAG para as mesmas perguntas.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%