Cambiar tema

Enrutamiento de consultas RAG en la práctica: colaboración entre múltiples bases vectoriales y recuperación inteligente

Easton editorial illustration: LOGIC 路由开关, SEMANTIC 路由开关, 三座向量库, RRF 合并托盘

A las tres de la madrugada, las alertas de producción volvieron a encenderse.

Miraba el panel de monitorización: la consulta del usuario sobre «ventas del Q3 en el este de China» había disparado el tiempo de respuesta hasta 12 segundos. En el entorno demo funcionaba perfectamente; ¿cómo se había ido todo al traste al desplegar? Lo peor: los logs mostraban que el sistema había ejecutado el flujo completo de razonamiento multi-salto solo para responder una consulta factual sencilla.

«Es como usar una bomba nuclear para matar un mosquito», comentó un compañero al ver la pantalla.

En ese momento entendí que el problema no era la precisión de la recuperación, sino la estrategia. El RAG tradicional actúa como un «recuperador sin criterio»: da igual lo que pregunte el usuario, siempre pasa por búsqueda vectorial + generación con un LLM. Las consultas simples se sobreprocesan; las complejas no reciben el soporte que necesitan.

En este artículo comparto cómo instalar un «controlador de tráfico» en tu sistema RAG: un enrutador de consultas que, según las características de cada pregunta, la envía al camino de recuperación más adecuado — vía rápida para hechos simples, vía profunda para razonamiento complejo y recuperación multi-fuente para respuestas completas.

En pocas palabras, esta solución salvó mi proyecto.

1. ¿Por qué hace falta el enrutamiento de consultas? — De la «recuperación ciega» a la distribución inteligente

Empiezo por los errores que cometí.

El año pasado construimos un RAG de atención al cliente para una tienda online. La base de conocimiento mezclaba información de productos, políticas de posventa, reglas logísticas y campañas de marketing. En pruebas todo iba bien; tras el lanzamiento, las quejas se duplicaron.

Al revisar los logs apareció un problema clásico: el usuario preguntaba «¿cuánto tarda el reembolso?» y el sistema devolvía plazos de envío y reglas promocionales. No era que la respuesta fuera incorrecta, sino que no estaba enfocada — demasiada información irrelevante confundía al usuario.

Ese es el primer dolor del RAG tradicional: interferencia de conocimiento. Si mezclas datos de todos los escenarios de negocio en una sola base vectorial, los resultados serán un batiburrillo. Preguntas del escenario A pueden devolver contenido «similar» del escenario B.

El segundo dolor es más sutil: eficiencia de respuesta.

Mira esta consulta: «¿Cuáles fueron las ventas del Q3 en el este de China?» Es una pregunta factual simple; bastaría con consultar la base de datos o hacer búsqueda por palabras clave. ¿Qué hace el RAG clásico? Codificación vectorial, similitud coseno, Top-K, generación con LLM… Un flujo completo que tarda 1-2 segundos y consume muchos recursos.

El tercer dolor: malinterpretación de la intención.

Si el usuario dice «encuentra el vídeo con la duración más corta», SelfQueryRetriever puede no entender que «duración de reproducción» es un filtro de metadatos. Si pregunta «¿afectó la huelga al precio de la acción?», hace falta razonamiento multi-salto (encontrar la huelga, la empresa relacionada y la evolución del precio); una sola búsqueda vectorial no basta.

Necesitamos un enrutador que «lea la situación»: identifique las características de la consulta y elija el camino de recuperación adecuado.

Es como el sistema de pedidos de un restaurante: mostrador rápido para comidas sencillas, cocina principal para platos complejos, ventanilla de delivery para reparto. Cada uno en su sitio, máxima eficiencia.

2. Arquitectura central del enrutamiento — Modelo de distribución en tres capas

La idea del enrutamiento es sencilla: procesar por capas, cada una con su función.

Diseñé el sistema en tres niveles:

Consulta del usuario

┌─────────────────────────────────┐
│  Capa de enrutamiento: clasificación de escenario │
│  (LLM / Semantic Router)         │
│  → python_docs / js_docs / go_docs│
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Capa de recuperación: bases vectoriales por escenario │
│  Chroma(python) / Chroma(js)...  │
│  → fragmentos top-k              │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│  Capa de fusión: integración con RRF │
│  RRF(d) = Σ 1/(k + rank(d))      │
│  → respuesta final               │
└─────────────────────────────────┘

Generación de respuesta

La capa de enrutamiento es el «cerebro». Analiza la consulta y decide qué camino de recuperación seguir. Hay dos enfoques principales: análisis lógico con LLM y coincidencia semántica con Semantic Router. Más adelante los detallo.

La capa de recuperación son las «manos». Cada escenario de negocio tiene su índice vectorial: documentación Python, JavaScript, Go, etc. El enrutador decide en qué base buscar; la capa de recuperación ejecuta la búsqueda.

La capa de fusión es el «árbitro». Cuando la consulta abarca varios escenarios, hay que combinar y ordenar los resultados. Aquí entra RRF (Reciprocal Rank Fusion): un algoritmo simple pero eficaz para ordenar multi-fuente.

Así lo implementa EnsembleRetriever de LangChain:

from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# Inicializar base vectorial de documentación Python
python_store = Chroma(
    persist_directory="./chroma_python",
    embedding_function=OpenAIEmbeddings()
)
python_retriever = python_store.as_retriever(
    search_kwargs={"k": 5}
)

# Inicializar base vectorial de documentación JavaScript
js_store = Chroma(
    persist_directory="./chroma_js",
    embedding_function=OpenAIEmbeddings()
)
js_retriever = js_store.as_retriever(
    search_kwargs={"k": 5}
)

# Fusionar varios recuperadores con RRF
ensemble_retriever = EnsembleRetriever(
    retrievers=[python_retriever, js_retriever],
    c=60  # Parámetro RRF, valor clásico
)

# Ejecutar recuperación
docs = ensemble_retriever.invoke("¿Cómo manejar callbacks asíncronos?")
print(f"Recuperados {len(docs)} fragmentos de documento")

Lo central es EnsembleRetriever: invoca varios recuperadores en paralelo y fusiona con RRF. c=60 es un valor empírico — si es demasiado alto, el ranking se aplana; si es muy bajo, los primeros puestos dominan demasiado.

En nuestras pruebas, esta arquitectura subió la precisión de recuperación del 72 % al 92 %. El coste: más tiempo de respuesta, porque consultar varios recuperadores en paralelo exige más cómputo.

3. Tres estrategias de enrutamiento en la práctica — Lógico, semántico y por metadatos

¿Cómo decide la capa de enrutamiento qué camino tomar? Hay tres enfoques habituales, cada uno con su escenario.

3.1 Enrutamiento lógico: el LLM como despachador

La idea más directa: que el LLM analice la intención y elija la fuente de datos.

La implementación en LangChain es concisa:

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_deepseek import ChatDeepSeek

# Prompt de enrutamiento
system_prompt = """Eres un experto en enrutamiento de consultas de programación.
Según el lenguaje implicado en la pregunta del usuario, enruta a la fuente correspondiente:

- Preguntas de Python → python_docs
- Preguntas de JavaScript → js_docs
- Preguntas de Go → golang_docs
- Si no puedes determinarlo → general_docs

Devuelve solo el nombre de la fuente de datos, sin contenido adicional."""

# Construir cadena de enrutamiento
prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{query}")
])

llm = ChatDeepSeek(model="deepseek-chat", temperature=0)
router_chain = prompt | llm | StrOutputParser()

# Ejecutar enrutamiento
query = "¿Cómo implementar un crawler asíncrono en Python?"
datasource = router_chain.invoke({"query": query})
print(f"Resultado del enrutamiento: {datasource}")  # Salida: python_docs

Ventaja del enrutamiento lógico: flexibilidad. El LLM entiende intenciones complejas, como «comparar los modelos de concurrencia de Python y Go», y puede devolver varias fuentes para luego usar Ensemble.

La desventaja también es clara: lentitud. Cada enrutamiento implica una llamada al LLM (+0,5-1 s de latencia) y el coste de API se acumula.

3.2 Enrutamiento semántico: coincidencia vectorial sin llamar al LLM

Si buscas velocidad, Semantic Router es mejor opción.

Funciona como un «if/else difuso»: defines reglas de enrutamiento (cada una con ejemplos de preguntas) y comparas la consulta del usuario con esos ejemplos por similitud vectorial. La regla que mejor coincida indica el camino.

Implementación con la librería semantic-router:

from semantic_router import Route, RouteLayer
from semantic_router.encoders import OpenAIEncoder

# Definir reglas de enrutamiento
python_route = Route(
    name="python_docs",
    utterances=[
        "Cómo leer archivos en Python",
        "Uso de decoradores en Python",
        "Cómo implementar programación asíncrona en Python",
        "Sintaxis de list comprehensions en Python",
    ]
)

js_route = Route(
    name="js_docs",
    utterances=[
        "Cómo manejar callbacks asíncronos en JavaScript",
        "Cómo manipular el DOM en JS",
        "Mecanismo del event loop en Node.js",
        "Diferencia entre Promise y async/await en JS",
    ]
)

# Crear capa de enrutamiento
route_layer = RouteLayer(
    encoder=OpenAIEncoder(),
    routes=[python_route, js_route]
)

# Ejecutar enrutamiento (sin llamada al LLM)
query = "¿Cómo se usan los generadores en Python?"
result = route_layer(query)
print(f"Resultado del enrutamiento: {result.name}")  # Salida: python_docs

El enrutamiento semántico es 3-5 veces más rápido que el basado en LLM. En nuestras pruebas, la API de OpenAI Embeddings responde en ~100 ms; una llamada al LLM supera los 500 ms.

Limitación: hay que predefinir las reglas. Si la consulta cae fuera del alcance definido, el enrutamiento falla (devuelve None). Encaja cuando los tipos de consulta son relativamente fijos.

3.3 Enrutamiento por metadatos: filtrado con campos estructurados

Si tu base de conocimiento tiene metadatos ricos (categoría, idioma, fecha, etc.), SelfQueryRetriever permite filtrado preciso.

from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import ChatOpenAI

# Definir campos de metadatos
metadata_field_info = [
    AttributeInfo(
        name="category",
        description="Categoría del documento: tutorial, api, guide, troubleshooting",
        type="string"
    ),
    AttributeInfo(
        name="language",
        description="Lenguaje de programación: python, javascript, golang",
        type="string"
    ),
    AttributeInfo(
        name="date",
        description="Fecha de publicación del documento",
        type="date"
    )
]

# Crear recuperador
llm = ChatOpenAI(model="gpt-4", temperature=0)
retriever = SelfQueryRetriever.from_llm(
    llm=llm,
    vectorstore=vectorstore,
    document_contents="Documentación técnica de programación",
    metadata_field_info=metadata_field_info,
    verbose=True
)

# La consulta se convierte automáticamente en filtro de metadatos
query = "Documentos tipo tutorial de Python, los más recientes"
docs = retriever.invoke(query)
# En el fondo genera condiciones de filtro:
# category == "tutorial" AND language == "python"
# ordenados por date descendente

Ventaja: precisión. El LLM traduce lenguaje natural a filtros estructurados antes de la búsqueda vectorial. Requisito: metadatos de calidad — sin categorías o etiquetas de idioma, este enfoque no sirve.

4. Colaboración multi-vectorial en la práctica — EnsembleRetriever en profundidad

Las estrategias anteriores resuelven «en qué base buscar». Pero algunas consultas abarcan varios escenarios, como «comparar enfoques de programación asíncrona en Python y JavaScript». Ahí hay que buscar en varias bases y fusionar resultados.

Lo central de EnsembleRetriever es el algoritmo RRF (Reciprocal Rank Fusion).

Principio del algoritmo RRF

La fórmula es simple:

RRF(d) = Σ 1/(k + rank(d))

Donde:

  • d es un documento concreto
  • rank(d) es su posición en un recuperador (desde 1)
  • k es el parámetro de suavizado; el valor típico es 60

Ejemplo: dos recuperadores posicionan el mismo documento así:

  • Recuperador A: documento X en puesto 2 → aporta 1/(60+2) = 0,0156
  • Recuperador B: documento X en puesto 5 → aporta 1/(60+5) = 0,0154
  • Puntuación total = 0,0156 + 0,0154 = 0,031

Todos los documentos se puntúan así y se ordenan por puntuación total.

¿Por qué RRF supera a una media ponderada simple? Porque usa la posición en el ranking, no la puntuación bruta de similitud. Distintos recuperadores devuelven escalas distintas (coseno 0-1 vs puntuación BM25); promediar directamente introduce sesgo. RRF evita ese problema.

Recuperación híbrida densa + dispersa

En proyectos reales solemos combinar búsqueda densa (vectorial) y dispersa (BM25).

La búsqueda vectorial encaja con coincidencia semántica; BM25 con palabras clave. Se complementan.

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma

# Recuperación dispersa: BM25
bm25_retriever = BM25Retriever.from_texts(
    documents_text_list,
    k=5
)

# Recuperación densa: vectorial
vector_retriever = Chroma.from_texts(
    documents_text_list,
    embedding=OpenAIEmbeddings()
).as_retriever(search_kwargs={"k": 5})

# Recuperación híbrida
ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6],  # BM25 0,4, vectorial 0,6
    c=60
)

# Ejecutar recuperación
query = "LangChain Agent tool calling"
docs = ensemble.invoke(query)

Consejo de ajuste: la asignación de pesos.

Nuestra experiencia:

  • Consultas de comprensión semántica («cómo implementar Q&A inteligente»): más peso vectorial (0,6-0,7)
  • Coincidencia exacta por palabras clave («novedades de Python 3.11»): más peso BM25 (0,5-0,6)
  • Escenario general: pesos equilibrados (0,5/0,5)

Evaluación de la calidad de recuperación

¿Cómo saber si EnsembleRetriever aporta valor? Puedes usar TruLens.

from trulens_eval import Feedback, TruChain
from trulens_eval.feedback.provider.openai import OpenAI

provider = OpenAI()

# Definir métricas de evaluación
relevance_feedback = Feedback(
    provider.relevance,
    name="Answer Relevance"
).on_input_output()

context_relevance_feedback = Feedback(
    provider.context_relevance,
    name="Context Relevance"
).on_input().on(context)

# Registrar cadena de evaluación
tru_recorder = TruChain(
    chain=ensemble_retriever_chain,
    feedbacks=[relevance_feedback, context_relevance_feedback],
    feedback_mode="with_chain"
)

# Ejecutar evaluación
with tru_recorder as recording:
    response = ensemble_retriever_chain.invoke({"query": test_query})

TruLens devuelve «relevancia de la respuesta» y «relevancia del contexto». En nuestras pruebas, la relevancia contextual media pasó de 0,72 con un solo recuperador vectorial a 0,91 con Ensemble.

5. Comparativa de rendimiento y mejores prácticas

Tanta teoría; ¿qué pasa en la práctica?

En el mismo conjunto de prueba (500 consultas, 4 escenarios de negocio) comparamos cuatro enfoques:

Estrategia de enrutamientoTiempo medio de respuestaPrecisión de recuperaciónEscenario adecuado
Sin enrutamiento (una sola base)1,2 s72 %Un solo dominio de negocio
Enrutamiento lógico (LLM)1,8 s85 %Multi-dominio, intención compleja
Enrutamiento semántico0,5 s88 %Respuesta rápida, tipos de consulta fijos
Ensemble RRF1,0 s92 %Escenarios mixtos, recuperación multi-fuente

Lectura de datos clave

El enrutamiento semántico es 3-4 veces más rápido que el lógico. Solo llama a la API de Embeddings (~100 ms); el lógico requiere LLM (~800 ms). Si los tipos de consulta son estables, el semántico es la primera opción.

Ensemble RRF tiene la mayor precisión. Varios recuperadores cubren distintos espacios semánticos y RRF equilibra sus fortalezas. Coste: algo más de latencia por consultas en paralelo.

Sin enrutamiento puede ser más lento. Parece contraintuitivo, pero una sola base devuelve muchos documentos irrelevantes; el LLM filtra más ruido y tarda más en generar.

Resumen de mejores prácticas

Según lo que aprendimos a base de prueba y error:

1. Escenarios simples → enrutamiento semántico

Si el dominio es claro (p. ej. solo documentación Python) y los tipos de consulta son fijos (preguntas, ejemplos de código, troubleshooting), Semantic Router directamente. Rápido y barato.

2. Razonamiento complejo → enrutamiento lógico

Consultas multi-salto o comparativas entre dominios se benefician del LLM. Ejemplo: «¿Qué diferencias hay entre los modelos de concurrencia de Python y Go?» — el LLM puede decidir buscar en ambas bases.

3. Recuperación multi-fuente → Ensemble

Si la intención es incierta, busca en varias bases y fusiona con RRF. Más robusto que un solo enrutador; limita el número de recuperadores — 3-4 como máximo, o la latencia explota.

4. Metadatos ricos → SelfQuery

Con metadatos bien definidos (categoría, idioma, fecha, autor), SelfQueryRetriever convierte consultas en filtros precisos y reduce ruido.

5. Estrategia dinámica

En producción usamos un enfoque híbrido: enrutamiento semántico primero (~100 ms); si la confianza cae por debajo del umbral (p. ej. 0,6), fallback al enrutamiento lógico. La mayoría de consultas simples responden rápido; las complejas se tratan bien.

Resumen

Al construir un sistema RAG, muchos invierten en afinar embeddings y prompts y olvidan el enrutamiento de consultas.

Una decisión simple — ¿vía rápida o profunda? — puede bajar el tiempo de respuesta de 1,8 s a 0,5 s y subir la precisión del 72 % al 92 %.

Al elegir estrategia, recuerda:

  • Consultas simples → enrutamiento semántico: velocidad y bajo costo
  • Razonamiento complejo → enrutamiento lógico: mejor comprensión semántica del LLM
  • Multi-fuente → Ensemble: RRF garantiza respuestas completas
  • Metadatos ricos → SelfQuery: filtrado preciso, menos ruido

Nuestro equipo en producción usa el combo híbrido: semántico en la primera capa, fallback lógico con baja confianza y Ensemble automático en escenarios multi-fuente. La tasa de resolución del chatbot de soporte pasó del 68 % al 89 %.

El código de ejemplo completo está en el repositorio de GitHub; puedes clonarlo y probarlo. Cualquier duda, coméntala abajo.

Implementar un sistema de enrutamiento de consultas RAG

Construir una arquitectura RAG inteligente con enrutamiento semántico, lógico y recuperación multi-fuente

⏱️ Estimated time: 45 min

  1. 1

    Step 1: Elegir estrategia de enrutamiento

    Según el escenario de negocio, elige el enfoque:

    • Tipos de consulta fijos → enrutamiento semántico (rápido, ~100 ms)
    • Varios dominios de negocio → enrutamiento lógico (alta precisión, ~800 ms)
    • Escenario mixto → Ensemble RRF (precisión máxima 92 %)
  2. 2

    Step 2: Implementar enrutamiento semántico

    Montaje rápido con la librería semantic-router:

    ```python
    from semantic_router import Route, RouteLayer
    from semantic_router.encoders import OpenAIEncoder

    python_route = Route(
    name="python_docs",
    utterances=["Programación asíncrona en Python", "Decoradores en Python"]
    )
    route_layer = RouteLayer(
    encoder=OpenAIEncoder(),
    routes=[python_route]
    )
    ```
  3. 3

    Step 3: Configurar EnsembleRetriever

    Fusionar resultados de varios recuperadores:

    ```python
    from langchain.retrievers import EnsembleRetriever

    ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6],
    c=60
    )
    ```

    c=60 es un valor empírico; ajusta los pesos según el tipo de consulta.
  4. 4

    Step 4: Evaluar calidad de recuperación

    Métricas con TruLens:

    • Relevancia de la respuesta (Answer Relevance)
    • Relevancia del contexto (Context Relevance)
    • Comparar métricas antes y después de optimizar

    En pruebas, la relevancia subió de 0,72 a 0,91 con Ensemble.

FAQ

¿Enrutamiento semántico o lógico: cuál es mejor?
Depende del escenario. El semántico responde rápido (~100 ms) y encaja cuando los tipos de consulta son estables; el lógico entiende mejor intenciones complejas o multi-dominio. En producción conviene combinarlos: semántico para clasificar rápido y fallback lógico con baja confianza.
¿Cómo configurar el parámetro RRF c de EnsembleRetriever?
c=60 es el valor clásico empírico. Cuanto mayor es c, más uniforme el ranking; cuanto menor, más peso tienen los documentos mejor posicionados. Empieza con 60 y ajusta según resultados; en proyectos reales, valores entre 40 y 80 suelen funcionar bien.
¿Cómo manejar fallos de enrutamiento?
Tres enfoques:

• Ruta por defecto: si la confianza semántica está por debajo del umbral, usar un recuperador predeterminado
• Fallback lógico: tras fallo semántico, llamar al LLM para decidir con precisión
• Recuperación multi-fuente: ante duda, Ensemble en varias bases y fusión con RRF
¿Qué requisitos de metadatos tiene SelfQueryRetriever?
Los documentos deben tener metadatos bien definidos: categoría (category), idioma (language), fecha (date), etc. Cuanto más ricos los metadatos, más preciso el filtrado. Si faltan o son irregulares, SelfQuery pierde efectividad.
¿La consulta paralela a varios recuperadores no es demasiado lenta?
En paralelo es más rápido que en serie, pero consume más recursos. Limita a 3-4 recuperadores como máximo. En pruebas, 3 en paralelo tardan ~1,0 s (~20 % más que uno solo) con una mejora de precisión superior al 20 %.
¿Cuánto impacta la estrategia de enrutamiento en el rendimiento del RAG?
El impacto es notable. Datos de prueba: sin enrutamiento, 72 % de precisión y 1,2 s; semántico, 88 % y 0,5 s; Ensemble RRF, 92 % y 1,0 s. La estrategia adecuada puede reducir el tiempo de respuesta más del 50 % y subir la precisión un ~20 %.

12 min de lectura · Publicado el: 13 may 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog