LocalLens
Arquitetura

Por que QVAC

As cinco chamadas do SDK que fazem a maior parte do trabalho, e por que vêm juntas.

QVAC é o SDK que roda o loop de IA local do LocalLens. O interessante dele para um projeto desse tamanho: um pacote cobre cinco trabalhos que de outra forma puxariam cinco bibliotecas diferentes.

As cinco chamadas do SDK que usamos

import {
  loadModel,
  ragChunk,
  ragIngest,
  ragSearch,
  completion,
  // and the lifecycle helpers:
  ragCloseWorkspace,
  ragDeleteWorkspace,
  close,
} from "@qvac/sdk";
ChamadaOnde é usadaO que faz
loadModelsrc/qvac.ts → ensureReadyCarrega o modelo de embedding (GTE_LARGE_FP16) e o modelo de chat (QWEN3_1_7B_INST_Q4 com fallback de 600M).
ragChunksrc/rag.ts → chunkDocumentQuebra o texto do documento em janelas de ~220 tokens com 40 tokens de overlap.
ragIngestsrc/qvac.ts → ingestChunksEmbeda chunks e armazena em um workspace nomeado.
ragSearchsrc/qvac.ts → searchEmbeda uma query e retorna os top-K chunks que deram match.
completionsrc/qvac.ts → answerFaz stream de um completion de chat a partir do modelo carregado.
ragCloseWorkspacesrc/qvac.ts → closeWorkspaceFecha (e opcionalmente apaga) o workspace em disco.
closesrc/qvac.ts → closeDesmonta o runtime QVAC quando o app sai.

Essa é a superfície de API inteira que o LocalLens usa. Sem loop manual de embedding, sem banco vetorial separado, sem token splitter customizado.

Ciclo de vida do modelo

Modelos QVAC carregam lazy no primeiro uso:

private async ensureReady(): Promise<void> {
  if (this.chatModelId && this.embeddingModelId) return;
  this.readyPromise ??= this.loadModels().finally(() => {
    this.readyPromise = undefined;
  });
  await this.readyPromise;
}

Duas consequências que vale conhecer:

  1. O custo de cold-start cai na primeira pergunta, não no boot. Isso mantém o bun run dev responsivo e te dá um momento claro de "carregando modelo…" para pendurar um hint na UI.
  2. Chamadas concorrentes compartilham um load em voo. Todo chamador espera a mesma readyPromise até resolver. Dois requests chegando no mesmo tick não disparam dois loadModel para o mesmo source.

Por que um modelo de fallback

O modelo de chat default é QWEN3_1_7B_INST_Q4. Em máquinas mais antigas ou menores ele pode falhar ao carregar. O gateway captura isso e cai para QWEN3_600M_INST_Q4:

try {
  this.chatModelId = await loadModel({ modelSrc: QWEN3_1_7B_INST_Q4, modelConfig });
} catch {
  this.chatModelId = await loadModel({ modelSrc: QWEN3_600M_INST_Q4, modelConfig });
}

O fallback é invisível para os chamadores. QvacGateway.answer mantém a mesma assinatura de streaming dos dois jeitos.

Por que um SDK para tudo?

O LocalLens poderia ter usado @xenova/transformers para embeddings, chromadb ou qdrant para vetores, e llama.cpp para completion. Cada um é uma escolha boa por conta própria. O custo de usar os três é a cola de integração que você teria que escrever: ciclo de vida do modelo, ciclo de vida do workspace, mapeamento de erros, iteração async. O QVAC entrega isso para você. É a maior parte do motivo desse app caber em oito arquivos.

Páginas upstream úteis

On this page