Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Workshop Prático: Sinergia com IA - Potencializando SBSE com LLMs

Objetivo: Utilizar um Modelo de Linguagem Grande (LLM) como assistente para formular um problema de otimização, ajudando a definir uma função de fitness complexa e a criar um operador de mutação inteligente.

Problema-alvo: Otimizar a alocação de tarefas em uma equipe de desenvolvimento de software, usando um LLM para definir os critérios de uma “boa” alocação e para sugerir uma heurística de melhoria (mutação).


Parte 1: Configuração do Ambiente

Nesta primeira etapa, vamos instalar as bibliotecas necessárias (openai para interagir com o LLM e deap para o algoritmo genético) e configurar nossa chave de API.

# @title Instalação das bibliotecas
!pip install openai deap numpy matplotlib -q

[notice] A new release of pip is available: 25.0.1 -> 25.2
[notice] To update, run: pip install --upgrade pip
# @title Importações e Configuração da API
import os
import random
import numpy as np
import matplotlib.pyplot as plt
from getpass import getpass
from typing import List, Dict, Tuple, Callable
from textwrap import dedent

from openai import OpenAI

from deap import base, creator, tools, algorithms

# --- Configuração da Chave de API da OpenAI ---
# Usamos getpass para não exibir a chave no notebook.
# Você pode obter uma chave em: https://platform.openai.com/api-keys
try:
    # Tenta carregar de uma variável de ambiente (melhor prática)
    api_key = os.environ['OPENAI_API_KEY']
    if not api_key.startswith('sk-'):
        raise ValueError("Chave de API inválida.")
    client = OpenAI(api_key=api_key)
    print("✅ Chave da API da OpenAI carregada da variável de ambiente.")
except (KeyError, ValueError):
    # Se não encontrar ou for inválida, pede para o usuário digitar
    print("🔑 Chave da API não encontrada no ambiente.")
    api_key = getpass('Por favor, insira sua chave da API da OpenAI: ')
    if not api_key.startswith('sk-'):
        print("❌ Chave de API inválida. Deve começar com 'sk-'.")
        client = None
    else:
        client = OpenAI(api_key=api_key)
        print("✅ Cliente OpenAI configurado com sucesso!")

# Configura a semente para reprodutibilidade dos resultados
random.seed(42)
np.random.seed(42)
🔑 Chave da API não encontrada no ambiente.
❌ Chave de API inválida. Deve começar com 'sk-'.

Parte 2: Usando “Persona Prompting” para Definir a Fitness

Nosso primeiro desafio é traduzir a ideia vaga de “boa alocação de tarefas” em uma função matemática que o algoritmo possa otimizar. Para isso, vamos pedir ajuda a um especialista: um LLM atuando como Gerente de Projetos Ágil.

2.1. Definição do Cenário

Primeiro, vamos modelar nosso problema em Python. Temos uma equipe com desenvolvedores de diferentes níveis de sênioridade e um conjunto de tarefas com diferentes estimativas de complexidade.

# @title Modelagem do Problema: Desenvolvedores e Tarefas

# Dicionário de desenvolvedores: ID -> {senioridade, custo/hora}
DEVELOPERS: Dict[int, Dict] = {
    0: {"name": "Ana (Sênior)", "level": "senior", "cost_hour": 150},
    1: {"name": "Bruno (Pleno)", "level": "mid", "cost_hour": 100},
    2: {"name": "Carla (Júnior)", "level": "junior", "cost_hour": 70},
    3: {"name": "Daniel (Sênior)", "level": "senior", "cost_hour": 160}
}

# Lista de tarefas: ID -> {descrição, complexidade em horas}
TASKS: Dict[int, Dict] = {
    0: {"desc": "Configurar CI/CD", "complexity": 20},
    1: {"desc": "Desenvolver tela de login", "complexity": 8},
    2: {"desc": "Criar CRUD de usuários", "complexity": 16},
    3: {"desc": "Refatorar módulo de pagamento", "complexity": 40},
    4: {"desc": "Escrever testes unitários para API", "complexity": 12},
    5: {"desc": "Corrigir bug visual no mobile", "complexity": 4},
    6: {"desc": "Otimizar query do banco de dados", "complexity": 24},
    7: {"desc": "Documentar a arquitetura", "complexity": 10}
}

NUM_DEVS = len(DEVELOPERS)
NUM_TASKS = len(TASKS)

print(f"👥 Temos {NUM_DEVS} desenvolvedores e {NUM_TASKS} tarefas.")
👥 Temos 4 desenvolvedores e 8 tarefas.

2.2. Criando o Prompt de Persona

Agora, vamos construir o prompt. Diremos ao LLM para agir como um especialista e nos ajudar a definir os objetivos de otimização. Note como descrevemos o contexto e o formato da resposta desejada.

# @title Interação com o LLM para definir objetivos de fitness

def get_fitness_objectives_from_llm() -> str:
    """
    Usa a técnica de Persona Prompting para pedir a um LLM que sugira
    objetivos de otimização para o problema de alocação de tarefas.

    Returns:
        str: A resposta do LLM com os objetivos sugeridos.
    """
    if not client:
        return "Cliente OpenAI não configurado. Usando texto mockado."

    persona_prompt = dedent(f"""
    Aja como um Gerente de Projetos Ágil experiente e especialista em otimização de equipes.

    Estou tentando otimizar a alocação de {NUM_TASKS} tarefas para uma equipe de {NUM_DEVS} desenvolvedores. Minha representação da solução é uma lista de inteiros, onde o índice é o ID da tarefa e o valor é o ID do desenvolvedor alocado.

    Minha equipe é a seguinte:
    {DEVELOPERS}

    As tarefas são:
    {TASKS}

    Preciso de sua ajuda para definir uma função de fitness multi-objetivo. Por favor, sugira 3 objetivos conflitantes que definem uma 'boa' alocação. Para cada objetivo, explique:
    1. O nome do objetivo (ex: 'Minimizar Custo Total').
    2. A lógica de negócio por trás dele.
    3. Como calculá-lo matematicamente a partir da lista de alocação.
    4. Se o objetivo deve ser minimizado ou maximizado.

    Formate sua resposta de forma clara e estruturada.
    """)

    print("🤖 Enviando prompt para o LLM...\n")
    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "Você é um assistente especialista em engenharia de software e otimização."},
                {"role": "user", "content": persona_prompt}
            ]
        )
        return response.choices[0].message.content
    except Exception as e:
        return f"Ocorreu um erro ao contatar a API: {e}"

# Executa a função e imprime a sugestão do LLM
llm_suggestion_fitness = get_fitness_objectives_from_llm()
print("💡 Sugestão do LLM para os Objetivos de Fitness:\n")
print(llm_suggestion_fitness)
💡 Sugestão do LLM para os Objetivos de Fitness:

Cliente OpenAI não configurado. Usando texto mockado.

2.3. Implementando a Função de Fitness

Com as excelentes sugestões do LLM (ou o texto mockado, caso a API falhe), podemos traduzir essa lógica para uma função Python que o DEAP possa usar. Vamos implementar os três objetivos: makespan, total_cost e mismatch_penalty.

# @title Tradução da sugestão do LLM para código Python

def evaluate_allocation(individual: List[int]) -> Tuple[float, float, float]:
    """
    Calcula os três objetivos de fitness para uma dada alocação (indivíduo).

    Parameters:
        individual (List[int]): Uma lista onde o índice é a tarefa e o valor é o dev.

    Returns:
        Tuple[float, float, float]: Uma tupla contendo (makespan, total_cost, mismatch_penalty).
    """
    workload = [0.0] * NUM_DEVS
    total_cost = 0.0
    mismatch_penalty = 0.0

    for task_id, dev_id in enumerate(individual):
        task = TASKS[task_id]
        dev = DEVELOPERS[dev_id]

        workload[dev_id] += task['complexity']
        total_cost += task['complexity'] * dev['cost_hour']

        if dev['level'] == 'junior' and task['complexity'] > 20:
            mismatch_penalty += 100
        elif dev['level'] == 'senior' and task['complexity'] < 8:
            mismatch_penalty += 25
        
    makespan = max(workload)
    return makespan, total_cost, mismatch_penalty

creator.create("FitnessMulti", base.Fitness, weights=(-1.0, -1.0, -1.0))
creator.create("Individual", list, fitness=creator.FitnessMulti)

toolbox = base.Toolbox()

toolbox.register("attr_dev", random.randint, 0, NUM_DEVS - 1)
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_dev, n=NUM_TASKS)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)

toolbox.register("evaluate", evaluate_allocation)

print("✅ Função de fitness e DEAP configurados.")

test_individual = toolbox.individual()
fitness_values = toolbox.evaluate(test_individual)
print(f"\n🧪 Testando um indivíduo aleatório: {test_individual}")
print(f"   - Fitness (Makespan, Custo, Penalidade): {fitness_values}")
✅ Função de fitness e DEAP configurados.

🧪 Testando um indivíduo aleatório: [0, 0, 2, 1, 1, 1, 0, 0]
   - Fitness (Makespan, Custo, Penalidade): (62.0, 16020.0, 0.0)

Parte 3: Usando “Chain-of-Thought” para Criar um Operador de Mutação

Um operador de mutação padrão poderia simplesmente trocar a alocação de uma tarefa para um desenvolvedor aleatório. Isso funciona, mas é “cego”. Podemos usar um LLM para sugerir uma heurística mais inteligente.

# @title Interação com o LLM para criar um operador de mutação inteligente

def get_mutation_operator_from_llm() -> str:
    """
    Usa a técnica de Chain-of-Thought (CoT) para pedir a um LLM que sugira
    um operador de mutação inteligente.

    Returns:
        str: A resposta do LLM com a sugestão.
    """
    if not client:
        return "Cliente OpenAI não configurado. Usando texto mockado."

    cot_prompt = dedent(f"""
    Estou usando um Algoritmo Genético para o problema de alocação de tarefas. A representação é uma lista de alocações `[dev_id_para_tarefa_0, ...]`.

    O operador de mutação padrão é o 'mutUniformInt', que re-aloca uma tarefa para um dev aleatório. Isso é simplista.

    Pense passo a passo e sugira um operador de mutação mais inteligente. A heurística deve tentar corrigir um problema óbvio na alocação, como focar em desenvolvedores sobrecarregados.

    Descreva:
    1. O nome da heurística.
    2. A lógica passo a passo.
    3. Por que é melhor que a mutação aleatória.
    4. Forneça um pseudocódigo claro.
    """)

    print("🤖 Enviando prompt CoT para o LLM...\n")
    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "Você é um especialista em algoritmos de otimização e meta-heurísticas."},
                {"role": "user", "content": cot_prompt}
            ]
        )
        return response.choices[0].message.content
    except Exception as e:
        return f"Ocorreu um erro ao contatar a API: {e}"

llm_suggestion_mutation = get_mutation_operator_from_llm()
print("💡 Sugestão do LLM para o Operador de Mutação:\n")
print(llm_suggestion_mutation)
💡 Sugestão do LLM para o Operador de Mutação:

Cliente OpenAI não configurado. Usando texto mockado.

3.1. Implementando o Operador de Mutação

Agora, traduzimos a sugestão do LLM para uma função Python e a registramos na toolbox do DEAP.

# @title Implementação do operador de mutação sugerido pelo LLM

def bottleneck_mutation(individual: List[int]) -> Tuple[List[int],]:
    """
    Implementa a heurística 'Mutação Guiada por Gargalo'.
    Move uma tarefa do desenvolvedor mais ocupado para o menos ocupado.
    """
    workloads = [0.0] * NUM_DEVS
    for task_id, dev_id in enumerate(individual):
        workloads[dev_id] += TASKS[task_id]['complexity']
    
    bottleneck_dev_id = np.argmax(workloads)
    idle_dev_id = np.argmin(workloads)

    if bottleneck_dev_id == idle_dev_id:
        task_to_mutate = random.randint(0, NUM_TASKS - 1)
        individual[task_to_mutate] = random.randint(0, NUM_DEVS - 1)
        return individual,

    tasks_of_bottleneck = [i for i, dev in enumerate(individual) if dev == bottleneck_dev_id]
    if not tasks_of_bottleneck:
        return individual,
    
    task_to_move = random.choice(tasks_of_bottleneck)
    individual[task_to_move] = idle_dev_id

    return individual,

toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", bottleneck_mutation)
toolbox.register("select", tools.selNSGA2)

print("✅ Operador de mutação inteligente registrado.")

original_ind = creator.Individual([0, 0, 0, 0, 1, 1, 2, 3])
print(f"\n🧪 Indivíduo Original: {original_ind}")
mutated_ind, = toolbox.mutate(original_ind.copy())
print(f"   Indivíduo Mutado: {mutated_ind}")
✅ Operador de mutação inteligente registrado.

🧪 Indivíduo Original: [0, 0, 0, 0, 1, 1, 2, 3]
   Indivíduo Mutado: [0, 0, 0, np.int64(3), 1, 1, 2, 3]

Parte 4: Execução e Análise

Com tudo configurado, vamos executar o algoritmo genético.

# @title Execução do Algoritmo Genético (NSGA-II)

def run_optimization():
    """Executa o fluxo de otimização completo."""
    population_size = 100
    generations = 50
    cx_prob = 0.7
    mut_prob = 0.2

    pop = toolbox.population(n=population_size)
    hof = tools.ParetoFront()
    
    stats = tools.Statistics(lambda ind: ind.fitness.values)
    stats.register("avg", np.mean, axis=0)
    stats.register("min", np.min, axis=0)

    algorithms.eaMuPlusLambda(pop, toolbox, mu=population_size, lambda_=population_size, 
                                cxpb=cx_prob, mutpb=mut_prob, ngen=generations,
                                stats=stats, halloffame=hof, verbose=True)
    
    return pop, stats, hof

final_pop, log, pareto_front = run_optimization()

print("\n🎉 Otimização concluída!")
print(f"🏆 Encontradas {len(pareto_front)} soluções na Fronteira de Pareto.")
gen	nevals	avg                         	min                   
0  	100   	[   69.52 16107.2     66.25]	[   40. 10960.     0.]
1  	83    	[   65.7  14571.8     89.25]	[   40. 10840.     0.]
2  	90    	[   64.64 13984.      82.75]	[   40. 10400.     0.]
3  	90    	[   62.66 13680.6     87.  ]	[   40. 10400.     0.]
4  	87    	[   61.06 13643.4     71.5 ]	[   40. 10400.     0.]
5  	90    	[   62.78 13424.6     73.75]	[   40. 10400.     0.]
6  	83    	[   64.4  13038.8     88.75]	[   40. 10160.     0.]
7  	89    	[   66.42 12788.4     88.75]	[  40. 9860.    0.]   
8  	90    	[   66.32 12744.      94.25]	[  40. 9860.    0.]   
9  	93    	[   68.94 12467.4     97.75]	[  40. 9800.    0.]   
10 	93    	[   70.14 12284.2    100.5 ]	[  40. 9800.    0.]   
11 	92    	[   74.78 11829.2    114.  ]	[  40. 9740.    0.]   
12 	87    	[   76.26 11783.8    120.  ]	[  40. 9740.    0.]   
13 	92    	[   76.54 11798.     114.25]	[  40. 9680.    0.]   
14 	82    	[   74.3  12028.4     92.75]	[  40. 9680.    0.]   
15 	92    	[   74.54 12016.8     93.  ]	[  40. 9680.    0.]   
16 	90    	[   77.82 11842.      98.  ]	[  40. 9500.    0.]   
17 	89    	[   75.68 11933.      93.25]	[  40. 9500.    0.]   
18 	89    	[   78.02 11747.      96.25]	[  40. 9380.    0.]   
19 	91    	[   77.54 11819.2     92.  ]	[  40. 9380.    0.]   
20 	86    	[   79.42 11699.2     95.75]	[  40. 9380.    0.]   
21 	95    	[   78.92 11718.      89.5 ]	[  40. 9380.    0.]   
22 	83    	[   79.66 11681.8     95.25]	[  40. 9380.    0.]   
23 	82    	[   83.46 11453.4    105.25]	[  40. 9380.    0.]   
24 	89    	[   81.06 11624.4     98.  ]	[  40. 9380.    0.]   
25 	94    	[   80.22 11742.6     96.  ]	[  40. 9380.    0.]   
26 	89    	[   78.24 11856.8     92.25]	[  40. 9380.    0.]   
27 	96    	[   78.58 11859.6     93.  ]	[  40. 9380.    0.]   
28 	90    	[   80.88 11633.2     98.75]	[  40. 9380.    0.]   
29 	90    	[   80.  11689.8    94.5]   	[  40. 9380.    0.]   
30 	96    	[   80.54 11696.8     95.25]	[  40. 9380.    0.]   
31 	92    	[   82.38 11528.2     99.  ]	[  40. 9380.    0.]   
32 	90    	[   81.28 11621.2     96.  ]	[  40. 9380.    0.]   
33 	90    	[   80.38 11642.4     94.25]	[  40. 9380.    0.]   
34 	87    	[   81.38 11570.6     95.5 ]	[  40. 9380.    0.]   
35 	90    	[   83.74 11420.8     98.75]	[  40. 9380.    0.]   
36 	85    	[   84.7 11411.4   101.5]   	[  40. 9380.    0.]   
37 	89    	[   83.44 11531.2    100.75]	[  40. 9380.    0.]   
38 	93    	[   85.04 11422.     101.  ]	[  40. 9380.    0.]   
39 	91    	[   84.08 11437.4     98.  ]	[  40. 9380.    0.]   
40 	86    	[   82.46 11511.6     93.75]	[  40. 9380.    0.]   
41 	93    	[   83.9 11473.4    98.5]   	[  40. 9380.    0.]   
42 	95    	[   83.68 11474.6     99.25]	[  40. 9380.    0.]   
43 	90    	[   81.08 11609.      92.5 ]	[  40. 9380.    0.]   
44 	91    	[   81.88 11558.4     93.5 ]	[  40. 9380.    0.]   
45 	96    	[   81.84 11551.      94.25]	[  40. 9380.    0.]   
46 	91    	[   81.54 11577.4     93.25]	[  40. 9380.    0.]   
47 	89    	[   81.28 11668.6     92.25]	[  40. 9380.    0.]   
48 	93    	[   80.7 11663.2    92.5]   	[  40. 9380.    0.]   
49 	86    	[   81.42 11563.4     93.5 ]	[  40. 9380.    0.]   
50 	88    	[   81.9  11562.2     97.25]	[  40. 9380.    0.]   

🎉 Otimização concluída!
🏆 Encontradas 94 soluções na Fronteira de Pareto.

4.1. Análise dos Resultados

A otimização multi-objetivo nos dá um conjunto de soluções que representam diferentes trade-offs. Vamos analisá-las.

# @title Análise das soluções na Fronteira de Pareto

print("--- Análise das Soluções na Fronteira de Pareto ---\n")

for i, solution in enumerate(pareto_front):
    makespan, cost, penalty = solution.fitness.values
    print(f"Solução #{i+1}:")
    print(f"  - Fitness: Makespan={makespan:.0f}h, Custo=R${cost:.2f}, Penalidade={penalty:.0f}")
    
    workload = [0.0] * NUM_DEVS
    dev_tasks = {dev_id: [] for dev_id in range(NUM_DEVS)}
    for task_id, dev_id in enumerate(solution):
        workload[dev_id] += TASKS[task_id]['complexity']
        dev_tasks[dev_id].append(TASKS[task_id]['desc'])
        
    print("  - Alocação:")
    for dev_id, tasks in dev_tasks.items():
        dev_name = DEVELOPERS[dev_id]['name']
        print(f"    - {dev_name}: {workload[dev_id]:.0f}h -> {tasks}")
    print("\n")
Fetching long content....

Conclusão

Neste laboratório, vimos como a IA generativa pode ser uma aliada no processo de SBSE. O LLM atuou como um consultor, ajudando a:

  1. Formular o Problema: Transformou um requisito vago em objetivos de fitness concretos.

  2. Projetar a Busca: Sugeriu uma heurística de mutação inteligente.

Isso demonstra uma nova fronteira na otimização, onde a criatividade humana é aumentada pela capacidade da IA.

🎉 LABORATÓRIO CONCLUÍDO! 🎉