Saltar al contenido principal

Scoring

iLAB Memory rankea observations con dos esquemas de scoring distintos: SearchScore para mem_search, ContextScore para los memories[] que devuelve mem_session_start. Comparten helpers pero combinan signals con pesos distintos — y sus valores no son comparables.

aviso

Nunca compares un valor de SearchScore con uno de ContextScore. Diferentes fórmulas, diferentes escalas, diferente significado semántico. El único invariante que comparten es el rango [0.0, 1.0]. Esto es Braess #5 — ver Architecture.

Los dos esquemas

SearchScore

Usado por mem_search. Combina la relevancia BM25 a tu query, recency y revision count. Más alto = más relevante a la query que hiciste.

ContextScore

Usado por mem_session_start.memories. Combina recency, revision count y una prioridad por type. Más alto = más saliente como contexto default a cargar.

SearchScore — relevancia a una query

SearchScore = 0.60 * fts5_rank
+ 0.25 * recency_score(updated_at)
+ 0.15 * revision_score(revision_count)
SignalPesoQué mide
fts5_rank0.60BM25 de SQLite FTS5, pre-normalizado a [0, 1]
recency_score0.25decay exponencial, half-life de 30 días
revision_score0.15lineal en revision_count, capeado en 10

El signal dominante es el match de la query. Recency es desempate secundario — una observation de 6 meses que clava la query igual le gana a una flamante que no matchea.

ContextScore — saliencia como contexto default

ContextScore = 0.50 * recency_score(updated_at)
+ 0.30 * revision_score(revision_count)
+ 0.20 * type_priority(type)
SignalPesoQué mide
recency_score0.50mismo decay exponencial, half-life de 30 días
revision_score0.30lineal en revision_count, capeado en 10
type_priority0.20peso por type (ver abajo)

Acá no hay query — el objetivo es "si tengo que cargar N memorias al arrancar la sesión, ¿cuáles N importan más para este usuario ahora?" Recency manda; type priority es un dedo en la balanza para los hechos estables del usuario.

Prioridades por type

TypePrioridad
profile1.00
preference0.90
decision0.70
pattern0.60
discovery0.50
summary0.30
type desconocido0.50
tip

Las observations de profile y preference flotan al tope de memories[] aun cuando son un poquito más viejas. discovery y pattern rankean más bajo — aparecen sobre todo vía search.

Helpers compartidos

Ambas fórmulas llaman las mismas primitivas. Hay una sola definición de recency_score y una sola de revision_score en el codebase (scoring.py), y cada composite las arma con sus propios pesos.

from ilab_memory.scoring import recency_score, revision_score

print(recency_score("2025-12-01T00:00:00+00:00")) # ~0.39 si "now" es 2026-04-20
print(revision_score(5)) # 0.5
print(revision_score(50)) # 1.0 (capeado en REVISION_CAP=10)
nota

Esto es Braess #3. Agregar un signal nuevo a un composite requiere actualizar el otro en el mismo PR. Los helpers compartidos son la única fuente de verdad.

¿Por qué dos esquemas?

Porque responden preguntas distintas.

  • mem_search: "Entre todo lo que tengo de este usuario, ¿qué está más cerca de este string de query?" — la relevancia a la query domina.
  • mem_session_start: "Entre todo lo que tengo de este usuario, ¿qué debería ver el LLM por default antes de que el usuario hable?" — recency y saliencia dominan.

Un esquema único no podría pesar el FTS rank tanto en 0.60 (cuando hay query) como en 0.0 (cuando no la hay). Separarlos mantiene cada fórmula honesta y tuneable.

No mezcles los scores

hits = mem.mem_search(user_id="alice", query="auth") # SearchScore
resp = mem.mem_session_start(user_id="alice") # ContextScore en resp.memories

# NO hagas esto:
all_obs = sorted(hits + resp.memories, key=lambda o: o.score, reverse=True)
# El sort no significa nada — los scores están en escalas incompatibles.

Si necesitás un ranking unificado entre ambos, calculalo vos desde las observations subyacentes (ej. cargá los registros completos vía mem_get_observation y aplicá tu propia fórmula). La librería se rehúsa deliberadamente a fusionar los dos.

Siguiente

  • Architecture / Braess #5 — por qué esta regla está enforzada a nivel de tipos (SearchScore y ContextScore son clases Pydantic frozen distintas).
  • Observations — cómo se ven los objetos scoreados.