
← Volver al blog
LLM UX acotado: inferencia de dos saltos, transmisión SSE, base estructural, y los límites del producto que mantienen una conversación de portafolio honesta.
La mayoría de los portafolios dejan de evolucionar. Quería que este se comportara un poco como un producto: i18n-first, interfaz de usuario compartida en un monorepo, y una capa de Lucas AI que responde en primera persona sobre mi trabajo — solo desde un contexto estructurado horneado en la implementación. Esta publicación es para ingenieros que quieren la imagen del sistema: forma de inferencia, transmisión, base estructural, y objetivos explícitos no alcanzados — no un recorrido por dónde viven los archivos o cómo ejecutar un fork localmente.
Las páginas estáticas son malas para las respuestas. Si tu ventaja es el juicio — alcance, compensaciones, cómo lo enviaste bajo restricción — un PDF y un formulario de contacto no escalan la curiosidad. Lucas AI es un destino dedicado (/[locale]/ai): pregunta sobre experiencia, el sitio o la característica en sí, sin pretender que estoy al otro extremo del cable en tiempo real.
El navegador posee la interfaz de usuario de la transcripción; el servidor posee política y gasto. Cada envío es POST /api/chat con { message, locale } — sin archivos adjuntos, sin llamadas a herramientas, sin campos ocultos.
El controlador construye un sistema, reenvía a Groq's OpenAI-compatible /v1/chat/completions, y proxies la transmisión ascendente como Eventos Enviados por Servidor (SSE) para que los tokens se rendericen incrementalmente sin almacenar la finalización completa en memoria en el borde. Ese es el mismo patrón que usarías detrás de cualquier API de inferencia rápida: trata la ruta como un adaptador delgado, mantén el formato de cable estable para el cliente.
La base estructural vive en el código de la aplicación: un objeto de carrera tipado se formatea una vez en una cadena CONTEXT e inyecta en el mensaje del sistema. No hay base de datos de vectores ni paso de recuperación en el momento de la solicitud — el modelo solo ve lo que serializaste cuando construiste la solicitud. Esa es una compensación de capacidad (no puedes responder de documentos arbitrarios) a cambio de latencia predecible, costo y superficie de auditoría.
llama-3.3-70b-versatile en Groq (anula GROQ_MODEL). La transmisión permanece activada; la ruta permite hasta 60s de generación — suficiente para una respuesta cuidadosa sin convertir el borde en un trabajador no acotado.llama-3.1-8b-instant (configurable como CLASSIFIER_MODEL). Es una llamada separada, no de transmisión con max_tokens: 5, temperature: 0, y un mensaje del sistema minimalista que colapsa la decisión en un solo token: CAREER o OFF_TOPIC.Si el veredicto es OFF_TOPIC, la API nunca llama al modelo grande. Transmite una negativa localizada fija como SSE para que el código del cliente coincida con una finalización “real” — sin sucursal de interfaz de usuario especial para derivar.
Cada solicitud lleva:
message: el turno actual del usuario, recortado, con un límite estricto de 2 000 caracteres (reducción de superficie de abuso e inyección de solicitud).locale: normalizado a un locale de sitio compatible. El mensaje del sistema termina con Visitante locale: … para que el modelo responda en el idioma del sitio, no en el idioma que la interfaz de usuario del navegador usa.Lo que no enviamos:
Entonces la “memoria” es: CONTEXT publicado + lo que todavía es visible en el cliente. Esa es una frontera deliberada: no hay historial de chat en el servidor, no hay sincronización entre dispositivos, no hay entrenamiento en conversaciones.
La interfaz de usuario persiste mensajes en sessionStorage (lucas-ai-messages). Refrescar en la misma pestaña mantiene el hilo; una nueva pestaña o dispositivo comienza limpio. Hay una segunda clave, lucas-ai-pending, usada cuando redirigimos al usuario después de un cambio de locale (más abajo).
Si GROQ_API_KEY falta, la ruta todavía devuelve SSE: transmite la cadena de error de configuración localizada para que el shell se degrade sin tirar la disposición.
El corpus de carrera comienza como registros tipados — roles, pilas, iniciativas, métricas, tipo (trabajo vs voluntario vs educación vs personal), rangos de tiempo y campos narrativos opcionales. Un formateador lo convierte en un bloque markdown- como envuelto en delimitadores explícitos, por ejemplo:
--- CONTEXT (verdad; no contradiga ni extienda más allá) ---
…
--- Fin del contexto ---
Lo que entra importa tanto como lo que queda fuera:
contexto / problema / solución / impacto cuando quieres que el modelo hable en resultados, no en palabras clave.Encima, el sistema es ley estricta: primera persona, no fingir estar vivo en Slack, no acceder a sistemas privados, no “Lucas dijo…”, CONTEXT como única fuente fáctica, y no fabricación más allá de lo que CONTEXT establece para detalles de implementación.
Esa es cómo conviertes “no alucinar” de un ambiente en ámbito de prueba: el modelo es tan inteligente como el paquete que envías, y el paquete se versiona como código.
El trabajo del modelo pequeño es solo enrutamiento, no utilidad. CAREER se define ampliamente: antecedentes, habilidades, trabajo enviado, juicio de producto, y preguntas legítimas sobre el sitio en sí — pila, canalización de localización, cómo se conecta el asistente — en la medida en que esa información existe en CONTEXT. OFF_TOPIC captura todo lo demás (clima, tarea, trivia no relacionada).
Tratar preguntas “meta” como dentro del alcance es una decisión de producto: un asistente de portafolio debe explicar sus propias condiciones de contorno sin abrir toda la web como fuente de conocimiento.
La locale de la ruta impulsa el idioma de respuesta (a través de la solicitud). Pero los usuarios a veces escriben en otro idioma mientras permanecen en la interfaz de usuario en inglés, por ejemplo.
Al enviar, el cliente ejecuta franc-min en la entrada (longitud mínima ~15 caracteres). Si el idioma detectado no coincide con la coincidencia ISO 639-3 esperada de la locale actual, no publicamos silenciosamente en la API. Mostramos una tarjeta de oferta: botones para router.push(/${targetLocale}/ai) para locales coincidentes, más “continuar en el idioma actual”. Si cambian de locale, almacenamos el mensaje pendiente en sessionStorage, navegamos y luego enviamos automáticamente después de montar para que la pregunta se ejecute con la locale correcta en el cuerpo JSON.
Ese es el comportamiento del producto: alinear el idioma del sitio con el idioma en que el usuario realmente escribe, en lugar de forzar al modelo a adivinar o mezclar políticas.
Lucas AI es un destino de navegación y una sección de inicio (insigna, titular, llamada a la acción, ejemplos de solicitudes) — no un widget flotante sobre el flujo de lectura. El chat de página completa mantiene el patrón opt-in y evita el patrón antipático del “copiloto sorpresa” donde la interfaz de usuario generativa lucha por la atención con el resto de la disposición.
llm_auth al cliente — sin cuerpo ascendente crudo (evitar filtrar clave o sugerencias de modelo).Lucas AI no es un asistente general arrojado en una página de marketing. Es un producto estrecho: un cuerpo de hechos que respaldas, una ruta de respuesta de transmisión, un modelo pequeño que solo decide “dentro del alcance o no”, y un servidor que olvida cada turno a propósito. El objetivo es un comportamiento predecible — lo que se envía al proveedor, lo que pagas a la API de inferencia por solicitud, y lo que el visitante puede tratar como fáctico.
Si construyes algo así, el apalancamiento no es elegir el modelo más grande. Es tratar el mensaje del sistema y CONTEXT como una especificación — simple, fáctica, verificable línea por línea — en lugar de copia de marketing, y decidir en la arquitectura cuándo se permite que el modelo grande se ejecute en absoluto (por ejemplo, solo después de una puerta temática).