latentSource

Construcción de sitios web personales impulsados por IA

Tu sitio de portafolio puede hacer más que mostrar contenido estático. Aprende a integrar chat de IA, RAG y herramientas agénticas en un sitio web personal.

·7 min read
Compartir
Construcción de sitios web personales impulsados por IA

Un sitio web personal que puede responder preguntas sobre ti vale más que uno que solo enumera tu currículum.

Construí este sitio, latentSource, para que fuera exactamente eso. La página "Acerca de" te dice quién soy. El chat te dice cualquier otra cosa: en qué trabajé, qué pienso sobre un tema o si estoy disponible para un proyecto. El chat lee de la misma base de conocimientos que alimenta el resto del sitio, por lo que las respuestas mantienen la coherencia. Todo funciona en Docker sobre un VPS económico, me cuesta unos pocos dólares al mes y mejora a medida que añado más artículos.

Este post es el análisis de la arquitectura. Qué contiene, qué hace cada pieza y a qué prestar atención. Los patrones funcionan para cualquier portafolio, agencia o sitio de consultoría que quiera ser algo más que estático.

La visión

El sitio tiene dos tareas. Una es el trabajo de portafolio tradicional: una página de inicio limpia, artículos, una sección sobre mí y una forma de contactarme. La otra es el rol de representante de IA: un chat que conoce mi trabajo y puede mantener una conversación real al respecto. Ambos trabajos comparten una base de conocimientos. Comparten una marca. Comparten un despliegue.

El chat maneja algunos patrones:

  • "¿En qué trabajaste en la empresa X?" — extrae información del currículum.
  • "¿Cómo construiste la función de búsqueda?" — extrae información de los blog posts.
  • "¿Puedes resumir tu experiencia con Postgres?" — combina artículos y currículum.
  • "¿Cómo puedo contactarte?" — activa una herramienta de contacto.

El chat es la característica diferenciadora. El contenido estático es la sustancia. Juntos hacen que el sitio sea útil en dos modos diferentes.

Arquitectura

El stack es intencionalmente aburrido.

Frontend — Next.js 16 con el App Router, React 19, Tailwind CSS 4. Server components por defecto. Renderizado en el servidor para SEO, hidratado en el cliente para la interactividad. El chat es un client component porque las respuestas en streaming necesitan actualizaciones en tiempo real.

Backend — FastAPI en Python. Un solo proceso. Maneja todas las rutas de la API que el frontend necesita: artículos, búsqueda, chat, almacenamiento de archivos y administración. Vive detrás del frontend, nunca expuesto públicamente. El frontend actúa como proxy de las llamadas a la API a través de rewrites de Next.js.

Base de datos — PostgreSQL con tres extensiones: pgvector (búsqueda semántica), pg_trgm (fuzzy matching) y unaccent (búsqueda insensible a acentos). Una base de datos. Un pool de conexiones. Fuente de verdad para todo.

Cache — Redis para el estado de la sesión, rate limiting y contexto de chat de corta duración. Opcional pero económico.

LLM — Gemini 2.5 Flash por defecto para chat y generación. El modelo de embeddings de Gemini para los vectores. La elección del modelo no importa realmente; lo que importa es mantener la integración delgada para poder cambiar de proveedor cuando los precios cambien.

Reverse proxy — Traefik al frente, manejando TLS vía Let's Encrypt. El contenedor del frontend tiene etiquetas de Traefik y es el único expuesto.

Despliegue — Docker Compose en un solo VPS. Tres servicios: frontend, backend, postgres (y redis si deseas sesiones). Un docker-compose.yml, un archivo .env, un comando de deploy.

Esa es toda la arquitectura. Nada exótico. Nada que cueste dinero por estar inactivo. En una imagen:

Integración de RAG

La base de conocimientos es el núcleo. Sin una buena recuperación (retrieval), el chat es solo un LLM genérico con mi nombre.

Tres fuentes de contenido terminan en la base de conocimientos:

Currículum — se procesa una vez, se divide en chunks por sección (historial laboral, educación, habilidades), se generan los embeddings y se almacena.

Artículos — cada blog post en el sitio genera sus embeddings automáticamente cuando se publica. El mismo contenido que lee el público es lo que el chat recupera.

Repositorios de GitHub — sincronizados a través de la API de GitHub. Archivos README, descripciones de proyectos y actividad reciente. Se actualiza diariamente mediante un cron job.

El flujo de recuperación es directo. El usuario hace una pregunta. El backend genera el embedding de la pregunta con el modelo de Gemini. Ejecuta una búsqueda híbrida contra la base de conocimientos: similitud de coseno con pgvector para coincidencia semántica y pg_trgm para búsqueda por palabras clave en caso de fallo. Los 5 mejores chunks se convierten en el contexto para el LLM. El LLM genera una respuesta basada en esos chunks. La respuesta se envía por streaming al usuario.

El truco es mantener la base de conocimientos sincronizada con el resto del sitio. Cada vez que publico un artículo, un hook vuelve a generar sus embeddings. Cada vez que actualizo mi currículum, un hook hace lo mismo. El chat siempre está al día con lo que el sitio dice sobre mí.

Capacidades agénticas

El chat no es solo recuperar y responder. Tiene herramientas (tools).

Programar una reunión — cuando la conversación indica que alguien quiere hablar, el modelo llama a una herramienta que redacta una invitación de calendario y la envía por Gmail. El usuario recibe una confirmación en el chat y un correo electrónico unos segundos después.

Enviar un código de verificación — para cualquier herramienta que tome datos de contacto del usuario, el modelo primero envía un código de verificación al correo o teléfono, luego espera a que el usuario confirme. Esto reduce drásticamente el spam.

Enviar un resumen de la conversación — al final de una conversación sustancial, el usuario puede pedir un resumen por correo electrónico. El modelo formatea la conversación y la herramienta la envía.

Generar un currículum personalizado — dada una descripción de puesto, el modelo genera un currículum a medida resaltando la experiencia más relevante. Útil cuando alguien pregunta sobre un rol específico.

Cada herramienta es una función de Python con un modelo de argumentos de Pydantic. El LLM elige las herramientas a través de structured output (el mismo patrón que un bucle ReAct). El agente corre en un bucle: pensar, actuar, observar, repetir, hasta que termina.

La interfaz de chat

El frontend del chat necesitaba tres cosas para sentirse bien:

Streaming. Los tokens aparecen a medida que se generan. Sin esto, el chat se siente roto. Con esto, incluso los modelos lentos se sienten responsivos.

Indicadores de herramientas. Cuando el modelo llama a una herramienta, la interfaz muestra una pequeña etiqueta: "Buscando en la base de conocimientos", "Enviando correo", para que el usuario sepa que algo está sucediendo. Sin indicadores, las llamadas a herramientas parecen bloqueos silenciosos.

Aislamiento de sesiones. Cada sesión del navegador obtiene su propio encabezado X-Chat-Session. Los límites de tasa (rate limits) del backend y el contexto de la conversación están limitados a esa sesión. Se limita a 5 sesiones concurrentes, 30 minutos de inactividad. Evita que un solo usuario sature el chat para todos los demás.

La implementación es un único componente de React, ChatPanel.tsx, además de una ruta de backend en /api/chat que utiliza server-sent events para el streaming. Unas 300 líneas de código de frontend. Quizás 200 líneas de backend.

Gestión de la base de conocimientos

La pieza que requiere más trabajo continuo es mantener la precisión de la base de conocimientos. Algunas cosas que ayudaron:

Fuente de verdad en la base de datos, no en archivos. Los archivos en disco son para la carga inicial y edición humana. Una vez que está en la base de datos, la base de datos manda. Esto evita la confusión de "pero el archivo dice X" / "pero la base de datos dice Y".

Una pestaña de administración dedicada. El sitio tiene una interfaz administrativa para gestionar prompts, ver conversaciones y clasificar entradas de la base de conocimientos. Cuesta un par de días construirla, pero ahorra cientos de horas durante la vida del sitio.

Flujo de aprobación para contenido nuevo. El seeder detecta nuevos archivos del disco, pero se añaden con estado: pendiente hasta que los apruebo. Esto detiene la publicación accidental de artículos a medio escribir.

Sincronización de GitHub como cron job. Descarga los archivos README diariamente. No lo hagas con más frecuencia. Cada llamada a la API cuesta algo, incluso cuando es gratuita, y los datos de GitHub obsoletos por unas horas no son un problema real.

Analítica y observabilidad

Algunas señales que resultaron útiles:

Registro de conversaciones. Cada chat se almacena. Es buscable y exportable. Te dice qué está preguntando la gente realmente, que suele ser diferente a lo que asumiste que preguntarían.

Métricas por herramienta. Contadores e histogramas de Prometheus para cada herramienta. Uso de tokens, percentiles de latencia, tasas de error. Expone las herramientas lentas y las que fallan sin que yo tenga que leer logs.

Sentry tanto en frontend como en backend. Captura excepciones reales. Es barato de configurar. No operes una aplicación de producción seria sin un rastreador de errores.

Umami para el tráfico. Vistas de página, artículos principales, referentes. Respetuoso con la privacidad, auto-hospedado, sin cookies. Omítelo si no te importa, pero a mí sí.

SEO y contenido

El mismo contenido que alimenta el chat también debe ser detectable por los motores de búsqueda. Algunas cosas que importaron:

Server-side rendering para artículos. Cada página de artículo se renderiza completamente en el servidor para que los rastreadores vean el contenido. Importante.

Datos estructurados. Esquemas JSON-LD BlogPosting y BreadcrumbList en cada artículo. Mejora cómo aparecen los artículos en los resultados de Google.

Imágenes OG dinámicas. Cada artículo obtiene una tarjeta social generada con el título, resumen y marca del sitio. Pillow las compone bajo demanda. Aumenta drásticamente el click-through cuando se comparten los artículos.

Sitemap y robots.txt. Aburrido, necesario, fácil de olvidar.

Generación de artículos asistida por IA con edición humana. Redacto borradores con un editor potenciado por Gemini y luego los reescribo con mi propia voz. Velocidad sin perder la autoría.

GEO: haciendo el sitio legible para la IA

Los motores de búsqueda ya no son los únicos rastreadores que importan. Los motores de respuesta (ChatGPT, Claude, Perplexity, Gemini) y los agentes que la gente conecta a sus editores también leen la web, y la leen de manera diferente. Quieren el contenido, no el decorado visual (chrome). Así que, junto al SEO clásico, hice una segunda pasada para lo que llaman GEO (Generative Engine Optimization) o preparación para agentes.

Señales de contenido en robots.txt. Más allá de los habituales allow y disallow, el archivo emite Content-Signal: search=yes, ai-input=yes, ai-train=no. Indexame, cítame en una respuesta, no entrenes con mis datos. Que cada rastreador lo respete está fuera de mis manos, pero una preferencia explícita es mejor que el silencio.

Markdown on request. Cada URL de artículo devuelve markdown puro cuando lo solicitas con Accept: text/markdown en lugar del HTML renderizado. Esto fue casi gratuito: los artículos ya viven como markdown en Postgres, por lo que el HTML que estás leyendo es el render, no la fuente. Un agente omite la navegación, la barra lateral, las etiquetas sociales y obtiene el artículo en una fracción de los tokens. La respuesta incluso lleva un encabezado X-Markdown-Tokens para que pueda decidir si la pieza cabe en su ventana de contexto antes de descargarla.

Un llms.txt en la raíz. Un mapa de markdown curado del sitio, generado a partir de la lista de artículos en vivo para que no se quede obsoleto, dirigiendo a los agentes al contenido y anunciando la negociación de markdown mencionada arriba.

Un grafo de entidades real. Nodos JSON-LD Organization y Person con sameAs y knowsAbout, y cada BlogPosting de artículo se vincula a ambos mediante @id. Así es como un motor de respuesta sabe quién escribió esto y qué afirma saber, en lugar de adivinar a partir de la página.

Verifiqué todo con isitagentready.com de Cloudflare, que escanea un sitio en cuanto a descubribilidad, accesibilidad de contenido, acceso de bots, descubrimiento de protocolos y comercio. Las casillas de lectura y descubrimiento están marcadas; los estándares de comercio (x402 y similares) no tienen sentido para un blog, así que los dejé pasar. Un error me dejó una cicatriz: el verificador señaló directivas en robots.txt que mi servidor nunca escribió. Cloudflare está frente a este dominio, y en zonas nuevas bloquea los rastreadores de IA por defecto, sirviendo a los agentes un robots.txt modificado con líneas Disallow: / que el origen nunca envió. Lo que emite tu servidor no es siempre lo que ven los agentes. Audita a través del edge o estarás auditando ficción. Profundizo más en todo esto, además de cómo convertir el blog en un servidor MCP, en su propio artículo.

Lecciones aprendidas al operarlo

Aveces me gustaría decirme a mí mismo algunas cosas antes de empezar:

Construye primero el esquema de la base de datos. Todo lo demás fluye de ahí. Si tus artículos, base de conocimientos, prompts y conversaciones viven en un esquema bien diseñado, el resto del sistema es principalmente plomería.

Externaliza los prompts a archivos, no al código. Un prompt que está hardcoded como un string de Python es un prompt que no puedes iterar sin un deploy. Un prompt que es un archivo Markdown es un prompt que puedes editar en dos segundos.

El chat será lento si no lo piensas. Usa streaming. Usa prefix caching. Elige un modelo rápido. No hagas que el usuario espere cinco segundos antes del primer token.

Los costos se acumulan. El nivel gratuito de cualquier proveedor de LLM es generoso, pero si tu sitio recibe tráfico, se te quedará corto. Rastrea el costo por conversación desde el primer día y ten un interruptor para bajar a un modelo más pequeño si los costos se salen del plan.

No necesitas feature flags ni pruebas A/B. Este es un sitio personal. Si algo está roto, arréglalo. No construyas infraestructura para problemas que no tienes.

El resultado final es un sitio que hace más que un portafolio estático. Responde preguntas. Programa reuniones. Aprende sobre ti con el tiempo. Y cuesta unos pocos dólares al mes mantenerlo. Ese es el trato.