Transformando dados de produto em entendimento do usuário, sem alucinações.

Problema

Eu conheci vários times de produto que tentam fazer pesquisa. Eles até fazem, mas o resto do time acaba ignorando os resultados. Não porque eles não querem usar, mas porque nem conseguem conectar o que descobriram com as decisões do dia a dia. A persona fica num arquivo no Figma ou num PPT. As transcrições de entrevistas vivem num doc do Notion que nem mesmo a pessoa que participou da entrevista volta pra revisitar.

Então eu vi que o gap não era esforço, era a forma que esses dados chegavam pro time. Por isso eu construí o Resense: para ajudar times de produto a extrair valor do que eles já tinham em casa, sem precisar de especialista pra interpretar e nem precisar gastar horas em artefatos de persona que vão pra última gaveta.

A stack de IA

O pipeline de geração mapeia dados brutos de produto por JTBD e guardrails anti-otimismo, estruturando em Synthetic Users via OpenRouter com Claude Sonnet em produção e Gemini Flash em desenvolvimento. Eu cheguei nessa estratégia de dois modelos testando: iteração em dev precisava de velocidade, produção precisava de qualidade de raciocínio para síntese complexa. Servem jobs diferentes.

A avaliação com RAG recupera chunks de fonte relevantes para ancorar a análise em dados reais de usuário. É isso que faz o time de produto confiar no output: cada insight rastreia de volta a uma fonte.

O prompt cache faz duas coisas: reduz custo de token e corta a latência de resposta em 63%. Numa ferramenta de pesquisa onde a gente está esperando uma síntese antes de uma reunião, essa diferença de velocidade importa tanto quanto o conteúdo em si.

Cada requisição passa por uma camada BYOK para o free tier: o usuário traz a própria chave de API (OpenRouter, Anthropic, OpenAI ou Google), que nunca sai do browser. Auto-detecção roteia para o provedor certo. O servidor nunca armazena nem loga as chaves de convidados.

As decisões de design

Calibrar confiança foi o problema de design mais difícil. LLMs parecem autoritativos mesmo quando estão chutando, então o maior risco do produto era entregar outputs que o time usaria sem questionar. Eu cheguei em três sistemas que atacam isso de frente.

Citações de fonte por atributo: todo insight num perfil Synthetic User linka de volta pra fonte de onde veio. Campos inferidos são marcados com [Inferido] no output. O usuário vê exatamente o que veio dos dados e o que foi inferência do modelo.

O guardrail anti-otimismo é uma lista de proibições no system prompt: quatro padrões que invalidam uma geração. A camada força o modelo a descrever pontos de fricção como situações específicas, quando, onde, o que acontece. Se o ponto de fricção for vago, a geração falha na validação.

O triangulation score mede diversidade de fontes (comportamental, atitudinal, contextual) com multiplicador: um tipo de fonte rende 0.6x de confiança, três tipos rendem 1.0x. O ConfidenceBadge renderiza como aviso, informação ou confirmação, e mostra exatamente quais tipos de fonte estão faltando.

Avaliação e melhorias ficam fora do caminho crítico, disparadas sob demanda. A chamada ao modelo para perfis fecha, aí o usuário dispara uma chamada separada para padrões e recomendações quando precisar. Dois segundos sob demanda mantém o loop principal rápido, que é onde o time passa a maior parte do tempo.

O modo de falha

Quando o modelo não tem dados suficientes para gerar um Synthetic User confiável, ele diz isso. Eu construí um painel DataHint que exibe três sinais: uma análise do que está faltando, os tipos de dados necessários e uma pergunta de pesquisa sugerida pra ir buscar. O painel usa progressive disclosure. A análise fica visível por padrão. As lacunas e a pergunta sugerida expandem na interação.

Eu decidi que mostrar um Synthetic User ruim, sem sinais de incerteza, era mais perigoso do que ser honesto sobre a falta de dados. Times que confiam em pesquisa ruim tomam decisões piores, e era exatamente isso que eu queria que o produto ajudasse a evitar.

Antes de qualquer dado chegar ao LLM, um anonimizador remove PII: emails, CPFs, telefones, endereços IP e nomes em contexto. O modelo recebe apenas texto anonimizado. Dados em repouso usam criptografia em nível de campo. Eu não tenho acesso ao conteúdo bruto dos usuários. Isso custa conveniência de administração. Valeu a pena.

How I work with AI

O Resense não começou como produto. Começou como o meu próprio processo. Synthetic Users são como eu chego numa primeira leitura para quem eu estou desenhando em horas em vez de semanas, e aí eu valido os que importam com pessoas reais. Eu uso como ponto de partida, não para substituir a pesquisa.

O mesmo instinto atravessa o resto do meu trabalho. Eu prototipo fluxos navegáveis no Claude Code e no Cursor, então a engenharia recebe algo que dá pra clicar em vez de uma tela estática, que é onde o handoff costuma quebrar. E construí um agente de design que carrega os meus princípios de design e um conjunto de Synthetic Users como contexto, rodando testes de conceito e usabilidade contra eles antes de eu sentar com um usuário real. Quando comparei os vereditos dele com sessões reais, ficaram próximos o suficiente pra confiar como primeira passada.

IA não é uma etapa que eu grudo no final. Ela está na pesquisa, no protótipo e no teste, pra eu gastar meu tempo nas decisões e não no setup.

O que vem a seguir

Eu quero Synthetic Users que evoluam sozinhos conforme novos dados chegam, sem regeneração manual. E um agente de pesquisa que aprenda com sinais de correção e sugira o que validar a seguir com base nos gaps que já foram sinalizados. O objetivo é um sistema que fica mais útil quanto mais o time usa.

Salvar links não te torna mais inteligente. Aprender com eles sim.

Problema

Eu era essa pessoa com centenas de links salvos que nunca ia abrir de novo. Eu salvava artigos, vídeos, referências. Mas quando precisava de alguma coisa, ficava procurando pelo título que mal lembrava, nas pastas que mal organizava. A ferramenta que eu usava organizava o que eu tinha salvo. O problema é que ela não me ajudava a aprender com isso nem a acessar o que eu precisava quando precisava.

Então eu construí o Coisas.space em torno de uma pergunta: e se o conteúdo que você salvou pudesse aparecer quando a gente precisa, sem precisar lembrar que existia?

A stack de IA

A categorização roda com gpt-4o-mini via OpenRouter a temperature 0.2. Eu cheguei nessa temperatura baixa testando: temperatura maior introduzia category drift, o mesmo link categorizado de formas diferentes em saves repetidos. 0.2 dá a consistência que uma feature de categorização precisa ter pra o usuário confiar.

A camada de recuperação conversacional usa a categoria como sinal de contexto escopado, uma abordagem RAG-like rápida o suficiente para UX consumer e precisa o suficiente para ser útil. Eu testei com embedding completo de documento e ficou mais lento do que os padrões reais de query justificavam.

Quando o contexto é raso, a recuperação faz fallback por três níveis: conteúdo visível da categoria ativa (últimos 3 dias), depois todo o conteúdo dessa categoria, depois todo o conteúdo independente de categoria. A estratégia mantém as respostas fundamentadas mesmo pra quem acabou de começar.

O algoritmo de matching do Foco Time usa IA semântica, então "Aprender React" traz links sobre hooks, components e JSX, inclusive links que nunca mencionam React no título. Resultados são cacheados em KV com invalidação por hash: quando um novo link é salvo, o hash muda, o cache invalida e a próxima requisição de matching regenera.

As decisões de design

Streaming foi inegociável pra mim. Qualquer categorização que exigisse espera quebraria o hábito de salvar. O produto só funciona se salvar parecer instantâneo. O link aparece imediatamente no grid com um estado pending, a categorização completa em background, o status progride de pending para processed para read.

Eu escopei a interface conversacional por categoria de propósito. Chat aberto sobre todo o conteúdo salvo produzia respostas irrelevantes nos testes, então estreitar o contexto foi a decisão que fez a feature funcionar. Quando o usuário troca de categoria com um chat aberto, um aviso inline aparece com duas opções: manter o contexto atual ou re-ancorar a conversa na nova categoria.

O comando /resumir usa progressive disclosure baseado em Fibonacci: a primeira chamada retorna 5 links. O usuário pode pedir mais, e a sequência expande para 8, 13, 21, 34. O usuário vai descobrindo o que tem, no ritmo que precisar.

O modo de falha

Quando o modelo não consegue categorizar um link com confiança (páginas com paywall, conteúdo ambíguo, URLs quebradas), o link vai para um estado Sem categoria com opção de override manual. O link está sempre lá, sempre acessível. A IA é uma camada aditiva sobre os dados, então quando ela falha, o link continua disponível.

A camada de provedor tem um fallback: se o OpenRouter retornar 402 (créditos esgotados), o sistema roteia automaticamente para o OpenAI com uma janela de retry de 1 hora. O status do serviço é rastreado em KV: provedor, último erro, tentativas falhas, último tempo de sucesso. O usuário nunca vê uma troca de provedor.

A extração de conteúdo usa silent failure: se a extração expira em 10 segundos, o link continua funcionando com seus metadados. Eu priorizei velocidade de resposta aqui, porque um link no grid já serve o usuário, mesmo sem o texto completo extraído.

Row-level security é aplicado no nível do banco de dados e na camada de aplicação. Cada tabela de dados do usuário tem políticas RLS vinculadas ao auth.uid(). Nenhum bug de aplicação pode vazar os dados de um usuário para outro.

O que vem a seguir

Eu quero adicionar leitura profunda via Firecrawl, extraindo o conteúdo real dos links salvos além dos metadados. E Gmail OAuth para puxar newsletters e threads direto para a biblioteca, transformando a caixa de entrada em um feed de aprendizado. O que eu mais quero é a camada de leitura, porque é o que vai fazer as respostas de IA ficarem ancoradas no que você de fato leu, e aí elas ficam realmente úteis.