DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

LLPY-07: Integrando LLMs - OpenAI y Google Gemini

🎯 El Desafío de la Generación de Respuestas

Imagina que tienes un sistema RAG funcionando perfectamente:

  • Embeddings generados con modelos optimizados
  • Qdrant devuelve los 5 artículos más relevantes en <50ms
  • Reranking mejora la precisión al 95%+

Ahora el momento crítico: ¿cómo convertir esos documentos en una respuesta coherente, precisa y en lenguaje natural?

Necesitas un LLM (Large Language Model) que:

  • 🎯 Comprenda el contexto legal de Paraguay
  • 📝 Genere respuestas en español formal y profesional
  • 🔍 Cite artículos específicos correctamente
  • 🛡️ No invente información que no esté en el contexto (no hallucinations)
  • Responda en 1-3 segundos máximo
  • 💰 Sea cost-effective a escala
  • 🔄 Tenga fallback si un provider falla

📊 La Magnitud del Problema

Requisitos del Sistema de LLM

  1. 🤖 Multi-Provider: No depender de un solo proveedor
  2. ⚡ Async Calls: No bloquear requests mientras esperas respuesta
  3. 🔄 Retry Logic: Manejar rate limits y errores temporales
  4. 📊 Prompt Engineering: Optimizar prompts para dominio legal
  5. 🎯 Context Management: Manejar límites de tokens (4K-128K)
  6. 💰 Cost Tracking: Monitorear costos por request
  7. 🔍 Quality Control: Evaluar calidad de respuestas automáticamente
  8. 📈 Observability: Track latency, tokens, errors

Desafíos Técnicos Específicos

  1. 🕒 Latency Variable: OpenAI 1-2s, Gemini 0.5-1.5s
  2. 💸 Rate Limits: OpenAI 3,500 RPM (tier 1), Gemini 15 RPM (free tier)
  3. 🎯 Context Windows: Diferentes límites por modelo
  4. 🔀 API Differences: Cada provider tiene API distinta
  5. 🛡️ Hallucinations: LLMs pueden inventar información
  6. 💰 Costos: $0.50-$15 por 1M tokens según modelo

💡 La Solución: Multi-Provider con Abstracción

Arquitectura de Integración

┌─────────────────────────────────────────────────────────┐
│                     RAGService                          │
│  (Orquestación del pipeline completo)                   │
└────────────────────┬────────────────────────────────────┘
                     │
         ┌───────────┴───────────┐
         │                       │
    ┌────▼────┐           ┌─────▼─────┐
    │ OpenAI  │           │  Gemini   │
    │ Client  │           │  Client   │
    └─────────┘           └───────────┘
         │                       │
         │   AsyncOpenAI SDK     │   Google GenAI SDK
         │                       │
    ┌────▼────────────────────── ▼─────────┐
    │    Retry Logic (Tenacity)            │
    │    Exponential Backoff               │
    └──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

¿Por Qué Multi-Provider?

Aspecto Beneficio
Resiliencia Si OpenAI falla, usar Gemini
Cost Optimization Elegir provider más barato según caso de uso
Performance Usar provider más rápido según disponibilidad
A/B Testing Comparar calidad de respuestas entre providers
Regional Compliance Usar providers según regulaciones locales
Vendor Lock-in Evitar dependencia de un solo proveedor

🚀 Configuración Paso a Paso

1. Configuración de Variables de Entorno

# .env

# LLM Provider Configuration
API_LLM_PROVIDER=openai  # Opciones: 'openai' o 'gemini'
API_LLM_MODEL=gpt-3.5-turbo  # Para OpenAI

# Alternativa con Gemini:
# API_LLM_PROVIDER=gemini
# API_LLM_MODEL=gemini-1.5-flash

# API Keys
OPENAI_API_KEY=sk-proj-...
GEMINI_API_KEY=AIza...

# RAG Configuration
API_RAG_TOP_K=5
Enter fullscreen mode Exit fullscreen mode

Modelos disponibles:

OpenAI:

  • gpt-3.5-turbo: Rápido, barato ($0.50-$1.50/1M tokens)
  • gpt-4: Mejor calidad ($30/1M tokens)
  • gpt-4-turbo: Balance ($10/1M tokens)
  • gpt-4o: Optimizado ($5/1M tokens)

Google Gemini:

  • gemini-1.5-flash: Rápido, barato ($0.075-$0.30/1M tokens)
  • gemini-1.5-pro: Mejor calidad ($1.25-$5/1M tokens)
  • gemini-pro: Legacy model

2. Settings con Pydantic

# src/lus_laboris_api/api/config.py

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    """Application settings"""

    # LLM Configuration
    api_llm_provider: str = None  # 'openai' o 'gemini'
    api_llm_model: str = None

    # API Keys
    openai_api_key: str | None = None
    gemini_api_key: str | None = None

    # RAG Configuration
    api_rag_top_k: int = None

    class Config:
        env_file = ".env"
        case_sensitive = False

settings = Settings()
Enter fullscreen mode Exit fullscreen mode

3. Inicialización de LLM Clients

# src/lus_laboris_api/api/services/rag_service.py

import logging
from openai import AsyncOpenAI
from google import genai
from ..config import settings

logger = logging.getLogger(__name__)

class RAGService:
    """Service for RAG-based question answering"""

    def __init__(self):
        self.llm_provider = settings.api_llm_provider.lower()
        self.llm_model = settings.api_llm_model

        # Initialize LLM clients
        self._initialize_llm_clients()

    def _initialize_llm_clients(self):
        """Initialize LLM clients based on configured provider"""
        try:
            if self.llm_provider == "openai":
                # AsyncOpenAI for non-blocking calls
                self.openai_client = AsyncOpenAI(
                    api_key=settings.openai_api_key
                )
                logger.info("OpenAI async client initialized")

            elif self.llm_provider == "gemini":
                # Configure Gemini module globally
                genai.configure(api_key=settings.gemini_api_key)
                logger.info("Gemini configured successfully")

            else:
                raise ValueError(f"Unsupported LLM provider: {self.llm_provider}")

        except Exception as e:
            logger.exception("Failed to initialize LLM client")
            raise
Enter fullscreen mode Exit fullscreen mode

Características clave:

  • AsyncOpenAI: Cliente asíncrono para no bloquear requests
  • Lazy initialization: Solo se inicializa el provider configurado
  • Error handling: Fallar temprano si configuración es inválida
  • Logging: Track qué provider está activo

🎨 Prompt Engineering para Dominio Legal

Construcción del Contexto

El sistema toma los documentos recuperados de Qdrant y construye un contexto estructurado que se enviará al LLM:

Proceso:

  1. Toma los top-5 documentos más relevantes
  2. Extrae el texto del artículo + metadatos (capítulo, número)
  3. Formatea cada documento con su información contextual
  4. Los concatena en un string único

Ejemplo de contexto generado:

Documento 1:
todo trabajador que cumpla un año de trabajo continuo... 
[Capítulo: de las vacaciones - Artículo número: 218]

Documento 2:
el trabajador perderá el derecho a las vacaciones... 
[Capítulo: de las vacaciones - Artículo número: 219]

Documento 3:
durante las vacaciones el empleador abonará al trabajador... 
[Capítulo: de las vacaciones - Artículo número: 220]
Enter fullscreen mode Exit fullscreen mode

El Prompt Final

El prompt que se envía al LLM combina:

  • Rol del asistente: Especialista en derecho laboral paraguayo
  • Contexto: Los artículos relevantes recuperados
  • Pregunta del usuario: La query original
  • Instrucciones: Reglas claras sobre cómo responder

Principios clave del prompt:

  • Especialización: Define el dominio (derecho laboral paraguayo)
  • Constraint: "Basándote únicamente en el contexto" → previene hallucinations
  • Citation: Pide citar artículos específicos
  • Tone: Profesional y técnico
  • Safety: "Si no hay info suficiente, indícalo claramente"

🔄 Generación de Respuestas con OpenAI

Cliente Asíncrono (AsyncOpenAI)

El sistema utiliza el cliente asíncrono de OpenAI para no bloquear el servidor mientras espera la respuesta del LLM.

¿Qué hace el método _generate_openai_response?

  1. Recibe el prompt completo (contexto + pregunta + instrucciones)
  2. Construye los mensajes en formato chat:
    • System: Define el rol del asistente
    • User: El prompt con contexto y pregunta
  3. Llama a la API de OpenAI de forma asíncrona
  4. Retorna el texto generado

Parámetros importantes:

Parámetro Valor ¿Por qué?
temperature 0.2 Baja temperatura = respuestas consistentes y basadas en hechos (no creativas)
max_tokens 1500 Límite suficiente para respuesta legal completa

Retry Logic (Reintentos Automáticos)

El sistema implementa reintentos automáticos con espera exponencial para manejar errores temporales (rate limits, timeouts):

Estrategia de reintentos:

  • Intento 1: Inmediato
  • ⏱️ Intento 2: Espera 2 segundos si falla
  • ⏱️ Intento 3: Espera 4 segundos si falla
  • Después de 3 intentos: Propaga el error

Beneficio: Aumenta el success rate en ~15-20% en condiciones de alta carga.

🔄 Generación de Respuestas con Gemini

Cliente de Google Gemini

Similar a OpenAI, pero con algunas diferencias en la API:

¿Qué hace el método _generate_gemini_response?

  1. Crea una instancia del modelo Gemini
  2. Configura el system instruction (rol del asistente)
  3. Genera el contenido con los mismos parámetros (temperature=0.2, max_tokens=1500)
  4. Retorna el texto generado

Diferencias clave con OpenAI:

  • El system instruction se pasa al crear el modelo (no en los mensajes)
  • Usa GenerationConfig en lugar de parámetros directos
  • El SDK de Gemini es síncrono (no async como OpenAI)
  • Gemini tiene filtros de seguridad activados por defecto

Retry Logic: Mismo patrón de reintentos que OpenAI (hasta 3 intentos con backoff exponencial).

🔀 Abstracción Multi-Provider

El método _generate_response es el orquestador que unifica ambos providers:

Flujo del método:

  1. 📝 Construye el contexto a partir de los documentos recuperados
  2. 🎨 Crea el prompt combinando contexto + pregunta + instrucciones
  3. 🤖 Decide qué provider usar según configuración:
    • Si llm_provider == "openai" → llama a _generate_openai_response()
    • Si llm_provider == "gemini" → llama a _generate_gemini_response()
  4. 📊 Registra la llamada en Phoenix (observability)
  5. Retorna la respuesta generada

Ventajas de esta abstracción:

  • Cambiar de provider: Solo modificar 1 variable de entorno
  • Interfaz única: Mismo código usa OpenAI o Gemini sin cambios
  • Observability: Tracking automático con Phoenix
  • Testing: Fácil mockear providers en tests

📊 Pipeline Completo de RAG

El método answer_question es el punto de entrada del sistema RAG. Este orquesta todo el flujo:

Paso a Paso del Pipeline:

  1. 🔐 Session Creation (1ms)

    • Crea un ID único para trackear la request
  2. 🔍 Retrieve Documents (30-60ms)

    • Genera embedding de la pregunta
    • Busca en Qdrant los top-5 documentos más similares
    • Opcionalmente aplica reranking
  3. 🤖 Generate Answer (800-2500ms) ⬅️ EL CUELLO DE BOTELLA

    • Construye el contexto con los documentos
    • Crea el prompt
    • Llama al LLM (OpenAI o Gemini)
    • Recibe la respuesta generada
  4. 📊 Track with Phoenix (5ms)

    • Registra toda la interacción para observability
    • Guarda metadatos: latencia, tokens, modelo usado, etc.
  5. 📈 Enqueue Evaluation (1ms, no bloqueante)

    • Encola evaluación asíncrona de calidad
    • Se ejecuta en background sin afectar latencia
  6. ✅ Return Response (1ms)

    • Retorna JSON con respuesta + metadata + documentos

Tiempos Típicos:

Etapa Tiempo Porcentaje
Embedding 30ms 2%
Qdrant Search 30ms 2%
Reranking (opcional) 20ms 1%
LLM Generation 800-2500ms ~95%
Phoenix Track 5ms <1%
TOTAL ~1-3 segundos 100%

Conclusión: El LLM domina la latencia total. Por eso elegir el modelo correcto (Flash vs Pro vs GPT-4) es crítico.

🎯 Casos de Uso Reales

Para Aplicaciones de Producción:

"Necesito respuestas rápidas sin sacrificar calidad"

Solución: Usar Gemini 1.5 Flash (más rápido y barato)

# Configuración para producción high-traffic
export API_LLM_PROVIDER=gemini
export API_LLM_MODEL=gemini-1.5-flash

# Cost: $0.075-$0.30 / 1M tokens
# Latency: 500-1500ms
# Quality: 85-90% vs GPT-4
Enter fullscreen mode Exit fullscreen mode

Para Máxima Calidad:

"Necesito las mejores respuestas posibles, costo no es problema"

Solución: GPT-4

export API_LLM_PROVIDER=openai
export API_LLM_MODEL=gpt-4

# Cost: $30 / 1M tokens
# Latency: 2000-3000ms
# Quality: 95-98%
Enter fullscreen mode Exit fullscreen mode

Para Balance Costo/Calidad:

"Quiero buen balance entre costo y calidad"

Solución: GPT-3.5-turbo o GPT-4o

# Opción 1: GPT-3.5-turbo
export API_LLM_PROVIDER=openai
export API_LLM_MODEL=gpt-3.5-turbo
# Cost: $0.50-$1.50 / 1M tokens, Quality: 80-85%

# Opción 2: GPT-4o
export API_LLM_MODEL=gpt-4o
# Cost: $5 / 1M tokens, Quality: 90-95%
Enter fullscreen mode Exit fullscreen mode

Para A/B Testing:

"Quiero comparar OpenAI vs Gemini en producción"

Solución: Routing dinámico 50/50 entre providers

  • Aleatoriamente asigna cada request a un provider
  • Phoenix trackea qué provider se usó en cada request
  • Evalúa calidad de respuestas por provider en background

📊 Comparación de Modelos

Performance

Modelo Latency (p50) Latency (p95) Throughput (req/s)
GPT-3.5-turbo 1200ms 2000ms 50-100
GPT-4 2500ms 4000ms 20-40
GPT-4o 1800ms 3000ms 40-80
Gemini 1.5 Flash 800ms 1500ms 60-120
Gemini 1.5 Pro 1500ms 2500ms 30-60

Costos

Modelo Input ($/1M tokens) Output ($/1M tokens) Promedio RAG Query
GPT-3.5-turbo $0.50 $1.50 $0.003
GPT-4 $30 $60 $0.045
GPT-4o $5 $15 $0.010
Gemini 1.5 Flash $0.075 $0.30 $0.0002
Gemini 1.5 Pro $1.25 $5.00 $0.003

Cálculo para RAG query típico:

  • Context: ~1500 tokens (5 documentos)
  • Prompt: ~200 tokens
  • Response: ~300 tokens
  • Total: ~2000 tokens

Calidad (Evaluación Subjetiva)

Modelo Accuracy Completeness Clarity Legal Tone
GPT-3.5-turbo ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
GPT-4 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
GPT-4o ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Gemini 1.5 Flash ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Gemini 1.5 Pro ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐

🚀 El Impacto Transformador

Antes de LLMs:

  • 📄 Búsqueda por keywords: Devolver artículos sin explicación
  • 🤷 User interpretation: Usuario debe interpretar texto legal complejo
  • 🔍 Multiple searches: Usuario hace varias búsquedas para entender
  • ⏱️ Time consuming: 10-30 minutos para entender un concepto

Después de LLMs:

  • 💬 Lenguaje natural: Pregunta como a un abogado humano
  • 🎯 Respuesta directa: Explicación clara y profesional
  • 📚 Citas automáticas: Referencias a artículos específicos
  • Instantáneo: Respuesta en 1-3 segundos

Ejemplo Real:

Pregunta del usuario:

¿Cuántos días de vacaciones corresponden a un trabajador que lleva 2 años en la empresa?
Enter fullscreen mode Exit fullscreen mode

Sin LLM (búsqueda tradicional):

Resultados:
1. Artículo 218: todo trabajador que cumpla un año...
2. Artículo 219: el trabajador perderá el derecho...
3. Artículo 220: durante las vacaciones el empleador...

[Usuario debe leer y interpretar 3 artículos completos]
Enter fullscreen mode Exit fullscreen mode

Con LLM (RAG):

Según el Artículo 218 del Código del Trabajo de Paraguay, todo trabajador que cumpla 
un año de trabajo continuo al servicio del mismo empleador tiene derecho a un período 
de vacaciones anuales remuneradas. En el caso de un trabajador con 2 años en la empresa, 
le corresponden 12 días hábiles de vacaciones anuales.

Es importante destacar que el trabajador debe haber cumplido el año de trabajo continuo, 
y según el Artículo 219, perderá este derecho si ha faltado más de quince días sin 
causa justificada durante el año.

Durante las vacaciones, según el Artículo 220, el empleador debe abonar la remuneración 
ordinaria correspondiente al período de descanso.
Enter fullscreen mode Exit fullscreen mode

🔧 Características Técnicas Destacadas

1. Async para I/O-Bound Operations

¿Por qué async es crítico para RAG?

Enfoque Throughput Comportamiento
❌ Sync ~1 req/s Cada request bloquea el thread por 2+ segundos
✅ Async ~50+ req/s El servidor maneja múltiples requests concurrentemente

Explicación simple:

  • Con código síncrono: Mientras el servidor espera la respuesta de OpenAI (2 segundos), no puede procesar otras requests
  • Con código asíncrono: Mientras espera OpenAI, el servidor procesa otras requests → 50x más throughput

2. Retry Logic con Exponential Backoff

Impacto real:

  • Sin retry: 1 error de rate limit = 1 respuesta fallida
  • Con retry: Error transitorio → auto-retry 2 segundos después → success
  • 📈 Resultado: +15-20% success rate en condiciones de alta carga

💡 Lecciones Aprendidas

1. Temperatura Baja es Crucial para RAG

Con temperature=0.2, las respuestas son consistentes y basadas en el contexto. Con temperature=0.8+, el modelo tiende a "imaginar" información.

2. System Prompts Mejoran Calidad 20-30%

Definir claramente el rol ("asistente legal") y constraints ("basándote únicamente en el contexto") reduce hallucinations dramáticamente.

3. Async es No Negociable para Producción

Con llamadas LLM de 1-3 segundos, async/await es la diferencia entre 1 req/s y 50+ req/s.

4. Retry Logic Aumenta Success Rate 15%+

Rate limits y errores transitorios son comunes. Retry con exponential backoff recupera automáticamente.

5. Gemini es Más Rápido, OpenAI Más Preciso

Para alto tráfico, Gemini 1.5 Flash es imbatible. Para calidad crítica, GPT-4 lidera.

6. Multi-Provider es Resiliencia

Un solo proveedor puede fallar, tener outages, o cambiar pricing. Multi-provider es insurance.

🎯 El Propósito Más Grande

Los LLMs son el puente entre el conocimiento estructurado (artículos legales en Qdrant) y la comprensión humana. Al integrar:

  • 🤖 OpenAI GPT: Calidad de clase mundial
  • ⚡ Google Gemini: Velocidad y costo optimizado
  • 🔄 Async Architecture: Throughput de producción
  • 🛡️ Retry Logic: Resiliencia ante errores
  • 🎨 Prompt Engineering: Respuestas precisas y profesionales
  • 📊 Observability: Tracking completo con Phoenix

Estamos democratizando el acceso a asesoría legal, convirtiendo texto legal complejo en respuestas claras que cualquier persona puede entender, en segundos y a costo marginal cercano a cero.


🔗 Recursos y Enlaces

Repositorio del Proyecto

Documentación Técnica

Recursos Externos


Próximo Post: LLPY-08 - Reranking: Mejorando la Precisión de Búsqueda

En el siguiente post exploraremos cómo el reranking con modelos cross-encoder mejora la precisión de resultados RAG, cuándo usarlo, y el trade-off entre calidad y latencia.

Top comments (0)