Como Criar uma Base de Conhecimento com IA para sua Equipe em 48 Horas (PT-BR)
Aprenda a construir uma base de conhecimento com IA usando Python, LangChain e Streamlit em apenas 48 horas. Economize tempo da sua equipe e organize a documentação. Guia completo para o mercado brasileiro.
No mês passado, nossa equipe de produto de 35 pessoas estava afogada em documentação. Tínhamos wikis, Google Docs, páginas Confluence, bancos de dados Notion, threads do Slack com decisões críticas enterradas e um drive compartilhado com centenas de PDFs onde ninguém conseguia encontrar nada. Parece familiar?
Passei um fim de semana construindo uma base de conhecimento com IA que permite a qualquer pessoa da equipe fazer perguntas em português simples e obter respostas precisas, extraídas de toda a nossa documentação. Tempo total de construção: cerca de 48 horas. Custo mensal de execução: R$ 750 (equivalente a US$ 153). A equipe passou de "não consigo encontrar aquele documento" para "é só perguntar na base de conhecimento" em menos de uma semana.
Aqui está exatamente como eu a construí, passo a passo, com estimativas de tempo para que você possa planejar sua própria construção.
O Que Vamos Construir
A arquitetura é chamada RAG — Retrieval-Augmented Generation (Geração Aumentada por Recuperação). Em vez de pedir a um modelo de IA para saber tudo (ele não sabe), nós o alimentamos com nossos próprios documentos e o deixamos pesquisar neles para responder a perguntas. Pense nisso como dar ao ChatGPT acesso ao cérebro da sua empresa.
A pilha de tecnologia:
- Python 3.11+ — a espinha dorsal
- LangChain — framework de orquestração para conectar LLMs a dados
- ChromaDB — banco de dados vetorial para armazenar embeddings de documentos
- OpenAI Embeddings (text-embedding-3-small) — converte texto em vetores pesquisáveis
- OpenAI GPT-4o-mini — gera respostas a partir do contexto recuperado
- Streamlit — interface web rápida e limpa
Ao final deste tutorial, você terá um sistema funcional onde os usuários digitam uma pergunta, o sistema encontra os trechos de documentos mais relevantes e um LLM sintetiza uma resposta com citações de fontes.
Hora 0-2: Configuração do Ambiente e Dependências
Primeiro, vamos estruturar o projeto. Crie um novo diretório e configure um ambiente virtual:
mkdir team-knowledge-base
cd team-knowledge-base
python -m venv venv
source venv/bin/activate # No Windows: venv\Scripts\activate
pip install langchain langchain-openai langchain-community
pip install chromadb
pip install streamlit
pip install python-dotenv
pip install pypdf docx2txt unstructured
pip install tiktoken
Crie um arquivo .env para sua chave de API:
OPENAI_API_KEY=sk-sua-chave-aqui
E configure a estrutura do projeto:
team-knowledge-base/
├── .env
├── app.py # Frontend Streamlit
├── ingest.py # Pipeline de processamento de documentos
├── query_engine.py # Lógica de consulta RAG
├── config.py # Configuração compartilhada
├── documents/ # Coloque seus documentos aqui
│ ├── pdfs/
│ ├── markdown/
│ └── text/
└── chroma_db/ # Armazenamento do banco de dados vetorial
Verificação de tempo: isso deve levar cerca de 30 minutos se você já estiver familiarizado com ambientes Python, até 2 horas se precisar instalar o Python e configurar as chaves de API.
Hora 2-8: Pipeline de Ingestão de Documentos
Esta é a parte mais crítica de todo o sistema. A forma como você divide seus documentos (chunking) determina a qualidade das suas respostas. Aprendi isso da maneira mais difícil — minha primeira tentativa usou chunks ingênuos de 1000 caracteres e as respostas eram péssimas.
A Estratégia de Chunking Que Realmente Funciona
Depois de testar cinco abordagens diferentes de chunking, aqui está o que funcionou melhor para nossa documentação de formato misto:
# config.py
CHUNK_SIZE = 1500 # caracteres por chunk
CHUNK_OVERLAP = 200 # sobreposição entre chunks
EMBEDDING_MODEL = "text-embedding-3-small"
LLM_MODEL = "gpt-4o-mini"
COLLECTION_NAME = "team_knowledge"
CHROMA_DIR = "./chroma_db"
Por que 1500 caracteres com 200 de sobreposição? Através de testes, descobri que:
- Muito pequeno (500 caracteres): As respostas carecem de contexto. O modelo obtém um fragmento de frase e não consegue sintetizar uma resposta útil.
- Muito grande (3000+ caracteres): A precisão da recuperação diminui. Quando um chunk cobre vários tópicos, ele é puxado para consultas onde apenas parte dele é relevante, adicionando ruído ao contexto.
- 1500 caracteres: Ponto ideal para parágrafos de documentação típicos. A maioria das ideias completas cabe nesta janela.
- 200 caracteres de sobreposição: Evita que ideias que se estendem por limites de chunk sejam perdidas. Crítico para listas numeradas e instruções passo a passo.
Agora, aqui está o script de ingestão:
# ingest.py
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
Docx2txtLoader,
UnstructuredMarkdownLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from config import *
load_dotenv()
def load_documents(docs_dir="./documents"):
'Carrega todos os tipos de documentos suportados da árvore de diretórios.'
documents = []
loaders_map = {
".pdf": PyPDFLoader,
".txt": TextLoader,
".docx": Docx2txtLoader,
".md": UnstructuredMarkdownLoader,
}
for root, dirs, files in os.walk(docs_dir):
for file in files:
ext = os.path.splitext(file)[1].lower()
if ext in loaders_map:
file_path = os.path.join(root, file)
try:
loader = loaders_map[ext](file_path)
docs = loader.load()
# Adiciona metadados de origem
for doc in docs:
doc.metadata["source"] = file_path
doc.metadata["filename"] = file
documents.extend(docs)
print(f" Carregado: {file} ({len(docs)} páginas/seções)")
except Exception as e:
print(f" Erro ao carregar {file}: {e}")
return documents
def chunk_documents(documents):
'Divide documentos em chunks sobrepostos.'
splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(documents)
print(f" Dividiu {len(documents)} documentos em {len(chunks)} chunks")
return chunks
def create_vector_store(chunks):
'Incorpora chunks e armazena no ChromaDB.'
embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
vector_store = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
collection_name=COLLECTION_NAME,
persist_directory=CHROMA_DIR,
)
print(f" Vector store criado com {len(chunks)} embeddings")
return vector_store
if __name__ == "__main__":
print("Passo 1: Carregando documentos...")
docs = load_documents()
print(f" Total de documentos carregados: {len(docs)}")
print("Passo 2: Dividindo documentos em chunks...")
chunks = chunk_documents(docs)
print("Passo 3: Criando o vector store...")
store = create_vector_store(chunks)
print("Concluído! A base de conhecimento está pronta.")
O RecursiveCharacterTextSplitter é fundamental aqui. Ele tenta dividir primeiro por quebras de parágrafo (\n\n), depois por quebras de linha, depois por frases e, em seguida, por palavras. Isso significa que seus chunks quase sempre conterão ideias completas, em vez de serem cortados no meio de uma frase.
Lidando com Diferentes Tipos de Documentos
Uma nota rápida sobre tipos de documentos, porque isso me deu trabalho:
- PDFs: PyPDF funciona para a maioria dos documentos, mas PDFs escaneados precisam de OCR. Se você tiver documentos escaneados, adicione
pip install pytesseracte useUnstructuredPDFLoaderem vez disso. - Google Docs: Exporte como .docx primeiro. A API do Google Drive pode automatizar isso, mas para o MVP, a exportação manual está ok.
- Confluence: Exporte espaços como HTML e, em seguida, use
UnstructuredHTMLLoader. Ele lida com formatação aninhada surpreendentemente bem. - Threads do Slack: Esta é a mais difícil. Escrevi um pequeno script que usa a API do Slack para exportar threads marcados como arquivos de texto. Isso é um tutorial separado, mas a chave é incluir o contexto da thread (nome do canal, participantes, data) como metadados.
Hora 8-16: Motor de Consulta
Agora a parte divertida — fazer a base de conhecimento realmente responder a perguntas.
# query_engine.py
import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from config import *
load_dotenv()
# Prompt personalizado que exige citação da fonte
PROMPT_TEMPLATE = (
"Você é um assistente útil para uma equipe de produto.\n"
"Use o seguinte contexto para responder à pergunta.\n"
"Se você não souber a resposta com base no contexto, diga isso honestamente.\n"
"Sempre cite qual(is) documento(s) você está usando como base.\n\n"
"Contexto:\n{context}\n\n"
"Pergunta: {question}\n\n"
"Resposta (cite suas fontes):"
)
def get_query_engine():
'Inicializa e retorna o motor de consulta RAG.'
embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
vector_store = Chroma(
collection_name=COLLECTION_NAME,
persist_directory=CHROMA_DIR,
embedding_function=embeddings,
)
retriever = vector_store.as_retriever(
search_type="mmr", # Maximum Marginal Relevance
search_kwargs={
"k": 6, # Retorna os 6 melhores chunks
"fetch_k": 20, # Considera os 20 melhores antes da filtragem MMR
"lambda_mult": 0.7, # Equilíbrio entre diversidade e relevância
}
)
llm = ChatOpenAI(model=LLM_MODEL, temperature=0.1)
prompt = PromptTemplate(
template=PROMPT_TEMPLATE,
input_variables=["context", "question"]
)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": prompt}
)
return qa_chain
def query(question):
'Consulta a base de conhecimento e retorna a resposta + fontes.'
engine = get_query_engine()
result = engine.invoke({"query": question})
sources = set()
for doc in result.get("source_documents", []):
sources.add(doc.metadata.get("filename", "Desconhecido"))
return {
"answer": result["result"],
"sources": list(sources),
}
Por Que MMR em Vez de Pesquisa de Similaridade Simples
Quero destacar a configuração search_type="mmr" porque ela faz uma enorme diferença. A pesquisa de similaridade padrão retorna os 6 chunks mais semelhantes à sua consulta — mas esses 6 chunks podem ser todos do mesmo documento, dizendo mais ou menos a mesma coisa. Isso é redundante e desperdiça sua janela de contexto.
MMR (Maximum Marginal Relevance) equilibra relevância com diversidade. Ele escolhe o primeiro chunk baseado puramente em similaridade e, para cada chunk subsequente, penaliza chunks que são muito semelhantes aos que já foram selecionados. O resultado são 6 chunks que são todos relevantes, mas cobrem diferentes aspectos do tópico.
O lambda_mult=0.7 controla esse equilíbrio: 1.0 é pura similaridade (sem diversidade), 0.0 é pura diversidade (pode não ser relevante). Descobri que 0.7 atinge o ponto ideal para consultas de documentação.
Hora 16-24: Frontend Streamlit
É hora de tornar isso utilizável por pessoas que não ficam com um terminal aberto o dia todo.
# app.py
import streamlit as st
from query_engine import query, get_query_engine
import time
st.set_page_config(
page_title="Base de Conhecimento da Equipe",
page_icon="search",
layout="wide"
)
st.title("Base de Conhecimento da Equipe")
st.markdown("Pergunte qualquer coisa sobre nossa documentação, processos ou decisões.")
# Inicializa o motor uma vez
if "engine" not in st.session_state:
with st.spinner("Carregando base de conhecimento..."):
st.session_state.engine = get_query_engine()
# Histórico do chat
if "messages" not in st.session_state:
st.session_state.messages = []
# Exibe histórico do chat
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
if msg.get("sources"):
with st.expander("Fontes"):
for src in msg["sources"]:
st.markdown(f"- {src}")
# Entrada
if prompt := st.chat_input("O que você gostaria de saber?"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
with st.spinner("Buscando na base de conhecimento..."):
start = time.time()
result = query(prompt)
elapsed = time.time() - start
st.markdown(result["answer"])
st.caption(f"Tempo de resposta: {elapsed:.1f}s")
if result["sources"]:
with st.expander("Fontes"):
for src in result["sources"]:
st.markdown(f"- {src}")
st.session_state.messages.append({
"role": "assistant",
"content": result["answer"],
"sources": result["sources"],
})
# Barra lateral
with st.sidebar:
st.header("Sobre")
st.markdown(
"Esta base de conhecimento pesquisa em toda a documentação da equipe "
"para responder às suas perguntas."
)
st.markdown("---")
st.markdown("**Dicas para melhores resultados:**")
st.markdown("- Seja específico em suas perguntas")
st.markdown("- Pergunte sobre um tópico por vez")
st.markdown("- Se a resposta parecer errada, tente reformular a pergunta")
Execute com streamlit run app.py e você terá uma base de conhecimento funcional com uma interface de chat.
Hora 24-36: Tornando-o Realmente Bom
A versão básica funciona, mas aqui está o que adicionei para torná-la pronta para produção para uma equipe de 35 pessoas:
1. Filtragem por Metadados
Nem todos os documentos são criados iguais. Uma especificação de produto da semana passada é mais relevante do que uma de dois anos atrás. Adicionei metadados baseados em data aos chunks e modifiquei o retriever para filtrar opcionalmente por intervalo de datas ou categoria de documento.
# Em ingest.py, ao carregar documentos:
doc.metadata["ingested_date"] = datetime.now().isoformat()
doc.metadata["category"] = determine_category(file_path) # Baseado na estrutura de pastas
2. Loop de Feedback
Adicionei botões de "gostei/não gostei" a cada resposta e os registrei em um banco de dados SQLite. Após uma semana, tive dados suficientes para identificar quais documentos estavam causando respostas ruins (geralmente porque estavam desatualizados ou mal estruturados) e quais padrões de consulta precisavam de um tratamento melhor.
3. Re-ingestão Automática
Configurei um cron job que executa o script de ingestão todas as noites em uma pasta sincronizada do Google Drive. Quando alguém atualiza um documento, a base de conhecimento capta as alterações em 24 horas. Para o drive compartilhado, usei rclone para sincronizar com o servidor antes da execução da ingestão.
4. Controle de Acesso
Para nossa equipe, isso não era uma grande preocupação (todos têm acesso a todos os documentos), mas se você precisar, o Streamlit suporta autenticação via streamlit-authenticator ou você pode colocá-lo atrás de um proxy OAuth como oauth2-proxy.
Hora 36-48: Testes, Ajustes e Implantação
Metodologia de Teste
Preparei um conjunto de testes com 50 perguntas que eu sabia as respostas. Em seguida, categorizei as respostas da base de conhecimento:
- Corretas e bem-referenciadas: 38 de 50 (76%)
- Parcialmente corretas (ideia certa, faltando detalhes): 7 de 50 (14%)
- Incorretas: 2 de 50 (4%)
- Disse corretamente "Não sei": 3 de 50 (6%)
76% de precisão na primeira tentativa foi bom o suficiente para lançar. Após ajustar o tamanho do chunk, a sobreposição e o prompt, consegui chegar a 84%. Os erros restantes foram principalmente devido a perguntas ambíguas ou documentos que continham informações contraditórias (o que é um problema de documentação, não um problema de IA).
Implantação
Implantei em uma pequena VM na nuvem (4GB RAM, 2 vCPUs — cerca de R$ 100/mês, equivalente a US$ 20). A configuração:
- Aplicativo Streamlit rodando atrás de um proxy reverso nginx
- SSL via Let's Encrypt
- Serviço systemd para reinício automático
- ChromaDB armazenado em disco (nenhum servidor de banco de dados separado necessário)
Detalhes de Custo
Aqui está o custo de execução para uma equipe de 35 pessoas:
| Item | Custo Mensal (BRL) | Custo Mensal (USD) |
|---|---|---|
| OpenAI API — Embeddings (re-ingestão + consultas) | R$ 115 | $23 |
| OpenAI API — GPT-4o-mini (respostas a consultas) | R$ 425 | $85 |
| Cloud VM (4GB RAM) | R$ 100 | $20 |
| Domínio + SSL | R$ 25 | $5 |
| Google Drive storage (documentos compartilhados) | R$ 100 | $20 |
| Total | R$ 765 | $153 |
Isso dá cerca de R$ 21,86 (US$ 4,37) por pessoa por mês. Para comparação, ferramentas comerciais de IA para bases de conhecimento como Guru AI ou Glean custam de US$ 15 a 25 por usuário por mês. Estamos economizando aproximadamente R$ 1.850-3.600 (US$ 370-720)/mês em comparação com soluções prontas, e temos controle total sobre nossos dados.
Lições Aprendidas (Da Maneira Difícil)
O tamanho do chunk importa mais do que a escolha do modelo. Gastei horas testando diferentes LLMs quando o problema real era minha estratégia de chunking. Chunks ruins resultam em respostas ruins. Nenhum modelo pode consertar uma recuperação de lixo.
Metadados não são opcionais. Quando um usuário pergunta "qual é a nossa política de reembolso?" e a base de conhecimento retorna a política de reembolso de 2023 em vez da versão atualizada de 2026, isso é pior do que não retornar nada. Metadados de data e atribuição de fonte são essenciais.
As pessoas fazem perguntas de forma diferente do que você espera. Eu presumi que as pessoas perguntariam coisas como "Qual é o processo de implantação para o serviço X?" Em vez disso, elas perguntaram "como faço para enviar código?" e "onde o X roda?". A engenharia de prompt necessária para lidar com perguntas casuais e vagas foi mais trabalhosa do que eu esperava.
Comece com 20 documentos, não 2.000. Inicialmente, tentei ingerir tudo de uma vez. Os custos de embedding dispararam, a qualidade foi inconsistente e a depuração foi dolorosa. Comece pequeno, valide as respostas e depois aumente a escala.
A resposta "Não sei" é um recurso. Treine o modelo (via prompt do sistema) para dizer "Não tenho informações sobre isso na base de conhecimento" em vez de adivinhar. Os usuários confiam mais no sistema quando ele é honesto sobre suas limitações.
Problemas Comuns e Soluções
"As respostas são muito genéricas." Aumente o número de chunks recuperados de 4 para 6-8 e certifique-se de que seus chunks sejam grandes o suficiente para conter ideias completas. Verifique também se as informações relevantes estão realmente no seu conjunto de documentos.
"Ele continua citando o documento errado." Seus embeddings podem estar desatualizados. Execute novamente o pipeline de ingestão. Verifique também se você tem documentos duplicados ou diferentes versões do mesmo documento no sistema.
"Está lento." O principal gargalo geralmente é a chamada ao LLM, não a recuperação. Mude para o GPT-4o-mini se ainda não o fez (é mais rápido e mais barato que o GPT-4o com quase a mesma qualidade para tarefas de Q&A). Além disso, armazene em cache as perguntas frequentes.
"Ele alucina." Diminua a temperatura para 0.0-0.1 e adicione instruções explícitas no prompt: "Responda apenas com base no contexto fornecido. Se o contexto não contiver a resposta, diga isso." Isso reduz drasticamente a alucinação.
O Que Vem Por Aí
Desde a implantação há três semanas, nossa equipe fez mais de 1.200 perguntas. As consultas mais populares são sobre processos de implantação, documentação de API e "quem decidiu isso e por quê" (razão pela qual a ingestão de threads de decisão do Slack foi tão valiosa).
Em seguida, estou planejando adicionar: memória conversacional (para que perguntas de acompanhamento funcionem naturalmente), integração com o Slack (faça perguntas diretamente em um canal) e detecção automática de documentos desatualizados (sinalize documentos que não foram atualizados em mais de 6 meses, mas ainda estão sendo citados).
Construir isso levou um fim de semana, mas a economia de tempo se acumula todos os dias. Se sua equipe está gastando mais de 30 minutos por semana procurando informações, isso se paga quase imediatamente.