
Una de las técnicas más interesantes cuando se empiezan a desarrollar agentes de inteligencia artificial es RAG (Retrieval-Augmented Generation).
La idea es sencilla: en lugar de enviar un documento completo a un modelo de inteligencia artificial cada vez que queremos hacer una pregunta, podemos dividir el documento en fragmentos, convertir esos fragmentos en vectores y buscar únicamente la información relacionada con la pregunta.
De esta forma, el modelo recibe solamente la información que necesita para generar la respuesta.
En este ejercicio se construirá un pequeño sistema RAG utilizando:
- Python.
- Gemini.
- Embeddings.
- Chunking.
- Similitud coseno.
- Búsqueda semántica.
No se utilizará ninguna base de datos vectorial externa. Todo se realizará con Python para poder observar cómo funciona internamente un sistema RAG.
¿Qué vamos a construir?
El programa tendrá este funcionamiento:
DOCUMENTO
│
▼
CHUNKING
│
▼
┌─────────────────┐
│ CHUNK 0 │
│ CHUNK 1 │
│ CHUNK 2 │
│ CHUNK 3 │
└─────────────────┘
│
▼
EMBEDDINGS
│
▼
ÍNDICE VECTORIAL
│
│
Pregunta usuario
│
▼
EMBEDDING PREGUNTA
│
▼
SIMILITUD COSENO
│
▼
CHUNKS RELEVANTES
│
▼
GEMINI
│
▼
RESPUESTA
La parte importante es que Gemini no recibe necesariamente todo el documento.
Primero Python busca qué partes del documento están relacionadas con la pregunta.
Después se envían a Gemini solamente esos fragmentos.
1. ¿Qué es RAG?
RAG significa:
Retrieval-Augmented Generation
Puede traducirse como:
Generación aumentada mediante recuperación de información.
Un sistema RAG combina dos procesos.
Recuperación
Primero se busca información relevante en una colección de documentos.
Generación
Después se entrega esa información a un modelo de lenguaje para que genere una respuesta.
Por tanto:
Pregunta
↓
Buscar información relevante
↓
Recuperar fragmentos
↓
Enviar fragmentos a Gemini
↓
Generar respuesta
Esto permite trabajar con documentación propia sin tener que introducir continuamente documentos completos en el prompt.
2. ¿Qué problema queremos solucionar?
Imaginemos que tenemos un documento de política de seguridad de una organización.
El documento contiene información sobre:
- control de acceso;
- autenticación;
- registros;
- copias de seguridad;
- incidentes de seguridad.
El usuario podría preguntar:
¿Qué medidas se recomiendan para controlar los accesos?
No necesitamos enviar todo el documento a Gemini.
Podemos localizar primero los fragmentos relacionados con el control de acceso y enviar solamente esos fragmentos.
3. Preparar Gemini
Es necesario disponer de una API Key de Gemini.
En macOS podemos configurar la variable de entorno:
export GEMINI_API_KEY="TU_API_KEY"
Después podemos comprobar que está disponible:
echo $GEMINI_API_KEY
Si devuelve la clave, Python podrá utilizarla.
4. El código completo
Vamos a crear un archivo llamado:
rag.py
El siguiente programa contiene todo el ejemplo.
No es necesario instalar una base de datos vectorial ni crear varios archivos.
from google import genai
import os
import math
import time
# ============================================================
# CONFIGURACIÓN
# ============================================================
API_KEY = os.environ.get("GEMINI_API_KEY")
if not API_KEY:
print("❌ No se ha encontrado GEMINI_API_KEY.")
print()
print("En macOS ejecuta:")
print('export GEMINI_API_KEY="TU_API_KEY"')
print()
raise SystemExit(1)
client = genai.Client(
api_key=API_KEY
)
# Modelos
MODELO_EMBEDDING = "gemini-embedding-001"
MODELO_GENERACION = "gemini-3.5-flash-lite"
# Número máximo de intentos cuando existe
# un error temporal del servicio.
MAX_REINTENTOS = 4
# ============================================================
# FUNCIÓN PARA LLAMAR A GEMINI CON REINTENTOS
# ============================================================
def ejecutar_con_reintentos(funcion, descripcion="operación"):
for intento in range(1, MAX_REINTENTOS + 1):
try:
return funcion()
except Exception as error:
print()
print(
f"⚠️ Error en {descripcion} "
f"(intento {intento}/{MAX_REINTENTOS})"
)
print(
f" {error}"
)
if intento < MAX_REINTENTOS:
espera = intento * 3
print(
f"⏳ Esperando {espera} segundos "
"antes de volver a intentarlo..."
)
time.sleep(espera)
else:
print()
print(
f"❌ No se ha podido completar "
f"{descripcion}."
)
return None
# ============================================================
# 1. CREAR DOCUMENTO DE EJEMPLO
# ============================================================
documento = """
POLÍTICA DE SEGURIDAD
Control de acceso
El control de acceso debe garantizar que solamente
los usuarios autorizados puedan acceder a los
recursos y sistemas de información.
Los permisos deben asignarse siguiendo el principio
de mínimo privilegio. Cada usuario debe disponer
únicamente de los permisos necesarios para realizar
sus funciones.
Autenticación
Los sistemas deben utilizar mecanismos adecuados
para verificar la identidad de los usuarios.
Las contraseñas deben almacenarse utilizando
mecanismos criptográficos adecuados y nunca deben
almacenarse en texto plano.
Los sistemas pueden utilizar autenticación
multifactor para incrementar la seguridad.
Registro de actividad
Los sistemas de información deben generar registros
de las actividades relevantes para la seguridad.
Los registros pueden utilizarse para detectar
accesos no autorizados, errores de autenticación
y otros incidentes.
Los registros deben protegerse frente a modificaciones
no autorizadas.
Copias de seguridad
La información crítica debe disponer de mecanismos
de copia de seguridad.
Las copias deben comprobarse periódicamente para
verificar que pueden utilizarse correctamente
cuando sea necesario.
Incidentes de seguridad
Los incidentes de seguridad deben registrarse,
analizarse y documentarse.
Cuando se detecte una actividad sospechosa se debe
realizar una investigación para determinar su origen,
alcance y posibles consecuencias.
"""
with open(
"seguridad.txt",
"w",
encoding="utf-8"
) as archivo:
archivo.write(documento)
print()
print("=" * 70)
print("🧠 SISTEMA RAG CON PYTHON + GEMINI")
print("=" * 70)
print()
print("📄 Documento creado: seguridad.txt")
# ============================================================
# 2. CHUNKING
# ============================================================
def dividir_en_chunks(texto, tamaño=500):
chunks = []
inicio = 0
while inicio < len(texto):
fin = inicio + tamaño
chunk = texto[inicio:fin]
chunks.append(chunk)
inicio = fin
return chunks
chunks = dividir_en_chunks(
documento,
tamaño=500
)
print()
print(
f"✂️ Documento dividido en {len(chunks)} chunks."
)
for numero, chunk in enumerate(chunks):
print()
print("-" * 70)
print(f"CHUNK {numero}")
print("-" * 70)
print(
chunk.strip()
)
# ============================================================
# 3. GENERAR EMBEDDING
# ============================================================
def obtener_embedding(texto):
def llamada():
return client.models.embed_content(
model=MODELO_EMBEDDING,
contents=texto
)
resultado = ejecutar_con_reintentos(
llamada,
"generación del embedding"
)
if resultado is None:
return None
return resultado.embeddings[0].values
# ============================================================
# 4. CREAR ÍNDICE VECTORIAL
# ============================================================
print()
print("=" * 70)
print("🔢 GENERANDO EMBEDDINGS")
print("=" * 70)
indice = []
for numero, chunk in enumerate(chunks):
print()
print(
f"🔢 Generando embedding "
f"del chunk {numero}..."
)
vector = obtener_embedding(
chunk
)
if vector is None:
print(
f"❌ No se pudo procesar "
f"el chunk {numero}."
)
print(
"El programa se detendrá para evitar "
"crear un índice incompleto."
)
raise SystemExit(1)
indice.append({
"id": numero,
"texto": chunk,
"embedding": vector
})
print()
print(
f"✅ Se han generado "
f"{len(indice)} embeddings."
)
# ============================================================
# 5. SIMILITUD COSENO
# ============================================================
def similitud_coseno(a, b):
producto = sum(
x * y
for x, y in zip(a, b)
)
norma_a = math.sqrt(
sum(
x * x
for x in a
)
)
norma_b = math.sqrt(
sum(
y * y
for y in b
)
)
if norma_a == 0 or norma_b == 0:
return 0
return producto / (
norma_a * norma_b
)
# ============================================================
# 6. BUSCAR CHUNKS RELEVANTES
# ============================================================
def buscar_chunks(
pregunta,
indice,
cantidad=3
):
print()
print(
"🔎 Generando embedding "
"de la pregunta..."
)
vector_pregunta = obtener_embedding(
pregunta
)
if vector_pregunta is None:
return None
resultados = []
for elemento in indice:
puntuacion = similitud_coseno(
vector_pregunta,
elemento["embedding"]
)
resultados.append({
"id": elemento["id"],
"texto": elemento["texto"],
"puntuacion": puntuacion
})
resultados.sort(
key=lambda x: x["puntuacion"],
reverse=True
)
return resultados[:cantidad]
# ============================================================
# 7. GENERAR RESPUESTA CON GEMINI
# ============================================================
def generar_respuesta(
pregunta,
resultados
):
contexto = "\n\n".join(
resultado["texto"]
for resultado in resultados
)
prompt = f"""
Eres un asistente especializado
en seguridad informática.
Responde a la pregunta utilizando
exclusivamente la información recuperada
del documento.
Si la información recuperada no permite
responder a la pregunta, indícalo claramente.
No inventes información.
PREGUNTA DEL USUARIO:
{pregunta}
INFORMACIÓN RECUPERADA DEL DOCUMENTO:
{contexto}
"""
print()
print(
"📤 Enviando a Gemini únicamente "
"los chunks recuperados..."
)
def llamada():
return client.models.generate_content(
model=MODELO_GENERACION,
contents=prompt
)
respuesta = ejecutar_con_reintentos(
llamada,
"generación de la respuesta"
)
if respuesta is None:
return None
return respuesta.text
# ============================================================
# 8. PREGUNTA DEL USUARIO
# ============================================================
print()
print("=" * 70)
print("❓ REALIZA UNA PREGUNTA")
print("=" * 70)
pregunta = input(
"\n🧑 Pregunta: "
)
# ============================================================
# 9. RECUPERAR INFORMACIÓN
# ============================================================
resultados = buscar_chunks(
pregunta,
indice,
cantidad=3
)
if resultados is None:
print()
print(
"❌ No ha sido posible generar "
"el embedding de la pregunta."
)
print()
raise SystemExit(1)
# ============================================================
# 10. MOSTRAR CHUNKS RECUPERADOS
# ============================================================
print()
print("=" * 70)
print("🔎 CHUNKS RECUPERADOS")
print("=" * 70)
for resultado in resultados:
print()
print("-" * 70)
print(
f"CHUNK: {resultado['id']}"
)
print(
f"SIMILITUD: "
f"{resultado['puntuacion']:.4f}"
)
print("-" * 70)
print(
resultado["texto"].strip()
)
# ============================================================
# 11. GENERAR RESPUESTA
# ============================================================
print()
print("=" * 70)
print("🤖 GEMINI ESTÁ GENERANDO LA RESPUESTA")
print("=" * 70)
respuesta = generar_respuesta(
pregunta,
resultados
)
# ============================================================
# 12. MOSTRAR RESPUESTA
# ============================================================
print()
print("=" * 70)
print("📋 RESPUESTA FINAL")
print("=" * 70)
if respuesta is not None:
print()
print(respuesta)
else:
print()
print(
"⚠️ Gemini no ha podido generar "
"la respuesta después de varios intentos."
)
print()
print(
"El proceso RAG sí se ha realizado "
"correctamente."
)
print()
print(
"Los chunks recuperados fueron:"
)
for resultado in resultados:
print()
print(
f"- Chunk {resultado['id']} "
f"(similitud "
f"{resultado['puntuacion']:.4f})"
)
print()
print("=" * 70)
print("✅ FIN DEL PROCESO RAG")
print("=" * 70)
5. Ejecutar el programa
Una vez creado rag.py, ejecutamos:
python3.12 rag.py
El programa comienza creando un documento llamado:
seguridad.txt
Después lo divide en diferentes fragmentos.
Podemos observar algo parecido a:
✂️ Documento dividido en 4 chunks.
Y el programa muestra cada uno:
----------------------------------------------------------------------
CHUNK 0
----------------------------------------------------------------------
POLÍTICA DE SEGURIDAD
Control de acceso
El control de acceso debe garantizar...
6. Generación de embeddings
A continuación aparece:
======================================================================
🔢 GENERANDO EMBEDDINGS
======================================================================
🔢 Generando embedding del chunk 0...
🔢 Generando embedding del chunk 1...
🔢 Generando embedding del chunk 2...
🔢 Generando embedding del chunk 3...
✅ Se han generado 4 embeddings.
Aquí aparece uno de los conceptos fundamentales de RAG.
Un embedding transforma un fragmento de texto en un vector numérico.
Por ejemplo, conceptualmente:
"El control de acceso..."
↓
EMBEDDING
↓
[0.12, -0.08, 0.34, 0.91, ...]
El vector real tiene muchas dimensiones.
La ventaja es que podemos comparar matemáticamente diferentes textos.
7. ¿Qué es el índice vectorial?
En el programa tenemos:
indice.append({
"id": numero,
"texto": chunk,
"embedding": vector
})
Por tanto, cada elemento del índice contiene tres cosas:
ID
│
├── Texto original
│
└── Vector del texto
Por ejemplo:
CHUNK 0
Texto:
"El control de acceso debe garantizar..."
Embedding:
[0.12, -0.08, 0.34, ...]
Este pequeño índice hace las funciones de una base de datos vectorial muy sencilla.
En sistemas RAG reales se pueden utilizar soluciones especializadas, pero para aprender es mucho más interesante observar primero cómo funciona el mecanismo internamente.
8. El usuario realiza una pregunta
El programa solicita:
======================================================================
❓ REALIZA UNA PREGUNTA
======================================================================
🧑 Pregunta:
Podemos escribir:
¿Qué medidas existen para controlar los accesos?
El programa convierte también esta pregunta en un embedding.
Tenemos entonces:
DOCUMENTO
│
├── Chunk 0 → vector
├── Chunk 1 → vector
├── Chunk 2 → vector
└── Chunk 3 → vector
PREGUNTA
│
└── vector
Ahora podemos comparar el vector de la pregunta con los vectores de los diferentes chunks.
9. Similitud coseno
Para comparar los vectores utilizamos la similitud coseno.
En el programa hemos implementado la fórmula directamente:
def similitud_coseno(a, b):
producto = sum(
x * y
for x, y in zip(a, b)
)
norma_a = math.sqrt(
sum(
x * x
for x in a
)
)
norma_b = math.sqrt(
sum(
y * y
for y in b
)
)
if norma_a == 0 or norma_b == 0:
return 0
return producto / (
norma_a * norma_b
)
No estamos preguntando a Gemini qué fragmento es relevante.
La comparación la realiza Python mediante operaciones matemáticas sobre los vectores.
Por ejemplo, podemos obtener:
CHUNK 0 → 0.8745
CHUNK 1 → 0.3121
CHUNK 2 → 0.4217
CHUNK 3 → 0.2038
Cuanto mayor sea la similitud, mayor será la relación semántica entre la pregunta y el fragmento.
10. Recuperación de los chunks
El programa ordena los resultados:
resultados.sort(
key=lambda x: x["puntuacion"],
reverse=True
)
Y recupera los tres primeros:
return resultados[:cantidad]
Por eso veremos:
======================================================================
🔎 CHUNKS RECUPERADOS
======================================================================
----------------------------------------------------------------------
CHUNK: 0
SIMILITUD: 0.8745
----------------------------------------------------------------------
El control de acceso debe garantizar...
Estos son los fragmentos que posteriormente se entregarán a Gemini.
11. La parte más importante del ejercicio
Aquí está la diferencia entre un sistema tradicional y nuestro RAG.
Sin RAG podríamos hacer:
Documento completo
↓
Gemini
↓
Respuesta
Con RAG hacemos:
Documento
↓
Chunks
↓
Embeddings
↓
Búsqueda
↓
Chunks relevantes
↓
Gemini
↓
Respuesta
Por tanto, Gemini recibe:
INFORMACIÓN RECUPERADA DEL DOCUMENTO:
[Chunk relevante 1]
[Chunk relevante 2]
[Chunk relevante 3]
y no necesariamente todo el documento.
12. ¿Por qué es importante?
Supongamos que tenemos un documento de 500 páginas.
Enviar las 500 páginas a Gemini para responder una pregunta concreta puede ser innecesario.
Podemos hacer:
500 páginas
↓
dividir
↓
muchos chunks
↓
crear embeddings
↓
buscar los más relevantes
↓
5 chunks
↓
Gemini
De esta forma, la recuperación de información se realiza antes de generar la respuesta.
Esto puede reducir el contenido enviado al modelo y facilitar el trabajo con documentación extensa.
13. ¿Dónde está realmente la inteligencia del RAG?
Es importante distinguir dos procesos.
Python
Python realiza:
- lectura del documento;
- división en chunks;
- generación de embeddings mediante la API;
- almacenamiento del índice;
- cálculo de similitud;
- ordenación de resultados;
- selección de los fragmentos relevantes.
Gemini
Gemini recibe los fragmentos recuperados y realiza la generación de lenguaje natural.
Podemos representarlo así:
PYTHON
│
┌───────────┼───────────┐
│ │ │
Chunking búsqueda similitud
│ │ │
└───────────┼───────────┘
│
▼
INFORMACIÓN
RELEVANTE
│
▼
GEMINI
│
▼
RESPUESTA
Esto ayuda a comprender que un sistema RAG no consiste simplemente en «meter documentos en una IA».
Existe un proceso de recuperación previo.
14. ¿Qué ocurre si Gemini devuelve un error 503?
Las API pueden devolver errores temporales.
Por ejemplo:
503 UNAVAILABLE
Esto puede ocurrir cuando un modelo está experimentando una elevada demanda.
Por este motivo hemos creado:
def ejecutar_con_reintentos(funcion, descripcion="operación"):
La función intenta realizar la operación varias veces.
Si falla:
⚠️ Error
↓
esperar
↓
volver a intentar
↓
volver a intentar
↓
...
De esta forma, un error temporal no provoca que todo el programa termine inmediatamente con un traceback.
Además, si finalmente no se puede generar la respuesta, el programa muestra los chunks recuperados.
Esto es importante en aplicaciones reales: un sistema que depende de una API externa debe contemplar errores de red, límites de uso y respuestas temporales del servicio.
15. Una prueba
Podemos realizar una pregunta como:
¿Qué medidas se recomiendan para controlar los accesos?
El sistema podría recuperar el fragmento relacionado con:
Control de acceso
El control de acceso debe garantizar que solamente
los usuarios autorizados puedan acceder a los
recursos y sistemas de información.
Los permisos deben asignarse siguiendo el principio
de mínimo privilegio...
Y después Gemini utilizará esa información para generar la respuesta.
Otra pregunta podría ser:
¿Cómo deben protegerse las contraseñas?
En este caso deberían recuperar mayor relevancia los fragmentos relacionados con la autenticación.
También podemos probar:
¿Qué se debe hacer con las copias de seguridad?
o:
¿Qué debe hacerse cuando se detecta una actividad sospechosa?
La recuperación cambiará dependiendo de la pregunta.
16. ¿Esto ya es un agente?
Este ejercicio es un paso importante, pero un RAG por sí mismo no tiene por qué ser un agente.
Aquí tenemos principalmente:
pregunta
↓
recuperación
↓
generación
Un agente puede incorporar además:
usuario
↓
AGENTE
│
├── consultar documentos
├── utilizar herramientas
├── analizar información
├── ejecutar funciones
├── tomar decisiones sobre el siguiente paso
└── generar respuesta
Por eso RAG es una pieza que puede formar parte de un agente más completo.
17. Lo que hemos aprendido
En este ejercicio hemos construido desde cero los principales componentes de un sistema RAG:
Documento
↓
Chunking
↓
Embeddings
↓
Índice vectorial
↓
Embedding de la pregunta
↓
Similitud coseno
↓
Recuperación de información
↓
Gemini
↓
Respuesta
Y lo hemos hecho utilizando únicamente Python y Gemini.
La parte más interesante es que podemos ver qué ocurre internamente en cada etapa, en lugar de utilizar directamente un framework que oculte todo el proceso.
18. Idea fundamental
Un RAG puede entenderse con una frase:
Primero busca la información relevante y después genera la respuesta utilizando esa información.
La arquitectura básica sería:
DOCUMENTOS
│
▼
CHUNKS
│
▼
EMBEDDINGS
│
▼
ÍNDICE VECTORIAL
│
│
PREGUNTA
│
▼
EMBEDDING PREGUNTA
│
▼
BÚSQUEDA SEMÁNTICA
│
▼
INFORMACIÓN RELEVANTE
│
▼
GEMINI
│
▼
RESPUESTA
Este mecanismo permite construir aplicaciones capaces de consultar documentación técnica, normativa, manuales, políticas de seguridad, procedimientos internos o grandes colecciones de documentos sin tener que enviar continuamente todo el contenido al modelo.