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, ejecutasragSearchcontra 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 fallbackQWEN3_600M_INST_Q4) víacompletion(). 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.