En este artículo aprenderás:
- Qué son la Generación Aumentada por Recuperación (RAG) y ChromaDB, y qué aporta cada una.
- Por qué combinar la API Web Unlocker de Bright Data con ChromaDB es una forma práctica de fundamentar un modelo de lenguaje en datos reales y actualizados.
- Cómo construir un pipeline de extremo a extremo que recopile contenido web, lo embeba localmente y responda preguntas sobre él, todo en tu propia máquina.
Antes de entrar en las herramientas y el código, conviene definir los conceptos y ver cómo encajan dentro de un flujo de trabajo RAG.
¿Qué es la Generación Aumentada por Recuperación (RAG)?
Un modelo de lenguaje grande solo conoce aquello con lo que fue entrenado. Pregúntale sobre una página publicada la semana pasada, una wiki interna o un catálogo de productos de nicho, y o bien supondrá o te dirá que no lo sabe. La Generación Aumentada por Recuperación cierra esa brecha.
La idea es sencilla. En lugar de depender de la memoria del modelo, almacenas tus propios documentos en un índice consultable. Cuando llega una pregunta, primero recuperas los fragmentos de texto más relevantes, luego aumentas el prompt pegando esos fragmentos como contexto y, finalmente, dejas que el modelo genere una respuesta fundamentada en ese material. El modelo sigue haciendo la redacción, pero los hechos provienen de datos que tú controlas.
Esto importa por dos razones. Las respuestas se mantienen actualizadas porque tú decides qué entra en el índice y cuándo actualizarlo. Y las respuestas son precisas porque el modelo razona sobre texto fuente real en lugar de completar huecos a partir de datos de entrenamiento. Para bases de conocimiento, asistentes de soporte, herramientas de investigación y cualquier cosa construida sobre información propietaria o en constante cambio, RAG se ha convertido en el enfoque predeterminado.
Con el lado de la recuperación cubierto, veamos dónde viven realmente esos documentos.
¿Qué es ChromaDB?
ChromaDB (generalmente llamado simplemente Chroma) es una base de datos vectorial de código abierto diseñada para aplicaciones de IA. Una base de datos convencional busca valores exactos; una base de datos vectorial busca significado. Lo hace almacenando embeddings, que son representaciones numéricas del texto donde las ideas similares quedan cerca en el espacio vectorial. Al consultarla, Chroma devuelve los fragmentos almacenados cuyos embeddings están más próximos a tu pregunta, incluso cuando ninguna palabra coincide exactamente.
Lo que hace a Chroma ideal para una primera implementación RAG es lo poco que exige. Se instala con un solo pip install, se ejecuta embebido dentro de tu proceso Python sin servidor separado que gestionar, y persiste todo en disco mediante PersistentClient para que tu índice sobreviva a un reinicio. Puedes conectar cualquier modelo de embedding, desde APIs de OpenAI o Google hasta un modelo local que nunca toque la red. Para un proyecto que necesite ejecutarse en un portátil y mantenerse privado, esa combinación es difícil de superar.
RAG vs Fine-Tuning: ¿Cuál es la diferencia?
Quienes se inician en este campo a menudo comparan RAG con el fine-tuning como si fueran opciones competidoras. Resuelven problemas diferentes:
- El fine-tuning modifica el modelo en sí. Lo reentrenas con ejemplos para que adapte su tono, formato o comportamiento. Es la herramienta adecuada cuando quieres que el modelo actúe de forma diferente, pero es lento, costoso y el conocimiento incorporado queda obsoleto en cuanto cambian tus datos.
- RAG deja el modelo intacto y cambia lo que ve en el momento de la consulta. Inyectas contexto relevante en el prompt en cada solicitud. Es la herramienta adecuada cuando quieres que el modelo sepa algo específico y actual, y actualizarlo es tan sencillo como añadir un documento a tu índice.
La regla general a la que llegan la mayoría de los equipos: si necesitas nuevo conocimiento, usa RAG; si necesitas nuevo comportamiento, considera el fine-tuning. Muchos sistemas en producción usan ambos. En este tutorial nos centramos exclusivamente en RAG, porque el objetivo es responder preguntas sobre contenido web que cambia con demasiada frecuencia como para reentrenar un modelo.
¿Por qué integrar Bright Data en un pipeline RAG + ChromaDB?
Un sistema RAG es tan bueno como los documentos que le alimentas. Datos de mala calidad producen respuestas de mala calidad con mucha confianza. Así que el verdadero cuello de botella en la mayoría de los pipelines no es la búsqueda vectorial ni el modelo, sino obtener material fuente limpio, fiable y actualizado desde el principio.
Eso es sencillo cuando tus datos ya están en una carpeta de PDFs. Se complica en cuanto tu conocimiento necesita provenir de la web abierta. Las páginas públicas se ocultan detrás de detección de bots, renderizan su contenido con JavaScript, sirven resultados diferentes según la región y lanzan CAPTCHAs a cualquier cosa que parezca automatizada. Mantener tus propios scrapers contra todo eso es un trabajo en sí mismo, y es un trabajo que se rompe cada vez que un sitio destino cambia su marcado.
Aquí es donde encaja la API Web Unlocker de Bright Data. Le envías una URL de destino y te devuelve el contenido de la página, gestionando las medidas anti-bot, la rotación de proxies, el renderizado JavaScript y la geolocalización en segundo plano. Incluso puedes pedirle que devuelva la página como Markdown limpio, lo que es casi ideal para RAG, ya que elimina la navegación y el contenido repetitivo y te deja texto legible para fragmentar y embeber. Sin automatización del navegador, sin grupo de proxies que supervisar.
Combinado con el almacén vectorial local de Chroma y un modelo ejecutado localmente, esto te da un pipeline que es actual donde necesita serlo y privado en todo lo demás: solo el paso de recopilación de datos accede a la web, mientras que el embedding, el almacenamiento, la recuperación y la generación permanecen en tu máquina.
Este patrón es especialmente útil para:
- Asistentes de investigación que responden preguntas sobre los últimos artículos, papers o documentación, en lugar de basarse en el snapshot de entrenamiento de un modelo.
- Herramientas de inteligencia competitiva que rastrean páginas de productos, precios o funcionalidades de sitios rivales.
- Bots de soporte interno fundamentados en documentación en vivo, para que las respuestas reflejen la documentación actual en lugar de una versión de meses atrás.
- Sistemas de monitoreo de mercado que extraen listados o noticias recientes y permiten a un analista consultarlos en lenguaje natural.
Al dejar que la infraestructura de datos web de Bright Data gestione la recopilación y que Chroma gestione la recuperación, obtienes un motor RAG listo para producción sin escribir una sola línea de código de scraping.
Cómo construir un pipeline RAG local con Bright Data y ChromaDB
En esta sección guiada, construirás un pipeline con tres etapas:
- Recopilar contenido web: Un script llama a la API Web Unlocker de Bright Data para obtener un conjunto de páginas y devolver cada una como Markdown.
- Embeber y almacenar: Un segundo script fragmenta ese contenido, genera embeddings localmente con un modelo sentence-transformer y escribe los vectores en ChromaDB.
- Recuperar y generar: Un script de consulta embebe tu pregunta, extrae los fragmentos más relevantes de Chroma y los pasa a un modelo local para producir una respuesta fundamentada con fuentes.
Nota: Este es uno de los muchos diseños posibles. Podrías reemplazar el modelo local por una llamada a API para obtener respuestas de mayor calidad, añadir un paso de re-ranking entre la recuperación y la generación, o apuntar la etapa de recopilación a cientos de URLs en lugar de tres. La estructura permanece igual.
Sigue los pasos a continuación para construir un pipeline RAG completamente local impulsado por la API Web Unlocker de Bright Data y ChromaDB.
Requisitos previos
Para seguir el tutorial, necesitas:
- Una cuenta de Bright Data con una zona Web Unlocker activa. Inicia sesión en tu panel, ve a Configuración de cuenta y copia tu Token de API (estará en formato UUID). Anota también el nombre de tu zona; necesitarás ambos.
- Python 3.10+ instalado localmente.
- Ollama instalado, con un modelo descargado para el paso de generación (este tutorial usa
llama3.1, pero cualquier modelo de chat funciona). Si prefieres usar un modelo alojado, el Paso 5 muestra dónde sustituir la llamada a API.

Paso 1: Configuración del proyecto
Crea un directorio de trabajo, configura un entorno virtual e instala las dependencias:
mkdir local-rag-pipeline && cd local-rag-pipeline
python -m venv venv
source venv/bin/activate # En Windows: venv\Scripts\activate
pip install requests chromadb sentence-transformers
La primera vez que ejecutes el pipeline, sentence-transformers descargará el modelo de embedding (varios cientos de megabytes). Después se carga desde caché y funciona sin conexión.
Descarga un modelo para el paso de generación si aún no lo has hecho:
ollama pull llama3.1
Paso 2: Crear la estructura del proyecto
Crea las carpetas en las que el pipeline escribirá:
mkdir -p data/raw data/chroma
La estructura de tu proyecto tendrá este aspecto:
local-rag-pipeline/
├── data/
│ ├── raw/ # Markdown sin procesar recopilado de la web
│ └── chroma/ # Índice ChromaDB persistido
├── collect.py # Etapa 1: obtener páginas mediante Bright Data
├── ingest.py # Etapa 2: fragmentar, embeber y almacenar
└── rag.py # Etapa 3: recuperar y generar
Paso 3: Recopilar datos web con Bright Data
Crea el archivo collect.py:
import json
import requests
from pathlib import Path
API_KEY = "your-brightdata-api-token-here"
ZONE = "web_unlocker1"
BASE_URL = "https://api.brightdata.com/request"
RAW_DATA_PATH = "data/raw/pages.json"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
TARGETS = [
"https://en.wikipedia.org/wiki/Retrieval-augmented_generation",
"https://en.wikipedia.org/wiki/Vector_database",
"https://en.wikipedia.org/wiki/Large_language_model",
]
def fetch_pages():
results = []
for url in TARGETS:
print(f"Fetching: {url}")
response = requests.post(
BASE_URL,
headers=HEADERS,
json={
"zone": ZONE,
"url": url,
"format": "raw",
"data_format": "markdown",
},
timeout=60,
)
response.raise_for_status()
results.append({"url": url, "content": response.text})
print(f" -> {len(response.text)} chars")
Path(RAW_DATA_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(RAW_DATA_PATH, "w") as f:
json.dump(results, f, indent=2)
print(f"Saved {len(results)} pages to {RAW_DATA_PATH}")
if __name__ == "__main__":
fetch_pages()
Reemplaza your-brightdata-api-token-here con tu token de API real y actualiza ZONE para que coincida con el nombre de tu zona Web Unlocker.
Esto es lo que hace cada parte:
API_KEYyZONE: Tus credenciales de Bright Data. El token de API es el token en formato UUID de la configuración de tu cuenta, no una contraseña de zona.TARGETS: Las páginas a ingestar. Los tres artículos de Wikipedia aquí proporcionan un corpus coherente sobre el que hacer preguntas. Sustituye por tus propias URLs; aquí es exactamente donde Web Unlocker demuestra su valor, ya que sitios de noticias, páginas de productos y aplicaciones con mucho JavaScript que bloquean las solicitudes ordinarias vuelven limpias a través de la API.fetch_pages: Itera por cada URL y envía una solicitud POST al endpoint de Web Unlocker. La opcióndata_format: "markdown"le indica a Bright Data que devuelva Markdown legible en lugar de HTML sin procesar, lo que te ahorra un paso de parseo. Los resultados se escriben en un único archivo JSON para que la siguiente etapa lo lea.
Nota: Algunas páginas pueden devolver un mensaje
bad_endpointsi un sitio está restringido bajo el modo de acceso inmediato de Bright Data. Este es el comportamiento esperado; Bright Data muestra el error en la respuesta en lugar de fallar silenciosamente. Contacta con tu Gerente de cuenta si necesitas acceso completo a un destino restringido.

Paso 4: Embeber el contenido en ChromaDB
Crea el archivo ingest.py:
import json
import chromadb
from chromadb.utils import embedding_functions
RAW_DATA_PATH = "data/raw/pages.json"
CHROMA_PATH = "data/chroma"
COLLECTION_NAME = "web_knowledge"
def chunk_text(text, size=800, overlap=100):
words = text.split()
chunks, i = [], 0
while i < len(words):
chunks.append(" ".join(words[i:i + size]))
i += size - overlap
return chunks
def main():
with open(RAW_DATA_PATH) as f:
pages = json.load(f)
client = chromadb.PersistentClient(path=CHROMA_PATH)
embed_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2"
)
collection = client.get_or_create_collection(
name=COLLECTION_NAME,
embedding_function=embed_fn,
)
documents, metadatas, ids = [], [], []
for page in pages:
for idx, chunk in enumerate(chunk_text(page["content"])):
documents.append(chunk)
metadatas.append({"source": page["url"], "chunk": idx})
ids.append(f"{page['url']}#{idx}")
collection.upsert(documents=documents, metadatas=metadatas, ids=ids)
print(f"Indexed {len(documents)} chunks from {len(pages)} pages")
print(f"Collection now holds {collection.count()} chunks")
if __name__ == "__main__":
main()
Esta etapa realiza el trabajo pesado de convertir texto sin procesar en algo consultable:
chunk_text: Divide cada página en ventanas superpuestas de aproximadamente 800 palabras. La fragmentación importa porque quieres recuperar pasajes concretos, no páginas enteras, y la superposición de 100 palabras evita que una oración que cruza un límite quede cortada por la mitad.SentenceTransformerEmbeddingFunction: Cargaall-MiniLM-L6-v2, un modelo de embedding pequeño y rápido que se ejecuta localmente. Chroma lo llama automáticamente cada vez que añades o consultas documentos, por lo que nunca manejas vectores directamente.get_or_create_collection: Abre la colección persistente en disco, creándola en la primera ejecución y reutilizándola después.collection.upsert: Escribe los fragmentos, sus metadatos e IDs estables en Chroma. Usarupserten lugar deaddsignifica que puedes volver a ejecutar el script tras recopilar contenido nuevo sin encontrar errores de IDs duplicados.
Paso 5: Construir el paso de recuperación y generación
Crea el archivo rag.py:
import sys
import requests
import chromadb
from chromadb.utils import embedding_functions
CHROMA_PATH = "data/chroma"
COLLECTION_NAME = "web_knowledge"
OLLAMA_URL = "http://localhost:11434/api/generate"
MODEL = "llama3.1"
PROMPT_TEMPLATE = """You are a research assistant. Answer the question using only the context below.
If the context does not contain the answer, say you don't have enough information.
Context:
{context}
Question: {question}
Answer:"""
def retrieve(question, n_results=4):
client = chromadb.PersistentClient(path=CHROMA_PATH)
embed_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2"
)
collection = client.get_collection(
name=COLLECTION_NAME, embedding_function=embed_fn
)
results = collection.query(query_texts=[question], n_results=n_results)
chunks = results["documents"][0]
sources = [m["source"] for m in results["metadatas"][0]]
return chunks, sources
def generate(question, chunks):
prompt = PROMPT_TEMPLATE.format(context="\n\n".join(chunks), question=question)
response = requests.post(
OLLAMA_URL,
json={"model": MODEL, "prompt": prompt, "stream": False},
timeout=120,
)
response.raise_for_status()
return response.json()["response"]
def main(question):
chunks, sources = retrieve(question)
answer = generate(question, chunks)
print("\n=== Answer ===")
print(answer.strip())
print("\n=== Sources ===")
for src in dict.fromkeys(sources): # eliminar duplicados, mantener orden
print(f" - {src}")
if __name__ == "__main__":
main(" ".join(sys.argv[1:]))
Este es el flujo:
retrieve: Embebe tu pregunta con el mismo modelo usado en la ingesta (esta coherencia es lo que hace funcionar la búsqueda por similitud) y le pide a Chroma los cuatro fragmentos más cercanos. Devuelve tanto el texto como la URL fuente de cada fragmento.generate: Construye un prompt que ancla el modelo al contexto recuperado y le instruye a reconocer cuando la respuesta no está disponible, que es la protección más efectiva contra las alucinaciones. Luego llama a un modelo local a través de la API REST de Ollama.main: Une ambas partes e imprime la respuesta junto con la lista de fuentes sin duplicados, de modo que cada respuesta sea rastreable hasta las páginas de las que proviene.
Si prefieres un modelo alojado, esta es la única función que cambia. Sustituye la llamada requests.post a Ollama por una llamada a la API de Anthropic u OpenAI y pasa el mismo prompt; la mitad de recuperación permanece exactamente igual.
Paso 6: Ejecutar el pipeline
Ejecuta las tres etapas en orden. Primero, recopila las páginas:
python collect.py
Luego fragmenta y embébbelas en Chroma:
python ingest.py

Ahora haz una pregunta:
python rag.py "What problem does retrieval-augmented generation solve?"
Paso 7: Inspeccionar los resultados
La consulta devuelve una respuesta fundamentada seguida de las fuentes en las que se apoyó:
=== Answer ===
Retrieval-augmented generation addresses the fact that a language model only
knows what it was trained on. By retrieving relevant passages from an external
index at query time and adding them to the prompt, the model can answer using
current, specific information it was never trained on, which also reduces
hallucination because the response is anchored to real source text.
=== Sources ===
- https://en.wikipedia.org/wiki/Retrieval-augmented_generation
- https://en.wikipedia.org/wiki/Large_language_model

Sugerencia de captura: la salida del terminal de una consulta rag.py, mostrando la respuesta generada y la lista de fuentes debajo.
Prueba algunas preguntas más para evaluar la calidad de la recuperación:
python rag.py "How does a vector database differ from a relational database?"
python rag.py "What are common limitations of large language models?"
Si una respuesta parece escasa, hay dos parámetros que vale la pena ajustar primero. Aumentar n_results en rag.py proporciona al modelo más contexto, lo que ayuda en preguntas amplias a costa de un prompt más largo. Ajustar size y overlap en ingest.py cambia cómo se fragmenta el texto; fragmentos más pequeños afinan las búsquedas precisas, fragmentos más grandes preservan más contexto circundante. Vuelve a ejecutar ingest.py tras cualquier cambio de fragmentación para que el índice lo refleje.
Todo el proceso funcionó sin una sola línea de código de scraping o de proxy. Bright Data entregó Markdown limpio desde la web, Chroma gestionó los embeddings y la búsqueda por similitud localmente, y un modelo local produjo la respuesta, todo ello fundamentado en las páginas que elegiste recopilar.
Ir más allá
Este pipeline es una base funcional que puedes expandir en varias direcciones:
- Sustituye el modelo local por una API alojada como Anthropic u OpenAI cuando necesites razonamiento más potente o contexto más largo, manteniendo intacta toda la mitad de recuperación.
- Añade un paso de descubrimiento con la API SERP de Bright Data para que el pipeline pueda encontrar páginas relevantes a ingestar a partir de una consulta de búsqueda, en lugar de trabajar con una lista fija de URLs.
- Extrae registros estructurados en lugar de texto libre usando la API Web Scraper de Bright Data, que cubre más de 120 dominios. El tutorial de RAG agéntico muestra este patrón de principio a fin.
- Usa el filtrado de metadatos de Chroma (el argumento
whereenquery) para limitar la recuperación a una fuente, fecha o categoría específica. - Añade una capa de re-ranking o búsqueda híbrida si tus documentos están llenos de nombres propios y códigos con los que la búsqueda semántica pura tiene dificultades.
- Programa actualizaciones periódicas, o expón todo a un asistente a través de un servidor MCP para que una herramienta como Claude Desktop pueda recopilar y consultar bajo demanda.
- Pasa de Chroma local a un almacén vectorial gestionado como Pinecone o pgvector cuando tu colección supere la capacidad de una sola máquina; la lógica de ingesta y recuperación se traslada con cambios mínimos.
Las posibilidades son prácticamente infinitas.
Conclusión
En este artículo, construiste un pipeline RAG local funcional combinando la API Web Unlocker de Bright Data con ChromaDB.
Chroma gestiona el embedding, el almacenamiento y la búsqueda por similitud localmente, de modo que tu índice y tus consultas nunca abandonan tu máquina. Un modelo local convierte el contexto recuperado en una respuesta fundamentada con fuentes citadas. Y Bright Data elimina la parte más difícil de todo el proceso: recopilar contenido de páginas fresco y limpio de la web abierta sin gestionar proxies, escribir scrapers ni luchar contra sistemas anti-bot.
A diferencia de un asistente sin código, este stack te da control total sobre cada capa: qué páginas recopilas, cómo las fragmentas y embébes, cuántas recuperas y qué modelo redacta la respuesta. Se integra de forma natural en cualquier plataforma de datos o IA más amplia y escala con tus necesidades.
Para construir pipelines más completos, explora la suite completa de herramientas de datos web de Bright Data, incluyendo Web Unlocker para páginas protegidas contra bots, la API SERP para datos de búsqueda y conjuntos de datos listos para usar para casos de uso comunes.
Regístrate para obtener una cuenta gratuita de Bright Data hoy mismo y comienza a fundamentar tus modelos en datos web reales.