Un multiplicador no rescata un cero
Nota, septiembre de 2026. El 28/09/2026, una auditoría externa encontró un veredicto falso en las cifras de abajo. La fe de erratas corrige el resultado de extremo a extremo a 35/48 (72,9 %) con el juez estricto y 37/48 (77,1 %) con el flexible. El resultado actual es una nueva ejecución cuyo protocolo se publicó antes de correrla: 77,1 % (37/48) en preguntas que el motor nunca había visto, juez oficial de LongMemEval (la página del benchmark). Este artículo sigue publicado tal como se escribió.
Pregúntele a un sistema de memoria «¿Qué turno tenía Admon el domingo?». Solo una cosa importa: ¿sabe encontrar la única sesión que contiene Admon?
El nuestro no podía. El fallo venía del diseño, así que ocurría cada vez.
El punto ciego
Mnemosyne OS ordena la memoria por similitud vectorial densa: embeddings e5, coseno, un solo ranking. Es la herramienta correcta para el significado. «La vez que me quejé del hotel» encuentra la queja sin compartir una palabra con ella. Un token literal raro es otro asunto. Un nombre propio, un identificador o un número de versión es exactamente lo que un embedder proyecta cerca del ruido. Nunca vio esa cadena. No tiene significado que colgarle. La sesión que usted necesita puede dormir en el puesto #800.
Lo medimos sobre LongMemEval full-haystack, con el mismo protocolo que la campaña del 72,9 %: unas 480 sesiones de distracción por pregunta, en una muestra de 48 preguntas. En 6 preguntas de 48 no volvía ni una sola sesión de evidencia. En 35 preguntas podíamos rastrear el chunk exacto portador de la respuesta. Faltaba del contexto servido 10 veces.
Un modelo no puede responder con un chunk que la búsqueda nunca le entregó.
El arreglo tentador, medido en cero
El código ya contenía la respuesta obvia: un boost de términos. Extraer los tokens distintivos de la pregunta. Cuando un chunk de memoria contiene uno literalmente, multiplicar su coseno por 2.
Pasaron dos cosas. Ambas nos enseñaron algo.
Primero, conectado ingenuamente, fue una catástrofe. Una pregunta en prosa lleva mayúscula en su primera palabra. Así que «What shift was Admon…» boosteaba «what», un token presente en casi todo chunk conversacional del universo. El recall de evidencia se desplomó de 38/48 a 17/48. Una regla sobrevivió: una mayúscula solo atestigua un nombre propio cuando la palabra no abre la frase.
Segundo, una vez domado, midió neutro de extremo a extremo. La aritmética dice por qué. Las sesiones que nos faltaban puntuaban un coseno cercano a cero, y 0,30 duplicado sigue por debajo de 0,80. Un multiplicador amplifica una señal que existe. No puede crearla. Los chunks que este punto ciego pierde no tienen ningún puesto, así que no hay nada que multiplicar.
Un segundo ranking en vez de un multiplicador más grande
El arreglo que funcionó jamás consulta los vectores.
Ordenar la bóveda dos veces. Una por coseno, más profundo que el presupuesto
(200 en vez de 32). Una léxicamente, con Okapi BM25. BM25 es el esquema de
ponderación de términos, con cuarenta años de edad, que los productos
gestionados de «búsqueda híbrida» ejecutan bajo el capó. Luego fusionar los
dos rankings con Reciprocal Rank Fusion: cada documento suma 1/(k + rango)
a través de los canales.
Rangos, nunca puntuaciones. Un coseno vive en [0,1]. Una suma BM25 no tiene cota. La búsqueda híbrida suele fallar cuando alguien normaliza una contra la otra. RRF nunca las compara.
La fusión por rangos trae además una propiedad de seguridad. Un documento presente en un solo ranking puede, como mucho, empatar con el #1 del otro canal. Nunca superará a un documento en el que ambos canales coinciden. Así que el canal léxico no puede apoderarse del presupuesto. Solo puede desplazar resultados vectoriales clasificados tan profundo que su propio recíproco se ha apagado. Y BM25 no devuelve nada para un documento que no comparte ningún término con la pregunta. Así que los canales solo se solapan parcialmente, y ahí es exactamente donde la fusión tiene algo que aportar.
El presupuesto de contexto no se movió: siguen siendo 32 fuentes. El canal cambia qué chunks se sirven, nunca cuántos.
Los números
| Medición (muestra full-haystack de 48 preguntas) | Solo vectores | + canal léxico |
|---|---|---|
| Preguntas con todas las sesiones de evidencia recuperadas | 38/48 | 41/48 (+6/−3) |
| Chunk portador de la respuesta en el contexto servido (35 rastreadas) | 25/35 | 30/35 (+6/−1) |
| Extremo a extremo, juez estricto | 29/48 | 37/48 = 77,1 % (p pareada = 0,0215) |
La búsqueda se mide de forma determinista: entradas idénticas al byte, ningún LLM en el bucle, reproducción gratuita. La cifra de extremo a extremo pasa por un juez LLM, y ese juez es ruidoso. Reproducir pasadas idénticas al byte en este montaje voltea unos 2,6 veredictos por 48. Así que este banco no puede resolver una brecha por debajo de ~5 preguntas. Por eso la búsqueda es el instrumento y la tasa de respuesta la confirmación. El +8 supera el piso con margen. Se reprodujo en dos pasadas independientes que coincidieron veredicto por veredicto.
Audítelo. Cada número de arriba se recalcula desde filas por pregunta
publicadas. Las pasadas crudas y los registros extraídos mecánicamente viven
en la
página de procedencia de los benchmarks.
Las pasadas crudas son las dos pasadas estrictas, los archivos de recall
determinista y las 48 preguntas nuevas. La clave de lectura de la campaña
está en
lexical-2026-08/.
Un comando, node verify.js, rederiva cada celda y falla ante cualquier
discrepancia.
Dos maneras de fabricar un resultado
Hay dos maneras clásicas de fabricar un resultado así. Ajustar los hiperparámetros sobre la muestra. Publicar solo la muestra sobre la que se desarrolló.
No hicimos ninguna de las dos.
Los parámetros son los valores por defecto de la literatura: BM25 k1=1,5,
b=0,75, y RRF k=60, la constante del artículo original. Los dejamos sin
ajustar a propósito. Nuestra muestra de 48 preguntas corre unos 13 puntos más
fácil que el benchmark del que proviene. Ajustarse a una muestra fácil fabrica
ganancias que no transfieren.
Luego las 48 preguntas nuevas: el canal nunca las había visto, y las extrajimos después de congelar el diseño. La ganancia transfirió: +4 preguntas con evidencia completa, +2 chunks portadores de respuesta, cero regresiones. Ni una pregunta salió peor servida. La categoría dominante de ganancias fue la misma que en la muestra de desarrollo: el razonamiento temporal.
Lo que no afirmamos
A principios de agosto medimos nuestro propio montaje de búsqueda aumentado con una API gestionada de búsqueda híbrida (Google Vertex AI Search). Ese montaje aumentado con Google marcó 37/48 con el juez estricto. El canal local alcanza ahora el mismo 37/48, sin API, sin cuenta, y sin que nada salga de su máquina.
Esa frase no dice que vencimos a nadie. Comparamos dos de nuestros propios montajes, bajo un protocolo que controlamos. Uno llamaba a un servicio externo, el otro no. El producto gestionado nunca fue puntuado de una forma que autorice una comparación entre productos.
Las demás reservas quedan impresas junto al número. Un solo benchmark. Una muestra de 48 preguntas más delgada y más fácil que su conjunto padre. Un juez del lado de la respuesta con un piso de ruido conocido. Las 48 preguntas nuevas eliminaron la objeción que más temíamos: haber ajustado en silencio nuestro propio conjunto de desarrollo. Aún se debe una segunda familia de benchmarks.
La fontanería
- El índice invertido es persistente: simples tablas SQLite dentro de la base de la bóveda, con un diccionario de términos con claves enteras. Una construcción en frío sobre una bóveda de 2 470 chunks tarda 19,5 s en naïf (un fsync por posting) y menos de un segundo en una sola transacción. Después es incremental: un ingest solo indexa lo que añadió.
- No FTS5, deliberadamente. El arnés de benchmark corre sobre
node:sqlite, que no tiene el módulo FTS5. La palanca habría sido inmedible ahí, y una palanca sin medir es indistinguible de una que nunca corrió. Los defaults de FTS5 (k1=1,2, el tokenizerunicode61) además habrían cambiado en silencio el ranking mismo que medíamos. - Un índice incompleto no devuelve nada, y el pipeline recae en el ranking vectorial, el comportamiento de ayer. Un índice a medio construir tiene frecuencias documentales erróneas y produce un ranking confiado y silenciosamente falso. Una función ausente vale más que una respuesta falsa.
- El aislamiento de memoria por aplicación se aplica dentro del canal mismo, con sus propios tests. Nada se delega en quien quiera llamarlo.
El canal se envía con la v1.3.8, activo por defecto. MNEMO_LEXICAL_FUSION=0
restaura el camino solo vectores byte a byte. Nuestras propias reservas exigen
esa salida de emergencia.
Un nombre propio que su memoria no oía ahora encuentra su sesión. La ganancia es local, medida, reproducida dos veces, y confirmada sobre 48 preguntas que el canal nunca había visto.