Ir al contenido

Sistema Límbico

El Sistema Límbico es una capa de scoring dinámico que se ejecuta sobre los resultados KNN de search_semantic, reordenando candidatos según cómo se ha utilizado el knowledge graph a lo largo del tiempo. Transforma una búsqueda puramente basada en similitud en una que también considera qué importa, qué es reciente y qué pertenece junto.

El nombre es una metáfora biológica deliberada. En el cerebro humano, el sistema límbico maneja tres funciones que este módulo replica computacionalmente:

  • Valencia emocional — los recuerdos importantes se refuerzan. Aquí, las entidades frecuentemente accedidas y bien conectadas puntúan más alto.
  • Olvido — los recuerdos no usados se desvanecen gradualmente. Aquí, las entidades no accedidas recientemente decaen en su score.
  • Asociación — las cosas que se disparan juntas se conectan. Aquí, las entidades que co-ocurren en resultados de búsqueda se refuerzan mutuamente.

La API de search_semantic extiende su output para incluir los scores calculados. Más allá del {name, entityType, observations, distance} original, cada resultado ahora incluye limbic_score (el score compuesto) y scoring (un desglose de cada componente).

CapacidadQué haceMetáfora biológica
SalienciaEntidades frecuentemente accedidas y bien conectadas rankean más altoValencia emocional — cosas importantes se recuerdan mejor
Decay TemporalEntidades no accedidas recientemente disminuyen gradualmente en rankOlvido — conocimiento sin usar se desvanece
Co-ocurrenciaEntidades que aparecen juntas frecuentemente se refuerzan entre síAsociación — cosas que disparan juntas, se cablean juntas
Decay Temporal de Co-ocurrenciaCo-ocurrencias recientes cuentan más que las antiguasAsociaciones recientes son más fuertes que las antiguas

Cada capacidad mapea a un factor en la fórmula de scoring. Son multiplicativos — un score fuerte requiere que los cuatro contribuyan.

Query Routing: Selección Dinámica de Estrategia

Sección titulada «Query Routing: Selección Dinámica de Estrategia»

MCP Memory v2 selecciona automáticamente la mejor estrategia de scoring basada en las características de la consulta. Esto se llama query routing.

EstrategiaPeso CosenoPeso LímbicoMejor Para
COSINE_HEAVY70%30%Queries factuales: definiciones, “qué es”, términos exactos
LIMBIC_HEAVY30%70%Queries exploratorias: “explícame todo”, contexto amplio
HYBRID_BALANCED50%50%Queries mixtas: relaciones, comparaciones

La función detect_query_type() analiza características lingüísticas:

Reglas de scoring:
- Palabras clave factuales ("qué es", "definición", "cómo funciona", "es un/una") → +2
- Palabras clave intermedias ("relación", "diferencia", "ejemplos", "comparar") → +1
- Palabras clave exploratorias ("explícame", "relación entre", "dime todo", "qué piensas") → -2
- Longitud de query ≤3 palabras → +1 (factual)
- Longitud de query ≥10 palabras → -1 (exploratoria)
- K limit ≤3 → +1 (preciso/factual)
- K limit ≥10 → -1 (exploratoria)
Routing final:
- Score ≥ 2 → COSINE_HEAVY
- Score ≤ -2 → LIMBIC_HEAVY
- En otro caso → HYBRID_BALANCED

Cuando el routing selecciona COSINE_HEAVY o LIMBIC_HEAVY, el score final combina similitud coseno con score límbico normalizado:

COSINE_HEAVY: final = 0.7 × cosine + 0.3 × limbic_norm
LIMBIC_HEAVY: final = 0.3 × cosine + 0.7 × limbic_norm
HYBRID_BALANCED: final = 0.5 × cosine + 0.5 × limbic_norm

Donde limbic_norm es min-max normalizado a través del conjunto de candidatos.

El campo routing_strategy se incluye en cada resultado de search_semantic:

{
"results": [{
"name": "FastMCP",
"routing_strategy": "cosine_heavy",
"limbic_score": 0.67,
...
}]
}

Esto te permite entender por qué un resultado particular rankeó donde rankeó.

El score límbico compuesto se calcula como:

score(e, q) = cosine_sim(q, e) × (1 + β_sal × importance(e)) × temporal_factor(e) × (1 + γ × cooc_boost(e, R))

Donde:

ComponenteRangoRol
cosine_sim(q, e)[0, 1]Relevancia base — similitud pura de embeddings desde KNN
(1 + β_sal × importance(e))[1, 1.5]Boost de salience — qué tan importante es la entidad
temporal_factor(e)[0.1, 1.0]Decaimiento temporal — qué tan reciente fue el acceso a la entidad
(1 + γ × cooc_boost(e, R))[1, ~1.05]Boost de co-ocurrencia — refuerzo por aparecer con entidades relacionadas

La fórmula es multiplicativa por diseño: un candidato que es similar, importante, reciente y co-ocurrente puntuará significativamente más alto que uno que solo coincide en similitud. A la inversa, una entidad decaída todavía puede rankear si es muy importante y relevante.

cosine_sim(q, e) = max(0, 1 - distance)

La similitud base viene directamente de la búsqueda KNN. distance es la distancia coseno almacenada en la tabla embeddings. El clamp max(0, ...) previene valores negativos para vectores distantes.

importance(e) = [log₂(1 + access_count) / log₂(1 + max_access)] × (1 + β_deg × min(degree, D_max) / D_max)

El score de importancia combina dos señales estructurales:

El primer factor normaliza la frecuencia de acceso de una entidad relativa a la entidad más accedida en el conjunto de candidatos:

access_factor = log₂(1 + access_count) / log₂(1 + max_access)

El logaritmo comprime la escala para que la diferencia entre 1 y 10 accesos importe más que entre 100 y 110. Esto evita que entidades accedidas muchas veces por accidente dominen permanentemente.

Ejemplo numérico: una entidad con 10 accesos cuando el máximo en el conjunto es 20:

access_factor = log₂(11) / log₂(21) = 3.459 / 4.392 ≈ 0.787

No la mitad (0.5), sino ~0.79 — el logaritmo preserva más señal en cuentas bajas.

El segundo factor premia a las entidades con muchas relaciones en el knowledge graph:

degree_factor = (1 + β_deg × min(degree, D_max) / D_max)

Una entidad en el centro de un knowledge graph (muchas relaciones y ) probablemente sea más importante que una aislada. El grado se limita a D_MAX (15) para evitar que las entidades hub dominen.

Una entidad con 10 accesos (máximo 20) y 8 relaciones (D_MAX = 15):

importance = [log₂(11) / log₂(21)] × (1 + 0.15 × 8/15)
= 0.787 × (1 + 0.08)
= 0.787 × 1.08
≈ 0.850

La señal de grado añade un modesto boost del 8% — intencional, ya que el grado es una señal secundaria comparada con la frecuencia de acceso.

temporal_factor(e) = max(TEMPORAL_FLOOR, exp(-LAMBDA_HOURLY × Δt_hours))

Donde:

  • Δt_hours = horas desde el último acceso a la entidad (vía search_semantic o open_nodes). Si la entidad nunca fue accedida, se usa created_at en su lugar.
  • LAMBDA_HOURLY = 0.0001 — la tasa de decaimiento exponencial por hora
  • TEMPORAL_FLOOR = 0.1 — el valor mínimo que puede alcanzar el factor

Con LAMBDA_HOURLY = 0.0001, la vida media del decaimiento es:

vida media = ln(2) / 0.0001 ≈ 6931 horas ≈ 289 días

Este es un decaimiento lento por diseño. El knowledge graph es un recurso de larga duración — las entidades no deberían desaparecer de los resultados solo porque no se han necesitado por unas semanas. Pero a lo largo de meses y años, las entidades no usadas retroceden gradualmente.

Tiempo desde último accesoΔt (horas)temporal_factor
1 hora10.9999
1 día240.9976
1 semana1680.9833
30 días7200.9305
90 días21600.8053
180 días43200.6485
1 año87660.4172
2 años175320.1740
3+ años26000+→ 0.1 (piso)
cooc_boost(e, R) = Σ_{r ∈ R, r ≠ e} log₂(1 + co_count(e, r)) × decay(last_co(e, r))

Esto suma los conteos de co-ocurrencia entre la entidad e y cada otra entidad r en el conjunto de resultados R, ponderados por decay temporal. El logaritmo suaviza la contribución:

co_countlog₂(1 + co_count)Multiplicador
11.00
52.58
103.4610×
505.6750×
1006.66100×

El factor decay(last_co) usa la misma vida media (~290 días) que compute_temporal_factor:

decay(last_co) = max(COOC_TEMPORAL_FLOOR, exp(-LAMBDA_HOURLY × Δt_hours))

Esto significa que las co-ocurrencias recientes boost más que las antiguas. Una entidad que apareció junto con otra ayer contribuye más al boost de co-ocurrencia que una que co-ocurrió hace 6 meses.

Tiempo desde co-ocurrenciaFactor de decay
1 hora0.9999
1 día0.9976
1 semana0.9833
30 días0.9305
90 días0.8053
1 año0.4172
2+ años→ 0.1 (piso)

Todas las constantes están a nivel de módulo en src/mcp_memory/scoring.py y son directamente editables.

ConstanteValorPropósitoEfecto de aumentar
BETA_SAL0.5Peso del boost de salience en el score compuestoMayor → importance importa más; una entidad con importance=1.0 obtiene boost de 1.5×
BETA_DEG0.15Peso del grado del grafo dentro de importanceMayor → las entidades bien conectadas puntúan más alto; bajo por diseño ya que el grado es secundario
D_MAX15Límite en el conteo de relaciones para normalización de gradoMayor → entidades con muchas relaciones siguen recibiendo boost; 15 es un umbral razonable para hubs
LAMBDA_HOURLY0.0001Tasa de decaimiento temporal por horaMayor → olvido más rápido; vida media actual ≈ 290 días
GAMMA0.01Peso del boost de co-ocurrencia en el score compuestoMayor → las entidades co-ocurrentes puntúan más alto; reducido desde 0.1 que dominaba el scoring 24×
RRF_K60Constante de suavizado para Reciprocal Rank FusionMayor → la posición en el rank importa menos; 60 es el valor estándar del paper original de RRF
EXPANSION_FACTOR3Multiplicador de over-retrieval KNNMayor → más candidatos para re-ranking a costa de computación; limit=10 → 30 candidatos
TEMPORAL_FLOOR0.1Valor mínimo para el decaimiento temporalMayor → las entidades viejas retienen más señal; 0.1 significa que el conocimiento se degrada pero nunca se destruye

El Sistema Límbico se basa en tres señales registradas durante el uso normal de la API:

CREATE TABLE entity_access (
entity_id INTEGER PRIMARY KEY REFERENCES entities(id) ON DELETE CASCADE,
access_count INTEGER NOT NULL DEFAULT 1,
last_access TEXT NOT NULL DEFAULT (datetime('now'))
);
  • Cuándo se registra: cada llamada a search_semantic (para resultados top-K) y open_nodes (para entidades abiertas).
  • Qué se almacena: access_count (incrementado en cada acceso) y last_access (actualizado al timestamp actual).
  • Comportamiento: best-effort — el registro ocurre después de construir la respuesta y no afecta el resultado actual.
CREATE TABLE co_occurrences (
entity_a_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
entity_b_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
co_count INTEGER NOT NULL DEFAULT 1,
last_co TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (entity_a_id, entity_b_id)
);
  • Cuándo se registra: después de search_semantic (para todos los pares en el conjunto top-K) y después de open_nodes (cuando se abren 2+ entidades juntas).
  • Qué se almacena: co_count (incrementado por par) y last_co (timestamp actualizado). Los pares se almacenan en orden canónico: entity_a_id < entity_b_id siempre, previniendo pares duplicados como (A, B) y (B, A).
  • Comportamiento: best-effort, post-respuesta, non-blocking.

Auto-Tuning: Optimización de GAMMA y BETA_SAL

Sección titulada «Auto-Tuning: Optimización de GAMMA y BETA_SAL»

Las constantes del limbic scoring (GAMMA y BETA_SAL) pueden ser ajustadas automáticamente vía grid search offline usando datos recolectados del A/B testing en shadow mode.

Los valores por defecto (GAMMA=0.01, BETA_SAL=0.5) funcionan bien como punto de partida, pero el balance óptimo depende de tus patrones de uso. El auto-tuning encuentra el punto ideal para tu knowledge graph específico.

  1. Recolectar datos: Shadow mode loguea cada query search_semantic con ambos rankings (baseline y límbico)
  2. Calcular métricas: ab_metrics.py calcula NDCG@K usando feedback implícito (eventos de re-acceso)
  3. Grid search: auto_tuner.py --tune explora combinaciones de GAMMA × BETA_SAL
  4. Aplicar suavemente: Nuevos valores se mezclan vía media móvil exponencial (10% nuevo, 90% viejo)
Ventana de terminal
# Analizar rendimiento actual
python scripts/auto_tuner.py --analyze
# Encontrar params óptimos y aplicar
python scripts/auto_tuner.py --tune
# Forzar valores específicos con blend suave
python scripts/auto_tuner.py --set-gamma 0.05 --set-beta 0.75
# Rangos personalizados
python scripts/auto_tuner.py --tune --gamma-range "0.001,0.01,0.05,0.1" --beta-sal-range "0.1,0.5,1.0"
MétricaDescripción
NDCG@KNormalized Discounted Cumulative Gain at K — qué tan bien el ranking coincide con relevancia implícita
Lift@KProporción de items relevantes en top-K vs total — más alto es mejor
GainMejora de NDCG sobre parámetros actuales
  • Mínimo 50 eventos con treatment=1 (grupo de tratamiento del A/B test)
  • Datos de feedback implícito (eventos de re-acceso vía record_access)
  • Cobertura suficiente del espacio de parámetros
graph TD
Q["Cadena de consulta"] --> Encode["1. Codificar<br/>engine.encode(query)<br/>→ float[384]"]
Encode --> KNN["2. KNN 3×<br/>search_embeddings(query, limit × 3)<br/>→ [{entity_id, distance}]"]
KNN --> Fetch["3. Obtener metadata<br/>access_data · degrees · co_occurrences"]
Fetch --> Rerank["4. Re-rankear<br/>rank_candidates()<br/>calcular limbic_score por candidato<br/>→ ordenar descendente → top-K"]
Rerank --> Build["5. Construir output<br/>{name, entityType, observations,<br/>distance, limbic_score, scoring}"]
Build --> Record["6. Registrar señales<br/>record_access(top-K ids)<br/>record_co_occurrences(top-K ids)<br/>best-effort, post-respuesta"]

Paso a paso:

  1. Codificar consulta — la cadena de consulta se convierte en un vector de 384 dimensiones usando el modelo de embeddings con prefijo task="query".
  2. KNN 3× — el vector se compara contra todos los embeddings de entidades vía sqlite-vec KNN, recuperando limit × EXPANSION_FACTOR candidatos (default: 3× el límite solicitado).
  3. Obtener metadata — para cada candidato, el sistema carga conteos de acceso, últimos tiempos de acceso, grados del grafo y datos de co-ocurrencia desde las tablas de tracking.
  4. Re-rankear — la función rank_candidates() computa el score límbico para cada candidato usando la fórmula anterior, ordena por score descendente y retorna el top-K.
  5. Construir output — los resultados se formatean con todos los campos: datos originales más limbic_score, desglose de scoring y distance.
  6. Registrar señales — los conteos de acceso y co-ocurrencias para las entidades retornadas se actualizan. Esto ocurre después de construir la respuesta, es best-effort y no afecta el resultado actual.

El Sistema Límbico extiende la API de search_semantic de forma retrocompatible.

search_semantic(query, limit)

Sin parámetros nuevos. El scoring ocurre automáticamente.

Cada resultado incluye nuevos campos junto a los originales:

{
"results": [{
"name": "SofIA - Sistema Multiagente",
"entityType": "Proyecto",
"observations": ["Sistema multiagente con roles especializados"],
"distance": 0.42,
"limbic_score": 0.67,
"scoring": {
"importance": 0.85,
"temporal_factor": 0.99,
"cooc_boost": 1.23
}
}]
}
CampoTipoDescripción
distancefloatDistancia coseno original de KNN — no es el score límbico
limbic_scorefloatScore compuesto que determina el orden de los resultados
scoringobjectDesglose de los tres componentes límbicos
scoring.importancefloatScore de salience (frecuencia de acceso + grado del grafo)
scoring.temporal_factorfloatFactor de decaimiento temporal (1.0 = recién accedida, → piso 0.1)
scoring.cooc_boostfloatBoost de co-ocurrencia por aparecer con entidades relacionadas
rrf_scorefloat?Presente solo en modo Búsqueda Híbrida

Los resultados se ordenan por limbic_score descendente (mayor primero), no por distancia coseno ascendente. Este es el cambio de comportamiento clave: la entidad más relevante-e-importante aparece primero, incluso si existe una entidad ligeramente más similar pero poco importante.

El tracking de co-ocurrencia se dispara en dos lugares:

  • search_semantic — para todos los pares de entidades en el conjunto top-K retornado
  • open_nodes — cuando se abren 2+ entidades juntas en la misma llamada

Ver la Referencia de Tools para la especificación completa de ambas tools.

El motor de scoring está en src/mcp_memory/scoring.py (~351 líneas). Expone las siguientes funciones públicas:

FunciónFirmaPropósito
rank_candidates()(candidates, access_data, degrees, co_occurrences, limit) → list[dict]Re-rankear resultados KNN con scoring límbico — modo semántico puro
rank_hybrid_candidates()(candidates, access_data, degrees, co_occurrences, limit) → list[dict]Re-rankear resultados fusionados con RRF con scoring límbico — modo híbrido
reciprocal_rank_fusion()(semantic_results, fts_results, k) → list[dict]Fusionar rankings KNN y FTS5 usando RRF
compute_importance()(access_count, max_access, degree) → floatCalcular importance(e) desde señales brutas
compute_temporal_factor()(last_access, created_at) → floatCalcular temporal_factor(e) desde timestamps
compute_cooc_boost()(entity_id, co_occurrences, result_ids) → floatCalcular cooc_boost(e, R) desde mapa de co-ocurrencias

Todas las constantes (BETA_SAL, BETA_DEG, D_MAX, LAMBDA_HOURLY, GAMMA, RRF_K, EXPANSION_FACTOR, TEMPORAL_FLOOR) son variables a nivel de módulo y pueden sobrescribirse al importar o patchearse para testing.

# Modo semántico puro
ranked = rank_candidates(
candidates=knn_results, # [{entity_id, distance}]
access_data=access_data, # {entity_id: {access_count, last_access}}
degrees=degrees, # {entity_id: int}
co_occurrences=co_occurrences, # {(a_id, b_id): co_count}
limit=10
)
# Modo híbrido (KNN + FTS5)
merged = reciprocal_rank_fusion(knn_results, fts_results, k=RRF_K)
ranked = rank_hybrid_candidates(
candidates=merged,
access_data=access_data,
degrees=degrees,
co_occurrences=co_occurrences,
limit=10
)

Recorrido de un cálculo de scoring completo para una entidad concreta.

Entidad: “FastMCP” (una entidad de framework)

  • Conteo de acceso: 10 (máximo en el conjunto de candidatos: 20)
  • Grado del grafo: 8 relaciones
  • Último acceso: hace 30 días
  • Co-ocurrencia con otros 3 resultados: conteos de [5, 2, 1]
  • Distancia coseno desde la consulta: 0.35

Paso 1 — Similitud base:

cosine_sim = max(0, 1 - 0.35) = 0.65

Paso 2 — Importance:

access_factor = log₂(11) / log₂(21) = 3.459 / 4.392 ≈ 0.787
degree_factor = 1 + 0.15 × (8/15) = 1 + 0.08 = 1.08
importance = 0.787 × 1.08 ≈ 0.850
salience_boost = 1 + 0.5 × 0.850 = 1.425

Paso 3 — Factor temporal:

Δt = 30 días × 24 = 720 horas
temporal_factor = exp(-0.0001 × 720) = exp(-0.072) ≈ 0.931

Paso 4 — Boost de co-ocurrencia:

cooc_boost = log₂(6) + log₂(3) + log₂(2) = 2.585 + 1.585 + 1.000 = 5.170
cooc_factor = 1 + 0.01 × 5.170 = 1.052

Paso 5 — Score límbico compuesto:

limbic_score = 0.65 × 1.425 × 0.931 × 1.052
= 0.65 × 1.425 × 0.931 × 1.052
≈ 0.908

El Sistema Límbico impulsó esta entidad desde una similitud bruta de 0.65 a un score compuesto de 0.908 — un incremento del 40% impulsado por importance (primario), recencia temporal (secundario) y co-ocurrencia (terciario).

  • Búsqueda Híbrida — cómo FTS5 y KNN se fusionan vía RRF antes del re-ranking límbico
  • Búsqueda Semántica — el pipeline KNN que produce los candidatos iniciales
  • Referencia de Tools — especificación completa de la API para search_semantic y open_nodes
  • Arquitectura — visión general del sistema incluyendo el lugar del módulo de scoring en el flujo de datos