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")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:
Formular o Problema: Transformou um requisito vago em objetivos de fitness concretos.
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! 🎉