Saltar al contenido

Un portfolio con un chatbot que responde sobre mi perfil profesional. Lo construí en público, con el repositorio abierto, porque en un perfil de IA aplicada el código verificable vale más que la descripción de un proyecto que nadie puede abrir.

Este portfolio y su chatbot

El único proyecto de mi perfil que quien me evalúa puede auditar entero: el repositorio es público y cada decisión de arquitectura está justificada por escrito, incluida la de no usar RAG.

  • Next.js
  • TypeScript
  • Tailwind CSS
  • Zod
  • Vercel

El problema

Un CV afirma cosas y hay que creerle. Quería que al menos un proyecto de mi perfil fuera enteramente auditable por quien lo lee.

El chatbot suma un riesgo que un sitio estático no tiene: un endpoint de modelo expuesto a internet es, sin límites, una factura abierta para cualquiera que encuentre la URL.

El contenido tiene que ser consistente entre el sitio, el bot y mi CV. Tres lugares afirmando lo mismo divergen; es cuestión de tiempo, no de disciplina.

Restricciones

  • Costo de infraestructura cero.
  • Repositorio público: cualquier decisión mala queda expuesta, y ningún dato sensible puede entrar al historial de Git.
  • El sitio tiene que ser completamente usable con el chatbot apagado.
  • Una sola fuente de verdad para los hechos: si un dato no está en el dossier, no existe para el sitio ni para el bot.

Decisiones

  1. 01

    Sin RAG, deliberadamente

    El corpus completo de mi perfil entra holgadamente en la ventana de contexto del modelo. La regla es que el retrieval se justifica cuando el corpus NO entra; acá no es el caso. Es la decisión de la que más orgulloso estoy del proyecto, porque la respuesta fácil era agregar un vector store y decir que hago RAG.

    Trade-off: Cada llamada envía el corpus entero, así que el costo por request es mayor y crece si el corpus crece. A esta escala es irrelevante, y el día que deje de serlo, la condición para revisar la decisión ya está escrita.

  2. 02

    El proveedor de LLM detrás de una interfaz

    El proveedor es la dependencia con más probabilidad de cambiar —por precio, disponibilidad o calidad— y la más fácil de aislar. Una variable de entorno selecciona la implementación y el resto del código no sabe cuál está activa.

    Trade-off: Una capa de indirección por adelantado, que normalmente evito. La acepto acá porque el eje de cambio está identificado, no supuesto.

  3. 03

    Contenido en JSON tipado, sin CMS

    El contenido cambia pocas veces por año y lo edita una sola persona. El esquema de tipos es la barrera: un proyecto marcado como archivado no puede renderizarse como enlace, y una recomendación no puede existir sin declarar la relación de quien la escribió. Esas reglas las hace cumplir el compilador, no la memoria.

    Trade-off: Editar contenido requiere un commit. Es aceptable, y hasta deseable: deja cada cambio de una afirmación pública en el historial.

  4. 04

    Entorno validado con Zod, fallando en build

    Dos requisitos que se contradicen: el repo tiene que compilar recién clonado sin credenciales, y un valor malformado no puede llegar a producción. La solución es que nada sea obligatorio pero todo valide su forma cuando está presente. Ausencia igual a degradación prevista; presencia inválida igual a build roto.

    Trade-off: Es más código que leer `process.env` directo, y obliga a pensar qué pasa visualmente cuando cada variable falta.

  5. 05

    Rate limit y tope de gasto como condición de arranque

    El chat solo se habilita si están el kill switch encendido, la clave del proveedor y el backend de rate limit. Si falta cualquiera de los tres, el chat queda apagado y el sitio se sirve completo.

    Trade-off: Más configuración antes del primer deploy del endpoint. Prefiero un portfolio sin chat que un chat usable como cómputo gratuito.

Arquitectura

Next.js con App Router y TypeScript. El sitio es enteramente Server Components y se prerenderiza estático: no envía JavaScript de cliente, salvo el selector de tema. El contenido vive en JSON tipado que se valida contra el esquema en tiempo de compilación, así que un dato que no cumple el contrato rompe el build en lugar de la página. El chatbot es un Route Handler que inyecta el corpus completo como contexto, con el proveedor detrás de una interfaz intercambiable, reglas duras en el prompt, rate limit por IP y kill switch. La entrada del usuario siempre viaja en su propio turno, nunca concatenada al prompt de sistema.

Resultado

  • Repositorio público y auditable, con un README que funciona como registro de decisiones.
  • Infraestructura de costo cero.
  • El sitio funciona completo con el chatbot apagado, que era el requisito de diseño principal.

Qué haría distinto hoy

  • El chatbot todavía no está deployado: primero va el sitio estático. Es una secuencia que me impuse a propósito, porque la tentación de mostrar el bot antes de que el resto esté sólido es fuerte.
  • Falta el conjunto de preguntas de referencia corriendo en cada cambio del prompt de sistema. Sin eso, cualquier ajuste al prompt es a ciegas — es el mismo argumento que uso para los evals en mi trabajo.
  • La política de Content-Security-Policy está pendiente a propósito: definirla antes de conocer los orígenes reales del endpoint de chat lleva a una política laxa que después nadie ajusta.