Demo técnica — Este post documenta la sesión de demostración del 28 de febrero de 2026. El objetivo no es presentar un benchmark ni un contrato de API, sino enseñar cómo estructurar una capa de conocimiento compartida sobre Azure AI Search para que varios agentes puedan recuperar contexto de forma consistente, observable y gobernable.
Antes de empezar: qué es y qué no es esta demo
En esta serie hemos usado el nombre Foundry IQ para referirnos a una capa de conocimiento construida alrededor de Azure AI Search, Azure AI Foundry y modelos de lenguaje. En la práctica, el patrón combina:
- índices de Azure AI Search preparados para búsqueda léxica, semántica y/o vectorial;
- un componente de recuperación agéntica o planificada;
- agentes de aplicación que reutilizan la misma base documental;
- trazabilidad de consultas, fuentes y decisiones de recuperación.
Conviene separar dos cosas:
- Capacidades de plataforma: Azure AI Search, Azure AI Foundry, modelos desplegados y mecanismos de recuperación soportados por la versión de API vigente.
- Código de aplicación: perfiles de consulta, logs normalizados, reglas de negocio, selección de fuentes y políticas internas.
En este artículo, los ejemplos de código representan principalmente la capa de aplicación. Evitamos fijar nombres concretos de clases de SDK, endpoints o versiones de API preview porque esas superficies pueden cambiar y deben verificarse siempre contra la documentación oficial vigente en el momento de implementación.
¿Qué vamos a demostrar?
La demo se organiza en cinco bloques:
- Infraestructura: qué componentes intervienen y cómo se separan responsabilidades.
- Primera consulta: una pregunta sencilla y cómo leer la traza de recuperación.
- Consulta compleja: descomposición de la pregunta, varias pasadas de recuperación y evaluación de cobertura.
- Integración multi-agente: cómo varios agentes consumen la misma capa de conocimiento.
- Comparativa operativa: perfiles rápido, equilibrado y profundo, con sus implicaciones de latencia, coste y calidad.
1. Infraestructura: qué tenemos montado
El entorno de demo parte de una idea sencilla: los agentes no deberían mantener cada uno su propio índice, sus propios documentos y sus propias reglas de recuperación. En su lugar, comparten una capa común de conocimiento.
┌─────────────────────────────────────────────────────────────────────┐
│ Azure AI Foundry │
│ │
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
│ │ Modelos LLM │ │ Capa de recuperación │ │
│ │ │ │ │ │
│ │ Planificación │───────▶│ Azure AI Search │ │
│ │ Síntesis │ │ Índices: │ │
│ │ Evaluación │ │ • idx-posts │ │
│ └─────────────────┘ │ • idx-news │ │
│ │ │ │
│ │ Recuperación híbrida, semántica │ │
│ │ y/o vectorial según el índice │ │
│ └──────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────────────────────────┼─────────────────┐ │
│ │ │ │ │
│ ┌─────┴─────┐ ┌──────────────┐ ┌──────┴──────┐ ┌───────┴──┐ │
│ │Discoverer │ │ Analyzer │ │ Writer │ │ Improver │ │
│ │Agent │ │ Agent │ │ Agent │ │ Agent │ │
│ └───────────┘ └──────────────┘ └─────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Responsabilidades por capa
| Capa | Responsabilidad |
|---|---|
| Índices de Azure AI Search | Almacenar documentos, fragmentos, metadatos, campos filtrables y señales de ranking. |
| Recuperación | Buscar documentos relevantes mediante búsqueda textual, semántica, vectorial o híbrida, según el diseño del índice. |
| Planificación | Reformular o descomponer preguntas cuando la consulta lo requiere. |
| Síntesis | Construir una respuesta usando únicamente el contexto recuperado. |
| Observabilidad | Registrar qué se preguntó, qué fuentes se consultaron, cuántos resultados se usaron y qué señales de calidad se obtuvieron. |
| Agentes de aplicación | Usar la capa de conocimiento para tareas concretas: deduplicar, analizar, redactar o revisar contenido. |
Supuestos del entorno
Para que el patrón funcione de forma razonable, los índices deben estar diseñados antes de llegar al agente:
- documentos fragmentados en unidades recuperables;
- identificadores estables de documento y fragmento;
- campos de metadatos para fecha, categoría, idioma, autor, origen y permisos;
- configuración de búsqueda adecuada al caso de uso;
- estrategia clara de actualización e invalidación de contenido;
- trazabilidad entre respuesta generada y documentos usados.
La recuperación agéntica no compensa un índice mal modelado. Si los chunks son demasiado grandes, los metadatos son pobres o no hay filtros de seguridad, la calidad del sistema se degrada aunque el modelo sea muy bueno.
2. Primera consulta: pregunta sencilla y traza de recuperación
Empezamos con una consulta simple:
¿Qué es Foundry IQ y cuáles son sus principales ventajas frente a un RAG clásico?
En la aplicación de demo no exponemos directamente la llamada de bajo nivel. Usamos un cliente interno que encapsula el proveedor de recuperación. Esto permite cambiar de versión de API, SDK o estrategia sin modificar todos los agentes.
# knowledge_client.py
from dataclasses import dataclass
from typing import Literal, Protocol
RetrievalDepth = Literal["rapida", "equilibrada", "profunda"]
@dataclass(frozen=True)
class RetrievalProfile:
name: str
depth: RetrievalDepth
max_documents: int
allow_query_rewrite: bool
allow_second_pass: bool
timeout_seconds: int
BALANCED_PROFILE = RetrievalProfile(
name="balanced",
depth="equilibrada",
max_documents=8,
allow_query_rewrite=True,
allow_second_pass=False,
timeout_seconds=10,
)
class RetrievalProvider(Protocol):
def retrieve(self, *, messages: list[dict], profile: RetrievalProfile) -> dict:
...
class KnowledgeClient:
"""
Cliente de aplicación.
No acopla los agentes al detalle de una versión concreta de SDK o API.
El proveedor real puede usar Azure AI Search, una API preview vigente,
un wrapper REST interno o una combinación de búsqueda + síntesis.
"""
def __init__(self, provider: RetrievalProvider):
self._provider = provider
def ask(self, question: str, profile: RetrievalProfile = BALANCED_PROFILE) -> dict:
messages = [
{
"role": "system",
"content": (
"Responde en español, con precisión técnica y usando solo "
"el contexto recuperado. Si falta evidencia, indícalo."
),
},
{
"role": "user",
"content": question,
},
]
return self._provider.retrieve(messages=messages, profile=profile)
La llamada desde un agente queda así:
result = knowledge_client.ask(
"¿Qué es Foundry IQ y cuáles son sus principales ventajas frente a un RAG clásico?"
)
print(result["answer"])
for source in result["sources"]:
print(f"- {source['id']} ({source['index']}) score={source.get('score')}")
Respuesta observada en la demo
Foundry IQ, en el contexto de esta arquitectura, es una capa de conocimiento compartida
para agentes. Su objetivo es centralizar la recuperación de información sobre una base
documental común, en lugar de que cada agente mantenga su propio índice o sus propias
copias de contexto.
Frente a un RAG clásico punto a punto, aporta tres ventajas principales:
1. Reutilización: varios agentes consultan los mismos índices y aplican criterios
coherentes de recuperación.
2. Gobernanza: los filtros, metadatos, umbrales y reglas de acceso pueden gestionarse
en una capa común.
3. Observabilidad: la aplicación puede registrar consultas, documentos recuperados,
cobertura, latencia y señales de calidad para mejorar el sistema de forma iterativa.
La diferencia importante no es solo tecnológica, sino arquitectónica: la recuperación
pasa de ser una función embebida en cada agente a convertirse en una capacidad compartida
de la plataforma.
Traza normalizada
La siguiente traza no pretende representar un esquema oficial de Azure AI Search. Es el formato normalizado que usamos en la aplicación para observar qué ha ocurrido durante la recuperación.
{
"query": "¿Qué es Foundry IQ y cuáles son sus principales ventajas frente a un RAG clásico?",
"profile": "balanced",
"query_rewrite_enabled": true,
"rewritten_queries": [
"Foundry IQ capa de conocimiento compartida",
"ventajas de una capa RAG centralizada frente a RAG clásico",
"arquitectura multi-agente con Azure AI Search"
],
"indexes_queried": [
"idx-posts",
"idx-news"
],
"documents_returned": 6,
"documents_used_for_answer": 4,
"second_pass_executed": false,
"stop_reason": "sufficient_context",
"latency_ms": 1840
}
Qué podemos leer en la traza:
- La pregunta se reformuló en varias consultas más específicas.
- Se consultaron dos índices internos.
- No hizo falta una segunda pasada de recuperación.
- El sistema encontró contexto suficiente para responder.
- La latencia observada fue razonable para una consulta interactiva.
3. Consulta compleja: planificación y recuperación iterativa
Ahora lanzamos una pregunta más exigente:
Analiza la evolución del stack de RAG en Azurebrains desde noviembre de 2025 hasta ahora. ¿Qué cambios arquitectónicos se han producido? ¿Cómo ha evolucionado el uso de la capa de conocimiento? ¿Qué decisiones siguen abiertas?
Esta pregunta no es una búsqueda puntual. Mezcla cronología, arquitectura, comparación y análisis. Para estos casos usamos un perfil más profundo.
DEEP_PROFILE = RetrievalProfile(
name="deep",
depth="profunda",
max_documents=16,
allow_query_rewrite=True,
allow_second_pass=True,
timeout_seconds=30,
)
result = knowledge_client.ask(
question=(
"Analiza la evolución del stack de RAG en Azurebrains desde noviembre de 2025 "
"hasta ahora. ¿Qué cambios arquitectónicos se han producido? ¿Cómo ha "
"evolucionado el uso de la capa de conocimiento? ¿Qué decisiones siguen abiertas?"
),
profile=DEEP_PROFILE,
)
Traza observada
{
"query": "Analiza la evolución del stack de RAG en Azurebrains desde noviembre de 2025...",
"profile": "deep",
"query_rewrite_enabled": true,
"rewritten_queries": [
"RAG Azurebrains noviembre 2025 arquitectura inicial",
"Azurebrains capa de conocimiento agentes recuperación",
"evolución arquitectura RAG multi-agente",
"decisiones pendientes gobernanza observabilidad RAG",
"Azure AI Search búsqueda híbrida semantic reranking agentes"
],
"indexes_queried": [
"idx-posts",
"idx-news"
],
"first_pass": {
"documents_returned": 12,
"coverage": "partial",
"missing_aspects": [
"decisiones abiertas",
"comparación entre arquitectura inicial y arquitectura actual"
]
},
"second_pass": {
"executed": true,
"reason": "partial_coverage",
"additional_queries": [
"decisiones pendientes arquitectura RAG Azurebrains",
"observabilidad evaluación calidad recuperación agentes"
],
"documents_returned": 5
},
"documents_used_for_answer": 9,
"stop_reason": "sufficient_context",
"latency_ms": 11240
}
Qué aporta la segunda pasada
En consultas complejas, una única búsqueda puede recuperar documentos relevantes pero incompletos. La segunda pasada permite:
- buscar explícitamente aspectos que no aparecieron en la primera recuperación;
- mejorar cobertura cronológica;
- distinguir entre documentos centrales y documentos de apoyo;
- reducir respuestas demasiado genéricas;
- detectar que falta evidencia antes de sintetizar.
Esto no significa que todas las consultas deban usar recuperación profunda. En una aplicación real, el perfil debe elegirse según el tipo de operación, el SLA de latencia y el coste aceptable.
4. Integración multi-agente: el patrón del blog
El patrón más importante de la demo es que varios agentes consumen la misma capa de conocimiento.
# agent_profiles.py
from dataclasses import dataclass
from typing import Literal
AgentName = Literal["discoverer", "analyzer", "writer", "improver"]
@dataclass(frozen=True)
class AgentProfile:
name: AgentName
default_retrieval_profile: RetrievalProfile
system_prompt: str
AGENT_PROFILES: dict[AgentName, AgentProfile] = {
"discoverer": AgentProfile(
name="discoverer",
default_retrieval_profile=RetrievalProfile(
name="discovery-fast",
depth="rapida",
max_documents=5,
allow_query_rewrite=True,
allow_second_pass=False,
timeout_seconds=8,
),
system_prompt=(
"Eres un agente de descubrimiento. Evalúa si una noticia o tema "
"ya está cubierto en la base de conocimiento."
),
),
"analyzer": AgentProfile(
name="analyzer",
default_retrieval_profile=DEEP_PROFILE,
system_prompt=(
"Eres un analista técnico. Evalúa novedad, impacto, profundidad "
"y relación con contenido existente."
),
),
"writer": AgentProfile(
name="writer",
default_retrieval_profile=BALANCED_PROFILE,
system_prompt=(
"Eres un redactor técnico. Usa las fuentes recuperadas como grounding "
"y evita afirmaciones no respaldadas."
),
),
"improver": AgentProfile(
name="improver",
default_retrieval_profile=BALANCED_PROFILE,
system_prompt=(
"Eres un revisor técnico. Detecta afirmaciones ambiguas, obsoletas "
"o sin evidencia suficiente."
),
),
}
Y un cliente común para todos:
class AgentKnowledgeService:
def __init__(self, knowledge_client: KnowledgeClient):
self._knowledge_client = knowledge_client
def query(
self,
*,
agent: AgentName,
user_message: str,
conversation_history: list[dict] | None = None,
override_profile: RetrievalProfile | None = None,
) -> dict:
agent_profile = AGENT_PROFILES[agent]
retrieval_profile = override_profile or agent_profile.default_retrieval_profile
messages = [
{
"role": "system",
"content": agent_profile.system_prompt,
}
]
if conversation_history:
messages.extend(conversation_history)
messages.append(
{
"role": "user",
"content": user_message,
}
)
return self._knowledge_client._provider.retrieve(
messages=messages,
profile=retrieval_profile,
)
Ejemplo de uso desde el agente de análisis:
result = agent_knowledge_service.query(
agent="analyzer",
user_message=(
"Evalúa si este tema aporta novedad suficiente para un artículo técnico: "
"'uso de recuperación agéntica sobre Azure AI Search para agentes editoriales'"
),
)
print(result["answer"])
Ventajas del patrón
Este diseño tiene varias ventajas prácticas:
- Menos duplicación: los agentes no implementan su propia recuperación desde cero.
- Mejor control de calidad: todos pasan por la misma política de fuentes y trazabilidad.
- Cambio más seguro: si cambia la API subyacente, se modifica el proveedor, no todos los agentes.
- Auditoría más sencilla: las consultas quedan registradas con un formato común.
- Evolución incremental: se pueden ajustar perfiles sin reescribir la lógica de negocio.
5. Comparativa operativa: rápido, equilibrado y profundo
En lugar de hablar de niveles como si fueran propiedades universales del servicio, en la aplicación usamos tres perfiles operativos.
| Perfil | Objetivo | Reformulación | Segunda pasada | Documentos máximos | Latencia observada | Uso recomendado |
|---|---|---|---|---|---|---|
rapida |
Responder o decidir rápido | Opcional | No | 3–5 | < 1 s a pocos segundos | deduplicación, checks simples, tool calls |
equilibrada |
Buen balance calidad/latencia | Sí | No normalmente | 6–10 | pocos segundos | grounding general, redacción asistida, preguntas normales |
profunda |
Maximizar cobertura | Sí | Sí | 12–20 | varios segundos o más | análisis, investigación, comparación, revisión compleja |
Ejemplo de ejecución comparativa:
import time
PROFILES = [
RetrievalProfile(
name="fast",
depth="rapida",
max_documents=5,
allow_query_rewrite=True,
allow_second_pass=False,
timeout_seconds=8,
),
BALANCED_PROFILE,
DEEP_PROFILE,
]
query = "Explica cómo mejora la planificación de consultas la calidad de recuperación en un sistema RAG."
for profile in PROFILES:
start = time.perf_counter()
result = knowledge_client.ask(query, profile=profile)
elapsed_ms = int((time.perf_counter() - start) * 1000)
print(f"\nPerfil: {profile.name}")
print(f"Latencia cliente: {elapsed_ms} ms")
print(f"Documentos usados: {result['trace']['documents_used_for_answer']}")
print(f"Motivo de parada: {result['trace']['stop_reason']}")
print(result["answer"][:500])
Resultado resumido
Perfil: fast
Latencia cliente: 740 ms
Documentos usados: 3
Motivo de parada: sufficient_context
Respuesta breve, útil para una explicación rápida, pero con menos matices.
Perfil: balanced
Latencia cliente: 1910 ms
Documentos usados: 5
Motivo de parada: sufficient_context
Respuesta más estructurada, con mejor separación entre planificación, recuperación
y síntesis.
Perfil: deep
Latencia cliente: 10480 ms
Documentos usados: 9
Motivo de parada: sufficient_context
Respuesta más completa, con ejemplos, límites del patrón y consideraciones de
producción.
La conclusión práctica es clara: no existe un perfil óptimo para todo. La recuperación profunda mejora cobertura, pero no debería usarse para cada interacción si el usuario espera una respuesta inmediata.
Patrones de producción aprendidos
Patrón 1: elegir profundidad por operación, no por agente
Un error habitual es asignar un perfil fijo a cada agente. En producción suele funcionar mejor decidir por tipo de operación.
def profile_for_operation(operation: str) -> RetrievalProfile:
if operation in {
"deduplication_check",
"quick_grounding",
"simple_fact_lookup",
}:
return RetrievalProfile(
name="operation-fast",
depth="rapida",
max_documents=5,
allow_query_rewrite=True,
allow_second_pass=False,
timeout_seconds=8,
)
if operation in {
"article_grounding",
"technical_review",
"source_validation",
}:
return BALANCED_PROFILE
if operation in {
"novelty_analysis",
"comparative_research",
"architecture_review",
}:
return DEEP_PROFILE
return BALANCED_PROFILE
El mismo agente puede necesitar una recuperación rápida para una comprobación puntual y una recuperación profunda para un análisis de novedad.
Patrón 2: tratar la traza como señal de calidad
La observabilidad no debería limitarse a latencia y errores HTTP. Para sistemas RAG o agentic retrieval conviene registrar señales de calidad.
def summarize_retrieval_quality(result: dict, query_id: str) -> dict:
trace = result["trace"]
sources = result.get("sources", [])
scores = [
source["score"]
for source in sources
if isinstance(source.get("score"), int | float)
]
avg_score = sum(scores) / len(scores) if scores else None
return {
"query_id": query_id,
"profile": trace.get("profile"),
"documents_returned": trace.get("documents_returned"),
"documents_used_for_answer": trace.get("documents_used_for_answer"),
"second_pass_executed": trace.get("second_pass", {}).get("executed", False),
"stop_reason": trace.get("stop_reason"),
"latency_ms": trace.get("latency_ms"),
"avg_score": avg_score,
}
Señales útiles para alertas o revisión:
- demasiadas consultas terminan por timeout;
- muchas respuestas usan pocos documentos;
- baja puntuación media de recuperación;
- ausencia de fuentes en respuestas que deberían estar fundamentadas;
- incremento brusco de latencia;
- aumento de consultas sin cobertura suficiente.
Patrón 3: historial conversacional con límites
Para agentes como writer, pasar historial puede mejorar la coherencia. Pero no conviene enviar todo sin control.
def compact_history(history: list[dict], max_turns: int = 6) -> list[dict]:
"""
Mantiene los últimos turnos relevantes.
En producción puede combinarse con resumen conversacional y filtros de seguridad.
"""
return history[-max_turns:]
conversation: list[dict] = []
first = agent_knowledge_service.query(
agent="writer",
user_message="Explica qué es una capa de conocimiento compartida para agentes.",
)
conversation.append(
{
"role": "user",
"content": "Explica qué es una capa de conocimiento compartida para agentes.",
}
)
conversation.append(
{
"role": "assistant",
"content": first["answer"],
}
)
second = agent_knowledge_service.query(
agent="writer",
user_message="Compárala ahora con un RAG clásico embebido en cada aplicación.",
conversation_history=compact_history(conversation),
)
Recomendaciones:
- limitar longitud de historial;
- no reenviar datos sensibles innecesarios;
- resumir turnos antiguos;
- conservar referencias de fuentes usadas;
- separar memoria conversacional de base documental.
Patrón 4: filtros y permisos desde el diseño del índice
Si la base de conocimiento es multi-equipo o multi-cliente, la seguridad no puede depender solo del prompt.
Buenas prácticas:
- incluir campos filtrables de tenant, equipo, clasificación y visibilidad;
- aplicar filtros antes de la síntesis;
- no mostrar citas de documentos que el usuario no pueda ver;
- registrar el contexto de identidad usado en cada consulta;
- probar casos negativos: usuarios sin permiso, documentos mixtos y cambios de rol.
Patrón 5: degradación controlada
En sistemas con LLMs y recuperación, los fallos parciales son normales: timeouts, falta de cobertura, índices en actualización o límites de cuota.
La aplicación debería poder responder de forma controlada:
def safe_answer(result: dict) -> str:
trace = result.get("trace", {})
stop_reason = trace.get("stop_reason")
if stop_reason == "insufficient_context":
return (
"No tengo contexto suficiente en la base de conocimiento para responder "
"con precisión. Puedo darte una respuesta general, pero debería revisarse "
"contra fuentes adicionales."
)
if stop_reason == "timeout":
return (
"La búsqueda no ha terminado dentro del tiempo esperado. Prueba con una "
"pregunta más acotada o ejecuta un análisis profundo en segundo plano."
)
return result["answer"]
Checklist para llevar este patrón a producción
Antes de usar una arquitectura de este tipo en un flujo crítico, revisaría al menos estos puntos:
- Los índices tienen metadatos suficientes para filtrar, auditar y ordenar.
- Los documentos están fragmentados con una estrategia coherente.
- La aplicación registra fuentes usadas por cada respuesta.
- Existen perfiles de recuperación separados por operación.
- Los timeouts están definidos y probados.
- Hay pruebas con preguntas sin respuesta.
- Hay pruebas con documentos contradictorios.
- La seguridad se aplica antes de generar la respuesta.
- Las respuestas indican incertidumbre cuando falta evidencia.
- Las APIs preview están aisladas detrás de un adaptador interno.
- Hay métricas de latencia, cobertura, errores y coste.
- Existe un proceso de revisión para contenido generado automáticamente.
Próximos pasos
Las siguientes mejoras naturales de esta arquitectura son:
- calibración de ranking y umbrales: medir qué documentos se usan realmente y ajustar la recuperación con datos;
- evaluación automática: crear conjuntos de preguntas esperadas y comparar cobertura, citas y estabilidad;
- segmentación multi-tenant: aplicar filtros de seguridad y visibilidad desde el índice;
- observabilidad avanzada: correlacionar consulta, documentos recuperados, respuesta final y feedback humano;
- degradación por coste y latencia: decidir cuándo responder rápido, cuándo profundizar y cuándo ejecutar en background.
El código de integración debe mantenerse deliberadamente desacoplado de la API concreta. En tecnologías preview, esa separación es lo que permite evolucionar sin reescribir todos los agentes.
Este artículo es parte de la serie Azurebrains RAG Series: