Después de mi primer "Hello World!" con agentes usando n8n y comparando LLMs vía API, me quedó una pregunta rondando: ¿qué pasa si en vez de pagar por token a cada consulta, entreno mi propio modelo especializado?
Con Psico-IA (el sucesor natural de PsicoEnLine@, mi sistema de informes psicométricos a punta de if-else) tenía el caso de uso perfecto: 337 informes reales acumulados desde 2015, todos con la misma estructura, todos redactados por analistas humanos siguiendo un patrón consistente. Si algún dataset estaba pidiendo a gritos un fine-tuning, era este.
El dilema: RAG, contexto, o fine-tuning
Mi primer instinto fue preguntarme si necesitaba RAG. Rápidamente descarté la idea: RAG sirve para recuperar información existente, y yo no necesitaba consultar informes viejos, necesitaba generar informes nuevos con el mismo estilo. Tampoco tenía sentido meter los 337 ejemplos como contexto en cada prompt: ni entran en la ventana de contexto, ni "enseñan" el patrón de forma robusta, solo lo muestran una vez de paso.
La respuesta correcta era SFT (Supervised Fine-Tuning): pares de instrucción/respuesta, donde la instrucción son los resultados crudos de las pruebas psicométricas, y la respuesta es la redacción profesional que hoy hace un analista a mano. Es exactamente el tipo de tarea repetitiva y estructurada donde el fine-tuning brilla.
Hardware: la restricción que lo definió todo
A diferencia de mi experimento anterior con n8n (donde simplemente pagaba por API), esta vez me propuse algo distinto: hacerlo con el menor hardware posible, sin comprar nada. Eso me llevó directo a QLoRA, la combinación de LoRA con cuantización de 4 bits, que permite entrenar solo un pequeño adaptador sobre un modelo base congelado y comprimido, en vez de reentrenar los miles de millones de parámetros completos.
Con QLoRA, un modelo de 7 mil millones de parámetros (elegí Qwen2.5-7B-Instruct, de Alibaba) entra cómodo en la GPU T4 de 16GB que Google Colab ofrece gratis. Evalué también si tenía sentido intentarlo con modelos más grandes como DeepSeek V4 Flash (284 mil millones de parámetros totales) o Kimi K2.6 (1 billón de parámetros), y ambos quedaron descartados de inmediato: aunque activan pocos parámetros por token gracias a su arquitectura MoE, igual necesitan tener todos sus expertos residentes en memoria, lo cual exige clusters de GPUs H100 y costos de cientos de dólares solo para el fine-tuning. Nada que ver con mi laptop ni con Colab gratis.
Preparando el dataset: la parte fea del trabajo
Mis 337 informes vivían en una tabla MySQL, con resultados de pruebas psicométricas mezclados en un solo campo de texto libre junto con la descripción del puesto y las conclusiones del analista. El primer desafío técnico fue separar correctamente qué era "input" (resultados numéricos + competencias requeridas) y qué era "output" (el análisis narrativo que el modelo debía aprender a redactar).
Armé un query que envuelve todo en JSON_OBJECT para evitar problemas de escapado con el texto libre:
SELECT JSON_OBJECT(
'instruction', CONCAT('Resultados de pruebas:\n', ...),
'output', s.observaciones
) as jsonl_row
FROM grupo_sujetos_tabla s
INNER JOIN grupo g ON g.codigo = s.codigo_grupo
WHERE s.observaciones IS NOT NULL AND s.observaciones <> ""
Y después, un script en Python con regex para separar la descripción del puesto (input) del análisis competencia por competencia (output), usando como marcador la frase "Tomando en cuenta esta información...". Sorpresa en el camino: no todos los informes seguían el mismo formato. Los que usaban pruebas proyectivas (tipo Wartegg) iban directo al análisis narrativo, sin descripción de puesto. Tuve que ajustar el script para reconocer ambos formatos sin descartar ninguno como "error".
El resultado final: un archivo .jsonl donde cada línea es un objeto JSON completo con instruction y output, el formato que espera Unsloth (el framework que terminé usando) para cargar datos de entrenamiento.
El entrenamiento: la teoría choca con la infraestructura gratis
El script de entrenamiento en sí fue relativamente sencillo: cargar Qwen 7B cuantizado en 4-bit, aplicarle adaptadores LoRA sobre las capas de atención y feed-forward, y entrenar con SFTTrainer de HuggingFace durante 3 épocas. Lo que no fue sencillo fue lidiar con las particularidades (por decirlo elegantemente) de Colab gratis:
- Colab borra todo lo que no esté en Google Drive cuando la sesión se reinicia. Perdí un entrenamiento completo de más de una hora por no montar Drive desde el principio.
- Escribir archivos grandes directo a Drive montado los puede corromper silenciosamente. Un archivo de varios gigabytes terminó en 0 bytes sin ningún error visible hasta que intenté usarlo.
- Unsloth le agrega su propio sufijo
_ggufal nombre de carpeta que le indiqué, generando una carpeta con nombre duplicado que casi me hace creer que el archivo final nunca se generó. - Colab no publica un límite mensual de horas gratis, solo un techo de 12 horas por sesión, con disponibilidad de GPU que fluctúa según demanda, sin ninguna garantía.
Después de tres intentos (dos de ellos perdidos por completo), logré exportar el modelo fusionado y cuantizado en formato GGUF de 4 bits, un archivo de 4.68 GB, listo para Ollama.
Corriendo el modelo en casa
Con Ollama instalado en mi Ubuntu (el mismo Ryzen 7 con 16GB RAM del experimento anterior, aunque esta vez con una GPU AMD que resultó tener apenas 2GB de VRAM, insuficiente para acelerar nada), el modelo corre en CPU con respuestas de entre 10 y 30 segundos: lento, pero funcional para validar calidad.
La primera prueba reveló un comportamiento inesperado: al pedirle que evaluara solo 4 competencias específicas de un candidato, el modelo generó un análisis de 12. La razón: mi dataset de entrenamiento nunca le enseñó explícitamente "evalúa solo estas competencias y ninguna más", simplemente aprendió el patrón general de "generar análisis de competencias" a partir de resultados de pruebas. Tuve que reforzar el system prompt con reglas explícitas y repetir la restricción en el prompt de usuario para que respetara el límite.
También noté que, al no darle el nombre del candidato, el modelo inventaba nombres que había visto en el entrenamiento ("Sr. Rodríguez", "Torrens"...), un recordatorio incómodo de que nunca anonimicé el dataset original antes de entrenar.
Resultados Preliminares
Comparando el modelo propio fine-tuneado contra lo que ya había probado en mi experimento anterior con n8n:
| Modelo | Resultado | Tiempo de Respuesta | Hardware/Costo |
|---|---|---|---|
| Groq + openai/gpt-oss-120b | Exitoso | 5 segundos | API, gratis hasta ahora |
| Gemini 2.5 flash lite | Exitoso | 7 segundos | API, centavos |
| DeepSeek-chat | Exitoso | 52 segundos | API, centavos |
| Qwen 7B fine-tuneado (Ollama local) | Exitoso, con ajuste de prompt | 15-25 segundos | CPU local, $0 en inferencia |
| Ollama + deepseek-r1:7b (sin fine-tuning) | Fallido | 301 segundos | CPU local |
A diferencia de mi deepseek-r1:7b "de fábrica", que fue lerdo y no siguió instrucciones en mi experimento anterior, el Qwen 7B fine-tuneado con mis propios 337 informes sí produce análisis coherentes con el estilo real de mis analistas. La diferencia entre un modelo genérico y uno especializado en mi dominio específico es notable, aunque el ajuste fino del prompt sigue siendo necesario para que respete restricciones puntuales.
Conclusión
El proyecto completo, de punta a punta, me costó menos de $5 en cómputo real: prácticamente todo el gasto fue tiempo, no dinero. Hice también el ejercicio de comparar contra usar la API de DeepSeek directamente para el mismo volumen estimado (unos 10,000 informes al año): en dólares puros, la API sigue siendo más barata que alojar mi propio modelo. Pero aquí entra una variable que no aparecía en mi experimento con n8n: estos son datos psicométricos de RRHH, información sensible sobre personas evaluadas para empleo. La decisión de tener el modelo corriendo en mi propia infraestructura, aunque cueste unos dólares más al año, no es una decisión de costo, es una decisión de control sobre dónde viven esos datos.
Veredicto
Para tareas de dominio muy específico y datos sensibles, fine-tunear un modelo pequeño open-weight con QLoRA en Colab gratis es bueno, bonito y casi gratis. El verdadero costo no está en el cómputo: está en pelear contra la infraestructura gratuita y en anonimizar bien los datos antes de entrenar (lección aprendida para la próxima ronda).