Blog Houssan Ali Hijazi
IA · Python · Android
← Voltar ao blog
Inteligência Artificial, Python

Como agrupar 200k produtos de lojas diferentes com embeddings e consenso multi-LLM

Contexto

O lojasparaguai.com.br é um comparador de preços de produtos das lojas do Paraguai, Ciudade del Este em sua maioria. O sistema obtem os dados dos preços dos produtos diariamente e atualiza na base de dados. O grande desafio enfrentado pelo projeto é o agrupamento de produtos, onde o mesmo produto pode ter nomes diferentes na lojas, deixando o agrupamento complexo de ser feito.

🎯 O problema

Várias lojas vendem o mesmo produto, mas cada uma escreve o nome de um jeito:

Loja A: iPhone 17 Pro Max 256gb Titânio - Apple
Loja B: APPLE IPHONE 17 PROMAX 256 GB Cor Titanium
Loja C: iPhone17 Pro Max - 256GB - novo lacrado

Para mostrar a comparação de preços em uma única página do produto, eu preciso saber que os três são o mesmo item. A primeira solução foi criar uma área na painel onde faríamos esse agrupamento manualmente, buscando produto, selecionando os produtos das lojas, subindo a foto e as especificações. Com 200k+ produtos no banco, agrupar manualmente é muito trabalhoso. Foram feitas algumas tentativas de automatizar esse processo. Abaixo mostro como foi o caminho.

🔧 Primeira tentativa: TF(Frequência do Termo)-IDF(Frequência Inversa do Documento)

Comecei pelo básico, TF-IDF + similaridade. Funciona mais ou menos assim: cada nome vira um vetor de palavras com pesos, e eu comparo os vetores.

Problemas encontrados:

  • TF-IDF é "burro", não entende que Promax e Pro Max são a mesma coisa
  • Em português, sinônimos e variações de marca quebram tudo (Galaxy S24 vs Samsung S24)
  • Acessórios acabavam agrupados com o produto principal (capa iPhone 17 virava grupo do iPhone 17)
  • Produto novo no catálogo? O algoritmo nunca tinha visto e errava feio

Tentei melhorar com regras manuais, listas de sinônimos, regex para normalizar marcas, blacklist de palavras como "case", "capa", "película". Virou um Frankenstein de manutenção infinita.


🧠 Segunda tentativa: embeddings semânticos

Troquei TF-IDF por Sentence-Transformers (BERT pré-treinado para português). Em vez de comparar palavras, compara significado.

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
emb = model.encode([
    'iPhone 17 Pro Max 256gb',
    'APPLE IPHONE 17 PROMAX 256 GB'
])
# similaridade: 0.94 (mesmo item)

Ganho imediato:

  • PromaxPro Max (o modelo entende que é uma variação da mesma palavra)
  • GalaxySamsung Galaxy (contexto semântico)
  • Português funciona bem (o modelo é multilingual)

Mas ainda errava em casos sutis:

  • iPhone 17 Pro vs iPhone 17 Pro Max, similaridade 0.91, mas são produtos diferentes
  • Cores e capacidades diferentes eram agrupadas

Embeddings resolvem bem o "óbvio". Casos sutis precisavam de algo mais inteligente.


⚙️ Solução final: pipeline híbrido em 2 fases

Fase 1, Clustering semântico (offline, grátis)

  • Calcula embeddings de todos os produtos
  • Agrupa por similaridade > 0.80 usando clustering aglomerativo
  • Reduz os produtos de 200k para produtos pré agrupados (batches menores para o LLM)
  • Passa cada grupo para o LLM, sem exceção

Fase 2, LLM (validação total)

  • Manda todos os grupos para Gemini/Claude/Ollama em paralelo
  • O LLM responde: "estes formam um grupo? qual é o nome canônico?"
  • Valida semântica (cor != modelo) e nomeia canonicamente
  • Salva metadata: quais LLMs concordaram com cada grupo

💰 Ganho: o clustering reduz o volume em 40x antes de chamar o LLM. O custo de API cai porque processamos batches menores, em vez de comparar 200k produtos pairwise.


🗳️ A sacada do consenso multi-LLM

Mesmo assim, LLM erra. Gemini às vezes inventa modelos. Llama foca demais em palavra-chave. Cada um tem um viés diferente.

Solução: rodar várias LLMs em paralelo e comparar as respostas.

lnp get_suggest_produtos_ai \
  --model gemini/gemini-2.5-flash-lite \
  --model groq/llama-3.3-70b-versatile \
  --model anthropic/claude-haiku-4-5 \
  --model ollama/qwen2.5:7b

Cada produto agrupado guarda quais LLMs concordaram com a sugestão:

{
  "nome_sugerido": "iPhone 17 Pro Max 256GB",
  "descricao": "Experimente a combinação perfeita de design e performance com o iPhone 17 Pro Max de 256GB. Capture momentos inesquecíveis com sua câmera de ponta e desfrute de velocidade excepcional para aplicativos e jogos. Com amplo espaço de armazenamento, você terá liberdade para guardar todos os seus arquivos importantes. Seu display vibrante e recursos avançados garantem uma experiência de usuário imersiva e produtiva. Descubra a inovação Apple em suas mãos.",
  "produtos": [
    { "id": 12345, "llms": ["gemini", "claude", "llama"] },
    { "id": 67890, "llms": ["gemini"] }
    ...
  ],
  "categoria_id": 35,
  "categoria_nome": "Placa de Vídeo",
  "marca_id": 1473,
  "marca_nome": "Zotac",
  "confianca": 0.95
  ...
  "llms": [
      "gemini", "claude", "llama"
  ]
}

Foi desenvolvido um comando que basicamente faz a busca no banco de sugestões de grupos e faz o agrupamento caso alguns parâmetros estejam contemplados. Além desse comando uma interface no admin foi desenvolvida para agrupar as sugestões.

$ lnp auto_group --min_llms_grupo=4 --min_llms_produto=4 --min_lojas=2 --min_produtos=2 --images=3 --add --update --category='notebook'

O comando acima, busca nas sugestões obtidas antes onde: --min_llms_grupo=4 - no mínimo 4 llms concordam com o grupo sugerido. --min_llms_produto=4 - no mínimo 4 llms concordam com os produtos sugeridos nos grupos. --min_lojas=2 - deve ter no mínimo 2 lojas no grupo --min_produtos=2 - deve ter no mínimo 2 produtos no grupo --images=3 - faz o download das imagens sugeridas pela api do serper caso o grupo esteja em 2 lojas e 2 produtos no mínimo. --add --update - Adiciona os novos grupos e atualiza os existentes --category - Filtra a categoria

━━━ Categoria: Notebook ━━━
   Marca: Apple
    [UPDATE] [MacBook Air M4 13.6 16GB 256GB] llms_grupo=8 | produtos=12 - Atual=12 - Novos=+0 [≥4 llms] | 📷 10
    [UPDATE] [MacBook Air M4 15.3 16GB 512GB] llms_grupo=6 | produtos=6 - Atual=6 - Novos=+0 [≥4 llms] | 📷 10
    [SKIP]   [Macbook Air M4 15.3" 24GB/512GB] sem produtos após filtro llms_produto
    [NOVO]   [Macbook Air M5 16Gb/512Gb 13"] llms_grupo=6 | produtos=4 [≥4 llms] | 📷 10
------------------------------------------------------------
Resumo por categoria:
  Notebook  total=4  novos=1  update=2  skip=1
------------------------------------------------------------
Resumo por marca:
  Apple    total=4  novos=1  update=2  skip=1

Isso reduziu drasticamente meu tempo de revisão. Em vez de olhar 4000 grupos um por um, eu foco nos casos em que as LLMs discordam entre si, exatamente os casos ambíguos em que minha decisão importa.