Escrito

Iba a resolver los saldos de clientes con embeddings, y el embedding no sabe sumar

6 de octubre de 20264 min de lectura

  • embeddings
  • pgvector
  • rag
  • erp

Estoy armando un chatbot adentro del ERP para que ayude en producción con consultas simples: detallar el pedido de un cliente, decir cuánto tiene que pagar. La idea es meter el asistente y después ir optimizándolo para que responda mejor. Venía de una semana de investigación teórica sobre cómo aplicar pgvector para búsqueda semántica sobre las descripciones del ERP, así que arranqué por donde venía mirando: embebo las tablas, busco por similitud, el modelo redacta la respuesta.

Hoy me di cuenta de que para la mitad de esas consultas eso está mal de entrada. No es un bug que encontré corriendo el sistema: es un error de diseño que agarré antes de ponerlo en producción, y por suerte antes y no después.

Qué es un embedding, en una línea

Un embedding es convertir un texto en una lista de números —un vector— de manera que dos textos que significan cosas parecidas quedan cerca en ese espacio. Eso es todo lo que hace. Buscar por embedding es preguntar "qué registros se parecen a esto" y traer los k más cercanos. En pgvector eso termina siendo un ORDER BY embedding <=> query_vector LIMIT k.

Escrito así queda a la vista el problema: recupera por parecido. No sabe sumar, no sabe contar, y no tiene idea de si lo que trajo está completo.

Por qué revienta justo en un ERP

Si pregunto cuánto tiene que pagar un cliente, la búsqueda vectorial me devuelve los k registros más parecidos. Si el cliente tiene 23 facturas y tengo k=5, el modelo suma 5 y me informa un total. No hay excepción, no hay error en los logs, no hay nada: hay un número mal, redactado con la misma seguridad que uno bien.

Y por el mismo motivo hay varias cosas más que no funcionan:

  • Los identificadores exactos son el punto ciego. El pedido 4821 y el 4812 son casi el mismo vector, porque cambiar un dígito mueve muy poco la representación. Te trae el pedido del vecino con total naturalidad.
  • El ERP cambia todo el tiempo. Un saldo embebido el martes está mal el miércoles. La foto queda vieja y nadie se entera.
  • "El cliente Pérez" no es un cliente. Si hay dos Pérez, la búsqueda por similitud elige uno y sigue de largo.

Lo que más me molestó es que es el mismo problema sobre el que ya había escrito con el sistema de research: un número sin fuente suena igual de convincente cuando acierta que cuando inventa. Lo tenía identificado en un proyecto y lo estaba repitiendo en otro, una capa más abajo.

Cómo lo estoy resolviendo

Separar las consultas en dos clases y rutearlas, en vez de meter todo por el mismo caño.

  • Exactas o numéricas —saldo, detalle de pedido, stock, vencimientos— no van por embeddings. Van por funciones con SQL que escribo yo: get_saldo_cliente(cliente_id), get_detalle_pedido(pedido_id). El modelo no escribe la query ni hace la cuenta: elige qué función llamar y rellena los parámetros. El SQL devuelve el total ya sumado y el modelo solo lo redacta. Usuario de base de solo lectura, nada de SQL libre contra producción.
  • Descriptivas —cómo se configuraba la máquina para tal material, qué productos tengo parecidos a este, documentación de proceso— ahí sí van embeddings y pgvector, que es para lo que sirven.
  • En el medio, resolver la entidad primero. "El cliente Pérez" se convierte en un cliente_id antes de cualquier cuenta, con búsqueda por trigram sobre la tabla de clientes, y si hay dos, el asistente pregunta cuál en vez de adivinar.

Aparte le voy a hacer mostrar la fuente —números de pedido, ids de factura— así el que lo usa en producción puede verificar. Es la misma idea del índice de confiabilidad que ya había armado en el sistema de research.

Esto último está diseñado, no terminado. La parte de las funciones la estoy armando ahora y todavía no la probé contra los saldos reales.

Qué le diría a otro

Antes de embeber nada, separá las preguntas que te van a hacer en dos listas: las que se contestan con un parecido y las que se contestan con una cuenta. Las de la segunda lista no son RAG, son SQL, y si las metés por el vector vas a tener respuestas lindas y mal. El embedding es buenísimo para "mostrame algo parecido a esto" y es la herramienta equivocada para "cuánto". Y el test que vale es comparar la respuesta del asistente contra el SUM de la base: si no coinciden, no importa lo bien redactada que esté.