Stack AI $0: explicação detalhada com exemplos
1. Visão geral
Muitas pessoas acham que construir um sistema de IA — agentes, RAG, chatbot interno — custa alguns milhares de dólares por mês porque precisa pagar API para OpenAI ou Anthropic. A realidade é diferente: existe uma stack com 8 camadas, cada camada tendo uma versão gratuita ou auto-hospedada, suficiente para você construir e rodar um sistema AI completo com custo $0. Este artigo passa por cada camada dessa stack, explica o papel de cada uma, com exemplos de código concretos para que você entenda como encaixá-las. Após ler, você saberá qual ferramenta escolher para cada camada e compreenderá onde estão os verdadeiros limites do “gratuito”.
2. Por que “$0” não significa “eternamente gratuito”
Antes de entrar em detalhes, preciso deixar claro uma coisa: $0 aqui significa custo de software/licença, não é custo de infraestrutura ilimitado. Vercel free tier tem limite de largura de banda, Supabase free tier tem limite de armazenamento, rodar Ollama localmente consome CPU/RAM da sua máquina. Esta stack é adequada para prototipar, aprender, fazer demos para pequenos clientes, não para escalar para milhões de usuários.
Nota: Se o tráfego real seu exceder o free tier, você ainda precisa pagar — apenas pagará pela infraestrutura (compute, storage) em vez de pagar ao fornecedor de LLM por token. Esta é uma distinção importante que você precisa entender antes de usar essa stack em produção real.
3. Camada Frontend: Next.js ou Streamlit
A camada de interface não precisa ser complexa. Se você quer uma UI bonita, use Next.js e faça deploy no Vercel free tier. Se você só precisa de um demo rápido para um pipeline AI, Streamlit é muito mais rápido porque não precisa escrever frontend separado.
# app.py
import streamlit as st
import requests
st.title("Assistente de Q&A de documentos internos")
question = st.text_input("Digite sua pergunta")
if st.button("Perguntar") and question:
response = requests.post(
"http://localhost:8000/ask",
json={"question": question},
timeout=30,
)
st.write(response.json()["answer"])
Este código cria um campo de entrada e um botão. Ao clicar no botão, ele chama a API backend (rodando localmente) e exibe a resposta. Execute com streamlit run app.py, Streamlit hospeda automaticamente em localhost:8501. Sem precisar escrever React, sem precisar construir pipelines — adequado para a fase de validar a ideia antes de investir em Next.js.
4. Camada de Orquestração de Agentes: LangGraph
Quando seu sistema não é apenas “chamar LLM uma vez e retornar o resultado”, mas requer múltiplas etapas (recuperar → sintetizar → validar → responder), você precisa de um orquestrador. LangGraph modela agentes como um grafo com estado, permitindo ramificações e iterações.
pip install langgraph langchain-ollama
from langgraph.graph import StateGraph, END
from typing import TypedDict
class AgentState(TypedDict):
question: str
answer: str
def retrieve_step(state: AgentState) -> AgentState:
state["answer"] = f"Buscando documentos para: {state['question']}"
return state
def generate_step(state: AgentState) -> AgentState:
state["answer"] += " -> resposta sintetizada"
return state
graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve_step)
graph.add_node("generate", generate_step)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)
app = graph.compile()
result = app.invoke({"question": "Qual é a política de garantia?", "answer": ""})
print(result["answer"])
Buscando documentos para: Qual é a política de garantia? -> resposta sintetizada
StateGraph define o fluxo de processamento com dois nós: retrieve e generate, cada nó recebe um estado e retorna um novo estado. set_entry_point designa o nó inicial, add_edge conecta as etapas. Ao chamar app.invoke(...), LangGraph executa sequencialmente pelo grafo e retorna o estado final. Tudo roda localmente, sem chamar nenhuma API paga.
5. Camada de Pipeline RAG: Chroma e Qdrant
RAG (Retrieval-Augmented Generation) precisa de uma vector store para armazenar embeddings e fazer busca por similaridade. Chroma roda embedded, sem precisar de servidor separado — adequado para protótipos.
pip install chromadb
import chromadb
client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection(name="policies")
collection.add(
documents=["Garantia do produto por 12 meses a partir da data de compra.",
"Devolução dentro de 7 dias se a caixa estiver intacta."],
ids=["doc1", "doc2"],
)
results = collection.query(query_texts=["política de garantia"], n_results=1)
print(results["documents"])
[['Garantia do produto por 12 meses a partir da data de compra.']]
PersistentClient salva dados no disco, em vez de apenas na memória, então os dados persistem entre reinicializações. collection.add calcula automaticamente embeddings para os documentos (Chroma tem uma função de embedding padrão). collection.query encontra o documento mais próximo em termos de vetor em relação à pergunta. Para datasets maiores, mude para Qdrant rodando localmente via Docker — API similar mas otimizado para escala média.
6. Camada LLM: Ollama rodando modelo localmente
Esta é a camada essencial que o liberta das contas de API. Ollama permite rodar Llama, Mistral, Gemma diretamente na sua máquina, sem limite de requisições.
ollama pull llama3.1
ollama run llama3.1 "Resuma a política de garantia em uma frase."
O produto tem garantia de 12 meses a partir da data de compra.
O comando ollama pull baixa o modelo para a máquina (o modelo é quantizado para rodar em CPU/GPU comum). O comando ollama run inicia o modelo e envia o prompt, retorna o resultado diretamente no terminal. Ollama também abre uma REST API em localhost:11434, então você pode integrar em código Python ou Java.
Se seu backend é Spring Boot, chamar Ollama via RestClient é muito simples:
import org.springframework.web.client.RestClient;
public class OllamaClient {
private final RestClient restClient = RestClient.create("http://localhost:11434");
public String generate(String model, String prompt) {
var response = restClient.post()
.uri("/api/generate")
.body(new OllamaRequest(model, prompt, false))
.retrieve()
.body(OllamaResponse.class);
return response.response();
}
record OllamaRequest(String model, String prompt, boolean stream) {}
record OllamaResponse(String response) {}
}
OllamaClient encapsula a chamada HTTP para Ollama. RestClient do Spring 6 substitui o antigo RestTemplate, escrita mais concisa. OllamaRequest define stream = false para receber a resposta completa de uma vez, em vez de fazer streaming de cada token — mais fácil de lidar para exemplos de demonstração.
7. Uso de Ferramentas via MCP (Model Context Protocol)
Se o agente apenas conversa com LLM, ele não consegue fazer nada prático. MCP é um protocolo de comunicação que ajuda agentes a chamar ferramentas externas — ler arquivos, consultar banco de dados, chamar GitHub API — através de uma interface unificada, sem precisar escrever integrações separadas para cada ferramenta.
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/data/docs"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
}
}
}
Este arquivo de configuração declara dois servidores MCP: um para sistema de arquivos (leitura/escrita de arquivos em /data/docs), um para GitHub (criar issues, ler PRs). Seu agente (LangGraph, Claude Code, ou qualquer cliente que suporte MCP) apenas precisa se conectar a esses servidores e chamar ferramentas conforme o esquema padrão, sem precisar escrever wrappers de API separados para cada serviço.
Nota: Servidores MCP rodando localmente via
npxnão custam nada de licença, mas alguns servidores exigem access tokens (por exemplo, GitHub token) — esses tokens ainda precisam vir do serviço original.
8. Agente de Código: Aider
Quando você quer que o agente escreva e edite código automaticamente no repositório, Aider é uma ferramenta CLI de código aberto que funciona bem com modelos locais via Ollama.
pip install aider-chat
aider --model ollama/llama3.1 --file src/main/java/OrderService.java
O comando acima abre o Aider, especifica o uso do modelo llama3.1 via Ollama (não via nenhuma API paga), e carrega o arquivo OrderService.java no contexto. Daqui você digita requisições em vietnamita ou inglês (“adicionar validação de quantidade negativa”), Aider gerará um diff e commitará automaticamente se você concordar.
9. Camada de Dados: SQLite, DuckDB, Supabase
Nem todo sistema de IA precisa de um cluster Postgres. SQLite é suficiente para armazenamento estruturado pequeno, DuckDB é ótimo para análise de dados em formato de arquivo (CSV, Parquet) sem precisar de servidor.
pip install duckdb
import duckdb
con = duckdb.connect()
result = con.execute("""
SELECT category, COUNT(*) AS total
FROM read_csv_auto('orders.csv')
GROUP BY category
ORDER BY total DESC
""").fetchall()
print(result)
[('electronics', 120), ('books', 45), ('clothing', 30)]
DuckDB lê diretamente arquivo CSV usando read_csv_auto, sem precisar de ETL ou importar em um banco de dados separado. A consulta SQL executa quase instantaneamente com dados de centenas de milhares de linhas — suficiente para a maioria dos casos de uso de análise interna. Quando você precisa de um backend com REST API e autenticação prontos, Supabase free tier fornece Postgres gerenciado com realtime e armazenamento.
10. Deployment: Docker, Cloudflare Workers, Hugging Face
Containerização com Docker ajuda você a fazer deploy consistentemente entre sua máquina de desenvolvimento e máquina de hospedagem gratuita.
# docker-compose.yml
services:
ollama:
image: ollama/ollama
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
app:
build: .
ports:
- "8000:8000"
depends_on:
- ollama
volumes:
ollama_data:
Este arquivo define dois serviços: ollama roda o modelo LLM, app é seu backend que depende de ollama para fazer inferência. docker compose up inicia ambos ao mesmo tempo, o volume ollama_data preserva os modelos baixados entre reinicializações de container. Quando você precisa de um demo público, você faz push da parte frontend para Cloudflare Workers (edge, free tier generoso) e modelo pequeno para Hugging Face Spaces para que outros possam testar sem precisar configurar suas máquinas.
11. Montando toda a stack juntos
O fluxo de execução real: o usuário digita uma pergunta no Streamlit → LangGraph recebe a requisição, chama Chroma para recuperar contexto relevante → incorpora contexto no prompt → envia para Ollama → se precisar de ferramenta (ler arquivo, consultar DB), o agente chama via MCP → resultado retorna para a UI. Tudo roda em uma única máquina ou container, nenhuma requisição sai para nenhuma API paga.
def answer_question(question: str) -> str:
docs = collection.query(query_texts=[question], n_results=2)
context = "\n".join(docs["documents"][0])
prompt = f"Com base nas informações a seguir:\n{context}\n\nResponda: {question}"
import ollama
response = ollama.generate(model="llama3.1", prompt=prompt)
return response["response"]
Esta função é o “coração” do RAG: obtém contexto do Chroma, o incorpora no prompt, chama Ollama para gerar a resposta final. Este é o padrão retrieve-then-generate mais básico, sem precisar de framework complexo se seu caso de uso for simples.
12. Escrevendo testes para a parte de processamento de dados
A parte mais fácil de testar em um pipeline RAG geralmente é o passo de divisão de texto (chunking) antes de incorporar o vetor. Se seu backend Java lidar com este passo, um teste unitário simples ajuda a evitar erro off-by-one ao dividir.
import java.util.List;
import java.util.ArrayList;
public class DocumentChunker {
public List<String> chunk(String text, int maxLength) {
List<String> chunks = new ArrayList<>();
for (int i = 0; i < text.length(); i += maxLength) {
chunks.add(text.substring(i, Math.min(i + maxLength, text.length())));
}
return chunks;
}
}
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DocumentChunkerTest {
@Test
void shouldSplitTextIntoFixedSizeChunks() {
DocumentChunker chunker = new DocumentChunker();
List<String> chunks = chunker.chunk("Garantia de 12 meses para todos os produtos", 10);
assertThat(chunks).hasSizeGreaterThan(1);
assertThat(chunks.get(0)).hasSize(10);
assertThat(String.join("", chunks)).isEqualTo("Garantia de 12 meses para todos os produtos");
}
}
Este teste afirma três coisas: texto longo é dividido em vários chunks, o primeiro chunk tem o comprimento correto, unindo todos os chunks deve resultar no texto original sem perder nenhum caractere. Este é o tipo de bug silencioso que pode acontecer quando você escreve sua própria lógica de chunking sem testar adequadamente.
13. Erros comuns e quando NÃO usar esta stack
- Modelos locais são muito mais lentos que APIs em nuvem se sua máquina não tiver GPU. Com uma CPU comum, uma requisição llama3.1 8B pode levar 5-15 segundos — aceitável para demo, não aceitável para um produto que precisa resposta instantânea.
- Free tier tem rate limits discretos. Vercel, Supabase, Cloudflare todos limitam requisições/mês; exceder o limite pode bloquear sua app sem você saber antecipadamente se não monitorar o dashboard.
- Chroma embedded não é adequado para multi-instância. Se você escalar seu backend para vários pods, cada pod tendo seu próprio arquivo Chroma causará inconsistência — nesse ponto você precisa mudar para servidor Qdrant ou vector store gerenciado.
- Servidores MCP locais aumentam a superfície de ataque. Um servidor MCP de sistema de arquivos permite o agente ler/escrever arquivos — se acidentalmente expor para a internet, essa é uma vulnerabilidade de segurança séria.
Nota: Se você está construindo um produto com SLA que precisa de latência baixa estável ou tráfego grande desde o início, esta stack $0 deveria ser usada apenas na fase de prototipagem. Quando for ao vivo de verdade, considere pagar por API LLM gerenciada ou instância GPU dedicada para garantir qualidade de serviço.
14. Conclusão
A stack $0 não é truque de marketing — é uma combinação real de ferramentas open source e free tiers suficiente para você construir um sistema de IA com RAG, agentes, uso de ferramentas e deployment completo sem precisar de cartão de crédito. A barreira real não é o dinheiro, é se você entende bem como encaixar cada camada, compreender os limites dos free tiers, e saber quando migrar para infraestrutura paga para garantir qualidade. Comece com essa stack para aprender e validar ideias, depois faça upgrade de cada camada conforme o produto realmente precisar escalar.