O Haystack, framework open source da empresa alemã deepset, tornou-se numa das opções mais discutidas para quem quer montar pipelines de RAG (geração aumentada por recuperação) sem depender do LangChain. Com cerca de 26,6 mil estrelas no GitHub em setembro de 2026 e uma cadência de lançamentos que incluiu a versão 3.0 a 20 de julho, a 3.1.0 a 25 de agosto e a correção 3.1.1 a 3 de setembro, o projeto está claramente em desenvolvimento ativo. Este tutorial mostra, passo a passo, como instalar o Haystack, construir um pipeline de indexação e recuperação, ligar um modelo gerador e acabar com um chatbot funcional que responde com base nos seus próprios documentos.
O objetivo aqui não é apenas copiar um exemplo de “olá mundo”. Vamos construir um projeto completo: carregar ficheiros de texto, dividi-los em blocos, gerar embeddings, guardá-los numa base de dados vetorial, recuperar os trechos relevantes para uma pergunta e passar esse contexto a um modelo de linguagem para gerar uma resposta. No final, terá um chatbot de linha de comandos que pode adaptar a qualquer conjunto de documentos seus.
O que é o Haystack e porque está a ganhar tração
O Haystack é um framework Python de código aberto, distribuído sob licença Apache 2.0, pensado para construir aplicações à volta de modelos de linguagem: pesquisa semântica, perguntas e respostas sobre documentos, RAG e, desde a versão 3.0, também agentes capazes de decidir e executar várias etapas de forma autónoma. O repositório oficial no GitHub é o deepset-ai/haystack, mantido pela empresa alemã deepset, e o catálogo de integrações contava, em setembro de 2026, com cerca de 183 entradas, cobrindo modelos, bases de dados vetoriais, ferramentas de observabilidade e serviços externos.
A grande diferença em relação a alternativas como o LangChain está na arquitetura. O Haystack organiza tudo à volta de componentes com entradas e saídas explicitamente nomeadas, ligados através de um objeto Pipeline. Em vez de encadear chamadas de forma implícita, cada ligação entre componentes é declarada com pipeline.connect("origem.saida", "destino.entrada"). Isto torna o fluxo de dados mais fácil de inspecionar, especialmente em pipelines maiores onde é preciso perceber exatamente o que entra e sai de cada bloco.
Vale notar que, à data deste artigo, a versão 3.2 do Haystack ainda circulava como candidata a lançamento (release candidate), com a documentação oficial a referir-se a ela como “3.2-unstable”. A versão estável mais recente confirmada é a 3.1.1, publicada a 3 de setembro de 2026. Este tutorial usa essa base estável, mas os conceitos aplicam-se diretamente a qualquer versão 3.x que venha a ser lançada depois.
Pré-requisitos e versões
Antes de avançar, confirme que tem o seguinte ambiente preparado. Isto poupa-lhe metade dos problemas mais comuns que vamos ver na secção de resolução de erros mais à frente.
| Requisito | Versão recomendada | Notas |
|---|---|---|
| Python | 3.10, 3.11 ou 3.12 | O ambiente de testes oficial mais recente referenciado usa Python 3.12 |
| haystack-ai | 3.1.1 (estável) | Instalado via pip, pacote principal do framework |
| sentence-transformers-haystack | Última disponível | Integração para embeddings locais, sem depender de API paga |
| pip | 23 ou superior | Versões antigas do pip falham a resolver algumas dependências de ML |
| Memória RAM | 4 GB livres, mínimo | O modelo de embeddings local ocupa memória ao carregar |
| Chave de API OpenAI (opcional) | N/A | Só necessária se optar pelo gerador OpenAI em vez de um modelo local |
Recomenda-se usar um ambiente virtual isolado para evitar conflitos com outras bibliotecas de machine learning que possa ter instaladas. Um sistema com 4 núcleos de CPU é suficiente para seguir este tutorial. Não é preciso GPU, ainda que a indexação de documentos grandes seja mais rápida com uma placa gráfica compatível com CUDA. Se o seu portátil for mais modesto, a indexação demora mais tempo mas continua a funcionar sem problemas, já que o modelo de embeddings usado aqui é propositadamente leve.
Passo 1: Criar o ambiente virtual e instalar o Haystack
Comece por criar uma pasta de projeto isolada com o seu próprio ambiente virtual Python. Isto evita que as dependências do Haystack entrem em conflito com outros projetos na sua máquina.
mkdir haystack-rag-tutorial
cd haystack-rag-tutorial
python3 -m venv venv
source venv/bin/activate # no Windows: venv\Scripts\activate
pip install --upgrade pip
pip install haystack-ai sentence-transformers-haystack
O pacote haystack-ai traz o núcleo do framework: a classe Pipeline, o Document, o InMemoryDocumentStore e os componentes base. O pacote sentence-transformers-haystack adiciona os embedders que correm localmente, sem necessidade de chave de API, o que é ideal para testar o pipeline sem custos antes de decidir se quer ligar um modelo pago.
A documentação oficial do Haystack e a página do pacote no PyPI são as referências mais fiáveis para confirmar a versão mais recente antes de instalar. Confirme que a instalação correu bem:
python3 -c "import haystack; print(haystack.__version__)"
Se o comando devolver um número de versão sem erros, está pronto para avançar. Se aparecer um erro de importação, normalmente é sinal de que o ambiente virtual não está ativo ou que a instalação falhou a meio, por exemplo por falta de espaço em disco durante a transferência dos modelos de embeddings.
Passo 2: Perceber os componentes principais do pipeline
Antes de escrever código, vale a pena perceber os blocos que vamos ligar. Um pipeline RAG típico no Haystack usa cinco tipos de componente:
- DocumentStore: guarda os documentos e os respetivos embeddings. Não é tecnicamente um componente de pipeline (não tem método
run()), é passado diretamente aos retrievers. - Embedder: converte texto em vetores numéricos. Há uma versão para documentos (
SentenceTransformersDocumentEmbedder) e outra para a pergunta do utilizador (SentenceTransformersTextEmbedderouOpenAITextEmbedder). - Retriever: compara o vetor da pergunta com os vetores guardados e devolve os documentos mais relevantes. O
InMemoryEmbeddingRetrieverfaz pesquisa semântica, enquanto oInMemoryBM25Retrieverfaz pesquisa por palavras-chave. - PromptBuilder: monta o texto final que vai para o modelo, combinando os documentos recuperados com a pergunta original, através de um template no estilo Jinja2.
- Generator: chama o modelo de linguagem (local ou via API) e devolve a resposta em texto.
A regra de ouro do Haystack é simples: cada ligação entre componentes tem de respeitar os nomes exatos de entrada e saída. Ligar a saída errada à entrada errada é, de longe, o erro mais comum para quem começa. Vamos ver isso em detalhe mais à frente, na secção dedicada aos erros mais frequentes.
Passo 3: Preparar os documentos de origem
Crie uma pasta documentos dentro do seu projeto e adicione alguns ficheiros de texto. Para este tutorial vamos usar notas simples sobre um produto fictício, mas o processo é idêntico para manuais técnicos, políticas internas ou documentação de uma API.
mkdir documentos
cat > documentos/produto.txt << 'EOF'
O plano Base custa 9 euros por mês e inclui 5 GB de armazenamento.
O plano Pro custa 29 euros por mês e inclui 100 GB de armazenamento e suporte prioritário.
O plano Empresa tem preço personalizado e inclui armazenamento ilimitado, SSO e um gestor de conta dedicado.
O cancelamento pode ser feito a qualquer momento nas definições da conta, sem taxas adicionais.
O período de teste gratuito dura 14 dias em qualquer plano pago.
EOF
Agora vamos escrever o script que carrega este ficheiro, divide o conteúdo em documentos individuais e os prepara para indexação. Crie um ficheiro chamado indexar.py:
from pathlib import Path
from haystack import Document
def carregar_documentos(pasta: str) -> list[Document]:
documentos = []
for ficheiro in Path(pasta).glob("*.txt"):
texto = ficheiro.read_text(encoding="utf-8")
for linha in texto.strip().split("\n"):
if linha.strip():
documentos.append(
Document(content=linha.strip(), meta={"origem": ficheiro.name})
)
return documentos
if __name__ == "__main__":
docs = carregar_documentos("documentos")
print(f"Carregados {len(docs)} documentos.")
for d in docs[:3]:
print("-", d.content)
Execute com python3 indexar.py. Deve ver algo parecido com isto:
Carregados 5 documentos.
- O plano Base custa 9 euros por mês e inclui 5 GB de armazenamento.
- O plano Pro custa 29 euros por mês e inclui 100 GB de armazenamento e suporte prioritário.
- O plano Empresa tem preço personalizado e inclui armazenamento ilimitado, SSO e um gestor de conta dedicado.
Para documentos mais longos, como manuais em PDF ou páginas inteiras de documentação, dividir por linha não chega. Nesse caso, o Haystack disponibiliza componentes de divisão como o DocumentSplitter, que corta o texto em blocos de um número fixo de palavras ou frases, preservando alguma sobreposição entre blocos para não perder contexto nas fronteiras.
Um exemplo prático de uso do splitter, para um ficheiro com parágrafos mais longos, fica assim:
from haystack.components.preprocessors import DocumentSplitter
splitter = DocumentSplitter(
split_by="word",
split_length=150,
split_overlap=30,
)
documentos_longos = [
Document(content=Path("documentos/manual.txt").read_text(encoding="utf-8"))
]
blocos = splitter.run(documents=documentos_longos)
print(f"O manual foi dividido em {len(blocos['documents'])} blocos.")
Os parâmetros split_length e split_overlap controlam, respetivamente, o tamanho de cada bloco em palavras e quantas palavras se repetem entre blocos consecutivos. Um valor de sobreposição entre 15% e 25% do tamanho do bloco costuma ser um bom ponto de partida: baixo o suficiente para não duplicar demasiado conteúdo indexado, alto o suficiente para não cortar uma ideia a meio.
Passo 4: Criar o armazém de documentos e gerar embeddings
Com os documentos prontos, o próximo passo é convertê-los em vetores e guardá-los. Vamos usar o InMemoryDocumentStore, que não precisa de nenhum serviço externo e é ideal para prototipagem. Em produção, o Haystack tem integrações para Elasticsearch, OpenSearch, Chroma e Qdrant, entre outras, todas com a mesma interface de retriever.
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from indexar import carregar_documentos
document_store = InMemoryDocumentStore(embedding_similarity_function="cosine")
embedder = SentenceTransformersDocumentEmbedder(
model="sentence-transformers/all-MiniLM-L6-v2"
)
embedder.warm_up()
documentos = carregar_documentos("documentos")
resultado = embedder.run(documentos)
document_store.write_documents(resultado["documents"])
print(f"{document_store.count_documents()} documentos guardados com embeddings.")
Guarde este código como criar_store.py e execute-o. Na primeira execução, o modelo all-MiniLM-L6-v2, da biblioteca Sentence Transformers, é transferido automaticamente da Hugging Face (cerca de 90 MB), o que pode demorar um minuto dependendo da sua ligação. Nas execuções seguintes, o modelo já fica em cache local.
A chamada a embedder.warm_up() é fácil de esquecer e é uma das causas mais comuns de erro: sem ela, o modelo subjacente nunca é carregado para memória e o run() falha com uma exceção a dizer que o componente não está pronto.
Passo 5: Montar o pipeline de recuperação e geração
Chegámos à parte central do tutorial: construir o pipeline que liga a pergunta do utilizador aos documentos certos e depois a uma resposta gerada. Vamos usar um modelo local via Hugging Face para o gerador, para que todo o tutorial funcione sem custos de API, e indicar depois como trocar para a OpenAI se preferir.
from haystack import Pipeline
from haystack.components.embedders import SentenceTransformersTextEmbedder
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from haystack.components.builders import PromptBuilder
from haystack.components.generators import HuggingFaceLocalGenerator
from criar_store import document_store
modelo_template = """
Responde à pergunta usando apenas a informação nos documentos abaixo.
Se a resposta não estiver nos documentos, diz que não sabes.
Documentos:
{% for doc in documents %}
{{ doc.content }}
{% endfor %}
Pergunta: {{ question }}
Resposta:
"""
pipeline_rag = Pipeline()
pipeline_rag.add_component(
"embedder_pergunta",
SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"),
)
pipeline_rag.add_component(
"retriever",
InMemoryEmbeddingRetriever(document_store=document_store, top_k=3),
)
pipeline_rag.add_component(
"prompt_builder",
PromptBuilder(template=modelo_template),
)
pipeline_rag.add_component(
"gerador",
HuggingFaceLocalGenerator(model="google/flan-t5-base"),
)
pipeline_rag.connect("embedder_pergunta.embedding", "retriever.query_embedding")
pipeline_rag.connect("retriever.documents", "prompt_builder.documents")
pipeline_rag.connect("prompt_builder.prompt", "gerador.prompt")
print("Pipeline montado com sucesso.")
Repare na estrutura das ligações: embedder_pergunta.embedding liga-se a retriever.query_embedding, e não a retriever.documents, uma troca fácil de fazer por engano quando se está a escrever depressa. Cada componente do Haystack documenta claramente quais são os nomes exatos das suas entradas e saídas através do método run(). Vale sempre a pena confirmar essa assinatura antes de escrever o connect(), principalmente quando o pipeline cresce e já tem seis ou sete componentes ligados entre si.
Passo 6: Executar o pipeline e testar perguntas
Com o pipeline montado, falta só executá-lo com uma pergunta real. Guarde o código abaixo como perguntar.py:
from pipeline_rag import pipeline_rag # ajuste o import conforme o seu ficheiro
pergunta = "Quanto custa o plano Pro e o que inclui?"
resultado = pipeline_rag.run({
"embedder_pergunta": {"text": pergunta},
"prompt_builder": {"question": pergunta},
})
print(resultado["gerador"]["replies"][0])
O resultado esperado, com base nos documentos de exemplo, é uma resposta próxima de:
O plano Pro custa 29 euros por mês e inclui 100 GB de armazenamento e suporte prioritário.
Se a resposta vier vazia ou desligada do contexto, o problema está quase sempre num dos dois sítios: o top_k do retriever está demasiado baixo e não está a devolver o documento certo, ou o modelo gerador escolhido é demasiado pequeno para seguir bem as instruções do prompt. Modelos como o flan-t5-base são leves e gratuitos, mas produzem respostas mais simples do que um modelo maior via API.
Passo 7: Trocar para um gerador OpenAI (opcional)
Se preferir respostas de qualidade mais consistente e tiver uma chave de API, a troca é simples porque o Haystack mantém a mesma interface entre geradores. Defina a variável de ambiente primeiro:
export OPENAI_API_KEY="a-sua-chave-aqui"
E substitua o componente gerador:
import os
from haystack.components.generators import OpenAIGenerator
if not os.environ.get("OPENAI_API_KEY"):
raise RuntimeError("Defina OPENAI_API_KEY antes de usar o gerador OpenAI.")
pipeline_rag.add_component("gerador", OpenAIGenerator(model="gpt-4o-mini"))
pipeline_rag.connect("prompt_builder.prompt", "gerador.prompt")
Note que, se for usar embeddings da OpenAI em vez dos locais do Sentence Transformers, é preciso trocar também o embedder de documentos e o de pergunta em conjunto. Nunca misture um modelo de embeddings para indexar com outro diferente para pesquisar, porque as dimensões dos vetores não vão coincidir e a pesquisa semântica deixa de funcionar corretamente. Este detalhe parece óbvio quando explicado, mas é responsável por uma boa parte dos relatos de "o retriever não devolve nada" em fóruns e grupos de discussão sobre o framework.
Passo 8: Construir um chatbot de linha de comandos
Agora vamos juntar tudo num script interativo, para que o pipeline responda a várias perguntas seguidas sem ter de recarregar o modelo a cada execução.
def chat():
print("Chatbot RAG com Haystack. Escreva 'sair' para terminar.\n")
while True:
pergunta = input("Pergunta: ").strip()
if pergunta.lower() in {"sair", "exit", "quit"}:
break
if not pergunta:
continue
resultado = pipeline_rag.run({
"embedder_pergunta": {"text": pergunta},
"prompt_builder": {"question": pergunta},
})
resposta = resultado["gerador"]["replies"][0]
print(f"Resposta: {resposta}\n")
if __name__ == "__main__":
chat()
Ao correr este script, pode fazer várias perguntas seguidas sobre os documentos carregados. Para testar com documentos diferentes, basta substituir o conteúdo da pasta documentos e correr novamente o script de indexação antes de iniciar o chat.
Passo 9: Adicionar uma base de dados vetorial persistente
O InMemoryDocumentStore perde tudo quando o processo termina, o que é aceitável para testes mas inviável em produção. Para persistência, o Haystack integra com o Qdrant, que suporta modo em memória, persistência em disco, Docker ou a versão cloud gerida. Instale a integração:
pip install qdrant-haystack
E troque o document store:
from haystack_integrations.document_stores.qdrant import QdrantDocumentStore
document_store = QdrantDocumentStore(
path="./qdrant_local",
index="documentos_produto",
embedding_dim=384, # tem de coincidir com o modelo de embeddings usado
recreate_index=False,
)
O parâmetro embedding_dim tem de corresponder exatamente à dimensão de saída do modelo de embeddings escolhido: o all-MiniLM-L6-v2 produz vetores de 384 dimensões. Usar um valor errado aqui é outra causa frequente de falhas silenciosas na indexação, porque o erro só aparece mais tarde, quando o retriever tenta comparar vetores de tamanhos diferentes.
Passo 10: Avaliar a qualidade das respostas
Construir o pipeline é só metade do trabalho. Antes de colocar o chatbot em produção, vale a pena testar sistematicamente se as respostas estão corretas e se o retriever está mesmo a devolver os documentos certos. Uma forma simples de fazer isto sem ferramentas adicionais é criar um pequeno conjunto de perguntas com respostas esperadas e comparar manualmente:
casos_teste = [
{"pergunta": "Quanto custa o plano Base?", "esperado": "9 euros"},
{"pergunta": "Quantos dias dura o teste gratuito?", "esperado": "14 dias"},
{"pergunta": "O plano Empresa inclui SSO?", "esperado": "sim"},
]
for caso in casos_teste:
resultado = pipeline_rag.run({
"embedder_pergunta": {"text": caso["pergunta"]},
"prompt_builder": {"question": caso["pergunta"]},
})
resposta = resultado["gerador"]["replies"][0]
ok = caso["esperado"].lower() in resposta.lower()
print(f"[{'OK' if ok else 'FALHOU'}] {caso['pergunta']} -> {resposta}")
Para projetos maiores, vale a pena avaliar separadamente a qualidade da recuperação (será que os documentos certos aparecem no top_k?) e a qualidade da geração (será que o modelo usa bem o contexto fornecido?). Misturar as duas fontes de erro torna muito mais difícil perceber onde está o problema quando algo corre mal.
Passo 11: Colocar múltiplas fontes e metadados no pipeline
Em projetos reais raramente há só um ficheiro de origem. O Haystack permite anexar metadados a cada documento e depois filtrar a recuperação por esses metadados, o que é útil quando se tem, por exemplo, documentação de vários produtos ou vários departamentos na mesma base.
from haystack import Document
documentos_com_meta = [
Document(content="O plano Pro custa 29 euros por mês.", meta={"departamento": "vendas"}),
Document(content="O suporte técnico responde em até 4 horas úteis.", meta={"departamento": "suporte"}),
]
resultado = retriever.run(
query_embedding=vetor_pergunta,
filters={"field": "meta.departamento", "operator": "==", "value": "vendas"},
)
Esta capacidade de filtrar por metadados antes ou durante a recuperação é particularmente útil em aplicações multi-tenant, onde é preciso garantir que a resposta a um utilizador nunca mistura documentos de outra conta ou de outro cliente.
Passo 12: Expor o chatbot como serviço web
Para além da linha de comandos, é comum querer expor o pipeline como uma API. Uma forma direta é envolver o pipeline num pequeno servidor FastAPI:
pip install fastapi uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from pipeline_rag import pipeline_rag
app = FastAPI()
class PerguntaRequest(BaseModel):
pergunta: str
@app.post("/perguntar")
def perguntar(req: PerguntaRequest):
resultado = pipeline_rag.run({
"embedder_pergunta": {"text": req.pergunta},
"prompt_builder": {"question": req.pergunta},
})
return {"resposta": resultado["gerador"]["replies"][0]}
Corra com uvicorn nome_do_ficheiro:app --reload e terá um endpoint /perguntar pronto a receber pedidos POST. Para produção, é recomendável adicionar limitação de taxa, autenticação e carregar o pipeline uma única vez na inicialização do servidor, em vez de o reconstruir a cada pedido.
Erros comuns e como resolvê-los
Depois de montar vários pipelines com o Haystack, estes são os problemas que aparecem com mais frequência, por ordem aproximada de frequência.
- Esquecer o warm_up() do embedder: sem esta chamada, o modelo subjacente nunca carrega e o componente falha ao correr.
- Ligar sockets incompatíveis: por exemplo, ligar a saída de
documentsa uma entrada que esperaquery_embedding. O erro costuma indicar claramente o nome do socket em falta. - Indexar documentos sem embeddings: se escrever documentos "em bruto" no document store e depois tentar usar um
InMemoryEmbeddingRetriever, a pesquisa semântica não vai funcionar porque não há vetores para comparar. - Misturar modelos de embeddings diferentes: indexar com um modelo e pesquisar com outro produz vetores de dimensões diferentes, o que causa erros ou resultados sem sentido.
- Chunking mal feito: blocos de texto demasiado grandes ultrapassam o contexto do modelo gerador. Blocos demasiado pequenos perdem significado e fragmentam respostas que deveriam vir de um único trecho.
- Faltar a chave de API: componentes como
OpenAIGeneratorouOpenAITextEmbedderfalham imediatamente sem a variável de ambiente correta definida. - Confundir DocumentStore com componente de pipeline: o document store não tem método
run()e não deve ser adicionado comadd_component(). É passado diretamente ao retriever no construtor. - Misturar PromptBuilder com geradores de chat: o
PromptBuilderproduz uma string de texto simples, enquanto geradores como oOpenAIChatGeneratoresperam mensagens estruturadas. Para esses casos, use oChatPromptBuilderem vez do builder de texto simples.
Tabela de resolução de problemas
| Sintoma | Causa provável | Solução |
|---|---|---|
| Erro "component is not warmed up" | Embedder usado sem chamar warm_up() | Chamar embedder.warm_up() antes do primeiro run() |
| Retriever devolve lista vazia | Documentos indexados sem embeddings | Confirmar que o embedder correu antes de write_documents() |
| Erro de dimensão de vetor | embedding_dim do document store não coincide com o modelo | Verificar a dimensão real do modelo e ajustar o parâmetro |
| connect() lança exceção de socket | Nome de entrada/saída incorreto | Consultar o método run() do componente para ver os nomes exatos |
| Resposta genérica ou vazia | top_k demasiado baixo ou modelo gerador fraco | Aumentar top_k e/ou trocar para um modelo maior |
| Erro de autenticação OpenAI | Variável OPENAI_API_KEY não definida | Exportar a chave antes de correr o script |
| Download do modelo demora muito | Primeira execução transfere o modelo da Hugging Face | Aguardar a transferência única (execuções seguintes usam cache) |
| Respostas repetem o prompt em vez de responder | Template mal formatado ou modelo demasiado pequeno | Rever o template Jinja2 e testar com um modelo maior |
| Memória esgotada ao indexar muitos documentos | Lote de indexação demasiado grande para o modelo local | Indexar em lotes menores ou usar um modelo de embeddings mais leve |
Haystack vs LangChain: qual escolher
Esta é a pergunta que mais aparece a quem começa neste espaço. Não há uma resposta universal, mas há diferenças práticas que ajudam a decidir.
| Critério | Haystack | LangChain |
|---|---|---|
| Modelo de composição | Componentes com sockets nomeados e Pipeline.connect() | Chains e runnables encadeados por operadores |
| Curva de aprendizagem inicial | Mais explícita, exige perceber nomes de sockets | Mais rápida para protótipos simples |
| Inspecionabilidade do fluxo | Alta, cada ligação é declarada explicitamente | Variável, depende do estilo de composição usado |
| Ecossistema de integrações | Cerca de 183 integrações catalogadas | Ecossistema mais amplo e mais antigo |
| Foco original | Pesquisa semântica e RAG, depois expandido a agentes | Generalista desde o início, chains e agentes |
| Licença | Apache 2.0 | MIT |
Na prática, quem já usou o LangChain e sentiu dificuldade em perceber exatamente o que se passa dentro de uma chain complexa tende a gostar da abordagem mais explícita do Haystack. Por outro lado, quem precisa de um ecossistema mais vasto de integrações prontas a usar, ou já tem experiência acumulada com chains, pode preferir manter-se no LangChain. Os dois frameworks resolvem o mesmo problema central, ligar recuperação de documentos a um modelo gerador, mas partem de filosofias de arquitetura diferentes. Não há vencedor absoluto nesta comparação, só um ajuste melhor ou pior ao estilo de trabalho de cada equipa.
Dicas avançadas para produção
Depois de ter o pipeline básico a funcionar, há um conjunto de ajustes que fazem diferença real quando o projeto sai do protótipo e vai para produção.
Combine pesquisa por palavras-chave com pesquisa semântica. Usar só o InMemoryEmbeddingRetriever falha em perguntas com termos muito específicos, como códigos de produto ou números de referência, que os embeddings nem sempre captam bem. Juntar um InMemoryBM25Retriever em paralelo e combinar os resultados (uma técnica conhecida como recuperação híbrida) costuma melhorar a precisão em conjuntos de documentos heterogéneos.
Adicione um passo de reranking. Depois do retriever inicial devolver, por exemplo, os 20 documentos mais próximos, um componente de reranking pode reordenar esses 20 com um modelo mais preciso (e mais lento) antes de passar só os 3 ou 5 melhores ao prompt builder. Isto equilibra velocidade com qualidade.
Monitorize o pipeline em produção. O catálogo de integrações do Haystack inclui ligações a ferramentas de observabilidade que permitem registar cada execução do pipeline, medir latência por componente e detetar quando o retriever começa a devolver documentos irrelevantes, por exemplo depois de uma atualização na base documental.
Reindexe de forma incremental. Em vez de apagar e recriar todo o document store sempre que um documento muda, use os metadados e o identificador de cada documento para atualizar ou remover apenas os registos afetados. Isto é especialmente importante em bases de conhecimento que mudam com frequência.
Trate o template do prompt como código. Pequenas alterações na formatação do template do PromptBuilder podem mudar significativamente a qualidade das respostas. Vale a pena versionar os templates e testar alterações com o mesmo conjunto de perguntas de controlo antes de as colocar em produção.
Custos: quando usar modelos locais e quando pagar por API
Uma dúvida recorrente de quem testa este tipo de pipeline pela primeira vez é perceber quando vale a pena trocar os modelos locais por serviços pagos. A resposta depende do volume de perguntas, da qualidade exigida e da latência tolerável, mas há alguns pontos de referência úteis para decidir.
Os embedders locais do Sentence Transformers, como o all-MiniLM-L6-v2 usado neste tutorial, não têm custo por chamada e correm inteiramente na sua máquina ou servidor. Em contrapartida, o tempo de indexação escala com o hardware disponível: numa CPU comum, indexar alguns milhares de documentos curtos demora segundos, mas dezenas de milhares de páginas já beneficiam claramente de uma GPU. Os embedders da OpenAI, por outro lado, cobram por token processado e têm latência de rede associada a cada pedido, mas eliminam a necessidade de gerir hardware de inferência.
Para o componente gerador, a diferença de qualidade entre um modelo local pequeno, como o flan-t5-base usado nos exemplos acima, e um modelo de fronteira acedido via API costuma ser visível em perguntas que exigem raciocínio sobre vários documentos ao mesmo tempo. Para casos de uso simples, como responder com um único facto já presente num documento, um modelo pequeno é muitas vezes suficiente e evita custos recorrentes. Para assistentes que precisam de sintetizar informação espalhada por vários documentos, um modelo maior via API costuma valer o custo adicional.
Uma estratégia comum em produção é começar com modelos locais durante o desenvolvimento e os testes, onde o volume de chamadas é alto mas a exigência de qualidade é mais tolerante, e só trocar para um modelo pago na fase final, quando o pipeline já está estável e o volume de tráfego real é conhecido. Isto evita gastar orçamento de API a testar e re-testar o mesmo pipeline dezenas de vezes durante o desenvolvimento.
Projeto completo: estrutura final de ficheiros
No final deste tutorial, a estrutura do projeto deve ter este aspeto:
haystack-rag-tutorial/
├── venv/
├── documentos/
│ └── produto.txt
├── indexar.py # carrega e divide os documentos
├── criar_store.py # gera embeddings e popula o document store
├── pipeline_rag.py # monta o pipeline de retriever + gerador
├── perguntar.py # script de teste com uma pergunta única
├── chat.py # chatbot interativo de linha de comandos
├── avaliar.py # conjunto de testes automáticos de qualidade
└── servidor.py # API FastAPI que expõe o pipeline
Com estes ficheiros, tem um projeto funcional que cobre todo o ciclo: indexação, recuperação, geração, avaliação básica e exposição como serviço. A partir daqui, a evolução natural é trocar o document store em memória por uma opção persistente como o Qdrant, adicionar mais documentos reais e ajustar o template do prompt ao tom que quer que o chatbot use.
Vale a pena guardar este projeto num repositório Git desde o início, mesmo antes de estar completo. Isto permite comparar versões do template de prompt ao longo do tempo, reverter uma alteração que piorou a qualidade das respostas e documentar, através das mensagens de commit, porque é que certas decisões de configuração foram tomadas. Um ficheiro requirements.txt com as versões exatas de haystack-ai e das restantes dependências também evita o clássico problema de "no meu computador funciona", quando o projeto passa de uma máquina de desenvolvimento para um servidor de produção.
Segurança: cuidados a ter num pipeline RAG exposto a utilizadores
Construir um pipeline RAG que funciona é uma coisa. Expô-lo a utilizadores reais sem abrir buracos de segurança é outra, e vale a pena tratar isto como parte do tutorial, não como um extra opcional.
O primeiro cuidado é a injeção de prompt. Se o conteúdo dos documentos indexados vier de fontes não confiáveis, como ficheiros carregados por utilizadores externos, um documento malicioso pode conter instruções escondidas que tentam manipular o modelo gerador a ignorar as regras do template e a executar instruções diferentes das pretendidas. A defesa mais prática é tratar sempre o conteúdo recuperado como dados, nunca como instruções, e reforçar isso explicitamente no template do prompt, pedindo ao modelo para responder apenas com base no conteúdo factual dos documentos e ignorar qualquer instrução embutida neles.
O segundo cuidado é o isolamento entre utilizadores ou contas, já referido na secção de metadados. Num sistema multi-tenant, um erro de filtro no retriever pode fazer com que a resposta a um utilizador inclua trechos de documentos que pertencem a outra conta. Testar explicitamente este cenário, com perguntas desenhadas para tentar "escapar" do filtro de metadados, deve fazer parte da rotina de testes antes de qualquer lançamento em produção.
O terceiro ponto é a gestão de chaves de API. Nunca escreva a chave da OpenAI ou de outro serviço diretamente no código-fonte. Use variáveis de ambiente, como fizemos neste tutorial, ou um gestor de segredos dedicado quando o projeto crescer para produção. Chaves de API expostas em repositórios públicos do GitHub continuam a ser uma das causas mais comuns de faturas inesperadas e de acesso não autorizado a contas de serviços de IA.
Perguntas frequentes
O Haystack é gratuito?
Sim. O framework é distribuído sob licença Apache 2.0 e pode ser usado gratuitamente em projetos comerciais e pessoais. Os custos que possam surgir vêm de serviços externos que decida ligar, como APIs de modelos pagos ou bases de dados vetoriais geridas na cloud.
Preciso de uma chave de API para seguir este tutorial?
Não. Os exemplos principais usam um modelo de embeddings local do Sentence Transformers e um gerador local via Hugging Face, ambos gratuitos e sem necessidade de registo. A chave de API só é necessária se optar pelo passo opcional de usar um gerador OpenAI.
Qual a diferença entre InMemoryDocumentStore e uma base vetorial persistente?
O InMemoryDocumentStore guarda tudo em memória e perde os dados quando o processo termina, o que é adequado para testes e protótipos. Uma base como o Qdrant grava os dados em disco ou num serviço dedicado, mantendo os documentos e embeddings disponíveis entre reinícios da aplicação, o que é necessário em produção.
O Haystack substitui o LangChain?
Não é uma questão de substituição direta, mas de escolha de arquitetura. Ambos resolvem RAG e construção de agentes, mas com filosofias diferentes: o Haystack aposta em componentes explícitos com sockets nomeados, o LangChain organiza-se à volta de chains e runnables. A escolha depende do estilo de trabalho da equipa e das integrações específicas de que precisa.
Como lido com documentos muito grandes, como PDFs de centenas de páginas?
Use um componente de divisão (splitter) antes do embedder, para cortar o texto em blocos de tamanho gerível, normalmente entre 100 e 500 palavras, com alguma sobreposição entre blocos consecutivos para não perder contexto nas fronteiras. Depois, embeba e indexe os blocos resultantes, nunca o documento inteiro de uma vez.
Posso usar modelos em português no pipeline?
Sim. Tanto os embedders do Sentence Transformers como os geradores da Hugging Face ou da OpenAI aceitam texto em português. Para melhores resultados de recuperação, procure modelos de embeddings multilingues ou treinados especificamente em português, em vez de assumir que um modelo treinado só em inglês vai captar bem nuances do idioma.
O que fazer se o pipeline responder com informação que não está nos documentos?
Isto costuma ser um sintoma de "alucinação" do modelo gerador, que ignora o contexto fornecido. Reforce no template do prompt a instrução explícita para responder apenas com base nos documentos e para admitir quando não sabe. Modelos maiores costumam seguir melhor essa instrução do que modelos muito pequenos.
Vale a pena migrar um projeto já feito em LangChain para o Haystack?
Só se houver uma razão concreta, como dificuldade em depurar o fluxo de dados existente ou necessidade específica de uma integração que só o Haystack oferece. Para a maioria dos projetos que já funcionam bem, a migração raramente compensa o esforço só por preferência de arquitetura.
