Un multiplicador no rescata un cero
Pregúntele a un sistema de memoria «¿Qué turno tenía Admon el domingo?» y solo una cosa importa: ¿sabe encontrar la única sesión que contiene Admon?
El nuestro no podía. No a veces — estructuralmente.
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. Pero un token literal raro — un nombre propio, un identificador, 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.
Medido sobre LongMemEval full-haystack — 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. Y en las 35 preguntas donde podíamos rastrear el chunk exacto portador de la respuesta, faltaba del contexto servido 10 veces.
No se puede responder con lo que el retrieval nunca le entregó.
El arreglo seductor, medido en nada
El código ya contenía la respuesta obvia: un boost de términos. Extraer los tokens distintivos de la pregunta y, cuando un chunk de memoria contiene uno literalmente, multiplicar su coseno por 2.
Pasaron dos cosas, ambas instructivas.
Primero, conectado ingenuamente, fue una catástrofe. Una pregunta en prosa lleva mayúscula en su primera palabra: «What shift was Admon…» boosteaba «what» — un token presente en casi todo chunk conversacional del universo — y el recall de evidencia se desplomó de 38/48 a 17/48. La regla que 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 están clasificados bajos — no están clasificados en ninguna parte.
Un segundo ranking, no 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 — el viejo y aburrido
esquema de ponderación de términos, cuarenta años de edad, el 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 manera clásica de arruinar la búsqueda híbrida es intentar normalizar una contra la otra. RRF nunca las compara.
La fusión por rangos trae además una propiedad de seguridad que conviene conocer antes de leer cualquier resultado: 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. 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 como BM25 no devuelve nada para un documento que no comparte ningún término con la pregunta, los canales solo se solapan parcialmente — exactamente donde la fusión tiene algo que aportar.
El presupuesto de contexto no se movió: siguen siendo 32 fuentes. Esto 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 (p pareada = 0,0215) |
El retrieval 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, que es ruidoso: reproducir corridas idénticas al byte en este montaje voltea unos 2,6 veredictos por 48 — una brecha por debajo de ~5 preguntas no es resoluble por este banco. Por eso el retrieval es el instrumento y la tasa de respuesta la confirmación. El +8 supera el piso con margen — y se reprodujo en dos corridas independientes que coincidieron veredicto por veredicto.
Audítelo. Cada número de arriba se recalcula desde filas por-pregunta
publicadas: las corridas crudas (las dos corridas estrictas, los archivos de
recall determinista, el holdout) y los ledgers extraídos mecánicamente viven en
la página de procedencia de los benchmarks,
con la clave de lectura de la campaña en
lexical-2026-08/.
Un comando — node verify.js — rederiva cada celda y falla ante cualquier
discrepancia.
La parte donde intentamos engañarnos, y fallamos
Hay dos maneras clásicas de fabricar un resultado así: ajustar los hiperparámetros sobre la muestra, y 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; RRF k=60, la constante del artículo original. Deliberadamente sin
ajustar: nuestra muestra de 48 preguntas corre unos 13 puntos más fácil que el
benchmark del que proviene, y ajustarse a una muestra fácil fabrica ganancias
que no transfieren.
Luego el holdout: 48 preguntas frescas que el canal nunca había visto, extraídas 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 misma categoría dominante de ganancias (razonamiento temporal) que en la muestra de desarrollo.
Lo que nos negamos a decir
El contexto de este trabajo: a principios de agosto medimos nuestro propio montaje de retrieval 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.
Lea bien esa frase, por lo que no dice. No dice que vencimos a nadie. Comparamos dos de nuestros propios montajes — uno llamando a un servicio externo, otro no — bajo un protocolo que controlamos. El producto gestionado nunca fue puntuado de una forma que autorice una comparación entre productos, y no vamos a fingir lo contrario.
Las demás reservas quedan impresas junto al número, donde deben estar: 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. El holdout eliminó la objeción que más temíamos — haber ajustado en silencio nuestro propio conjunto de desarrollo — pero aún se debe una segunda familia de benchmarks.
Las partes aburridas que lo hacen real
- El índice invertido es persistente — simples tablas SQLite dentro de la base de la bóveda, un diccionario de términos con claves enteras. Construcción en frío sobre una bóveda de 2 470 chunks: 19,5 s en naïf (un fsync por posting), menos de un segundo en una sola transacción. Incremental después: 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 — no 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 — la salida de emergencia que
nuestras propias reservas exigen.
Lo que podemos decir cabe en una línea: un nombre propio que su memoria no oía ahora encuentra su sesión — localmente, medido, reproducido dos veces, y confirmado sobre preguntas nunca vistas.