DEV Community

Moon Robert
Moon Robert

Posted on

Fine-tuning vs RAG: Cuándo Usar Cada Enfoque para LLMs en Producción

Fine-tuning vs RAG: Cuándo Usar Cada Enfoque para LLMs en Producción

Tienes un modelo de lenguaje y un problema concreto que resolver. El modelo no sabe suficiente sobre tu dominio, responde de forma genérica o simplemente no maneja el tono y formato que necesitas. La pregunta surge inevitablemente: ¿entreno el modelo con mis datos, o le doy acceso a una base de conocimiento externa?

Esta decisión —fine-tuning vs RAG— tiene consecuencias reales: en costos de infraestructura, en la frescura de las respuestas, en el esfuerzo de mantenimiento y en cuánto control tenés sobre el comportamiento del modelo. No existe una respuesta universal, pero sí existe una forma sistemática de llegar a la correcta para tu caso.


Qué es RAG y por qué se volvió el punto de partida

RAG (Retrieval-Augmented Generation) conecta un LLM a una fuente de información externa en tiempo de inferencia. El flujo es simple: el usuario hace una pregunta, un sistema de recuperación busca los fragmentos relevantes en una base de datos vectorial (o tradicional), y esos fragmentos se inyectan en el prompt junto con la pregunta original. El modelo genera su respuesta usando ese contexto.

from openai import OpenAI
from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

# Setup básico de RAG
embedder = SentenceTransformer("all-MiniLM-L6-v2")
client = OpenAI()

def retrieve(query: str, index: faiss.Index, corpus: list[str], k: int = 3) -> list[str]:
    query_vec = embedder.encode([query])
    _, indices = index.search(np.array(query_vec, dtype="float32"), k)
    return [corpus[i] for i in indices[0]]

def answer_with_rag(query: str, index: faiss.Index, corpus: list[str]) -> str:
    chunks = retrieve(query, index, corpus)
    context = "\n\n".join(chunks)

    response = client.chat.completions.create(
        model="claude-sonnet-4-6",
        messages=[
            {"role": "system", "content": "Respondé usando únicamente el contexto provisto."},
            {"role": "user", "content": f"Contexto:\n{context}\n\nPregunta: {query}"}
        ]
    )
    return response.choices[0].message.content
Enter fullscreen mode Exit fullscreen mode

La gran ventaja de RAG es que el conocimiento vive fuera del modelo. Actualizás tus documentos y el sistema automáticamente empieza a usar la información nueva, sin tocar los pesos del LLM. Para equipos que trabajan con datos que cambian frecuentemente —precios, regulaciones, documentación técnica, artículos de soporte— esto es fundamental.

RAG funciona especialmente bien cuando:

  • El conocimiento cambia con frecuencia (diario, semanal).
  • Necesitás trazabilidad: poder citar la fuente exacta de cada respuesta.
  • Tu corpus es grande pero hetereogéneo (miles de documentos de distintos dominios).
  • Querés empezar rápido sin un ciclo de entrenamiento.
  • La información es propietaria y no puede "hornearse" en un modelo compartido.

Dónde RAG tiene fricción:

  • La calidad depende críticamente del retriever. Si el sistema de recuperación trae los fragmentos equivocados, el modelo fabrica respuestas o se contradice.
  • Aumenta la latencia por la búsqueda vectorial y el contexto adicional.
  • La ventana de contexto tiene límite: no podés inyectar todo un documento de 200 páginas.
  • El modelo base puede no entender el formato o la jerga de tu dominio incluso con el contexto correcto.

Qué es el fine-tuning y cuándo tiene sentido

El fine-tuning ajusta los pesos del modelo usando ejemplos de entrada/salida específicos de tu dominio. El modelo aprende patrones, terminología, formato y estilo que difieren de lo que vio en preentrenamiento.

Hay distintos niveles de fine-tuning:

  • Full fine-tuning: ajustás todos los parámetros. Costoso, pero máximo control.
  • LoRA / QLoRA: ajustás matrices de bajo rango. Mucho más eficiente, y es el estándar actual para la mayoría de los casos.
  • Instruction tuning: enseñás al modelo a seguir instrucciones en un formato específico.
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
import datasets

# Fine-tuning con LoRA usando TRL
model_name = "meta-llama/Llama-3.1-8B-Instruct"

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

model = AutoModelForCausalLM.from_pretrained(model_name, load_in_4bit=True)
model = get_peft_model(model, lora_config)

# Dataset en formato conversacional
dataset = datasets.load_dataset("json", data_files="training_data.jsonl")

training_args = TrainingArguments(
    output_dir="./finetuned-model",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    logging_steps=10,
    save_strategy="epoch"
)

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset["train"],
    args=training_args,
    dataset_text_field="text"
)

trainer.train()
Enter fullscreen mode Exit fullscreen mode

El fine-tuning enseña comportamiento, no hechos. Esta distinción es central para entender la dicotomía fine-tuning vs RAG. Si necesitás que el modelo responda en un formato JSON específico, use terminología médica correctamente, adopte el tono de tu marca, o siga un protocolo de conversación —eso es comportamiento, y el fine-tuning lo maneja mucho mejor que inyectar instrucciones en el prompt.

Fine-tuning funciona especialmente bien cuando:

  • Necesitás un formato de salida muy específico y constante (JSON estructurado, código en un dialecto particular, reportes).
  • El modelo base no maneja bien la terminología de tu dominio aunque se la expliques en el prompt.
  • Querés reducir el tamaño del prompt (instrucciones horneadas = menos tokens = menos costo).
  • El conocimiento que necesitás es estable y no cambia frecuentemente.
  • Tenés restricciones de latencia muy estrictas y no podés pagar el overhead del retrieval.

Dónde el fine-tuning tiene fricción:

  • Necesitás datos de entrenamiento de calidad, y generarlos es trabajo real.
  • El proceso tarda horas o días, no minutos.
  • Una vez que el modelo está entrenado, el conocimiento queda congelado en esa versión.
  • El modelo puede "olvidar" capacidades generales si el fine-tuning es agresivo (catastrophic forgetting).
  • Requiere infraestructura de entrenamiento (GPUs, almacenamiento de checkpoints, pipelines de evaluación).

La comparación directa: criterios para decidir

Cuando se plantea el debate fine-tuning vs RAG, los criterios más útiles para decidir son:

Dinamismo del conocimiento

Situación Enfoque
Datos que cambian a diario (precios, stock, noticias) RAG
Políticas que se actualizan mensualmente RAG con re-indexado periódico
Terminología de dominio estable Fine-tuning
Protocolo de atención al cliente que no cambia Fine-tuning

Tipo de problema

El modelo no sabe la información → RAG. El modelo base conoce español perfectamente y sabe razonar; simplemente no tiene acceso a tu documentación interna.

El modelo sabe la información pero no responde como querés → Fine-tuning. Si el modelo base entiende el concepto pero produce el formato incorrecto, usa un tono equivocado, o mezcla idiomas, entrenalo para que ajuste su comportamiento.

Costo total de propiedad

RAG tiene costos corrientes más altos: embeddings, almacenamiento vectorial, llamadas de API con contextos más largos. Fine-tuning tiene un costo inicial alto (entrenamiento, evaluación, hosting del modelo), pero puede abaratar la inferencia si lográs reducir el tamaño del prompt o usar un modelo más pequeño que con fine-tuning alcanza la calidad de uno más grande.

Un cálculo simple para comparar:

def costo_mensual_rag(
    queries_por_mes: int,
    tokens_prompt_base: int,
    tokens_contexto_promedio: int,
    tokens_respuesta: int,
    precio_input_per_1k: float,
    precio_output_per_1k: float,
    costo_vectordb_mensual: float
) -> float:
    tokens_input_totales = (tokens_prompt_base + tokens_contexto_promedio) * queries_por_mes
    tokens_output_totales = tokens_respuesta * queries_por_mes

    costo_llm = (tokens_input_totales / 1000 * precio_input_per_1k + 
                 tokens_output_totales / 1000 * precio_output_per_1k)

    return costo_llm + costo_vectordb_mensual

def costo_mensual_finetuned(
    queries_por_mes: int,
    tokens_prompt_reducido: int,  # sin instrucciones largas
    tokens_respuesta: int,
    precio_input_per_1k: float,
    precio_output_per_1k: float,
    costo_hosting_mensual: float  # GPU para servir el modelo
) -> float:
    tokens_input_totales = tokens_prompt_reducido * queries_por_mes
    tokens_output_totales = tokens_respuesta * queries_por_mes

    costo_llm = (tokens_input_totales / 1000 * precio_input_per_1k + 
                 tokens_output_totales / 1000 * precio_output_per_1k)

    return costo_llm + costo_hosting_mensual

# Ejemplo para 100k queries/mes
print(costo_mensual_rag(100_000, 500, 1500, 300, 0.003, 0.015, 200))
print(costo_mensual_finetuned(100_000, 200, 300, 0.0015, 0.008, 800))
Enter fullscreen mode Exit fullscreen mode

Casos donde la respuesta es "los dos"

La dicotomía fine-tuning vs RAG es a veces falsa. Hay escenarios donde ambos enfoques se complementan:

Asistente médico especializado: fine-tuneás el modelo para que hable en términos clínicos correctos, siga el protocolo de respuesta adecuado y no dé diagnósticos directos —eso es comportamiento. Luego agregás RAG sobre la base de datos de medicamentos actualizada con las últimas aprobaciones y contraindicaciones —eso es conocimiento dinámico.

Soporte técnico de software: fine-tuneás para que el modelo siempre responda en el formato [Problema] → [Causa] → [Solución] y use la terminología exacta de tu producto. RAG sobre la documentación y el historial de tickets resueltos le da acceso al conocimiento específico de cada versión.

La arquitectura combinada típica:

Usuario
  │
  ▼
[Retriever] ──── busca en VectorDB ────► [Chunks relevantes]
  │                                              │
  ▼                                              ▼
[Fine-tuned LLM] ◄─────── prompt con contexto ──┘
  │
  ▼
Respuesta formateada y en tono correcto
Enter fullscreen mode Exit fullscreen mode

El fine-tuning se encarga del "cómo responder" y RAG del "con qué información responder". Esta separación es limpia y mantenible.


Marco de decisión para equipos en producción

Antes de comprometerte con cualquier arquitectura, recorrés estas preguntas en orden:

1. ¿El problema es de conocimiento o de comportamiento?

  • ¿El modelo base, con el contexto correcto en el prompt, ya da la respuesta que necesitás? → RAG (el problema es acceso a información).
  • ¿Aunque le des toda la información en el prompt sigue respondiendo mal, en el formato incorrecto, o ignorando restricciones? → Fine-tuning (el problema es comportamiento).

2. ¿Con qué frecuencia cambia la información?

  • Cambios frecuentes o impredecibles → RAG.
  • Estable por meses → Fine-tuning posible.

3. ¿Tenés datos de entrenamiento?

Fine-tuning requiere ejemplos de calidad. Una regla práctica: necesitás al menos 100-500 ejemplos bien formados para ver mejoras significativas, y más de 1.000 para resultados robustos. Si no los tenés y generarlos es caro, RAG te da un punto de partida mucho más rápido.

4. ¿Cuáles son tus restricciones de latencia?

RAG agrega al menos 50-200ms de overhead por el retrieval vectorial. Si servís en edge, en dispositivos móviles, o tenés SLAs muy estrictos, fine-tuning (especialmente en modelos pequeños) puede ser la única opción viable.

5. ¿Necesitás trazabilidad?

Auditorías, regulaciones o simplemente transparencia con el usuario sobre las fuentes → RAG siempre tiene ventaja. Podés devolver exactamente qué fragmento fundamentó cada respuesta.

# RAG con trazabilidad de fuentes
def answer_with_sources(query: str, index, corpus: list[dict]) -> dict:
    # corpus es lista de {"content": str, "source": str, "page": int}
    chunks = retrieve(query, index, [c["content"] for c in corpus])

    sources = [c for c in corpus if c["content"] in chunks]

    response = generate_response(query, chunks)

    return {
        "answer": response,
        "sources": [{"url": s["source"], "page": s["page"]} for s in sources]
    }
Enter fullscreen mode Exit fullscreen mode

Conclusión

La elección entre fine-tuning vs RAG no es ideológica ni de tendencia —es arquitectónica. RAG resuelve el problema de acceso al conocimiento de forma dinámica y trazable. Fine-tuning resuelve el problema de comportamiento, formato y adaptación profunda al dominio. Muchos sistemas maduros terminan usando ambos, pero es más inteligente empezar con el enfoque más simple que resuelva tu problema concreto y agregar complejidad solo cuando los datos justifican la inversión.

Si estás arrancando hoy: implementá RAG primero. Es más rápido, más flexible y te da información real sobre cómo se comportan los usuarios con tu sistema. Con esa información podés identificar patrones de fallas que justifiquen un ciclo de fine-tuning posterior.

Si ya tenés un sistema RAG en producción y ves que el retrieval es bueno pero las respuestas siguen siendo inconsistentes en formato o tono —ese es el momento de considerar fine-tuning.


¿Estás evaluando alguno de estos enfoques para tu proyecto? Dejá tu caso en los comentarios: qué dominio, qué volumen de datos y qué restricciones manejás. Con esos detalles puedo orientarte hacia la arquitectura que más sentido tiene para tu situación específica.

Top comments (0)