Volver al inicio

RAG: búsqueda inteligente de documentos para desarrolladores

Generación aumentada por recuperación (RAG) combina la búsqueda de fragmentos relevantes de documentos con la generación de respuestas LLM. El artículo desglosa las etapas: análisis de formatos, segmentación semántica, búsqueda híbrida (vectores+BM25+RRF), reclasificación y optimizaciones para bases de datos grandes.

RAG en la práctica: código y algoritmos de búsqueda en bases de datos
Advertisement 728x90

Generación Aumentada por Recuperación: Búsqueda Inteligente de Documentos para Empresas

RAG combina búsqueda vectorial con IA generativa para ofrecer respuestas precisas a partir de repositorios privados de documentos. El sistema recupera contenido relevante de PDFs, archivos Excel, código y más, luego genera explicaciones basadas estrictamente en los materiales originales, reduciendo significativamente las falsedades.

El proceso comienza con el preprocesamiento: los documentos se analizan, se dividen en fragmentos inteligentes considerando su tipo de contenido y se convierten en embeddings almacenados en una base de datos vectorial.

Extracción de Texto de Formatos de Archivo

El sistema soporta múltiples formatos sin pérdida de datos. Cada formato utiliza un extractor específico.

Google AdInline article slot
SUPPORTED = {
    '.pdf':  lambda f: ''.join(p.extract_text() or '' for p in PyPDF2.PdfReader(f).pages),
    '.docx': lambda f: '\n'.join(p.text for p in Document(f).paragraphs),
    '.pptx': lambda f: '\n'.join(
        shape.text
        for slide in Presentation(f).slides
        for shape in slide.shapes
        if hasattr(shape, "text") and shape.text.strip()
    ),
    '.xlsx': extract_xlsx,
    '.csv':  extract_csv,
    # .txt, .json, .py, .js, etc.
}

La salida es texto limpio y listo para procesar.

Segmentación Inteligente por Tipo de Contenido

El texto no se divide por tamaño fijo; se segmenta semánticamente. Primero, se detecta el tipo de contenido:

def detect_content_type(text):
    if re.search(r'<table|</td>|<tr>', text, re.IGNORECASE):
        return 'table'
    tab_lines = sum(1 for line in text.splitlines() if '\t' in line)
    if tab_lines > max(3, len(text.splitlines()) * 0.4):
        return 'table'
    if re.search(r'function\s+\w+\s*\(|\bdef\s+\w+\s*\(|\bclass\s+\w+\b', text):
        return 'code'
    return 'prose'
  • Tablas: Incluyen encabezados repetidos en cada fragmento
  • Código: Se separa por funciones/clases sin romper la lógica
  • Prosa: Límites semánticos usando embeddings de frases

Para tablas:

Google AdInline article slot
def split_table_by_rows(text, chunk_size):
    lines = [l for l in text.splitlines() if l.strip()]
    header = lines[0]
    chunks = []
    cur_lines = [header]
    for line in lines[1:]:
        if sum(len(l) for l in cur_lines) + len(line) >= chunk_size:
            chunks.append('\n'.join(cur_lines))
            cur_lines = [header, line]
        else:
            cur_lines.append(line)
    if cur_lines:
        chunks.append('\n'.join(cur_lines))
    return chunks

La segmentación semántica usa similitud coseno entre frases adyacentes con umbral adaptativo (percentil 25).

Generación de Embeddings

Los fragmentos se vectorizan usando modelos como text-embedding-3-small (1536 dimensiones). Los vectores y metadatos se almacenan en la base de datos.

def get_embeddings(texts, batch_size=100):
    embeddings = []
    for i in range(0, len(texts), batch_size):
        resp = requests.post(
            "https://openrouter.ai/api/v1/embeddings",
            headers=HEADERS,
            json={
                "model": "openai/text-embedding-3-small",
                "input": texts[i:i + batch_size],
                "encoding_format": "float"
            },
        ).json()
        embeddings.extend(d["embedding"] for d in resp.get("data", []))
    return np.array(embeddings)

Búsqueda Multietapa

La consulta se transforma en un vector, se reformula en tres variaciones mediante LLM, y luego se procesa mediante búsqueda híbrida.

Google AdInline article slot

Búsqueda Híbrida: Vectores + BM25

  • Búsqueda vectorial: Similitud coseno en espacio de embeddings
  • BM25: TF-IDF con k1=1.5, b=0.75 para coincidencias exactas

Los resultados se fusionan usando RRF:

# Fusión RRF
for rank, idx in enumerate(sorted_by_vector):
    rrf_scores[idx] += 1.0 / (k_rrf + rank)
for rank, idx in enumerate(sorted_by_bm25):
    rrf_scores[idx] += bm25_normalized[idx] * (1.0 / (k_rrf + rank))

Reclasificación y Filtros

Se evalúan los 50 candidatos top mediante LLM, seleccionándose 5–10 mejores. Reglas de inclusión forzada:

  • Citas entre "comillas"
  • Números/IDs (regex: \b(№|N|number)\s*\d+\b)
quote_matches = re.findall(r'«([^»]+)»|"([^"+])"', query)
for match in quote_matches:
    quote = (match[0] or match[1]).strip()
    if len(quote) > 5:
        for i, chunk in enumerate(chunks_data):
            if quote in chunk['content']:
                forced_indices.append(i)
                break

Optimizaciones de Rendimiento

Búsqueda Jerárquica

Primero, se identifican los documentos principales mediante anotaciones; luego, se busca dentro de sus fragmentos.

doc_summaries = [chunks_data_by_doc[doc_id][0]['content'][:500] for doc_id in all_doc_ids]
doc_embs = get_embeddings(doc_summaries)
sims = np.dot(doc_embs_normalized, query_embedding_normalized)
top_doc_ids = [all_doc_ids[i] for i in np.argsort(sims)[::-1][:5]]
chunks_data = [ch for ch in chunks_data if ch['doc_id'] in top_doc_ids]

Búsqueda Iterativa

Para consultas complejas, múltiples rondas refinen los resultados usando retroalimentación de LLM.

Conclusiones Clave

  • RAG minimiza las falsedades generando respuestas estrictamente a partir de fragmentos relevantes
  • La búsqueda híbrida (vectores + BM25 + RRF) mejora la precisión entre un 20% y un 30% respecto a la búsqueda puramente vectorial
  • La segmentación semántica preserva el contexto en tablas y código, reduciendo el ruido
  • El enfoque jerárquico acelera la búsqueda en corpora grandes (>10k documentos)
  • Los filtros forzados aseguran la recuperación de hechos que contienen números o texto entre comillas

— Editorial Team

Advertisement 728x90

Leer después