
← Volver al blog
Diseño del sistema para Lucas AI: indicaciones estratificadas, recuperación léxica, memoria de sesión, presupuestos de tokens y un clasificador de pre-vuelo en una superficie de chat de portafolio acotada.
Este sitio es un portafolio, pero la superficie de Lucas AI está construida intencionalmente como un pequeño producto: una experiencia dedicada /{locale}/ai, gasto predecible y respuestas que se mantienen en primera persona solo a partir de contexto estructurado—no un asistente genérico con ambición de escala web.
Esta publicación es una inmersión técnica profunda en cómo está cableado ese sistema: qué se ensambla por solicitud, por qué está estratificado de esa manera y qué restricciones son deliberadas.
Los portafolios estáticos responden bien a preguntas de una sola vez, pero fallan en profundidad. Las preguntas de seguimiento (“¿cómo se enfocó eso?”, “¿qué pila se utilizó?”) desaparecen en PDFs o fuerzan un bucle humano.
Las integraciones de chat ingenuas solucionan el modelo de interacción pero crean nuevos fallos: contexto ilimitado, empleadores inventados, expansión silenciosa del alcance y cada pregunta facturada contra el modelo más grande que se configuró. Un portafolio no necesita un chatbot de mundo abierto; necesita una interfaz estrecha con una base de hechos auditables.
Lucas AI se encuentra en esa brecha: UX acotada, conocimiento acotado, límites explícitos sobre lo que “Lucas” puede reclamar.
De extremo a extremo, un envío sigue siendo POST /api/chat con JSON (message, locale, opcional sessionSummary / recentTurns). La ruta posee política y gasto; el navegador posee la interfaz de usuario de transcripción y persistencia del lado del cliente (sessionStorage).
Capas, en orden de ejecución:
llama-3.1-8b-instant de forma predeterminada), no en streaming, max_tokens: 5, temperature: 0, veredicto de una sola palabra: OFF_TOPIC vs continuar. Si OFF_TOPIC, la API devuelve SSE ficticio con una negativa localizada para que la ruta del cliente coincida con una transmisión normal. En caso de error o tiempo de espera del clasificador, el controlador falla abierto y ejecuta el modelo principal—las puertas económicas no deben bloquear el tráfico legítimo. Los hosts pueden establecer CHAT_SKIP_CLASSIFIER para omitir esta llamada por completo cuando se realiza una prueba de carga o cuando reducir a la mitad las llamadas del proveedor vale la pena perder el atajo.buildChatPrompt compone el mensaje del sistema; el modelo principal recibe el sistema + el mensaje actual del usuario (no una transcripción de chat de varios mensajes en el cable).POST …/v1/chat/completions (predeterminado llama-3.3-70b-versatile en Groq, sobrescritura GROQ_MODEL), respuesta proxy como SSE al cliente. max_tokens limitado a través de CHAT_MAX_TOKENS (256–8192, predeterminado 2048) vincula UX con la economía de unidades.sessionStorage; cada solicitud puede adjuntar un resumen recortado y una porción de giros recientes para que el servidor mantenga la continuidad sin almacenar el chat en el lado del servidor.Alineación de localidad (aún política del lado del cliente): al enviar, franc-min clasifica la escritura del visitante (longitud mínima ~15 caracteres). Si el idioma detectado no coincide con la coincidencia esperada de ISO 639-3 de la localidad del sitio actual, la interfaz de usuario no llama silenciosamente a la API—muestra una tarjeta de oferta para cambiar de localidad (router.push a la coincidencia /{locale}/ai) o continuar en el idioma actual. Eso mantiene la localidad en el cuerpo JSON alineada con el idioma en el que el usuario está realmente escribiendo, en lugar de forzar al modelo a adivinar. Un mensaje pendiente sobrevive a un cambio de localidad a través de sessionStorage y envío automático después del montaje.
El movimiento arquitectónico importante es la separación: ley central estable (quién es Lucas AI, voz, reglas de base) vs fragmentos dinámicos (bloque de sesión, fragmentos recuperados, bloque de preguntas frecuentes canónico opcional). Esa división mantiene la indicación auditables, y le da a la caché de indicaciones una oportunidad de lucha porque el prefijo permanece estable mientras que solo el contexto relevante cambia por turno.
El diseño evita “enviar todo siempre”. Incluir el perfil estructurado completo en cada solicitud escala mal: el costo de tokens crece con el corpus, las preguntas no relacionadas pagan por hechos no relacionados y se acerca más a los límites del proveedor a medida que crece el perfil.
En su lugar:
systemPrompt (corePromptText.ts) — reglas estables: primera persona, transparencia (“no escribiendo en vivo”), ley de base, reglas de matemáticas de duración, comportamiento de localidad. Mantenido mínimo y fijo para que la caché de prefijo (donde el proveedor deduplica el texto del sistema de apertura) tenga una oportunidad de ayudar.CONTEXT — agregados después del prefijo compartido, claramente delimitados (--- CONTEXT (canónico / FAQ) ---, memoria de sesión, extractos recuperados) y cerrados con --- Fin de fragmentos de CONTEXT --- más una línea Visitante localidad: … para que el modelo responda en el idioma del sitio.La separación de dominios se aplica en el momento de creación de fragmentos, no solo en la prosa. Un solo objeto lucasPersonalContext es la fuente de verdad; buildKnowledgeChunkIndex deriva fragmentos con etiquetas KnowledgeDomain (core_bio, mindset, working_style, strengths, experience, duration, portfolio_*, boundaries, etc.). Eso le da a la recuperación una superficie tipada para puntuar en lugar de una pared de texto indiferenciada.
Lo que el modelo está permitido tratar como hecho son siempre los fragmentos que entraron en esa solicitud—ámbito de prueba, no una vibra.
Hay dos ideas de “enrutamiento” diferentes en la pila; confundirlos pierde el diseño.
La intención heurística (routeIntent en router.ts) no es una llamada LLM. Es una clasificación impulsada por palabras clave y expresiones regulares en cubos gruesos (recruiter, engineering, project, blog, portfolio_meta, navigation_contact, conversational, general). Esa intención solo sesga la puntuación de fragmentos léxicos—la latencia permanece plana, el costo es cero.
Ejemplos:
navigation_contact — aumenta indirectamente el comportamiento adyacente al contacto a través de audiencias de fragmentos.portfolio_meta — aumenta los dominios portfolio_* en selectChunks.recruiter — aumenta duration, strengths, fragmentos etiquetados por reclutador.conversational — evita sobre-fetching de bloques de experiencia pesados.Compensación: las heurísticas enrutan mal. La recuperación compensa con una alternativa: si nada puntúa por encima de cero, inyecta core-bio + boundaries para que el modelo todavía tenga una base mínima.
El clasificador de temas de pre-vuelo es la puerta LLM: CAREER vs OFF_TOPIC con un sistema de indicaciones dedicado (buildClassifierPrompt). Eso es política (“¿es este realmente el producto correcto?”), no ajuste de recuperación. Es más flexible que las expresiones regulares pero cuesta una ronda adicional; de ahí las banderas de omisión y el comportamiento de falla abierta.
“RAG” a menudo implica incrustaciones y una base de datos de vectores. Esta implementación no usa esos. La recuperación es léxica: tokeniza el mensaje del usuario en un conjunto de palabras, puntúa cada fragmento por aciertos de palabras clave + superposición de subcadena en el texto del fragmento, agrega impulsos basados en intención (por ejemplo, etiquetas de audiencia de reclutador, aumento de portfolio_meta de dominios portfolio_*), ordena, luego toma los mejores fragmentos bajo límites duros.
Los fragmentos son unidades semánticas pequeñas en relación con el perfil completo: por ejemplo, un fragmento por empleador (compactExperience), fragmentos separados para la canalización de i18n vs la función de chat vs hechos de monorepo. Los fragmentos pequeños importan porque:
maxChunkChars) sin dejar caer el perfil completo.Las consultas en forma de blog se depriorizan en la recuperación (intent === "blog" aplica un sesgo negativo) porque el MDX del blog no se indexa como fragmentos de conocimiento aquí. El sistema no pretende que exista recuperación de artículos completos cuando el corpus no está presente.
La relevancia es código transparente: peso de palabras clave 3, peso de token-en-cuerpo 0.35, impulsos de audiencia/intención en la parte superior. Eso es fácil de auditar y económico de ejecutar; la compensación es sinonimia y paráfrasis—si el usuario nunca se superpone léxicamente con el contenido del fragmento, se depende de fragmentos alternativos.
Las implementaciones ingenuas reproducen historial completo o insertan corpora completos. Ambos explotan el costo y la latencia.
Estrategias concretas en código:
estimateTokens — heurística de longitud de carácter / 4; sin dependencia de tokenizador; lo suficientemente bueno para presupuesto, no contabilidad de grado de facturación.maxSessionBlockTokens: el resumen de sesión en curso y los giros formateados recientes se recortan de forma iterativa (rebanada hasta 85% hasta estar bajo el límite).maxRecentTurnPairs pares (predeterminado 3), cada mensaje limitado a maxCharsPerTurn (predeterminado 700). El cliente envía hasta ocho mensajes anteriores; el servidor aplica el presupuesto más estricto.Usuario snippet + Asistente snippet), limitado por el cliente (~2500 caracteres), luego sanitizeSessionSummary del servidor (maxSessionSummaryChars, predeterminado 900). No hay resumen de LLM en el lado del servidor—costo predecible, pero ruidoso si los usuarios pegan paredes de texto (mitigado por límites).maxRetrievedChunks (predeterminado 5), presupuesto maxRetrievedTokens (predeterminado 2200 estimado), parada temprana cuando agregar otro fragmento excedería el presupuesto (con un piso de al menos dos fragmentos cuando sea posible).id en un Set para que el mismo bloque de empleador no se duplique.compressChunkText trunca con puntos suspensivos cuando un fragmento excede maxChunkChars.maxSystemChars trunca la cadena del sistema ensamblada con un marcador visible [sistema truncado].La tensión del producto siempre es riqueza de contexto vs costo. Los valores predeterminados favorecen intencionalmente indicaciones más pequeñas; los operadores ajustan a través de las variables de entorno CHAT_MAX_*. CHAT_DEBUG_METRICS=1 registra una línea JSON por solicitud (intención, recuentos de fragmentos, desglose aproximado de tokens) para que pueda ver el presupuesto en producción sin adivinar.
La reproducción completa del chat en el sistema no escala: el costo de tokens crece linealmente con la longitud de la conversación y los hilos largos ahogan la señal recuperada.
Esta pila utiliza un híbrido:
La continuidad es por lo tanto lo mejor posible, no una reproducción verbatim—apropiada para una superficie de preguntas y respuestas de portafolio, no programación en pareja. No hay almacenamiento de chat en el lado del servidor: no hay sincronización entre dispositivos, no hay entrenamiento en conversaciones; la postura de privacidad permanece simple.
Circuito corto de preguntas frecuentes canónico (tryCanonicalAnswer) — una pequeña biblioteca de patrones para preguntas recurrentes (“¿quién eres?”, “¿por qué Lucas AI?”, “fortalezas principales”, experiencia con mucho frontend). Al tocar, se inyecta un bloque denso ### Hechos canónicos y la recuperación se limita más (rebanada a como máximo tres fragmentos) para evitar tokens redundantes.
Estatico vs dinámico es explícito en el orden de ensamblaje: systemPrompt + fecha, luego secciones dinámicas opcionales, luego marcadores finales + línea de localidad. Ese orden es deliberado para caché y para la diferenciación humana cuando el CONTEXTO se desvía.
Clasificador vs principal — la indicación del clasificador está estrechamente limitada (salida de una sola palabra); la indicación principal lleva la ley de comportamiento completa. Mantenerlos separados evita la “ayuda del clasificador” y mantiene el mensaje del sistema del modelo grande enfocado en voz y base.
No construido intencionalmente:
Simplificado a propósito:
Extensiones futuras (si se necesitan): incrustaciones para contenido de blog una vez que esté indexado; almacenamiento de sesión en el lado del servidor si la continuidad entre pestañas debe ser confiable sin el cliente; recodificador si la cardinalidad de fragmentos crece lo suficiente como para que la puntuación léxica se sienta quebradiza.
Lucas AI se lee mejor como una superficie de producto con una especificación: el objeto lucasPersonalContext y los fragmentos derivados son el contrato, el sistema es ley, los presupuestos de tokens son guardafuegos y el clasificador más la ruta de flujo de temas fuera de tema definen lo que el producto se niega a ser.
Si está construyendo algo similar, el apalancamiento no es el modelo general más grande—es decidir qué entra en la indicación cada turno y medirlo. Un portafolio no necesita contexto infinito; necesita respuestas honestas y acotadas—y una arquitectura lo suficientemente aburrida como para mantenerlas de esa manera.