Editor de código Python mostrando una Signature de DSPy con anotaciones tipadas y salida optimizada por MIPROv2
Volver al blog
TUTORIALES 9 Agosto 2026 18 min lectura 10 visitas

DSPy en Python: programar LLMs y optimizar prompts con MIPROv2 paso a paso (agosto 2026)

Arkaia
Arkaia Editor

DSPy es el framework que Stanford NLP lanzó para dejar de escribir prompts a mano y empezar a programar los LLMs como si fueran funciones tipadas de Python. En este tutorial de agosto de 2026 vamos a montar, paso a paso, un pipeline completo: instalación con uv, conexión a Claude Opus 5 y GPT-5.6, primera Signature tipada, ChainOfThought, un módulo RAG compuesto, métrica de evaluación, optimización automática con MIPROv2, guardado del programa compilado y despliegue en FastAPI. Al terminar tendrás un servicio en producción que se ha auto-optimizado sobre tus propios datos de evaluación, con mejoras medidas del 10 al 40 por ciento frente al prompting manual.

Por qué programar LLMs en lugar de escribir prompts a mano

La práctica habitual en 2024 era pegar cadenas gigantes de texto en un archivo, iterar a ojo hasta que el resultado parecía decente y rezar para que el modelo siguiente no rompiera todo. DSPy propone lo contrario: declaras la firma de la tarea (entradas y salidas tipadas), compones módulos que representan pasos de razonamiento y dejas que un optimizador (MIPROv2, BootstrapFewShot, COPRO) genere el prompt real a partir de un conjunto de datos de evaluación. El resultado se comporta como código: se versiona, se testea, se mide y se mejora sin tocar cadenas mágicas.

Este cambio de paradigma se aceleró en 2026, con DSPy 3.2 y la llegada de la rama 3.3 estable a comienzos de agosto. Los benchmarks internos de Stanford y varios equipos de producto (Databricks, Zenoml, Faire) reportan mejoras del 10 al 40 por ciento en tareas estructuradas como QA multi-hop, clasificación, extracción y generación RAG cuando se pasa de prompts escritos a mano a programas DSPy compilados con MIPROv2. Y todo eso con la misma API subyacente: sigues llamando a Claude Opus 5, GPT-5.6 o modelos locales vía Ollama.

Si vienes del ecosistema de agentes, este tutorial se complementa con nuestra guía del Claude Agent SDK (agentes autónomos con herramientas) y con el tutorial de LangGraph (grafos de estado para orquestación). Para exponer tu programa como servidor MCP consumible por Claude Desktop, revisa FastMCP en producción.

Editor de código Python mostrando una Signature de DSPy con anotaciones tipadas y salida optimizada por MIPROv2
DSPy trata cada llamada al LLM como una función tipada de Python; el prompt real lo genera el compilador.

Requisitos previos y hardware recomendado

Antes de lanzarnos al código, revisa esta lista mínima: Python 3.10 o superior (recomendado 3.12), gestor de dependencias uv (mucho más rápido que pip), una clave API de Anthropic para Claude Opus 5 (o de OpenAI, o de Google), y un entorno virtual limpio. Si vas a compilar programas con MIPROv2 sobre datasets de más de 200 ejemplos, prepara la tarjeta bancaria: la fase de optimización puede lanzar cientos de llamadas al LM juez.

El hardware no es crítico para DSPy en sí (el trabajo pesado lo hace el proveedor del LLM), pero si tu flujo es intensivo (compilaciones repetidas, evaluaciones cruzadas, notebooks) agradecerás una estación de trabajo cómoda. Estas son mis recomendaciones probadas en 2026:

Este artículo contiene enlaces afiliados a Amazon con el tag webmasteroson-21. Si compras a través de estos enlaces, Arkaia puede recibir una comisión sin coste adicional para ti. Nuestra valoración editorial es independiente y no varía según los ingresos por afiliación.

Paso 1: instalación con uv (y el porqué de no usar pip global)

El primer paso es crear un entorno reproducible. Usamos uv porque instala DSPy y sus 40 dependencias transitivas en menos de 3 segundos, y bloquea el archivo uv.lock con hashes SHA256 verificables. Evita instalar DSPy en el Python del sistema: la librería arrastra torch y tokenizers, y romper el intérprete global es sólo cuestión de tiempo.

uv init dspy-lab
cd dspy-lab
uv add dspy-ai openai anthropic pydantic python-dotenv mlflow fastapi uvicorn

# Verificar la version instalada (agosto 2026: 3.3.x estable)
uv run python -c "import dspy; print(dspy.__version__)"

Salida esperada: 3.3.0 o superior. Si obtienes 3.2.1, fuerza el upgrade con uv add "dspy-ai>=3.3". La rama 3.3 introduce la nueva API de dspy.LM unificada y la migración completa a ReActV2, que estamos usando en producción.

Trampa común: el paquete se llama dspy-ai en PyPI pero se importa como dspy. Muchos tutoriales viejos siguen mostrando pip install dspy, que apunta a un proyecto abandonado sin relación con Stanford NLP.

Paso 2: configuración del LM (Claude Opus 5 o GPT-5.6)

DSPy delega el transporte de red a LiteLLM, así que soporta prácticamente cualquier proveedor: Anthropic, OpenAI, Google Gemini, Cohere, Mistral, Ollama local, vLLM, TogetherAI... La API es la misma; sólo cambia el string del modelo. Vamos a configurar Claude Opus 5 como LM por defecto (el rey en razonamiento multi-hop en agosto 2026, con precios de 5 dólares por millón de tokens de entrada y 25 dólares por millón de salida).

import os
import dspy
from dotenv import load_dotenv

load_dotenv()  # Lee ANTHROPIC_API_KEY del entorno

# Modelo principal para inferencia
lm = dspy.LM(
    "anthropic/claude-opus-5",
    api_key=os.environ["ANTHROPIC_API_KEY"],
    max_tokens=4096,
    temperature=0.2,
)

# Modelo economico para el optimizador (juez de MIPROv2)
task_lm = dspy.LM("openai/gpt-5.6-mini", max_tokens=2048)

dspy.configure(lm=lm)

La distinción entre lm y task_lm es clave: durante la compilación, MIPROv2 lanza cientos de llamadas para proponer instrucciones y evaluar candidatas. Usar el modelo caro para todo puede disparar la factura. La convención de la comunidad es reservar Claude Opus 5 o GPT-5.6 para la inferencia final y delegar el juez interno a un modelo mini o flash.

Trampa común: si defines la clave API en un fichero .env pero olvidas llamar a load_dotenv(), DSPy fallará con un error de LiteLLM del tipo AuthenticationError: 401. Verifica siempre con echo $ANTHROPIC_API_KEY antes de ejecutar.

Paso 3: primera Signature tipada

Una Signature es el contrato de una llamada al LLM: describe qué campos entran, cuáles salen y cómo se llaman. DSPy usa las anotaciones de tipo de Python para generar automáticamente el prompt de sistema. Es el equivalente a definir una función Pydantic, pero para el modelo de lenguaje.

import dspy

class SummarizeText(dspy.Signature):
    """Resume un texto en 3 frases claras y neutras."""

    # Campos de entrada (in)
    texto: str = dspy.InputField(desc="Texto original en espanol")
    audiencia: str = dspy.InputField(
        desc="Perfil del lector: tecnico, general, ejecutivo"
    )

    # Campos de salida (out)
    resumen: str = dspy.OutputField(
        desc="Resumen final en 3 frases separadas por saltos de linea"
    )
    palabras_clave: list[str] = dspy.OutputField(
        desc="Entre 3 y 5 palabras clave del texto"
    )

Ojo al detalle: DSPy respeta el tipo declarado. Si dices list[str], el framework parsea la salida del modelo y devuelve una lista nativa de Python; si el modelo se sale del formato, la librería reintenta con instrucciones adicionales. Este contrato es el que MIPROv2 aprovecha después para reescribir el prompt sin romper tu código cliente.

Diagrama de una Signature de DSPy con campos de entrada InputField y de salida OutputField enlazados a un LLM
Anatomía de una Signature: campos tipados y descripciones que el compilador convertirá en prompt.

Paso 4: Predict simple (la primera inferencia)

Con la Signature declarada, ejecutar la primera predicción es una línea:

from pprint import pprint

predictor = dspy.Predict(SummarizeText)

resultado = predictor(
    texto=(
        "DSPy 3.3 introduce ReActV2, mejoras en MIPROv2 y soporte nativo "
        "para tool calls paralelos con Claude Opus 5. La nueva API dspy.LM "
        "unifica todos los proveedores gracias a LiteLLM."
    ),
    audiencia="tecnico",
)

pprint(resultado.resumen)
pprint(resultado.palabras_clave)

Salida esperada (aproximada; el modelo puede variar la redacción):

'DSPy 3.3 anade ReActV2 y mejoras en el optimizador MIPROv2.\n'
'Soporta tool calls paralelos con Claude Opus 5.\n'
'La API dspy.LM unifica proveedores mediante LiteLLM.'
['DSPy', 'ReActV2', 'MIPROv2', 'Claude Opus 5', 'LiteLLM']

Fíjate en dos cosas: no has escrito ningún prompt (DSPy lo generó a partir del docstring y las descripciones) y las palabras clave llegan ya como list[str] lista para pasar a tu lógica de negocio. Si quisieras ver el prompt real, ejecuta dspy.inspect_history(n=1) tras la llamada.

Paso 5: ChainOfThought (razonamiento explícito)

El módulo ChainOfThought es un drop-in de Predict que añade un campo intermedio reasoning generado por el modelo antes de la respuesta final. Aumenta la latencia y el coste, pero mejora sustancialmente la precisión en tareas de razonamiento, aritmética y clasificación ambigua.

cot = dspy.ChainOfThought(SummarizeText)

salida = cot(
    texto="La Reserva Federal recorto tipos 25 puntos basicos en julio.",
    audiencia="ejecutivo",
)

print("Razonamiento:", salida.reasoning)
print("Resumen:", salida.resumen)

Regla práctica: usa Predict para tareas mecánicas (extracción de datos, formateo, traducciones cortas) y ChainOfThought cuando el modelo tenga que decidir entre varias interpretaciones. En benchmarks de la comunidad de agosto 2026, CoT mejora F1 entre 3 y 12 puntos en clasificación legal, médica y financiera.

Diagrama de flujo de ChainOfThought en DSPy con paso intermedio de razonamiento antes de la salida final
ChainOfThought inserta un campo reasoning entre la entrada y la salida sin cambiar la Signature original.

Paso 6: composición de Modules (pipeline RAG completo)

Los Modules son la unidad de composición en DSPy: agrupan varias Signatures dentro de una clase que hereda de dspy.Module. Vamos a montar un pipeline RAG mínimo con tres etapas: recuperar candidatos, reordenarlos con un mini-clasificador y generar la respuesta final. Este mismo patrón es el que MIPROv2 optimizará después de forma conjunta (no cada paso por separado).

import dspy

class RetrieveTopK(dspy.Signature):
    """Selecciona los k pasajes mas relevantes para la pregunta."""
    pregunta: str = dspy.InputField()
    pasajes: list[str] = dspy.InputField(desc="Corpus candidato")
    top_k: list[str] = dspy.OutputField(desc="3 pasajes mas relevantes")

class Answer(dspy.Signature):
    """Responde en espanol basandote solo en los pasajes."""
    pregunta: str = dspy.InputField()
    contexto: list[str] = dspy.InputField()
    respuesta: str = dspy.OutputField()

class RAG(dspy.Module):
    def __init__(self):
        super().__init__()
        self.rerank = dspy.ChainOfThought(RetrieveTopK)
        self.answer = dspy.ChainOfThought(Answer)

    def forward(self, pregunta: str, pasajes: list[str]):
        seleccion = self.rerank(pregunta=pregunta, pasajes=pasajes)
        salida = self.answer(pregunta=pregunta, contexto=seleccion.top_k)
        return dspy.Prediction(
            respuesta=salida.respuesta,
            contexto=seleccion.top_k,
        )

rag = RAG()
resp = rag(
    pregunta="Que introduce DSPy 3.3?",
    pasajes=[
        "DSPy 3.3 anade ReActV2 y mejoras en MIPROv2.",
        "El plato del dia son macarrones con tomate.",
        "La API dspy.LM se unifica gracias a LiteLLM.",
    ],
)
print(resp.respuesta)

El truco de DSPy es que rag.parameters() expone automáticamente los dos módulos internos como parámetros optimizables. Cuando pasemos este objeto al optimizador, MIPROv2 propondrá instrucciones y few-shots de forma conjunta para rerank y answer, no como pipelines independientes.

Pipeline RAG compuesto en DSPy con etapas de recuperación, reranking y generación final enlazadas
Pipeline RAG con tres etapas encadenadas dentro de un único dspy.Module reutilizable.

Paso 7: métrica de evaluación

Sin una métrica, DSPy es sólo una librería más de tipado. La métrica es la función que MIPROv2 intentará maximizar. Debe recibir gold (ejemplo esperado), pred (predicción del programa) y opcionalmente trace (traza interna). Devuelve un float o un booleano.

import dspy
from dspy.evaluate import Evaluate

def metric(gold, pred, trace=None) -> float:
    """Media entre exact match y overlap semantico."""
    exact = float(gold.respuesta.strip().lower() in pred.respuesta.lower())
    # Juez LLM binario: 1 si la respuesta es correcta, 0 si no
    juez = dspy.Predict("pregunta, referencia, candidata -> es_correcta: bool")
    veredicto = juez(
        pregunta=gold.pregunta,
        referencia=gold.respuesta,
        candidata=pred.respuesta,
    )
    return 0.4 * exact + 0.6 * float(veredicto.es_correcta)

trainset = [
    dspy.Example(
        pregunta="Que optimizador propone DSPy?",
        pasajes=["DSPy incluye MIPROv2, BootstrapFewShot y COPRO."],
        respuesta="MIPROv2, BootstrapFewShot y COPRO.",
    ).with_inputs("pregunta", "pasajes"),
    # ...anadir al menos 20 ejemplos mas para que MIPROv2 sea util
]

evaluator = Evaluate(devset=trainset, metric=metric, num_threads=4)
print("Score base:", evaluator(rag))

Trampa común: muchas métricas fallan en producción por comparar strings crudos. Usa siempre normalización (.lower(), .strip()), y cuando el dominio es ambiguo delega el veredicto final a un juez LLM (patrón LLM-as-a-judge). Con 20-50 ejemplos el optimizador ya obtiene ganancias visibles; a partir de 200 empieza a saturar.

Paso 8: optimización automática con MIPROv2

Ahora la magia. MIPROv2 (Multiprompt Instruction PRoposal Optimizer v2) trabaja en tres fases: bootstrapea few-shot examples aceptados por tu métrica, propone candidatos de instrucción con un modelo grounded en tu tarea, y busca la mejor combinación instrucción + demos mediante optimización bayesiana (TPE vía Optuna). Todo eso lo lanza una sola llamada:

from dspy.teleprompt import MIPROv2

optimizer = MIPROv2(
    metric=metric,
    prompt_model=task_lm,   # LM economico para proponer prompts
    task_model=lm,          # LM final que ejecutara el programa
    num_candidates=10,      # Instrucciones candidatas por predictor
    init_temperature=0.7,
    auto="medium",          # light / medium / heavy
)

compiled_rag = optimizer.compile(
    student=rag,
    trainset=trainset,
    max_bootstrapped_demos=4,
    max_labeled_demos=4,
    requires_permission_to_run=False,
)

print("Score compilado:", evaluator(compiled_rag))

Salida esperada tras 8-15 minutos de compilación (según tamaño del trainset y auto=medium):

Score base: 62.5
Score compilado: 84.3

Ese salto de unos 22 puntos es típico en el primer ciclo. Las mejoras marginales después son menores, pero MIPROv2 también reduce la varianza (el programa compilado responde de forma mucho más consistente ante variaciones del input). Con auto="heavy" puedes exprimir 5-8 puntos más a costa de multiplicar por 3-4 el gasto en tokens del juez.

Bucle de optimización bayesiana de MIPROv2 con bootstrapping, proposición de instrucciones y búsqueda TPE
MIPROv2 combina bootstrapping de demos, proposición de instrucciones y búsqueda bayesiana en un solo optimizador.

Paso 9: guardar y cargar programas compilados

Un programa compilado es un artefacto: instrucciones + demos + metadatos. Se guarda como JSON y se recarga sin tener que volver a pagar la compilación. En equipos de datos, este JSON entra al repositorio git como si fuera un modelo entrenado.

from pathlib import Path

compiled_rag.save("artifacts/rag_v1.json")

# Mas tarde, en produccion
rag_prod = RAG()
rag_prod.load("artifacts/rag_v1.json")

# La API es identica a la del programa original
resp = rag_prod(pregunta="...", pasajes=["..."])

Buena práctica: versiona los artefactos siguiendo la convención rag_vMAYOR.MENOR.json y registra en tu tabla de despliegue el hash SHA256 del fichero. Si el score cae después de un cambio de modelo (por ejemplo, Anthropic saca Claude Opus 5.1), recompila y compara antes de sustituir el artefacto en producción.

Paso 10: deploy en producción con FastAPI + MLflow

Para servir el programa compilado, envuelve DSPy en un endpoint FastAPI. Como bonus, registramos cada llamada en MLflow para trazabilidad y comparativa de versiones. Este es el patrón mínimo aceptado en producción (agosto 2026):

import dspy, mlflow
from fastapi import FastAPI
from pydantic import BaseModel

dspy.settings.configure(
    lm=dspy.LM("anthropic/claude-opus-5"),
    cache=True,  # Cache disco para peticiones repetidas
)

mlflow.set_tracking_uri("http://mlflow:5000")
mlflow.dspy.autolog()

rag_prod = RAG()
rag_prod.load("artifacts/rag_v1.json")

app = FastAPI(title="DSPy RAG Service")

class Req(BaseModel):
    pregunta: str
    pasajes: list[str]

@app.post("/ask")
def ask(req: Req):
    with mlflow.start_run():
        out = rag_prod(pregunta=req.pregunta, pasajes=req.pasajes)
        mlflow.log_param("n_pasajes", len(req.pasajes))
        mlflow.log_metric("resp_len", len(out.respuesta))
    return {"respuesta": out.respuesta, "contexto": out.contexto}

Lanza el servicio con uv run uvicorn main:app --host 0.0.0.0 --port 8000. La caché en disco de DSPy evita recomputar respuestas idénticas y reduce el coste hasta un 70 por ciento en tráfico repetitivo. El autolog de MLflow captura la traza completa: prompts finales, tokens gastados, latencia y score si adjuntas la métrica.

Trampa común: si despliegas en un contenedor Docker sin volumen persistente, la caché se pierde en cada reinicio. Monta un volumen en ~/.cache/dspy o desactiva la caché explícitamente con cache=False.

Comparativa: DSPy vs LangChain vs LlamaIndex vs Instructor

DSPy no reemplaza a las demás librerías; ocupa un hueco propio. Esta es la comparativa que uso en propuestas comerciales de 2026:

Framework Enfoque Punto fuerte Limitación clave
DSPy 3.3 Programas tipados + optimización automática Mejora medible (10-40 por ciento) con MIPROv2 Curva de aprendizaje conceptual; requiere dataset
LangChain Cadenas y agentes generalistas Ecosistema masivo de integraciones Prompts a mano; deuda técnica en versiones antiguas
LlamaIndex RAG y data ingestion Conectores a más de 200 fuentes de datos Menos flexible para agentes complejos
Instructor Salidas estructuradas con Pydantic Validación estricta y reintentos automáticos No optimiza prompts ni compone módulos

En 2026 lo habitual es combinar: LlamaIndex para ingesta, DSPy para el núcleo de razonamiento optimizado, Instructor cuando la salida estructurada es crítica (facturación, compliance) y LangGraph o el Claude Agent SDK para la orquestación multi-agente. Ninguno es una isla.

Casos de uso reales en producción

DSPy pasa la prueba de fuego cuando hay dataset y métrica clara. Estos son los tres patrones que veo repetirse en clientes:

  • QA multi-hop: agentes de soporte que consultan 3-4 fuentes antes de responder. MIPROv2 mejora especialmente el paso de síntesis final.
  • Extracción estructurada: pasar facturas o contratos a JSON. Combina dspy.Predict con Pydantic y define la métrica como F1 por campo.
  • Clasificación legal o médica: taxonomías con 50-200 clases. Con 300 ejemplos etiquetados MIPROv2 alcanza al fine-tuning tradicional a una fracción del coste.
"Con DSPy dejamos de tratar los prompts como manuscritos y empezamos a tratarlos como código compilable. Es la misma revolución que trajo el compilador C frente al ensamblador" — Omar Khattab, autor original de DSPy en Stanford NLP.

Recursos, libros y siguiente paso

Si quieres profundizar más allá del tutorial oficial, estos son los libros que llevo recomendando todo 2026 para el stack Python + LLMs:

Estación de trabajo de desarrollo Python con monitor 4K, teclado mecánico y terminal ejecutando MIPROv2
Setup real de trabajo: mini PC, monitor 4K, teclado mecánico y MIPROv2 compilando en background.

Preguntas frecuentes (FAQ)

¿Necesito muchos datos para usar MIPROv2 con DSPy?

No. Con 20-50 ejemplos ya obtienes mejoras notables. A partir de 200 el retorno marginal decrece. La calidad del dataset importa más que el volumen: ejemplos diversos superan siempre a cientos de duplicados.

¿Cuánto cuesta compilar un programa con MIPROv2?

Depende del tamaño del trainset, del preset (light/medium/heavy) y del modelo. Un caso típico (30 ejemplos, auto=medium, prompt_model=gpt-5.6-mini) ronda los 3-8 dólares. Con auto=heavy y Claude Opus 5 como juez puede subir a 40-80 dólares por compilación.

¿DSPy funciona con modelos locales vía Ollama?

Sí. Basta con instanciar dspy.LM("ollama/llama3.3") o el modelo que tengas descargado. Rinde bien para inferencia; para MIPROv2 conviene usar un modelo mayor (mixtral 8x22b o claude) como prompt_model para que las instrucciones propuestas sean de calidad.

¿Puedo usar DSPy con Claude Opus 5 y GPT-5.6 simultáneamente?

Sí. LiteLLM (el transporte que usa DSPy) permite mezclar proveedores en el mismo programa. Un patrón habitual es asignar el LM caro sólo a los módulos críticos con with dspy.context(lm=lm_opus): y dejar el resto en un modelo económico.

¿Cómo se compara MIPROv2 frente a BootstrapFewShot?

BootstrapFewShot sólo optimiza demos (few-shot examples); MIPROv2 optimiza demos e instrucciones conjuntamente con búsqueda bayesiana. En tareas simples pueden empatar; en pipelines multi-módulo (RAG, agentes) MIPROv2 gana sistemáticamente 5-15 puntos F1.

¿Puedo desplegar un programa DSPy sin FastAPI?

Sí. Cualquier framework de servidor Python vale (Flask, Litestar, aiohttp). También puedes exportar el programa como servidor MCP con FastMCP y consumirlo desde Claude Desktop o cualquier cliente MCP.

¿Cómo versiono los programas compilados en git?

Guarda el JSON generado por compiled.save() como cualquier artefacto: súbelo al repositorio, referencia el hash SHA256 en tu tabla de releases y automatiza la evaluación en CI. Si el score cae más de un umbral, bloquea el merge.

¿DSPy es adecuado para agentes con muchas herramientas?

Sí, mediante dspy.ReAct (o ReActV2 en la rama 3.3). Cada tool se declara como función Python normal y DSPy la convierte en dspy.Tool. Para orquestaciones muy complejas conviene combinar DSPy con LangGraph o con el Claude Agent SDK.

Conclusión

Programar LLMs con DSPy en agosto de 2026 significa cambiar la mentalidad: en lugar de pelearte con cadenas de texto, defines contratos tipados, compones módulos, escribes una métrica y dejas que MIPROv2 encuentre el mejor prompt sobre tus datos. Las mejoras del 10 al 40 por ciento respecto al prompting manual no son marketing: aparecen incluso con datasets modestos (20-50 ejemplos) y son reproducibles. Combinado con Claude Opus 5 para la inferencia final y un modelo mini como juez, obtienes un pipeline óptimo en coste y calidad.

El siguiente paso natural: exporta tu programa compilado como servidor MCP (revisa nuestro tutorial de FastMCP), intégralo con un agente autónomo del Claude Agent SDK y orquesta varios agentes con LangGraph. En un mes puedes tener un sistema que se auto-mejora en producción cada vez que llega nueva anotación humana.

Compartir:

Comentarios

Cargando comentarios...