O problema: ensinar o que ainda não ensinou
Tenho um curso de programação com 47 aulas. Cada aula tem notes (onde explico as coisas) e labs (onde o aluno pratica). E tenho um problema: às vezes uso conceitos nos labs que ainda não expliquei nas notes.
“Beleza, neste exercício usa map para transformar a lista.”
O problema? Não expliquei o que diabos é map até três aulas depois.
Isso acontece mais do que você imagina. Você tem o material na cabeça, pula de um lugar para outro, e sem perceber assume que o aluno sabe coisas que ainda não contou para ele. O resultado: frustração, confusão, e alunos que acham que são burros quando o burro é você.
A solução manual seria revisar cada lab, anotar que conceitos usa, e verificar que foram explicados antes. Mas tenho 47 aulas com vários notebooks cada uma. Não vai rolar.
A solução: busca semântica com ChromaDB
A ideia é simples:
- Extrair os conceitos de cada notebook (o que se ensina, o que se usa)
- Guardá-los em um banco de dados que entenda significado, não só texto
- Para cada conceito usado em um lab, verificar que existe em notes anteriores
Isso de “entender significado” é fundamental. Se nas notes eu digo “função de ordem superior” e no lab uso “higher-order function”, um grep não vai encontrar nada. Mas semanticamente são a mesma coisa.
Aqui que entra o ChromaDB: um banco de dados de vetores que converte texto em embeddings e permite buscar por similaridade. Traduzindo: você guarda texto, e depois pode perguntar “tem algo parecido com isso?” e ele te devolve os mais similares.
ChromaDB em 5 minutos
ChromaDB é como SQLite mas para embeddings. Um só arquivo (ou pasta), sem servidor, sem configuração. Instala, usa, e pronto.
pip install chromadb
# Ou se usa uv:
uv add chromadb
O conceito básico
Em um banco normal você guarda linhas com colunas. No ChromaDB você guarda documentos com embeddings:
import chromadb
# Criar cliente (persistente em disco)
client = chromadb.PersistentClient(path="./meu_db")
# Criar uma "coleção" (como uma tabela)
collection = client.get_or_create_collection(
name="conceitos",
metadata={"hnsw:space": "cosine"} # Distância cosseno
)
# Guardar documentos
collection.add(
ids=["c1", "c2", "c3"],
documents=["função pura", "loop for", "recursão"],
metadatas=[
{"aula": "class_010", "tipo": "notes"},
{"aula": "class_015", "tipo": "notes"},
{"aula": "class_020", "tipo": "notes"},
]
)
É isso. O ChromaDB automaticamente:
- Gera embeddings dos documentos (por padrão usa
all-MiniLM-L6-v2) - Os indexa para busca rápida
- Os persiste em disco
Buscar por similaridade
results = collection.query(
query_texts=["higher-order function"],
n_results=3
)
print(results["documents"])
# [['função de ordem superior', 'função pura', 'recursão']]
print(results["distances"])
# [[0.23, 0.45, 0.67]] # Menor = mais similar
Viu? Busquei “higher-order function” e encontrou “função de ordem superior” mesmo o texto sendo completamente diferente. Essa é a magia dos embeddings.
O sistema completo: validação curricular
Agora vamos construir o sistema que valida para não vacilar. O código real está no meu projeto, mas aqui te dou a versão simplificada para você entender o conceito.
Passo 1: Extrair conceitos dos notebooks
Primeiro precisamos tirar os conceitos de cada notebook. Isso faço com um LLM (Gemini Flash via OpenRouter), mas você poderia fazer com regexes se for corajoso:
def extract_concepts_from_notebook(notebook_path: Path) -> list[dict]:
"""
Extrai conceitos de um notebook Jupyter.
Returns:
Lista de {"name": "conceito", "category": "introduces|uses"}
"""
content = get_notebook_content(notebook_path)
# Chamar o LLM para extrair conceitos
response = llm.chat(
messages=[
{"role": "system", "content": EXTRACTION_PROMPT},
{"role": "user", "content": content}
]
)
return json.loads(response)
O LLM classifica cada conceito como:
- introduces: Se ensina com explicação
- uses: Se usa assumindo conhecimento prévio
Passo 2: Guardar no ChromaDB
Agora guardamos os conceitos com seus metadados:
from chromadb.utils import embedding_functions
# Usar modelo multilíngue (português + inglês)
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
collection = client.get_or_create_collection(
name="course_concepts",
embedding_function=embedding_fn,
metadata={"hnsw:space": "cosine"}
)
# Guardar conceitos
for class_id, concepts in course_concepts.items():
for concept in concepts:
collection.add(
ids=[f"{class_id}:{concept['name']}"],
documents=[concept["name"]],
metadatas=[{
"class_id": class_id,
"category": concept["category"],
"source_type": concept["source_type"], # notes ou labs
}]
)
Passo 3: Validar a progressão
Aqui vem o interessante. Para cada conceito “usado” em um lab, verificamos que existe algo similar em notes anteriores:
def validate_curriculum(course_concepts: dict) -> list[str]:
"""
Valida que os labs não usem conceitos não ensinados.
Returns:
Lista de erros encontrados
"""
errors = []
known_concepts = set()
# Processar aulas em ordem
for class_id in sorted(course_concepts.keys()):
class_data = course_concepts[class_id]
# Adicionar conceitos introduzidos em notes aos conhecidos
for c in class_data:
if c["source_type"] == "notes" and c["category"] == "introduces":
known_concepts.add(c["name"].lower())
# Validar conceitos usados em labs
for c in class_data:
if c["source_type"] == "labs" and c["category"] == "uses":
if not is_concept_known(c["name"], known_concepts):
errors.append(
f"{class_id}: '{c['name']}' usado sem ensinar"
)
return errors
A função is_concept_known é onde entra o ChromaDB. Não fazemos match exato, fazemos busca semântica:
def is_concept_known(concept: str, known_concepts: set) -> bool:
"""Verifica se um conceito é conhecido (exato ou semântico)."""
# 1. Match exato
if concept.lower() in known_concepts:
return True
# 2. Busca semântica
results = collection.query(
query_texts=[concept],
n_results=3,
where={"category": "introduces"} # Só buscar em "introduces"
)
# Se há algo muito similar (distância < 0.3), consideramos conhecido
if results["distances"][0] and results["distances"][0][0] < 0.3:
return True
return False
Passo 4: O relatório
Executando a validação sobre meu curso, obtenho um relatório bonitinho:
# Relatório de Validação Curricular
Foram encontrados 17 problemas:
## class_006_abstraciones_abstraccion_funcion
- **refatorar**: Conceito 'refatorar' usado em lab mas não ensinado
- Arquivo: `labs/0.funciones_basicas.ipynb`
## class_020_secuencias_while
- **memoização**: Conceito 'memoização' usado em lab mas não ensinado
- Arquivo: `labs/2.generar_secuencias.ipynb`
## class_026_funciones_orden_superior
- **map**: Conceito 'map' usado em lab mas não ensinado
- Arquivo: `labs/0.ejercicios_aplicadores_listas.ipynb`
Agora sei exatamente o que preciso arrumar.
Detalhes que importam
Modelo de embeddings multilíngue
Se seu conteúdo está em português, use um modelo multilíngue:
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
O modelo padrão (all-MiniLM-L6-v2) está treinado principalmente em inglês e pode dar resultados estranhos com português.
Distância cosseno vs euclidiana
Para texto, use distância cosseno:
collection = client.get_or_create_collection(
name="concepts",
metadata={"hnsw:space": "cosine"} # ← Isso
)
A distância cosseno mede o ângulo entre vetores, ignorando a magnitude. Isso é o que você quer para similaridade semântica.
Converter distância para similaridade
ChromaDB retorna distância (menor = mais similar). Se quiser similaridade (maior = mais similar):
similarity = 1 - distance
Com distância cosseno, o range é [0, 2], então a similaridade fica em [-1, 1]. Na prática, para textos similares costuma estar em [0.5, 1].
Persistência
ChromaDB tem dois modos:
# Em memória (se perde ao fechar)
client = chromadb.Client()
# Persistente (salva em disco)
client = chromadb.PersistentClient(path="./data/chroma")
Para um sistema de validação que você vai executar repetidamente, use persistente. Assim não recalcula embeddings toda vez.
Filtros com where
Você pode filtrar resultados por metadados:
# Só buscar em notes
results = collection.query(
query_texts=["função"],
where={"source_type": "notes"}
)
# Só buscar em aulas anteriores à 020
results = collection.query(
query_texts=["função"],
where={"class_num": {"$lt": 20}}
)
Isso é crucial para a validação: só queremos buscar em conceitos que já foram ensinados.
Alternativas ao ChromaDB
ChromaDB não é a única opção. Aqui você tem outras:
| Ferramenta | Prós | Contras |
|---|---|---|
| ChromaDB | Simples, sem servidor, boa documentação | Limitado a milhões de vetores |
| Pinecone | Escalável, managed | Pago, vendor lock-in |
| Weaviate | Poderoso, GraphQL API | Mais complexo de configurar |
| Qdrant | Rápido, Rust | Menos conhecido |
| pgvector | Se já usa PostgreSQL | Requer PostgreSQL |
Para um projeto como esse (milhares de conceitos, não milhões), ChromaDB é perfeito. Se precisar escalar para bilhões de vetores ou alta disponibilidade, veja as alternativas.
O hook de pre-commit
Para isso ser útil de verdade, integrei no workflow do git:
#!/bin/bash
# .git/hooks/pre-commit
echo "🔍 Validando progressão curricular..."
if python bin/concept_index.py validate; then
echo "✓ Validação bem-sucedida"
exit 0
else
echo "❌ Há conceitos usados sem ensinar"
echo " Execute: make concept-validate-report"
exit 1
fi
Agora, toda vez que tento fazer commit, o sistema verifica que não estou vacilando. Se há violações, o commit é bloqueado e me diz o que arrumar.
Conclusão
ChromaDB é uma dessas ferramentas que quando você descobre pensa “como vivi sem isso?”. É SQLite para embeddings: simples, local, e funciona.
O caso de uso que te mostrei (validação curricular) é só um exemplo. Bancos de dados vetoriais servem para:
- Busca semântica em documentos
- RAG (Retrieval-Augmented Generation) para LLMs
- Detecção de duplicatas semânticas
- Recomendações baseadas em similaridade
- Clustering de conteúdo
E o melhor: a barreira de entrada é mínima. Instala, guarda documentos, busca. Nada de configurar servidores, schemas, ou índices complicados.
Se você tem um problema onde precisa encontrar “coisas parecidas com isso”, experimente ChromaDB. O pior que pode acontecer é funcionar bem demais e você se perguntar por que não usou antes.
TL;DR: ChromaDB é um banco de dados vetorial local e simples. Usamos para verificar que um curso de programação não use conceitos antes de ensiná-los, usando busca semântica para detectar conceitos similares mesmo quando o texto é diferente.
Este artigo foi escrito originalmente em espanhol e traduzido com a ajuda de IA.