Quando reformulei meu site pessoal, não queria apenas um portfólio estático. Queria que o site fosse capaz de responder perguntas sobre minha trajetória e conhecimento técnico. Foi então que surgiu a ideia de construir um assistente de IA utilizando RAG (Retrieval-Augmented Generation).
Arquitetura do Assistente de IA
A arquitetura do assistente de IA ficou aproximadamente assim:
- Enriquecendo os documentos antes do embedding: Minha base de conhecimento é formada por arquivos Markdown versionados no próprio projeto. O primeiro passo é transformar esses arquivos em chunks utilizando RecursiveCharacterTextSplitter. Porém, apenas dividir o conteúdo não era suficiente. Uma pergunta feita pelo visitante nem sempre utiliza as mesmas palavras presentes no documento. Por isso adicionei ao conteúdo de cada chunk algumas perguntas prováveis relacionadas àquele conhecimento. Por exemplo, um documento que contém informações sobre minha stack pode carregar também perguntas como: 'Essas perguntas são incorporadas ao texto antes da geração do embedding.'
- Geração dos embeddings: Utilizei o Xenova/multilingual-e5-small através do @huggingface/transformers. O modelo é carregado em memória e executado localmente utilizando CPU. Um detalhe importante do E5 é a utilização dos prefixos de tarefa: Isso é importante porque o modelo foi treinado para diferenciar esses dois tipos de entrada. Os embeddings resultantes possuem 384 dimensões e são armazenados diretamente no PostgreSQL.
- Busca por similaridade: Consigo armazenar os embeddings junto aos dados do próprio sistema e realizar a busca por similaridade diretamente no banco. A distância por cosseno pode ser calculada utilizando o operador : E o Drizzle ORM permite incorporar essa operação às consultas de forma tipada. Na prática, o fluxo de recuperação fica dentro da própria infraestrutura do PostgreSQL, sem precisar adicionar uma camada externa de armazenamento vetorial.
- Controle do contexto: Implementei um limiar de distância. No meu caso, os chunks recuperados precisam ter uma distância por cosseno inferior a 0.35 para serem considerados relevantes. Se nenhum resultado atingir esse limite, o sistema não injeta um contexto arbitrário na LLM. Em vez disso, utiliza um prompt básico orientando o modelo a não inventar informações sobre minha trajetória.
Conclusão
O projeto acabou servindo como uma forma prática de explorar o que acontece dentro de um pipeline de RAG. Mais do que simplesmente chamar uma LLM, existem vários problemas interessantes antes da geração: como representar o conhecimento, como fazer o retrieval encontrar o conteúdo certo, como medir relevância e quando decidir que não existe contexto suficiente para responder.
Mais detalhes técnicos e implementação estão disponíveis em meu blog: [https://josehenriquedev.com/blog/como-implementei-o-backend-do-meu-site-pessoal-rag-local-com-bun-elysia-e-pgvector](https://josehenriquedev.com/blog/como-implementei-o-backend-do-meu-site-pessoal-rag-local-com-bun-elysia-e-pgvector).