🎯 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
- 🤖 Multi-Provider: No depender de un solo proveedor
- ⚡ Async Calls: No bloquear requests mientras esperas respuesta
- 🔄 Retry Logic: Manejar rate limits y errores temporales
- 📊 Prompt Engineering: Optimizar prompts para dominio legal
- 🎯 Context Management: Manejar límites de tokens (4K-128K)
- 💰 Cost Tracking: Monitorear costos por request
- 🔍 Quality Control: Evaluar calidad de respuestas automáticamente
- 📈 Observability: Track latency, tokens, errors
Desafíos Técnicos Específicos
- 🕒 Latency Variable: OpenAI 1-2s, Gemini 0.5-1.5s
- 💸 Rate Limits: OpenAI 3,500 RPM (tier 1), Gemini 15 RPM (free tier)
- 🎯 Context Windows: Diferentes límites por modelo
- 🔀 API Differences: Cada provider tiene API distinta
- 🛡️ Hallucinations: LLMs pueden inventar información
- 💰 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 │
└──────────────────────────────────────┘
¿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
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()
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
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:
- Toma los top-5 documentos más relevantes
- Extrae el texto del artículo + metadatos (capítulo, número)
- Formatea cada documento con su información contextual
- 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]
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
?
- Recibe el prompt completo (contexto + pregunta + instrucciones)
- Construye los mensajes en formato chat:
- System: Define el rol del asistente
- User: El prompt con contexto y pregunta
- Llama a la API de OpenAI de forma asíncrona
- 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
?
- Crea una instancia del modelo Gemini
- Configura el system instruction (rol del asistente)
- Genera el contenido con los mismos parámetros (temperature=0.2, max_tokens=1500)
- 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:
- 📝 Construye el contexto a partir de los documentos recuperados
- 🎨 Crea el prompt combinando contexto + pregunta + instrucciones
- 🤖 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()
- Si
- 📊 Registra la llamada en Phoenix (observability)
- ✅ 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:
-
🔐 Session Creation (1ms)
- Crea un ID único para trackear la request
-
🔍 Retrieve Documents (30-60ms)
- Genera embedding de la pregunta
- Busca en Qdrant los top-5 documentos más similares
- Opcionalmente aplica reranking
-
🤖 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
-
📊 Track with Phoenix (5ms)
- Registra toda la interacción para observability
- Guarda metadatos: latencia, tokens, modelo usado, etc.
-
📈 Enqueue Evaluation (1ms, no bloqueante)
- Encola evaluación asíncrona de calidad
- Se ejecuta en background sin afectar latencia
-
✅ 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
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%
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%
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?
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]
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.
🔧 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
- GitHub: lus-laboris-py
Documentación Técnica
-
RAG Service:
src/lus_laboris_api/api/services/rag_service.py
-
Config:
src/lus_laboris_api/api/config.py
-
RAG Endpoint:
src/lus_laboris_api/api/endpoints/rag.py
Recursos Externos
- OpenAI API Docs: platform.openai.com/docs
- Google Gemini Docs: ai.google.dev/gemini-api/docs
- Tenacity (Retry): tenacity.readthedocs.io
- Prompt Engineering Guide: promptingguide.ai
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)