Experiencia · Bases de conocimientos y búsqueda semántica
Construimos el corpus del que un asistente tiene permiso para responder, con reglas sobre qué entra, cómo se divide, cómo se busca y qué pasa cuando la fuente cambia.
Un asistente que responde a los clientes es exactamente tan bueno como el corpus del que lee. Nos ocupamos del corpus: extracción desde documentos y el sitio, división en fragmentos, indexación, recuperación, la puerta de aprobación humana y la actualización. Incluye la respuesta a la pregunta de cuándo NO vale la pena la búsqueda semántica.
Ya construidoExisten implementaciones propias para cada etapa, pero su estado difiere y debe decirse abiertamente. Lo que funciona con vectores propios: la memoria de un tablero interno usado a diario, con columna `vector(1536)`, índice `ivfflat` sobre distancia coseno, umbral de similitud 0,35 y un bucle que reindexa cada 30 minutos solo lo que ha cambiado. Lo que funciona con vectores en el proveedor: la base de conocimientos de los agentes de voz, sincronizada hacia el motor de recuperación del proveedor con fragmentos de 500 y solapamiento de 100. Lo que funciona en producción **sin** vectores: la base de conocimientos de la plataforma de asistentes de texto — extracción, división, configuración por asistente y recuperación híbrida están escritas y funcionan, pero la ruta de indexación escribe la columna de vector con `null`, por tanto la puntuación híbrida se degrada a léxica, y el código lo dice por sí mismo mediante la etiqueta `keyword-only`. Lo que está escrito y aún no está activado en producción: el pipeline propio sobre `pgvector` de la plataforma de voz, cuya migración lleva en la cabecera la mención “LOCAL ONLY — do not push to prod”. Un servicio se vende con este mapa sobre la mesa, no sin él.
Un asistente que habla con los clientes no «sabe» nada. En cada pregunta, alguien busca previamente algunos fragmentos de texto y se los pone delante, y él formula la respuesta a partir de ellos. La calidad de la respuesta se decide casi por completo en esa etapa de búsqueda, no en la elección del modelo. Por eso este servicio trata sobre el corpus y la recuperación, no sobre el modelo.
El trabajo tiene cuatro partes que se rompen de forma independiente una de otra. La extracción: de qué archivos y qué páginas sacamos texto utilizable, y qué hacemos cuando no podemos. La división: cuán grandes son los fragmentos, cuánto se solapan, dónde se corta una frase. La recuperación: cómo se eligen los fragmentos para una pregunta concreta, cuántos y según qué criterio. La actualización: qué pasa cuando el documento fuente se modifica. Un sistema puede ser impecable en tres de ellas e inútil por culpa de la cuarta.
Hay una etapa más, que muchos se saltan y que cambia por completo el resultado: la puerta de aprobación. Un corpus que crece solo, a partir de conversaciones, pronto llega a contener respuestas que nadie ha validado. El bucle que construimos va al revés: cuando el agente dice «no tengo esta información», la pregunta se registra con el estado «en espera», una persona recibe una notificación y responde, y su respuesta pasa al estado «aprobado» y solo entonces entra en el corpus. El conocimiento crece a partir de carencias reales, con una persona en la cadena.
Y la parte impopular: la búsqueda semántica no siempre es la elección correcta. Para una plataforma pública propia la rechazamos deliberadamente. El corpus es el propio texto de la plataforma, dividido en pasajes cortos que llevan cada uno la página de la que vienen, puntuados por solapamiento de palabras. Sin vectores, sin repositorio vectorial, sin una solicitud adicional en la red y, más importante, sin ninguna fuente fuera del repositorio. El asistente solo puede repetir lo que ya está escrito en el sitio, y cualquier respuesta se verifica en una página escrita por una persona. Este es todo el mecanismo antiinvención.
Qué incluye
El trabajo, por componentes
Extraemos texto y decimos claramente cuándo no podemos
A partir de texto simple, markdown, registros, CSV, JSON, DOCX y PDF. El DOCX se desempaqueta como un archivo y se lee el documento XML del interior; el PDF pasa por el extractor estándar con conservación de la disposición en la página, y si este falta existe una reserva que lee los operadores de texto directamente del archivo. El caso importante es aquel en el que no funciona: un PDF escaneado, que es una imagen, no contiene texto seleccionable, y el sistema devuelve un mensaje explícito: sube DOCX o texto, o pasa antes el archivo por reconocimiento óptico. No tenemos reconocimiento óptico implementado y no fingimos que lo tenemos.
Dividimos en fragmentos con solapamiento, cortando entre frases, no a través de ellas
Los valores predeterminados que usamos: 500 unidades léxicas con 50 de solapamiento para el pipeline de voz, 1.200 caracteres con 150 de solapamiento para el de texto; en el lado del proveedor de voz, 500 con 100. El solapamiento existe porque una frase cortada por la mitad entre dos fragmentos se vuelve inutilizable para ambos. El separador de frases está escrito para las lenguas con las que trabajamos: reconoce el final de frase seguido de una mayúscula latina **o cirílica**, incluidas las diacríticas rumanas. Un separador escrito para inglés corta mal en rumano y aún peor en ruso.
Indexamos con vectores donde lo hemos activado, y decimos dónde no
El modelo de representación predeterminado es `text-embedding-3-small`, con 1.536 dimensiones, solicitado a través de nuestra puerta de modelos, no directamente al proveedor — así se puede cambiar de proveedor sin tocar el código de la base de conocimientos. En la base, la columna es de tipo vector con índice `ivfflat` sobre distancia coseno. Existe una estrategia de reserva escrita en la migración: si la extensión vectorial no está disponible, el esquema se crea en cualquier momento con índice trigram sobre texto, y la recuperación cambia a similitud léxica. No es hipotético — se escribió precisamente porque los permisos sobre la extensión no están garantizados en cualquier instalación.
Recuperación híbrida, con los pesos a la vista
El puntaje final es 0,7 de la similitud vectorial más 0,3 de la coincidencia de palabras, con la parte léxica normalizada. Se puntúa un conjunto de candidatos y se retienen los primeros fragmentos, y el contexto enviado al modelo corta cada fragmento a una longitud máxima, para que un solo documento largo no ocupe todo el espacio. La configuración es por asistente, en la base: recuperación activada o desactivada, número máximo de fragmentos por pregunta (por defecto 5, limitado entre 1 y 20), umbral de similitud, tipo de búsqueda — semántica, léxica o híbrida — y peso de la parte léxica.
Gate de aprobación: el conocimiento crece desde las carencias, con una persona en el circuito
Cuando el agente responde que no tiene la información, la pregunta se escribe en una tabla con el estado «en espera», marcada con el canal del que llegó — voz o widget. Una persona recibe la notificación y responde directamente al mensaje; un webhook toma la respuesta, pasa la entrada al estado «aprobado», y desde ahí entra en el contexto del agente. Los estados posibles son cuatro: en espera, aprobado, ignorado, duplicado — y los duplicados se vinculan a la pregunta original mediante una referencia, para que no se acumulen diez formulaciones de la misma carencia. La entrega al agente se hace una sola vez por entrada aprobada.
Actualización: qué ocurre cuando la fuente cambia
Al añadirlo se calcula la huella SHA-256 del contenido; si el texto es idéntico a algo ya indexado, se reutiliza y no se paga una nueva representación vectorial. Cuando el documento se modifica, sus fragmentos se borran por completo y se reescriben — reemplazo completo por fuente, no actualización parcial, porque una actualización parcial deja detrás fragmentos huérfanos de la versión antigua, y esos son los que producen respuestas contradichas por el sitio. La fuente tiene un ciclo de estados visible: en espera, en procesamiento, en reprocesamiento, listo o fallido, y la reindexación se puede pedir puntualmente, por fuente.
No reindexamos lo que no cambió
En un bucle que se ejecuta cada treinta minutos sobre una lista que solo crece, recalcular la representación para un elemento intacto es un pago recurrente por un vector que no podía moverse. Nuestro bucle compara el momento del último toque con el momento de la última indexación y salta lo que no se modificó. Las representaciones se solicitan en lotes, no una por una. Son detalles de coste, pero el coste mensual de una base de conocimientos se decide casi por completo aquí.
Un catálogo conectado no es una lista cargada
La diferencia no es de formato, es de momento. Una lista cargada es verdadera en el momento de la carga: el precio, el stock y los productos nuevos quedan congelados hasta que alguien recuerda volver a cargarla — y por lo general se descubre que nadie lo recuerda, cuando un cliente pregunta por un precio de hace tres meses. Un catálogo conectado se lee en el momento de la pregunta, desde la fuente que lleva el registro. Lo que hemos construido en esta dirección: un extractor que reconoce las colecciones de productos en un JSON o CSV por las claves habituales y, cuando la estructura es inusual, pide ayuda a un modelo con la obligación de responder estrictamente en una forma dada; y un recorrido que lee el precio de la página viva de la tienda. La mejor ruta sigue siendo la interfaz estructurada propia de la tienda, donde exista — pero eso se verifica por tienda, no se supone.
Recorrido de un sitio: primero el mapa, después la exploración
Se busca primero el mapa del sitio, en las variantes habituales de nombre, y se despliegan los mapas anidados en dos niveles. Solo si no existe mapa se pasa a exploración en anchura, estrictamente sobre el mismo origen, con un límite de páginas configurable (por defecto 50, tope 200). La estrategia efectivamente usada se informa de vuelta — mapa o exploración — porque de ella se lee si un resultado pobre viene del sitio o del método. Se respeta el archivo de exclusión del sitio.
Qué aspecto tiene
El recorrido, paso a paso.
01
Establecemos qué puede saber el asistente, antes de cualquier importación
La lista de fuentes admitidas y, igual de importante, la lista de las rechazadas, con el motivo. Un archivo interno de precios de compra, un export de contactos, un archivo de correos — son exactamente las cosas que entran en el corpus «para que tenga contexto» y luego se convierten en respuesta para un cliente. Entregamos el inventario de fuentes con decisión y motivo para cada una, más la regla sobre lo que no puede entrar nunca.
02
Importamos, dividimos y verificamos lo que salió leyendo realmente fragmentos
Extracción, división con superposición, indexación. La etapa de verificación no se salta: se leen fragmentos al azar del corpus resultante. Ahí aparecen las tablas transformadas en cadenas de palabras pegadas, los PDF escaneados que produjeron cero texto y las páginas de menú de navegación que entraron como documentos independientes. Entregamos el corpus más el informe con lo que no se pudo extraer y por qué.
03
Ajustamos la recuperación sobre preguntas reales, no sobre preguntas cómodas
Se construye un conjunto de preguntas a partir de lo que realmente preguntan los clientes, incluidas las formulaciones mal escritas y las de dos idiomas mezclados, y se ajustan el número de fragmentos, el umbral y el peso de la parte léxica. El objetivo no es que cada pregunta reciba una respuesta, sino que las preguntas sin cobertura en el corpus reciban «no tengo esta información». Entregamos el conjunto de preguntas con los resultados y la configuración final.
04
Ponemos el gate de aprobación y el bucle de crecimiento
Registro de las preguntas sin respuesta, notificación a la persona que aprueba, la vía de respuesta, el paso al corpus y la vinculación de duplicados. Entregamos el bucle funcional y la instrucción escrita para quien aprueba — incluido lo que no se copia de la pregunta del cliente.
05
Entregamos el procedimiento de reactualización, con el nombre de quien lo ejecuta
Qué fuente se reactualiza, con qué frecuencia, quién pulsa y cómo se ve que ocurrió. Aquí decimos abiertamente la limitación: en las plataformas que operamos, la reindexación es una acción solicitada, no una programada automáticamente. Un procedimiento sin el nombre de una persona al lado es un procedimiento que no se ejecuta.
El recorrido de una pregunta por el sistema: las fuentes aprobadas entran en extracción y partición, los fragmentos llegan al índice; ante la pregunta se puntúan los candidatos — vectorial y léxicamente — y solo unos pocos fragmentos llegan al contexto; y cuando no se encuentra nada, la pregunta sale por la rama de aprobación humana y vuelve al corpus.
Los datos
Qué tocamos, dónde están y cuánto se quedan
Las preguntas que hace cualquiera que tenga un delegado de protección de datos — hechas aquí antes de que las haga él.
Qué llega al corpus
Solo lo aprobado explícitamente: documentos cargados, páginas elegidas, pares pregunta-respuesta, textos escritos a mano. Las fuentes están tipificadas — texto, documento, sitio, pares pregunta-respuesta — y tienen un propósito declarado: productos, documentación, preguntas frecuentes u otros. Cada fuente tiene su propio interruptor, y el interruptor se comprueba en cada consulta de recuperación, no solo en la interfaz — una fuente apagada no puede llegar al contexto ni por error.
Dónde están los fragmentos y sus representaciones
En la base de datos de la plataforma respectiva, junto al resto de los datos del cliente, no en un servicio externo separado de búsqueda. Para las instalaciones en PostgreSQL, en tablas propias con la extensión vectorial activada explícitamente; para la plataforma de asistentes de texto, en MySQL, con la representación en una columna JSON y el cálculo de la distancia hecho en la aplicación. El segundo modelo funciona a pequeña escala y no escala a cientos de miles de fragmentos — es una limitación de arquitectura, dicho como tal.
Datos personales en conversaciones
El corpus no debería contener datos personales, pero el bucle de aprendizaje puede traerlos, porque la pregunta de un cliente es texto escrito por el cliente. Por eso la entrada al corpus pasa por la aprobación de una persona: ahí se filtra. La regla operativa que va con esto: quien aprueba respuestas recibe la instrucción explícita de no copiar al corpus los nombres, los números de teléfono ni los detalles del pedido de la pregunta original.
Qué se conserva sobre las consultas
Existe una tabla de registro de las consultas de recuperación, con las puntuaciones obtenidas, el tiempo de recuperación y el número de fragmentos devueltos. Es la herramienta con la que se responde a «por qué dijo eso» — sin ella, ajustar los umbrales se vuelve una adivinanza. El registro se conserva mientras sea útil para el ajuste y entra en los plazos de retención de la plataforma, no en un régimen separado.
Qué no existe y se declara como tal
No existe la aplicación de un plazo de expiración a los documentos: el campo está declarado en el esquema, pero nada lo lee, así que un documento no desaparece solo del corpus. No existe reindexación automática programada en las plataformas del cliente — la reactualización se solicita explícitamente, por fuente. No existe reordenación con un modelo dedicado ni puntuación léxica de tipo BM25; lo que llamamos «híbrido» es una suma ponderada entre coincidencia de palabras y distancia vectorial, y esa formulación es la correcta.
Un caso
Una base de conocimientos que creció a partir de las preguntas a las que el agente no sabía responder
La situación
Una cadena de restaurantes con entrega tenía un agente vocal por teléfono y un asistente en la página web. El corpus inicial se había cargado una vez, en el lanzamiento. El problema no era que el agente se equivocara, sino que decía a menudo «no tengo esa información» — ante preguntas perfectamente razonables que nadie había previsto cuando se escribió el corpus. Cada una de esas respuestas era una conversación perdida, y la lista de ellas no existía en ninguna parte.
Qué construimos
Convertimos la falta en una entrada de sistema. Cuando el agente responde que no tiene la información, la pregunta se escribe en una tabla con el estado «en espera» y con el canal del que vino — voz o widget. La persona responsable por parte del cliente recibe una notificación en el canal de mensajería que ya usa, con la instrucción de responder directamente al mensaje. Un webhook recoge la respuesta, cambia la entrada al estado «aprobado», y desde ahí la respuesta entra en el contexto del agente. Las formulaciones distintas de la misma carencia se vinculan a la entrada original mediante una referencia, y la entrega al agente se hace una sola vez por entrada, para que una respuesta aprobada no se reinyecte en cada ciclo.
Qué salió
El corpus dejó de ser un archivo cargado en el lanzamiento y se convirtió en una lista de carencias reales, ordenada por los clientes, no por nuestras suposiciones. Quien aprueba ve exactamente qué se preguntó y en qué canal, así que escribe la respuesta en la forma en que se va a usar. Los estados son cuatro — en espera, aprobado, ignorado, duplicado — porque «ignorado» es un resultado legítimo: algunas preguntas no deben recibir nunca una respuesta automática.
Qué no dice el caso
El bucle funciona exactamente tanto como funciona la persona dentro de él. Si nadie responde a las notificaciones, las entradas «en espera» se acumulan y el agente sigue sin saber: el mecanismo hace visible la ausencia, no la resuelve por sí solo. La segunda limitación, igual de importante: la implementación fue cableada para el flujo de este cliente, con la tabla, la notificación y la inyección escritas para él. Como función genérica, disponible desde la interfaz para cualquier cuenta, todavía no existe.
Preguntas
Lo que nos pregunta la gente antes de llamar
¿Por qué responde mal el asistente si la información está en los documentos cargados?
Casi siempre porque no se encontró el fragmento correcto, no porque el modelo sea «malo». Las causas frecuentes, en el orden en que las verificamos: fragmentos demasiado grandes, en los que la información útil se pierde entre otras; corte por la mitad de la frase, sin solapamiento; demasiado pocos fragmentos llevados al contexto; o un corpus en el que la misma información aparece en tres versiones distintas y la antigua puntúa mejor. El registro de consultas, con las puntuaciones obtenidas, muestra cuál de ellas es el caso — sin él, se adivina.
¿De verdad usan búsqueda semántica en todas partes?
No, y es más honesto decir exactamente dónde. Con vectores propios: la memoria de un board interno usado a diario, en columna vectorial con índice por distancia coseno y umbral de similitud 0,35. Con vectores del proveedor: la base de conocimiento de los agentes vocales, sincronizada hacia el motor de recuperación del proveedor. **Sin** vectores, hoy, en producción: la base de conocimiento de la plataforma de asistentes de texto — la ruta de indexación escribe la columna vectorial vacía, así que la recuperación funciona de forma léxica, y el código etiqueta por sí mismo el módulo como `keyword-only`. También existe un pipeline propio completamente escrito, con la migración marcada «no publicar en producción» hasta que se confirmen los derechos sobre la extensión vectorial. Ese es el mapa real.
¿Qué pasa cuando modificamos un documento ya indexado?
Los fragmentos de la fuente se borran por completo y se reescriben desde la nueva versión. Hemos elegido la sustitución completa en lugar de la actualización parcial porque lo difícil no es añadir, sino eliminar: los fragmentos que quedan de la versión antigua siguen siendo recuperados y producen respuestas que el cliente contradice con la página de inicio. En la adición también hay una comprobación de huella del contenido — si el texto es idéntico, no se recalcula nada y no se paga en vano.
¿Se actualiza solo cuando cambia el sitio o el catálogo?
No automáticamente, en las plataformas que operamos hoy. La reindexación se pide explícitamente, por fuente, mediante una acción dedicada. Existe una sola excepción, en un sistema interno nuestro, donde un bucle se ejecuta cada treinta minutos y reindexa solo lo que ha cambiado. La programación automática también se puede construir y es una etapa separada; lo que no hacemos es dar la impresión de que ya existe. También hay un campo de expiración declarado en el esquema que nadie lee — así que un documento no sale solo del corpus.
¿Cuál es la diferencia entre un catálogo conectado y una lista de productos cargada?
El momento de la verdad. La lista cargada es correcta el día de la carga; al día siguiente, el precio, el stock y los productos nuevos divergen en silencio, y la divergencia se descubre cuando un cliente pide un precio que ya no existe. El catálogo conectado se lee en el momento de la pregunta, desde el sistema que lleva el registro, así que no puede quedarse atrás. El coste es otro: aparece una dependencia de la disponibilidad de ese sistema y hay que decidir qué dice el asistente cuando la fuente no responde — siendo la respuesta correcta «no puedo confirmar el precio ahora», no el último valor conocido.
¿Pueden indexar PDF escaneados?
No directamente. Un PDF escaneado es una imagen dentro de un envoltorio PDF y no contiene texto seleccionable; nuestro extractor detecta eso y devuelve un mensaje explícito en lugar de indexar un documento vacío — el peor resultado posible siendo una fuente marcada «lista» que no contiene nada. La solución es el reconocimiento óptico antes de la importación, o proporcionar el documento en DOCX o texto. Reconocimiento óptico no lo tenemos implementado en la cadena actual.
¿Funciona en rumano y ruso?
Sí, y hay un detalle que importa más de lo que parece. La división en fragmentos corta entre frases, y nuestra regla reconoce el final de frase seguido de mayúscula latina **o** cirílica, incluidos los diacríticos rumanos. Un separador escrito para inglés no reconoce el inicio de una frase en cirílico y produce fragmentos pegados, con efecto directo sobre la calidad de la recuperación. Para la parte vocal, el modelo de representación usado por el proveedor es multilingüe.
¿Cuándo NO vale la pena la búsqueda semántica?
Cuando el corpus es pequeño, estable y escrito por vosotros — por ejemplo, el contenido de vuestro propio sitio. En una plataforma pública propia elegimos deliberadamente la recuperación por solapamiento de palabras sobre el propio texto de la plataforma, dividido en pasajes cortos que llevan cada uno la página de origen. Sin vectores, sin almacén vectorial, sin una petición adicional de red y, esencialmente, sin ninguna fuente fuera del almacén. El asistente solo puede repetir lo que ya dice el sitio, y cada respuesta se puede verificar en una página escrita por una persona. Cuando lo principal es que no se invente nada, eso supera a la búsqueda semántica.
¿Nos mantienen cautivos en su plataforma?
Las fuentes son tuyas y permanecen en su forma original; los fragmentos y las representaciones están en la base de datos de vuestra instalación, no en un servicio externo de búsqueda al que solo nosotros tenemos acceso. El modelo de representación se solicita mediante una propia puerta de modelos, por lo que el cambio de proveedor no afecta el código de la base de conocimientos. Sin embargo, las representaciones vectoriales están ligadas al modelo que las produjo: cambiar de modelo exige reindexar todo el corpus. Es una operación de cómputo, no gratuita, y es mejor saberlo al principio que enterarse a mitad.
En qué se basan las afirmaciones anteriores (29 fuentes)
29 de ellas son código y archivos de nuestros repositorios. No publicamos su nombre ni la línea: juntos, en una sola página, describirían con demasiada precisión cómo están construidos sistemas que no son solo nuestros. Los repasamos contigo, en el repositorio, a petición — la verificación sigue siendo posible, solo que se hace en una conversación.