Melissa: como construí um agente de IA com voz e memória

A arquitetura da Melissa, meu agente de IA em Go: voz em streaming, memória episódica e semântica, grounding e tarefas em segundo plano, com as pesquisas que fundamentam suas decisões.

Emanuel Nunes
Por Emanuel Nunes
Publicado em
Atualizado em

Um agente pessoal precisa lidar com uma dificuldade que aparece logo depois da primeira conversa: continuar fazendo sentido na segunda. Ele precisa recuperar o que foi dito, perceber quando uma informação mudou e executar uma tarefa sem perder o assunto que estava em andamento.

Adicionar o histórico inteiro ao prompt resolve uma parte pequena desse problema. Conforme a conversa cresce, fatos antigos, resultados de ferramentas e preferências do usuário passam a disputar o mesmo espaço. Uma resposta incorreta pode voltar como contexto e influenciar a próxima. A memória passa a fazer parte do problema de confiabilidade do agente.

Foi a partir dessa questão que construí a Melissa, um agente de IA pessoal escrito em Go, com interação por voz, memória persistente e ferramentas para trabalhar no computador. A arquitetura foi tomando forma a partir de pesquisas sobre memória de longo prazo, personalização e processamento assíncrono, adaptadas às restrições de uma conversa em tempo real.

Neste artigo, vou explicar como essas decisões se conectam: do áudio que chega ao sistema até a informação que merece ser lembrada, passando pela execução de ferramentas e pela continuidade entre sessões.

Como a arquitetura da Melissa organiza uma conversa

A Melissa concentra a coordenação no processo local. O modelo de linguagem participa da geração de respostas e das decisões sobre ferramentas; o código em Go controla a sessão, os registros de memória, a execução das ações e o ciclo de vida das tarefas.

O projeto possui uma entrada para uso direto, por voz ou texto, e outra como servidor de conversação, com comunicação por WebSocket. Essa segunda entrada permite que o Ilusion ofereça a interface enquanto a Melissa mantém o motor da conversa. O processamento local convive com serviços externos de transcrição, geração de linguagem e síntese de voz.

No caminho principal, a sessão recebe a fala transcrita, monta o contexto relevante, seleciona ferramentas compatíveis com o pedido e inicia a resposta. Quando uma ferramenta é necessária, seu resultado retorna ao contexto para que o modelo possa interpretá-lo. O texto destinado ao usuário segue para a interface e para a síntese de voz.

Ao mesmo tempo, um fluxo separado observa trechos da conversa e extrai informações que podem ser úteis depois. Essa divisão organiza duas necessidades com ritmos diferentes: responder ao turno atual e atualizar o conhecimento acumulado sobre o usuário.

Voz em streaming: a resposta precisa começar antes de terminar

Uma conversa por voz torna os atrasos muito perceptíveis. Esperar a transcrição inteira, depois a resposta completa e só então gerar o áudio cria uma sequência de pausas que quebra o diálogo.

O pipeline atual separa essas etapas: Deepgram faz a transcrição em streaming, o cliente de LLM se comunica com a OpenRouter, e a Cartesia recebe segmentos de texto para produzir a fala. Na execução com microfone local, a captura utiliza miniaudio, com Silero VAD para detectar atividade de voz. O projeto também integra comandos locais via Vosk e verificação de locutor com CAM++.

A sessão encaminha frases para a síntese enquanto os tokens da resposta ainda estão chegando. Os segmentos compartilham o mesmo contexto de síntese para preservar sua sequência. O código procura limites úteis de frase e trata casos como valores numéricos, nos quais um ponto pode ser separador de milhar.

Há também um filtro incremental para blocos internos de raciocínio presentes na saída textual de alguns modelos. Ele reconhece inclusive marcações que chegam divididas entre diferentes fragmentos do streaming, encaminhando para a fala apenas o conteúdo visível.

Outro detalhe está nas interrupções. Fragmentos como “e…” ou “porque…” podem indicar que o usuário está continuando a frase. O tratamento desses conectivos é restrito, preservando comandos curtos como “pare”. O ritmo da conversa depende de decisões pequenas distribuídas pelo pipeline.

Memória de um agente de IA: contexto recente e conhecimento persistente

Uma preferência estável e um acontecimento de hoje precisam de tratamentos diferentes. “Prefiro respostas curtas” pode orientar várias conversas. “A reunião de hoje atrasou” tem um contexto temporal específico.

Essa distinção aparece no PRIME, que combina memória episódica, memória semântica e personalização das respostas. O trabalho relaciona episódios às interações históricas do usuário e memória semântica a informações mais duradouras, que evoluem ao longo do tempo. Na Melissa, essa separação orienta tanto os tipos persistidos quanto a montagem do contexto.

Memória de trabalho e continuidade episódica

A memória de trabalho reúne eventos recentes do ambiente, como mudanças de janela e observações dos monitores de sistema. Ela possui capacidade e tempo de retenção limitados, adequados ao contexto imediato.

A continuidade da conversa usa outro mecanismo: um diário persistente de interações. Ao abrir uma nova sessão, a Melissa recupera uma seleção das trocas recentes, preservando sua ordem e o momento em que aconteceram. A configuração atual consulta uma janela de até 12 horas, com limites de quantidade e tamanho para o trecho restaurado.

Esse cuidado resolve um caso simples: fechar e reabrir a interface durante uma conversa. A reconexão técnica recebe um marcador explícito de continuidade, e respostas que perderam a pergunta correspondente são removidas do início do trecho recuperado. Reabrir a interface deve preservar o assunto que ainda faz sentido.

Memória semântica e perfil consolidado

Informações persistentes ficam no SQLite, acompanhadas de origem, datas, tipo e metadados. A busca por proximidade semântica utiliza chromem-go e embeddings. Quando essa busca não está disponível ou não encontra resultados, a recuperação possui uma alternativa textual.

Para compor o prompt, a sessão combina um perfil consolidado, chamado no código de Global Prior, com uma pequena seleção de extrações recentes, o Local Consistency. Os dois blocos têm limites de tamanho. O conteúdo integral continua armazenado e pode ser consultado pelas ferramentas de memória.

Quando há informação nas duas camadas, o prompt inclui instruções para interpretar o contexto recente à luz do perfil existente. Essa é uma adaptação prática da personalização estudada no PRIME: uma fala isolada precisa ser compreendida dentro do contexto em que ocorreu.

Onde o grafo de memória entra

O SQLite também armazena entidades e relações: pessoas, projetos, tópicos e categorias podem se conectar às memórias. A camada de persistência oferece operações de travessia, busca de caminhos e categorização hierárquica.

A referência aqui é o Mnemis, que combina recuperação por similaridade com seleção sobre um grafo hierárquico. No paper, essas rotas procuram reunir evidências relevantes tanto pelo significado quanto pela estrutura em que estão organizadas.

Na Melissa, a inspiração aparece na estrutura de entidades, relações e categorias. A recuperação usada na conversa continua apoiada em busca vetorial ou textual e filtros de memória; a navegação hierárquica está disponível na camada de armazenamento. Essa distinção explica como a organização do conhecimento pode evoluir sem obrigar toda pergunta a percorrer o grafo.

Extração assíncrona: decidir o que merece virar memória

Salvar uma conversa e compreender o que vale reter são operações diferentes. Uma saudação, uma confirmação e uma mudança importante de preferência não justificam o mesmo custo de processamento.

O Shadow Listener recebe os turnos em segundo plano e mantém um buffer dos trechos ainda não analisados. Um filtro local verifica se existe conteúdo relevante do usuário antes de solicitar uma extração ao modelo. Na configuração atual, o disparo por lote exige pelo menos seis entradas acumuladas e um intervalo mínimo de três minutos entre extrações. O encerramento da sessão também pode processar o conteúdo pendente.

Uma mudança importante foi separar o histórico da sessão do buffer de extração. Depois que um lote é encaminhado, ele sai da fila de pendências. Isso evita reenviar repetidamente a mesma conversa ao extrator e gerar novas versões da mesma memória. A análise estruturada adicional que antes era acionada por fala deixou de fazer parte desse caminho de ingestão.

O Warp-Cortex propõe processamento assíncrono capaz de influenciar um fluxo principal sem interrompê-lo. A Melissa adapta esse princípio com goroutines e chamadas separadas ao modelo. O compartilhamento de pesos e a manipulação de cache estudados no paper pertencem à implementação daquele trabalho; aqui, a separação acontece na orquestração da aplicação.

Depois da extração, cada candidato passa por uma verificação simples de vínculo lexical com o diálogo. Esse filtro elimina parte dos conteúdos desconectados, mas ainda precisa de uma segunda etapa para avaliar redundância e conflito.

CommitGate: a memória também precisa de critérios de entrada

Um conteúdo pode parecer relevante para a busca e, mesmo assim, induzir uma resposta errada. O ER-MIA investiga ataques de injeção de memória que exploram justamente a recuperação por similaridade. A consequência arquitetural é direta: a proximidade entre embeddings precisa ser acompanhada de critérios sobre o conteúdo que será armazenado.

Na Melissa, o CommitGate organiza essa triagem em três etapas:

  1. Substância: verifica o tamanho mínimo do conteúdo e limita a quantidade diária de extrações.
  2. Redundância: compara o candidato com memórias próximas. Quando identifica uma duplicata, reforça o registro existente.
  3. Conflito: examina candidatos semanticamente próximos usando heurísticas de negação e oposição lexical.

Os limiares atuais de similaridade são 0,92 para duplicatas e 0,75 para iniciar a análise de possíveis conflitos. São parâmetros da implementação da Melissa. O ER-MIA fundamenta o problema de segurança; esses valores não constituem uma garantia de proteção demonstrada pelo paper.

O alcance dessa barreira também depende das condições de execução. A análise semântica precisa do serviço de embeddings, e o código permite continuar salvando em determinadas falhas dessa dependência. A consolidação possui seu próprio caminho de gravação. Por isso, descrevo o CommitGate como uma camada de triagem da extração, com heurísticas que podem ser aprimoradas.

Uma informação extraída pelo modelo ainda precisa de contexto, origem e revisão. Esse princípio vale especialmente para preferências inferidas: a memória deve poder acompanhar mudanças do usuário ao longo do tempo.

Consolidação: transformar episódios em contexto útil

Se cada trecho extraído permanecesse isolado, o agente acumularia fragmentos difíceis de usar em conjunto. A consolidação procura produzir uma síntese que possa orientar as próximas conversas.

O Think-in-Memory propõe recuperar informações antes da resposta e atualizar a memória depois da interação. A ideia de post-thinking orienta o motor de reflexão da Melissa: ao finalizar uma sessão, ele reúne extrações recentes, considera o perfil já existente e produz uma versão atualizada do resumo persistente.

O projeto também possui uma consolidação acionada ao entrar em standby. A referência ao Memory Bear e à manutenção dinâmica da memória aparece nesse ciclo de síntese. O objetivo é preparar contexto útil para a próxima interação sem acrescentar esse processamento ao caminho de cada resposta falada.

Reforço e esquecimento

O Mnemosyne combina memória em grafo, filtros de entrada, decaimento temporal e reforço por acesso. Na Melissa, operações de recuperação atualizam o último acesso e reforçam os registros encontrados. A rotina de manutenção implementa uma redução de força baseada no tempo:

snovo=satual⋅e−λΔts_{\text{novo}} = s_{\text{atual}} \cdot e^{-\lambda \Delta t}

Nessa regra, ss representa a força do registro, Δt\Delta t é o tempo desde o último reforço ou acesso, em dias, e λ\lambda controla a taxa de decaimento. O código utiliza taxas diferentes para memórias episódicas, semânticas e genéricas de longo prazo.

A aplicação dessa regra acontece quando a manutenção é executada. As rotinas de consolidação pós-sessão e standby já estão conectadas ao agente; a rotina noturna está implementada como uma entrada que precisa ser chamada por um agendador. Essa separação mantém explícito quando o processamento acontece.

Grounding: manter a resposta ligada ao que foi consultado

A memória sobre o usuário resolve apenas parte da continuidade. Resultados de ferramentas também precisam atravessar os turnos com sua procedência preservada.

Considere uma consulta à agenda seguida de “esse horário está certo?”. A Melissa registra a evidência da ferramenta separadamente da resposta gerada: nome da operação, argumentos, resultado, domínio e horário da observação. Na continuação, pode recuperar essa evidência para esclarecer o que foi consultado.

Esses registros têm prazo de validade e também podem ser restaurados após uma reconexão. O contexto informa ao modelo quando está reutilizando uma consulta anterior. Pedidos como “consulte de novo” e determinadas mudanças de período podem gerar uma nova leitura.

A fala do agente e a evidência da ferramenta têm papéis diferentes. Essa separação ajuda a evitar que uma explicação produzida pelo próprio modelo seja tratada como se fosse o resultado original de uma consulta.

É a aplicação prática do problema que discuti no artigo sobre arquiteturas de RAG e grounding: fornecer evidência relevante ao gerador e controlar como ela participa da resposta.

Ferramentas e tarefas sem perder o fio da conversa

Enviar todos os esquemas de ferramentas a cada turno acrescenta contexto mesmo quando o usuário só quer conversar. A Melissa utiliza um roteador lexical local para selecionar operações relacionadas ao pedido, em domínios como agenda, arquivos, projetos, navegador e memória.

Algumas leituras inequívocas têm um caminho ainda mais direto. Uma consulta reconhecida de agenda, por exemplo, pode ser encaminhada à ferramenta sem uma inferência dedicada apenas a escolhê-la. O resultado continua passando pelo modelo para ser apresentado com o contexto e o tom da conversa. Alterações de dados seguem o fluxo de execução correspondente.

Quando faltam argumentos para uma operação, o registro de ferramentas inclui um mecanismo de solicitação de informação ao usuário. A sessão também possui uma verificação para pedidos que exigem ação externa, evitando encerrar determinados turnos com uma afirmação de execução sem chamada de ferramenta.

Trabalho em segundo plano

Para atividades que podem continuar enquanto a conversa segue, o projeto possui um gerenciador de tarefas persistido em SQLite. Ele aceita sequências explícitas de ações e objetivos executados por um subagente com acesso a ferramentas.

Cada tarefa tem estado, progresso e resultado. A concorrência é limitada, o trabalho pode ser cancelado e a conclusão gera uma notificação. Os subagentes têm limites de tempo e iterações, além de restrições que impedem criar outros subagentes ou abrir perguntas interativas durante a execução em segundo plano.

A persistência também trata reinicializações: tarefas que ficaram abertas são marcadas como interrompidas. Isso permite distinguir um resultado concluído de uma operação cujo processo terminou antes de entregar a resposta.

Observar o sistema faz parte da arquitetura

Para melhorar uma conversa por voz, preciso entender onde o tempo foi gasto e qual informação chegou ao modelo. A Melissa registra consumo de tokens, chamadas de ferramentas, contexto enviado e diferentes marcos de latência: primeiro retorno ao usuário, primeiro token visível e primeiro áudio.

Essa distinção é útil porque um aviso de recebimento pode chegar rapidamente enquanto a consulta ainda está em andamento. Medir apenas o tempo total esconderia o que o usuário percebeu durante a espera.

O repositório possui testes para comportamentos específicos, como restaurar o diálogo após reconexões, manter evidências de ferramentas separadas, limitar o contexto recente, filtrar marcações no streaming e acompanhar o estado das tarefas. São verificações que ajudam a preservar as decisões da arquitetura conforme o projeto evolui.

A continuidade emerge da coordenação entre memória, evidência e execução. É essa coordenação que venho construindo na Melissa: recuperar informação com contexto, registrar o que foi realmente observado e manter a conversa compreensível enquanto o sistema trabalha.