Ollama: Pipeline RAG
RAG dá à IA um cofre de conhecimento — em vez de adivinhar respostas, ela recupera a verdade dos documentos.
📋 Pré-requisitos: Você deve dominar o seguinte primeiro
- Aula 13: Integração com LangChain
1. O Que Você Vai Aprender
- Arquitetura RAG: Carregamento de documentos → Fragmentação → Embedding → Armazenamento vetorial → Recuperação → Geração
- Uso de modelos Ollama Embeddings
- Seleção de banco de dados vetorial: Chroma / FAISS / Qdrant
- Estratégias de fragmentação de documentos e ajuste de parâmetros
- SupportBot V3 da Alice: Base de conhecimento de produtos RAG
2. Uma História Real de Uma Empreendedora SaaS
(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:
# 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
(1) Fluxo do Pipeline de Ponta a Ponta
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
(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
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:
# Execution successful
5. Carregamento e Fragmentação de Documentos
(1) Carregadores de Documentos
| Loader | Formato | Instalação |
|---|---|---|
| PyPDFLoader | 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 |
▶ Exemplo 2: Carregamento e Fragmentação de Documentos
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:
# 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
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:
# Execution successful
▶ Exemplo 4: Armazenamento Vetorial com FAISS
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:
# Execution successful
7. Pipeline RAG Completo e SupportBot V3
▶ Exemplo 5: Pipeline RAG Completo
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:
# Function defined successfully
8. Exemplo Abrangente: Sistema de Atendimento ao Cliente RAG SupportBot V3
# ============================================
# 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
- Pipeline RAG de seis etapas: Carregar → Fragmentar → Embed → Armazenar → Recuperar → Gerar
- nomic-embed-text é o modelo de embedding padrão do Ollama, 768 dimensões, melhor custo-benefício
- RecursiveCharacterTextSplitter é a ferramenta de fragmentação preferida; chunk_size=500 é um bom ponto de partida
- Chroma para desenvolvimento, FAISS para recuperação de alta velocidade, Qdrant para produção
- Chain RAG composta com LCEL: retriever | format | prompt | llm | parser
- SupportBot V3 usa RAG para basear respostas em evidência, eliminando problemas de "fabricação"
📝 Exercícios
- Básico (⭐): Use OllamaEmbeddings para armazenar 5 textos de FAQ de produtos no Chroma, e realize uma busca de similaridade.
- 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.
- 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.