Inicio / Artículos / Un chatbot de portfolio diseñado para decir «no lo sé» en lugar de adivinar

Un chatbot de portfolio diseñado para decir «no lo sé» en lugar de adivinar

Cómo se mantiene honesto un chatbot de portfolio: un control de honestidad bloqueado, cinco notas escritas a mano en el prompt en lugar de RAG y una frase de rechazo fija que además sirve como detector de lagunas.

1768 palabras

Dale a cualquier modelo de lenguaje el nombre de alguien de quien nunca ha oído hablar y lo describirá igualmente, con seguridad y detalle: una trayectoria profesional, un conjunto de habilidades, incluso opiniones sobre sus puntos fuertes. Nada de eso existe. «Alucinación» es una palabra amable para eso. El modelo no está leyendo mal nada. Está fabricando a una persona de la nada, con fluidez y sin una pizca de duda.

Para un chatbot en un portfolio, ese es todo el problema. Cada detalle inventado es una frase que una reclutadora podría leer el lunes y repetir en una entrevista el jueves, y al dueño del sitio le tocaría explicar que su propia web se inventó algo sobre él. No hay buena manera de tener esa conversación.

Así que la pregunta de diseño se invierte. Conseguir que un modelo hable con soltura sobre alguien lleva una tarde. Conseguir que diga, de forma concreta y contable, que no sabe algo lleva mucho más, y ahí está el trabajo interesante.

El bot en este caso se llama PATS. Vive en una esquina del sitio, responde solo a partir de notas escritas a mano y, cuando una pregunta se sale de ellas, da una única frase fija y remite al visitante al correo del dueño. Esa frase fija acabó siendo la pieza más útil del proyecto. La ausencia de base de datos vectorial fue la segunda.

Ni RAG ni base de datos vectorial: unas cuatro mil palabras de notas escritas a mano más una regla, que el bot no puede improvisar. Las cifras siguientes se midieron en el despliegue en producción el 13 de septiembre de 2026.

Un control que no gira

Empecemos por lo que un visitante puede tocar. PATS tiene tres ajustes: honestidad, humor y concisión. Dos funcionan. El humor va de clínico hasta una posición etiquetada como «max sarcasm», y el tono cambia por completo. La concisión va de una narración divagante que ofrece más de lo que pediste hasta respuestas de una sola línea.

La honestidad está fija en 100. Tiene las mismas cinco posiciones que los otros dos controles, en la misma fila y con la misma forma, así que parece un control real y no un rótulo. Las cuatro posiciones por debajo de 100 están en gris y, donde las otras filas muestran su valor actual, esta dice «bloqueado». Ninguna posición responde.

En el código es una constante, con un comentario que explica la decisión:

/**
 * Not a parameter. It is displayed as a percentage because that is the joke,
 * and it is a `const` because a lowerable honesty setting would contradict
 * the one rule the whole site is built on.
 */
export const HONESTY = 100;

Nunca se envía al modelo. Ni siquiera está en el cuerpo de la petición, así que ni una petición fabricada a mano puede bajarla.

La idea viene de Interstellar, donde la personalidad de un robot es ajustable y es la tripulación quien la ajusta. Cooper pone el humor de TARS al 75 por ciento. La honestidad está en 90, porque TARS explica que la honestidad absoluta no siempre es la forma más diplomática ni más segura de comunicarse con seres emocionales.

El control de humor se quedó. La honestidad pasó a 100, porque el motivo de TARS para guardarse ese último diez por ciento no se aplica aquí. TARS cuida la moral de una tripulación en el espacio profundo, mientras que PATS responde preguntas sobre un currículum, donde poco tiene carga emocional. Un diez por ciento menos de honestidad no compraría tacto en ese contexto y solo costaría precisión.

Un sitio que defiende que un fallo ruidoso es mejor que una respuesta errónea pero plausible debería tener exactamente un ajuste que nunca se mueva, y ese es la honestidad. Un control que una reclutadora puede intentar mover y descubrir soldado defiende la idea mejor que cualquier párrafo que la afirme. Lo que sigue es la maquinaria detrás de esa afirmación.

Por qué no hay base de datos vectorial

Todo lo que PATS puede afirmar vive en cinco archivos markdown: un perfil, proyectos, estilo de trabajo, intereses y unas FAQ, unas cuatro mil palabras en total. Al cargar el módulo se unen en un solo system prompt, y ese prompt completo sale con cada mensaje. No hay recuperación, ni embeddings, ni almacén vectorial, ni troceado, ni búsqueda por similitud.

Es lo primero que pregunta la gente, normalmente con una ceja levantada, así que aquí va el razonamiento.

Gran parte de los argumentos a favor de RAG tienen que ver con el coste. Enviar un corpus entero con cada petición es inasumible, así que recuperas el puñado de fragmentos que parecen más relevantes. El caché de prompts ha cambiado ese cálculo a este tamaño. El prompt completo ocupa 10.870 tokens y, tras la primera petición, se sirve desde caché a cerca de una décima parte del precio estándar de entrada. El resultado es menos de un céntimo por mensaje, incluso con el ajuste más caro. Frente a una cifra así, ninguna arquitectura de recuperación sale a cuenta, y cualquier versión habría supuesto otro servicio, otro esquema y otra cosa que se puede romper.

El segundo motivo fue el decisivo. Un recuperador que falla hace más daño que no tener ninguno. Si el fragmento correcto no vuelve, la pregunta llega igual y el modelo tiene que responder con las manos vacías, que es justo la condición que produce una invención segura de sí misma. Ese es el fallo que el proyecto pretende evitar, así que RAG habría colado un modo de fallo silencioso en un sistema construido para no tener ninguno. El control de honestidad bloqueado y la ausencia de base vectorial son la misma decisión, tomada dos veces.

Pero no hay que estirar demasiado esa afirmación. Abarca unos diez mil tokens de notas que nunca superarán lo que escribe una sola persona. No es un veredicto de que RAG haya muerto ni de que las bases vectoriales no sirvan. Con diez veces ese tamaño las cifras se invierten y una capa de recuperación se gana su sitio, y lo mismo ocurre si el bot debe responder a partir de bases de código reales en lugar de notas sobre ellas. Ninguna de las dos condiciones se da hoy, y adoptar la recuperación solo porque todo el mundo lo espera sería el verdadero error.

El cargador es deliberadamente ruidoso con las lagunas. Si falta uno de los cinco archivos o está vacío, la build falla, así que nunca llega a producción un bot con un agujero en su conocimiento con el que pueda tropezar un visitante.

La frase de rechazo como herramienta de medición

Siempre que las notas no contienen la respuesta, PATS cierra su respuesta con una sola frase que nunca varía ni en un carácter:

That's not in my notes. You can ask the owner directly at their email address.

(«Eso no está en mis notas. Puedes preguntar directamente al dueño en su correo». La frase real nombra al dueño y da la dirección real). La frase no está ahí por cortesía. Es un instrumento.

Una página de administración privada analiza las conversaciones registradas y encuentra las preguntas que PATS no pudo responder buscando exactamente esa cadena. No le pide a un modelo que clasifique cada respuesta. Compara cadenas, en código:

if (row.role !== "assistant" || !row.content.includes(FALLBACK_LINE)) continue;
// The visitor turn immediately before it, in the same session.
for (let j = i - 1; j >= 0; j--) {
  if (rows[j].session_id !== row.session_id) break;
  if (rows[j].role === "user") {
    gaps.push({ question: rows[j].content, at: rows[j].created_at });
    break;
  }
}

La alternativa era una segunda llamada de clasificación por cada mensaje, que etiquetara cada respuesta como contestada o no contestada. Eso duplicaría el coste y la latencia en la ruta crítica para averiguar lo que una comparación de cadenas ya sabe, y dejaría la lista de lagunas en una forma que un modelo podría parafrasear hasta hacerla desaparecer. Con una cadena fija hay una llamada al modelo por turno, y la detección de lagunas sale gratis.

Cada pregunta que PATS no puede responder se convierte en una línea concreta de una lista de notas pendientes. La ignorancia del bot es la funcionalidad, y la única parte del sistema que dice qué construir a continuación.

El precio es que la frase queda congelada. Reformularla dejaría huérfanas, sin hacer ruido, todas las lagunas ya registradas, así que cambiarla cuenta como un cambio de especificación, no como una corrección de texto, y se registra como tal.

Un matiz mantiene todo en pie. PATS también rechaza preguntas que podría responder, como la remuneración, detalles migratorios o la familia. Esas reciben una negativa breve y cordial que deliberadamente no usa la frase de rechazo. Si la usara, la lista de lagunas se llenaría de preguntas que nadie piensa responder y el instrumento dejaría de medir aquello para lo que existe. Un límite deliberado no es una laguna.

La mitad de cazatalentos

PATS significa Portfolio Assistant and Talent Scout, y la gente suele leer la segunda mitad al revés. No está ahí para encontrar talento para el visitante. Está atento a oportunidades para el dueño.

Escribe en el chat una dirección de correo, una URL de LinkedIn o un enlace de agenda, o menciona que estás contratando, y el dueño recibe un correo. Lo hace una expresión regular, no una llamada al modelo. Tres detalles del detector llevaron más tiempo que la propia funcionalidad.

Primero, el destinatario es una constante fija en el código y nunca se deriva de nada escrito en el chat. Escriba lo que escriba el visitante, el correo llega a la bandeja del dueño o a ninguna parte. Ese es todo el diseño antiabuso, y es mucho más corto que las alternativas.

Segundo, el patrón de número de teléfono exige un carácter que no sea dígito a ambos lados. Los chats aquí están llenos de cifras como 10,870 tokens y 4,325 rows, y un detector que confunde un recuento de tokens con un número al que llamar da falsas alarmas sin parar.

Tercero, las sugerencias de prompt del propio widget nunca deben activarlo. «¿Qué busca el dueño?» es un chip clicable, lo que descarta el vocabulario de contratación como disparador por sí solo. La gente deja de fiarse de un detector que salta con sus propias sugerencias.

El bug fue más interesante que la funcionalidad. Las sesiones se marcaban como avisadas antes de intentar el envío, en la misma columna que guardaba por qué se había disparado el aviso. Si luego el envío fallaba, por ejemplo porque faltaba la clave de la API de correo o se había alcanzado el límite diario, la sesión quedaba marcada para siempre como imposible de notificar, incluso después de arreglar la causa. Lo que se quería era un correo por sesión, pero la comprobación dependía de una columna escrita antes de que existiera ningún correo. Ahora depende de una marca de tiempo que solo se fija cuando vuelve un 2xx.

Probarlo

PATS está en la esquina inferior derecha del portfolio, tras un botón de chat en la página de inicio. Pregúntale algo que no pueda saber de ninguna manera y observa cómo se niega a adivinar. El stack es Next.js App Router, TypeScript, Supabase Postgres y zod.