Sistema Límbico
¿Qué es el Sistema Límbico?
Sección titulada «¿Qué es el 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).
Las Cuatro Capacidades
Sección titulada «Las Cuatro Capacidades»| Capacidad | Qué hace | Metáfora biológica |
|---|---|---|
| Saliencia | Entidades frecuentemente accedidas y bien conectadas rankean más alto | Valencia emocional — cosas importantes se recuerdan mejor |
| Decay Temporal | Entidades no accedidas recientemente disminuyen gradualmente en rank | Olvido — conocimiento sin usar se desvanece |
| Co-ocurrencia | Entidades que aparecen juntas frecuentemente se refuerzan entre sí | Asociación — cosas que disparan juntas, se cablean juntas |
| Decay Temporal de Co-ocurrencia | Co-ocurrencias recientes cuentan más que las antiguas | Asociaciones 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.
Las Tres Estrategias
Sección titulada «Las Tres Estrategias»| Estrategia | Peso Coseno | Peso Límbico | Mejor Para |
|---|---|---|---|
COSINE_HEAVY | 70% | 30% | Queries factuales: definiciones, “qué es”, términos exactos |
LIMBIC_HEAVY | 30% | 70% | Queries exploratorias: “explícame todo”, contexto amplio |
HYBRID_BALANCED | 50% | 50% | Queries mixtas: relaciones, comparaciones |
Algoritmo de Detección
Sección titulada «Algoritmo de Detección»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_BALANCEDFórmula de Blending
Sección titulada «Fórmula de Blending»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_normLIMBIC_HEAVY: final = 0.3 × cosine + 0.7 × limbic_normHYBRID_BALANCED: final = 0.5 × cosine + 0.5 × limbic_normDonde limbic_norm es min-max normalizado a través del conjunto de candidatos.
Output de API
Sección titulada «Output de API»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ó.
Fórmula de Scoring
Sección titulada «Fórmula de Scoring»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:
| Componente | Rango | Rol |
|---|---|---|
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)
Sección titulada «cosine_sim(q, e)»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.
Subfórmula: importance(e)
Sección titulada «Subfórmula: importance(e)»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:
Frecuencia de acceso
Sección titulada «Frecuencia de acceso»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.787No la mitad (0.5), sino ~0.79 — el logaritmo preserva más señal en cuentas bajas.
Grado del grafo
Sección titulada «Grado del grafo»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.
Ejemplo combinado
Sección titulada «Ejemplo combinado»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.850La 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.
Subfórmula: temporal_factor(e)
Sección titulada «Subfórmula: temporal_factor(e)»temporal_factor(e) = max(TEMPORAL_FLOOR, exp(-LAMBDA_HOURLY × Δt_hours))Donde:
Δt_hours= horas desde el último acceso a la entidad (víasearch_semanticoopen_nodes). Si la entidad nunca fue accedida, se usacreated_aten su lugar.LAMBDA_HOURLY = 0.0001— la tasa de decaimiento exponencial por horaTEMPORAL_FLOOR = 0.1— el valor mínimo que puede alcanzar el factor
Características del decaimiento
Sección titulada «Características del decaimiento»Con LAMBDA_HOURLY = 0.0001, la vida media del decaimiento es:
vida media = ln(2) / 0.0001 ≈ 6931 horas ≈ 289 díasEste 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.
Ejemplos numéricos
Sección titulada «Ejemplos numéricos»| Tiempo desde último acceso | Δt (horas) | temporal_factor |
|---|---|---|
| 1 hora | 1 | 0.9999 |
| 1 día | 24 | 0.9976 |
| 1 semana | 168 | 0.9833 |
| 30 días | 720 | 0.9305 |
| 90 días | 2160 | 0.8053 |
| 180 días | 4320 | 0.6485 |
| 1 año | 8766 | 0.4172 |
| 2 años | 17532 | 0.1740 |
| 3+ años | 26000+ | → 0.1 (piso) |
Sub-fórmula: cooc_boost(e, R)
Sección titulada «Sub-fórmula: cooc_boost(e, R)»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_count | log₂(1 + co_count) | Multiplicador |
|---|---|---|
| 1 | 1.00 | 1× |
| 5 | 2.58 | 5× |
| 10 | 3.46 | 10× |
| 50 | 5.67 | 50× |
| 100 | 6.66 | 100× |
Decay Temporal de Co-ocurrencia
Sección titulada «Decay Temporal de Co-ocurrencia»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-ocurrencia | Factor de decay |
|---|---|
| 1 hora | 0.9999 |
| 1 día | 0.9976 |
| 1 semana | 0.9833 |
| 30 días | 0.9305 |
| 90 días | 0.8053 |
| 1 año | 0.4172 |
| 2+ años | → 0.1 (piso) |
Constantes Ajustables
Sección titulada «Constantes Ajustables»Todas las constantes están a nivel de módulo en src/mcp_memory/scoring.py y son directamente editables.
| Constante | Valor | Propósito | Efecto de aumentar |
|---|---|---|---|
BETA_SAL | 0.5 | Peso del boost de salience en el score compuesto | Mayor → importance importa más; una entidad con importance=1.0 obtiene boost de 1.5× |
BETA_DEG | 0.15 | Peso del grado del grafo dentro de importance | Mayor → las entidades bien conectadas puntúan más alto; bajo por diseño ya que el grado es secundario |
D_MAX | 15 | Límite en el conteo de relaciones para normalización de grado | Mayor → entidades con muchas relaciones siguen recibiendo boost; 15 es un umbral razonable para hubs |
LAMBDA_HOURLY | 0.0001 | Tasa de decaimiento temporal por hora | Mayor → olvido más rápido; vida media actual ≈ 290 días |
GAMMA | 0.01 | Peso del boost de co-ocurrencia en el score compuesto | Mayor → las entidades co-ocurrentes puntúan más alto; reducido desde 0.1 que dominaba el scoring 24× |
RRF_K | 60 | Constante de suavizado para Reciprocal Rank Fusion | Mayor → la posición en el rank importa menos; 60 es el valor estándar del paper original de RRF |
EXPANSION_FACTOR | 3 | Multiplicador de over-retrieval KNN | Mayor → más candidatos para re-ranking a costa de computación; limit=10 → 30 candidatos |
TEMPORAL_FLOOR | 0.1 | Valor mínimo para el decaimiento temporal | Mayor → las entidades viejas retienen más señal; 0.1 significa que el conocimiento se degrada pero nunca se destruye |
Registro de Señales
Sección titulada «Registro de Señales»El Sistema Límbico se basa en tres señales registradas durante el uso normal de la API:
Tabla entity_access
Sección titulada «Tabla entity_access»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) yopen_nodes(para entidades abiertas). - Qué se almacena:
access_count(incrementado en cada acceso) ylast_access(actualizado al timestamp actual). - Comportamiento: best-effort — el registro ocurre después de construir la respuesta y no afecta el resultado actual.
Tabla co_occurrences
Sección titulada «Tabla co_occurrences»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 deopen_nodes(cuando se abren 2+ entidades juntas). - Qué se almacena:
co_count(incrementado por par) ylast_co(timestamp actualizado). Los pares se almacenan en orden canónico:entity_a_id < entity_b_idsiempre, 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.
Por Qué Ajustar?
Sección titulada «Por Qué Ajustar?»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.
Cómo Funciona
Sección titulada «Cómo Funciona»- Recolectar datos: Shadow mode loguea cada query
search_semanticcon ambos rankings (baseline y límbico) - Calcular métricas:
ab_metrics.pycalcula NDCG@K usando feedback implícito (eventos de re-acceso) - Grid search:
auto_tuner.py --tuneexplora combinaciones de GAMMA × BETA_SAL - Aplicar suavemente: Nuevos valores se mezclan vía media móvil exponencial (10% nuevo, 90% viejo)
# Analizar rendimiento actualpython scripts/auto_tuner.py --analyze
# Encontrar params óptimos y aplicarpython scripts/auto_tuner.py --tune
# Forzar valores específicos con blend suavepython scripts/auto_tuner.py --set-gamma 0.05 --set-beta 0.75
# Rangos personalizadospython 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étricas
Sección titulada «Métricas»| Métrica | Descripción |
|---|---|
| NDCG@K | Normalized Discounted Cumulative Gain at K — qué tan bien el ranking coincide con relevancia implícita |
| Lift@K | Proporción de items relevantes en top-K vs total — más alto es mejor |
| Gain | Mejora de NDCG sobre parámetros actuales |
Requisitos
Sección titulada «Requisitos»- 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
Flujo Completo del Pipeline
Sección titulada «Flujo Completo del Pipeline»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:
- Codificar consulta — la cadena de consulta se convierte en un vector de 384 dimensiones usando el modelo de embeddings con prefijo
task="query". - KNN 3× — el vector se compara contra todos los embeddings de entidades vía sqlite-vec KNN, recuperando
limit × EXPANSION_FACTORcandidatos (default: 3× el límite solicitado). - 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.
- 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. - Construir output — los resultados se formatean con todos los campos: datos originales más
limbic_score, desglose descoringydistance. - 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.
Transparencia de la API
Sección titulada «Transparencia de la API»El Sistema Límbico extiende la API de search_semantic de forma retrocompatible.
Input: sin cambios
Sección titulada «Input: sin cambios»search_semantic(query, limit)Sin parámetros nuevos. El scoring ocurre automáticamente.
Output: campos extendidos
Sección titulada «Output: campos extendidos»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 } }]}| Campo | Tipo | Descripción |
|---|---|---|
distance | float | Distancia coseno original de KNN — no es el score límbico |
limbic_score | float | Score compuesto que determina el orden de los resultados |
scoring | object | Desglose de los tres componentes límbicos |
scoring.importance | float | Score de salience (frecuencia de acceso + grado del grafo) |
scoring.temporal_factor | float | Factor de decaimiento temporal (1.0 = recién accedida, → piso 0.1) |
scoring.cooc_boost | float | Boost de co-ocurrencia por aparecer con entidades relacionadas |
rrf_score | float? | Presente solo en modo Búsqueda Híbrida |
Ordenamiento
Sección titulada «Ordenamiento»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.
Alcance del tracking
Sección titulada «Alcance del tracking»El tracking de co-ocurrencia se dispara en dos lugares:
search_semantic— para todos los pares de entidades en el conjunto top-K retornadoopen_nodes— cuando se abren 2+ entidades juntas en la misma llamada
Ver la Referencia de Tools para la especificación completa de ambas tools.
Módulo: scoring.py
Sección titulada «Módulo: scoring.py»El motor de scoring está en src/mcp_memory/scoring.py (~351 líneas). Expone las siguientes funciones públicas:
| Función | Firma | Propó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) → float | Calcular importance(e) desde señales brutas |
compute_temporal_factor() | (last_access, created_at) → float | Calcular temporal_factor(e) desde timestamps |
compute_cooc_boost() | (entity_id, co_occurrences, result_ids) → float | Calcular 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.
Uso en el pipeline
Sección titulada «Uso en el pipeline»# Modo semántico puroranked = 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)Ejemplo Resuelto
Sección titulada «Ejemplo Resuelto»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.65Paso 2 — Importance:
access_factor = log₂(11) / log₂(21) = 3.459 / 4.392 ≈ 0.787degree_factor = 1 + 0.15 × (8/15) = 1 + 0.08 = 1.08importance = 0.787 × 1.08 ≈ 0.850salience_boost = 1 + 0.5 × 0.850 = 1.425Paso 3 — Factor temporal:
Δt = 30 días × 24 = 720 horastemporal_factor = exp(-0.0001 × 720) = exp(-0.072) ≈ 0.931Paso 4 — Boost de co-ocurrencia:
cooc_boost = log₂(6) + log₂(3) + log₂(2) = 2.585 + 1.585 + 1.000 = 5.170cooc_factor = 1 + 0.01 × 5.170 = 1.052Paso 5 — Score límbico compuesto:
limbic_score = 0.65 × 1.425 × 0.931 × 1.052 = 0.65 × 1.425 × 0.931 × 1.052 ≈ 0.908El 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).
Relacionado
Sección titulada «Relacionado»- 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_semanticyopen_nodes - Arquitectura — visión general del sistema incluyendo el lugar del módulo de scoring en el flujo de datos