Por qué la memoria de un chat se queda en un 25% de recall y cómo arreglar la recuperación
Un encargo freelance real muestra un sistema de memoria con almacenamiento perfecto pero un 25% de recall. Reescritura de consultas, HyDE, reranking y recuperación en varios saltos, y si conviene arreglar o sustituir.
Una memoria de chat que aguanta durante una sesión y luego se queda en blanco con todo lo que tenga más de unos meses es un patrón que se repite una y otra vez en las apps de compañía con IA. Un encargo freelance publicado en Upwork convierte esa queja difusa en mediciones, así que sirve mejor como caso de estudio que cualquier ejemplo inventado.
Las cifras del cliente
El recuento del propio cliente: unos 5.000 mensajes almacenados, 300 eventos episódicos y cerca de 3.500 conclusiones semánticas. Eligieron ocho datos concretos que el usuario había contado en junio y preguntaron por ellos como lo haría hoy un usuario real. Solo volvieron dos, un 25% de aciertos, aunque cada mensaje original seguía guardado palabra por palabra. Al escribir los recuerdos no se pierde nada.
Esa última afirmación hay que tomársela en serio. Falle lo que falle, el almacenamiento no es el culpable.
Y no era un prototipo de fin de semana. El stack funcionaba sobre Postgres con pgvector, con recuperación en dos fases con un reranker y con código propio de orquestación y extracción. Guardaba varios tipos de memoria (eventos episódicos, conclusiones semánticas, un registro de las personas de la vida del usuario y estilo procedimental), cada uno con su propia ruta de recuperación. Cada turno estaba instrumentado para registrar qué se recuperaba, qué se descartaba y qué llegaba finalmente al modelo. El equipo también había probado mem0 con 120 de sus propios mensajes y tenía una instancia de Hindsight en modo sombra. Experimentos sensatos, y el recall se quedó en el 25%.
Dónde se rompe de verdad
Una sola prueba deja el problema a la vista. Escribe "qué palabras usa mi padre" y el sistema devuelve mensajes anteriores en los que el usuario se queja de su padre. El único mensaje que lo cita no aparece.
No es una rareza de este código. La similitud de embeddings sobre un historial de chat se comporta exactamente así, porque el texto más cercano a una pregunta casi siempre es otra pregunta. Una consulta y su respuesta ocupan barrios distintos del mismo espacio vectorial, y la similitud del coseno no puede saber que van juntas. Así que el almacén de memoria está bien. El punto débil está en la orquestación, es decir, en lo que se elige, se ordena y se mete en el contexto en el momento de generar.
Cuatro arreglos, de menor a mayor esfuerzo
Reescribe la consulta. No generes el embedding de la pregunta tal cual. Haz que un LLM produzca varias formulaciones con la forma que tendría la respuesta: frases exactas que usa el padre del usuario, palabras con las que lo llama, citas directas atribuidas a él. Así la búsqueda apunta a cómo es una respuesta en lugar de a cómo es una pregunta.
HyDE. Gao y sus colegas propusieron en 2022 ir un paso más allá. No conviertas la pregunta en términos de búsqueda; pide al LLM que escriba una respuesta hipotética y genera el embedding de esa respuesta. Una respuesta inventada cae mucho más cerca de una real que la propia pregunta. Para este fallo en concreto es, probablemente, el remedio más directo.
Reordena con algo más que la similitud. Con un conjunto de candidatos en la mano, ordenar solo por cercanía semántica reproduce el error original a menor escala. Puntúa la coincidencia de entidades, la probabilidad de que un fragmento sea una respuesta, si está junto a un turno que plausiblemente la contiene y el contexto temporal. Pero recuerda el techo: un reranker solo reordena lo que le entregó la recuperación y no puede sacarse de la manga algo que nunca salió.
Recupera en varios saltos. Una sola pasada sirve para preguntas sobre un único dato. Con las agregadas se rompe. Una pregunta como "qué apodos me ha puesto mi padre" tiene la respuesta repartida en cinco mensajes a lo largo de tres meses. Resolverlo exige una primera recuperación amplia, agrupar los candidatos por tipo, comprobar si lo reunido basta y otra búsqueda si no basta. Es la misma forma de recuperar, reflexionar y decidir si buscar de nuevo que usan Self-RAG y Corrective RAG (CRAG). Ponle un límite firme, ya sea un tope de saltos o una prueba de confianza, o dará vueltas sin fin o encarecerá cada respuesta.
Un bucle mínimo tiene este aspecto:
def retrieve_memory(user_query, vector_db, llm, max_hops=2):
rewrites = llm.rewrite(
query=user_query,
instruction=(
"Turn this question into 3-5 retrieval-oriented phrasings "
"that describe what the ANSWER would say, not what the "
"question says."
),
)
candidates = []
for q in rewrites:
candidates += vector_db.search(q, top_k=20)
ranked = []
for hop in range(max_hops):
ranked = rerank(
user_query,
candidates,
factors=[
"semantic_relevance",
"entity_match",
"answer_likelihood",
"conversational_relationship",
"temporal_context",
],
)
if llm.is_sufficient(user_query, ranked[:10]):
break
follow_up = llm.next_query(user_query, ranked[:10])
candidates += vector_db.search(follow_up, top_k=20)
return ranked[:10]
Nada de esto es exótico. Lo que importa es el orden: primero arregla el recall y la construcción de la consulta, después el reranking, y solo entonces piensa en añadir infraestructura.
Consejo para el cliente
La petición de auditoría tenía tres partes: dónde se escapa la memoria entre el almacenamiento y la generación, si el diseño aguanta diez veces más datos y una recomendación clara entre arreglar lo que tienen o adoptar algo nuevo.
Dónde se escapa. Entre la recuperación y la generación, y justo en la construcción de la consulta, antes de que el reranking pueda ayudar en absoluto.
Con diez veces más. El diseño con varios tipos de memoria y el registro son sólidos y escalarán. La búsqueda por embeddings en una sola pasada no, porque más datos significan más preguntas parecidas compitiendo por los mismos huecos de candidatos. Las capas de reescritura y de varios saltos pasan de opcionales a necesarias mucho antes de esa cifra.
Arreglar o sustituir. Arreglar lo suyo. mem0 y Hindsight son capas de almacenamiento de memoria. Ninguna toca la orquestación, y es en la orquestación donde desaparece el 75% que falta. Cambiar de base de datos no ayudará a una consulta que sigue buscando lo que no es.
A veces un sistema de memoria no olvida nada. Responde a una pregunta que nadie hizo, porque la consulta y la evidencia nunca buscaban lo mismo.