LocalLens
Arquitectura

Flujo de un request

Una sola pregunta, trazada de punta a punta a través del retrieval y el LLM.

El flujo de preguntas es la ruta caliente de LocalLens. Cada round-trip de chat atraviesa cuatro actores en un orden fijo: el caller, la clase de workflow, el QVAC gateway y el prompt builder.

El flujo tiene dos mitades:

  • Retrieval (RAG): embebes la pregunta con GTE_LARGE_FP16, ejecutas ragSearch contra el workspace y devuelves los top-K chunks. Este lado no escribe ni una sola palabra de la respuesta.
  • Generación (LLM): le pasas los chunks recuperados más la pregunta a QWEN3_1_7B_INST_Q4 (o su fallback QWEN3_600M_INST_Q4) vía completion(). Este es el chat model que efectivamente compone la respuesta, streameándola token por token.

Paso a paso

1. Embed y search (RAG)

LocalLensApp.askBrain(id, question) (src/locallens.ts) llama a QvacGateway.search(workspace, question, 5). El gateway embebe la pregunta con GTE_LARGE_FP16 y ejecuta ragSearch contra el workspace QVAC del brain. El resultado es una lista de SearchHit:

type SearchHit = {
  id: string;
  relativePath: string;
  chunkIndex: number;
  content: string;
  score?: number;
};

Top-K está fijo en 5. Ese número balancea tamaño de prompt contra calidad de respuesta y funciona bien con brains chicos. Si lo cambias, fíjate en la context window del chat model — 4096 tokens por default.

2. Construye el grounded prompt (la costura)

buildGroundedHistory(question, hits) en src/rag.ts devuelve un ChatMessage[] de dos mensajes. Esta es la costura entre el lado RAG (que produjo los hits) y el LLM (que está a punto de leerlos):

  • un mensaje system con las reglas: usar solo hechos de los excerpts, citar con corchetes, responder en el idioma de la pregunta, sin chain-of-thought oculto;
  • un mensaje user que lista los excerpts en un bloque numerado, seguidos de la pregunta.

Los excerpts numerados son lo que el modelo reproduce como [1] y [2] cuando cita. El LLM nunca ve los embeddings ni los scores — solo este prompt empaquetado.

3. Streamea la completion del LLM

QvacGateway.answer(history) es donde el chat LLM hace su trabajo. Llama a completion() de QVAC con stream: true sobre QWEN3_1_7B_INST_Q4 (el modelo Qwen3 instruct cuantizado en Q4 con 1.7B parámetros, default), y entrega cada evento contentDelta como un AsyncGenerator<string>. askBrain acumula esos tokens hasta formar el string final.

En máquinas que no pueden cargar el 1.7B, el gateway hace fallback transparente a QWEN3_600M_INST_Q4 la primera vez que cargan los modelos (ver QvacGateway.loadModels). El generator streameado tiene la misma forma en ambos casos, así que nada río arriba lo nota.

captureThinking: true deja fuera del output visible cualquier token de razonamiento interno, y kvCache: true permite al runtime reusar el estado de atención del prefijo para follow-ups.

Este es el paso donde la respuesta efectivamente se escribe. Sin él tendrías chunks rankeados pero sin prosa.

4. Devuelve las citas

Los hits que se usaron para construir el prompt regresan al caller como ChatAnswer.citations:

type ChatAnswer = {
  answer: string;
  citations: { id; relativePath; chunkIndex; score? }[];
};

Eso es lo que la CLI imprime y lo que la UI renderiza debajo de la respuesta. La respuesta es el output del LLM; las citas son la lista de evidencia del lado RAG. Las dos piezas viajan juntas para que el lector pueda verificar la afirmación contra la fuente.

Lo que no está en este flujo

  • Sin reingesta. Hacer una pregunta nunca toca el filesystem.
  • Sin mutación del store. El JSON store es read-only en el camino de la pregunta.
  • Sin segundo llamado al LLM. Una completion por pregunta. El embedding model dispara una vez por pregunta (para el search), el chat LLM dispara una vez por pregunta (para la respuesta).

Mantener el read path así de delgado es por qué los follow-ups se sienten rápidos, incluso en hardware modesto. Dos llamadas al modelo, cero escrituras a disco, y el LLM nunca ve más que los top-K chunks más la pregunta.

On this page