Lo que aprendimos construyendo nuestro propio widget de bot de soporte (chatea con tu contenido)
Construimos nuestro propio widget de bot de soporte: una burbuja de chat que incrustas en un sitio web para que las personas puedan hacer preguntas y recibir respuestas desde tu contenido (docs, base de conocimiento, paginas de servicio, politicas). Si alguna vez has entregado uno de estos, sabes que el pitch es facil y la realidad es complicada. Los problemas que aparecen rara vez son "problemas del modelo". Son problemas de ingenieria:- el retrieval falla de formas predecibles
- la latencia mata la confianza antes de que llegue el primer token
- el control de acceso es facil de implementar mal
- la inyeccion de prompt se convierte en "envenenamiento de contenido"
- las respuestas deben ser auditables o terminas con riesgo de soporte
Que estabamos construyendo (requisitos y restricciones)
Definimos algunos requisitos no negociables desde el inicio:- Primer token rapido: la gente debe ver una respuesta rapido, incluso si el asistente necesita "buscar" en segundo plano.
- Respuestas fundamentadas: las respuestas deben estar soportadas por contenido que realmente publicamos.
- Read-only por defecto: el asistente no debe mutar contenido.
- Listo para multi-locale: nuestro sitio esta localizado; el asistente no debe mezclar idiomas a la ligera.
- Escalado seguro: cuando no pueda responder, debe dirigir al usuario a contacto/soporte sin alucinar.
El asistente debe comportarse como un ingeniero cuidadoso leyendo nuestro sitio, no como un escritor creativo improvisando.
Un modelo mental util: "bot de soporte = busqueda + navegacion + sintesis"
Un buen asistente de soporte hace tres cosas en secuencia:- Buscar: identificar paginas/secciones candidatas.
- Navegar: abrir paginas, escanear encabezados, seguir enlaces, refinar la busqueda.
- Sintetizar: producir una respuesta corta y correcta con indicios de de donde salio.
Leccion 1: un RAG solo por chunks falla cuando la respuesta cruza paginas
El retrieval Top-K por chunks falla de formas consistentes:- la respuesta esta dividida entre dos paginas
- la "sintaxis exacta" esta en un bloque de codigo que no rankeo
- la respuesta correcta esta en una pagina con solapamiento lexico debil
- la pregunta del usuario esta poco especificada, asi que el match por embeddings es "lo bastante cercano"
- tono correcto
- detalles incorrectos
- sin forma de verificar
Leccion 2: dale al asistente primitivas deterministas (docs como archivos)
Las personas no responden preguntas de docs leyendo cinco parrafos al azar. Nosotros:findla pagina- la
open - escaneamos encabezados
- buscamos tokens exactos
- seguimos enlaces internos
- repetimos hasta poder probar la respuesta
- cada pagina es un "archivo"
- los directorios representan secciones (o segmentos del path de la URL)
- el asistente puede
ls,find,catygrep
- auditables (puedes registrar tool calls)
- deterministas (la misma consulta produce las mismas lecturas)
- escalables (puede explorar sin que hardcodees flujos)
Arquitectura (la version mas simple que funciona)
A alto nivel terminamos con tres capas:Browser widget
-> /api/support-chat (stream)
-> Retrieval layer (lexical + vector)
-> Tool layer ("filesystem" over content)
-> Answer synthesis (grounded prompt + citations)
-> Observability (logs + traces + eval hooks)
El punto clave: tratamos "leer nuestro contenido" como una herramienta, no como un efecto secundario del retrieval.
Modelo de datos: paginas, chunks y un arbol de paths
Mantuvimos la representacion del contenido intencionalmente aburrida:page: URL/slug canonical + title + locale + visibilidad + lastmod + headingschunk:{ page, chunk_index, text, embedding, tokens, hash }path_tree:{ "services/seo": { isPublic: true, groups: [] }, ... }
- el asistente necesita un mapa de "que existe"
- el control de acceso se vuelve estructural (podas paths antes de que empiecen las sesiones)
ls/findse vuelve rapido y cacheable
Pipeline de ingestion: convertir un sitio web en una superficie de conocimiento fiable
Esto fue el mayor sumidero de tiempo. "Indexa tus docs" suena facil hasta que ves como es el contenido real:- copy de marketing + componentes
- MDX y encabezados que no mapean limpio a HTML
- paginas de navegacion que repiten bloques
- variantes por locale que divergen parcialmente
- Descubrir paginas: sitemap + lista de rutas conocidas.
- Fetch de HTML renderizado: lo que usuarios y crawlers realmente ven.
- Extraer contenido principal: quitar nav, footers, banners de cookies, UI repetida.
- Normalizar: colapsar whitespace, quitar query strings de tracking en enlaces.
- Segmentar:
- registro a nivel pagina para navegacion y citas
- registros a nivel chunk para retrieval
- Anotar:
- locale
- visibilidad (publico, solo clientes, interno)
- outline de headings
- enlaces internos salientes
Retrieval: combina lexico y vector antes de "preguntar al modelo"
No confiamos en una sola estrategia de retrieval. Hacemos:- busqueda lexica para tokens exactos (ideal para identificadores, siglas, codigos de error)
- busqueda vectorial para match semantico (ideal para preguntas vagas)
- rechazar paginas que no coinciden con el locale del usuario (salvo que lo pida)
- preferir paginas canonicas frente a tag pages / listados duplicados
- preferir paginas con fuerte solapamiento de headings con los terminos de la query
Diseno de herramientas: que puede hacer el asistente (y que no)
Implementamos una superficie de herramientas pequena y estricta: Permitido:- listar directorios:
ls /services - buscar paths:
find -name "billing" - leer una pagina completa:
cat /services/seo - buscar dentro del contenido:
grep -ri "canonical" /
- escribir o editar contenido
- traer URLs arbitrarias
- llamadas de red arbitrarias
La leccion de latencia: filesystems reales son demasiado lentos para chat interactivo
Si levantas un sandbox/container por sesion para proveer un filesystem real:- el cold start se vuelve visible
- el chat se siente roto
- te tienta agregar complejidad como warm pools
lsyfindresuelven desde el arbol de paths cacheadocatrecompone la pagina desde chunks almacenados (ordenados porchunk_index)- los resultados se cachean por sesion (y en parte globalmente, cuando es seguro)
Cachea lo que se repite: arbol de directorios, paginas y "targets de grep"
Cachear "respuestas" es debil porque las preguntas varian. Lo que se repite en conversaciones reales es:- listar las mismas secciones
- abrir las mismas 5-10 paginas importantes
- hacer grep de los mismos tokens
- el arbol de paths
- paginas completas reconstruidas
- candidatos recientes de grep
RBAC: el control de acceso debe ser estructural, no basado en prompt
Si algunos docs no son publicos (borradores, notas internas, docs solo para clientes), no puedes depender de prompting. Aplicamos RBAC antes de que el asistente ejecute un solo tool call:- construir un arbol de paths scopeado por usuario
- podar todo lo que el usuario no puede acceder
- aplicar el mismo filtro a cada query y lectura de pagina
ls un archivo, no puede catlo y no puede citarlo.
Ese es el unico modelo mental que aguanta bajo presion.
Guardrails: como paramos respuestas equivocadas pero confiadas
Agregamos algunas reglas que redujeron mucho las malas respuestas:Regla 1: sin evidencia, no hay respuesta
Si el asistente no puede encontrar contenido de soporte con herramientas, debe:- decir que no pudo verificar
- mostrar lo que reviso (paginas o secciones)
- ofrecer un siguiente paso (contacto/soporte)
Regla 2: prefiere citar antes que parafrasear en detalles criticos
Cuando la respuesta depende de wording exacto (requisitos, limitaciones, copy legal):- citar la(s) frase(s) relevante(s) de la pagina que leyo
- mantener la sintesis al minimo
Regla 3: se estricto con locale y paginas canonicas
Si el usuario esta en una ruta de locale, el asistente debe:- preferir contenido de ese locale
- evitar mezclar idiomas en una sola respuesta
- volver al locale por defecto solo si la pagina del locale no existe
El "problema del grep": la feature clave necesita un plan en dos fases
Un grep recursivo ingenuo es lento si lee todo a traves de la red. Usamos un enfoque en dos fases:- filtro grueso usando el indice (que paginas podrian contener el token)
- filtro fino en memoria sobre texto de pagina cacheado para extraer matches exactos + contexto
UX del widget: la UI hace o rompe la confianza
Iteramos la UI varias veces antes de que el widget se sintiera fiable. Lo que mas importo:- streaming de tokens con layout estable (evitar reflow saltarin)
- estados claros:
- "Searching..."
- "Reading page..."
- "Answering..."
- citas cortas en linea:
- "From: /services/seo"
- "From: /legal/privacy"
- fallbacks que no se sienten como fracaso:
- "Revise X e Y pero no pude confirmarlo; aqui esta como contactarnos."
Observabilidad: registra las lecturas, no solo los tokens
Si quieres mejorar calidad, necesitas saber que paso. Registramos:- tool calls (paths listados/leidos/buscados)
- que paginas se usaron como evidencia
- longitud de la respuesta final y buckets de latencia
- tasas de "no se pudo verificar"
- tasas de escalado (clicks a contacto)
- que paginas carecen de informacion importante?
- que preguntas nunca encuentran evidencia?
- que paginas generan confusion y necesitan reestructuracion?
Una checklist practica de construccion (lo que hariamos otra vez)
Si estas construyendo un widget de soporte que chatea con tu contenido, empezariamos por:- indexar el sitio renderizado y guardar registros a nivel pagina
- combinar retrieval lexico + vectorial
- agregar una capa de exploracion por herramientas (files + grep)
- podar contenido por RBAC antes de iniciar sesiones
- mantener herramientas read-only por defecto
- agregar la regla "sin evidencia, no hay respuesta"
- hacer visible "searching/reading/answering" en la UI
- registrar tool calls para poder depurar fallos