aporia
O assistente de estudos que eu uso pra matemática. Ele escolhe um jeito de trabalhar pra cada conversa, guarda o próprio log e recebe minhas páginas escritas à mão pelo tablet.
- status
- ativo
- stack
- Go / React / SQLite / Claude Code / Tailscale
- tópicos
- software-engineering / ai
O aporia é o assistente de estudos que eu uso pros problemas de matemática que resolvo à mão no tablet. O nome é a palavra grega pra estar travado sem saída, que é o estado em que eu costumo abrir ele. As dores que tentei resolver com esse projeto são as seguintes:
- Estudar com IA parece um movimento óbvio, mas é difícil não cair na “preguiça mental” de querer ter o resultado mastigado pela IA (e portanto não estudar de forma efetiva);
- É difícil ter uma visão de como seus estudos estão fluindo e capturar um log de como você trabalha e quais dúvidas recorrem dia após dia, mas essa parece ser a perspectiva mais interessante para a qual se apontar uma IA.
Uma sessão é uma sequência de conversas com um modelo, e cada conversa roda sob um move: um prompt com o seu próprio jeito de trabalhar. Um me dá a próxima heurística pra tentar num problema, nunca a resposta, para que eu mesmo resolva o problema. Um ataca uma solução que eu acho que está pronta1. Um aperta um conceito que eu sei discutir mas não sei usar (por exemplo, eu posso olhar para um problema de grafos e reconhecer que vou precisar fazer uma busca em profundidade, mas eu não sei dizer o que levou minha intuição até ali). Um acompanha um capítulo ou uma aula. Toda conversa deixa um log, e a sessão seguinte começa do que esses logs dizem.
Os moves são skills do Claude que eu já usava pra estudar, reescritas pra não lerem minhas anotações nem apontarem umas pras outras. Tudo que elas precisam chega por fora.
O código ainda não é público. Pretendo publicá-lo em breve.
Uma sessão
flowchart LR accTitle: Uma sessão do aporia accDescr: Pickup, por um modelo pequeno a partir dos digests da última sessão; a abertura, por mim; o router, por um modelo pequeno; a conversa, por mim com um modelo maior; o digest, por um modelo pequeno; e de volta pra próxima abertura. pick["Pickup<span class='who'>modelo pequeno, da última sessão</span>"] open["Abertura, talvez com páginas<span class='who'>eu</span>"]:::eu route["Router escolhe um move<span class='who'>modelo pequeno</span>"] conv["Conversa<span class='who'>eu + modelo maior</span>"]:::par dig["Digest de três linhas<span class='who'>modelo pequeno</span>"] pick --> open --> route --> conv --> dig --> open
Abrir uma sessão num tópico roda o pickup, que lê os digests da última sessão e propõe algumas coisas pra começar. Eu pego uma delas ou escrevo a minha própria abertura, com fotos das páginas se eu tiver. Um modelo pequeno lê a abertura contra as condições escritas no topo de cada move, escolhe um e registra no log uma frase dizendo por quê. Quando a conversa termina, o digest registra o que foi atacado, como terminou e qual foi a maior lacuna. Se o router errou, eu digo qual move eu queria e uma conversa nova abre com ele, com as mesmas páginas. A correção fica guardada, pra eu poder ajustar as condições depois. Fechar a sessão roda um move fixo de encerramento, e depois eu marco na mão o quanto eu domino cada região do tópico.
Como funciona por dentro
O servidor é um processo Go só. Ele serve um app React, uma API HTTP e um WebSocket, e guarda tudo num SQLite, com as imagens das páginas em arquivos do lado. Os moves são arquivos markdown no repositório, com hash calculado na inicialização, e uma conversa fica presa à versão com que abriu, então editar um prompt nunca muda uma conversa em andamento.
O modelo fica atrás de um processo worker que se comunica via JSON delimitado por linha via stdio. O servidor reinicia ele com backoff se ele morrer, e uma conversa aberta naquele momento termina como interrompida. O único worker de verdade roda a CLI claude em modo headless, no login da minha assinatura: um processo de vida longa por conversa aberta, e um novo, com um JSON schema, pra cada chamada do router, do digest ou do pickup. Ele roda sem ferramentas, sem servidores MCP, sem skills e sem settings, que é o que mantém o meu próprio CLAUDE.md fora do prompt.
O tablet chega no meu notebook pelo Tailscale. O tailscale serve serve o HTTPS no nome do notebook dentro da tailnet e repassa pro servidor com um header que carrega o meu login, e o servidor responde 403 pra qualquer coisa sem ele. É o HTTPS que deixa o app ser instalado como PWA com share target, então um print compartilhado pro aporia cai na conversa aberta da última sessão que eu tinha na tela, até quatro páginas de uma vez. As respostas são renderizadas como Markdown com LaTeX.
Fun fact: O trabalho que permitiu que a mensagem de abertura de um turno receba screenshots de páginas de solução passou pelo ekky, em quatro tasks.
Um exemplo
No dia 16 eu levei uma solução escrita à mão de um problema do Intermediate Algebra da AoPS e perguntei se estava certa. O router mandou pro move que ataca trabalho pronto, que por design não dá veredito. Levou uns cinco turnos pra aparecer que eu tinha copiado o problema errado: o livro tinha 1/(√2 − 2) e eu resolvi 1/(2 − √2). O modelo tinha as duas linhas lado a lado bem antes disso e mesmo assim me fez achar. A segunda conversa, no problema certo, retornou a resposta correta.
O que ainda pode melhorar
- Os moves são só diálogo, então quando algo precisa ser calculado, quem calcula sou eu. A correção que eu deixei especificada permite que o move de conferência rode código e diga “certo” ou “errado” só quando uma execução resolve a questão. Ainda não está construída.
- Só existe um worker, e ele precisa de uma CLI do Claude Code logada na minha máquina. O segundo worker, pra quem a fronteira foi construída (por exemplo, a API de outro provedor de LLMs), não existe.
- O tablet só funciona enquanto o notebook está acordado e na tailnet. O service worker não guarda nada em cache, então não tem modo offline.
- O digest é a opinião do modelo sobre mim, e o router e o pickup leem ele de novo. Um digest ruim entorta a próxima escolha. Os digests ficam visíveis e dá pra reconstruir, o que ajuda menos do que parece.
- Até agora eu passei mais horas construindo ele do que estudando com ele.
- Eu queria muito que o agente validasse a minha solução por si mesmo, mas encontrei estudos apontando que LLMs não são tão boas em avaliar se uma linha argumentativa está correta sem escrever sua própria solução, então me contento com atacar os meus próprios argumentos.↩