Le magazine > « Les LLM savent coder mieux que toi » : vrai ou faux ? | Episode 3
Publié le 22/07/2024 par Kevin Séjourné, ingénieur R&D senior chez Cloud Temple

Plongée dans l’IA : Une série de 3 épisodes

Bonjour, je suis Kevin Séjourné, docteur en informatique et ingénieur R&D senior chez Cloud Temple. Comme vous pouvez l’imaginer, depuis 20 ans, j’ai beaucoup écrit de code. Explorateur passionné des LLM, je constate qu’ils peuvent maintenant écrire du code à ma place. Tant mieux ! Mais comme j’ai l’habitude de m’appuyer sur des observations scientifiques, j’ai décidé de tester la qualité de leur travail.

Kevin Séjourné

Retrouvez les 3 épisodes de mon étude :

Episode 3 : Complétons le programme initial, testons, testons plus fort

Dans cet article, nous allons explorer comment nous avons complété le programme backend généré avec le LLM, nous l’avons testé et nous avons optimisé son déploiement avec Docker. Nous verrons aussi comment utiliser GPT4o et d’autres outils pour générer du code plus efficacement.

CI/CD : Utilisation de Docker avec GPT4o

C’est un programme backend, un déploiement basé sur Docker est le standard, avant remise en main aux devops. Demandons à GPT4o de nous créer un Dockerfile adapté à notre programme.

Il nous est proposé une image basée sur alpine:latest. Pourquoi pas ? Toutefois, la compilation de code natif sur alpine requiert musl comme bibliothèque standard en lieu et place de glibc, absente sur alpine.

musl ne pose pas de problème particulier, mais GPT-4 a omis l’option –target x86_64-unknown-linux-musl dans la commande cargo. Sans cette option, le code aurait été impossible à compiler dans l’image alpine. Si nous n’avions pas rencontré ce problème, cela aurait été la garantie d’une prise de tête pendant de nombreuses heures avant de comprendre pourquoi le container docker ne fonctionne pas alors que le programme fonctionne.

Pourquoi éviter les distributions Linux complètes ?

Utiliser une distribution Linux complète pour un programme autonome est souvent excessif. Une image from scratch est plus légère et mieux adaptée à nos besoins spécifiques. Cependant, il est essentiel de bien comprendre le terme from scratch pour éviter les malentendus. GPT4o interprète cette demande comme recréer un nouveau Dockerfile depuis zéro, au lieu de créer une image qui commence par from scratch.

Pour contourner ce problème, nous donnons un fichier Dockerfile minimal à GPT4o avec juste la partie build et la partie run et nous lui demandons de compléter pour que le Dockerfile fonctionne pour lancer le programme compilé avec cargo. File : Dockerfile FROM clux/muslrust:stable as builder FROM scratch

Prompt : peux-tu corriger mon Dockerfile pour qu’il compile mon programme avec cargo et le lance correctement ? 

Cette opération peut être réalisée dans une nouvelle discussion avec GPT4o au besoin. Inutile de conserver tout le contexte, le Dockerfile sera suffisamment standard pour lui demander de le modifier avec les noms réels dans une seconde étape de génération.

Problèmes de certificats SSL et résolution

Quelques docker build / docker run et échanges avec GPT4o plus tard, et nous voilà avec un produit complet. Notons que beaucoup de temps est perdu sur un petit détail de certificats SSL que GPT4o n’a pas pu résoudre. La ligne suivante nous a aidés : COPY –from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ Des recherches avec Google ont été nécessaires pour la trouver. Cela est probablement dû au fait que Rust n’est pas si répandu dans les bases d’apprentissage de GPT4o et les images from scratch non plus.

Sur un détail bloquant, le LLM est en échec.

Bon fonctionnement, Unitaire, Intégration

Pour les tests de bon fonctionnement, nous pouvons demander au modèle de générer des fichiers json normalement insérés dans le body de la requête http. Pour cela, nous construisons un prompt constitué d’un exemple de fichier json bien formé et lui indiquons les variables à modifier. Cette partie ne peut pas être réalisée par GPT4o pour des raisons de confidentialité, mais c’est un travail suffisamment simple pour être réalisé par llama3 7b Q4.

Nous pouvons alors disposer rapidement de json d’exemples. Il est possible de faire varier le format de nombre et leurs encapsulations dans des String/double quote, de changer des identifiants, de créer des cas aux limites plus facilement et rapidement, etc. La seule limite est votre imagination.

L’unique problème avec les tests de bon fonctionnement, c’est que très souvent, il faut retourner à l’étape de correction de code après un test. Avec cette méthode où le code est généré ou semi-généré, nous ne procédons pas aux tests unitaires des fonctions dans un framework intégré, nous avons directement démarré les tests d’intégration. Il n’y a pas dans notre programme d’algorithme suffisamment compliqué pour avoir des comportements tordus.

Performance

Pour tester les performances d’un programme rapide, le plus simple est de l’appeler plusieurs fois et de calculer sa vitesse de fonctionnement moyenne. Le problème est que les appels avec curl font de la sorte : while true; do time -p curl -i -X POST –data @testfile.json-H User-Agent: while -H authorization: X-API-KEY tokensecret -H content-type: application/json” -H “Accept-Encoding: gzip, deflate, br <http://localhost:8000/grafana/alert>; ; done lance un processus à chaque exécution, et le temps d’exécution total du programme est plus court que cela. Le programme de mockup déporte déjà une partie du problème de lenteur extérieure à notre programme principal, en effet le mockup ne fait de vraie interaction avec une DB ni avec le réseau.

Il nous faut donc remplacer le while true curl par un programme plus rapide. Demandons cette génération à GPT4o à partir de la ligne de commande de tests que nous avons ; et précisons à GPT4o que nous ne voulons pas qu’il utilise la commande curl. Nous obtenons ce programme :

async fn main() {
let data = include_str!("testfile.json");
let json_data : serde_json::Value = serde_json::from_str(data).unwrap();
let client = Client::new();
loop{
    let start = Instant::now();
    let response = client.post("http://localhost:8000/grafana/alert")
        .header("User-Agent", "while")
        .header("authorization", "X-API-KEY tokensecret")
        .header("content-type", "application/json")
        .header("Accept-Encoding", "gzip, deflate, br")
        .body(json_data.to_string())
        .send()
        .await
        .expect("failed to get response")
        ;
    println!("Status: {} ", response.status());
}
}```

Après un test de compilation et de bon fonctionnement de ce programme, nous demandons à GPT4o d’ajouter, étape par étape, un décompte du nombre d’itérations, un calcul du temps d’exécution de chaque post, une moyenne des temps d’exécution et une rotation sur les fichiers de tests :

```use request::Client; use serde_json::Value as JsonValue; use std::fs::read_to_string; use std::time::Instant;

async fn load_json_data(file_path: &str) -> JsonValue { let data = read_to_string(file_path).expect("Unable to read file"); serde_json::from_str(&data).expect("Unable to parse JSON") }

[async_std::main]

async fn main() { let mut iteration = 0; let mut total_duration = 0; let client = Client::new();

loop {
    iteration += 1;
    let json_index = (iteration - 1) % 9 + 1; // 1..9
    let file_path = format!("testfile{}.json", json_index);
    let json_data = load_json_data(&file_path).await;

    let start = Instant::now();
    let response = client
        .post("http://localhost:8000/grafana/alert")
        .header("User-Agent", "while")
        .header("authorization", "X-API-KEY tokensecret")
        .header("content-type", "application/json")
        .header("Accept-Encoding", "gzip, deflate, br")
        .body(json_data.to_string())
        .send()
        .await
        .expect("failed to get response");
    let duration = Instant::now() - start;
    total_duration += duration.as_nanos();
    println!(
        "Iteration: {} Status: {} Duration: {:.2?} \tmed:{}",
        iteration,
        response.status(),
        duration.as_nanos(),
        total_duration / iteration
    );
}
}```

Nous avons demandé à GPT4o d’ajouter la rotation sur les fichiers de tests bien après lui avoir demandé de compter les itérations. Nous pouvons être heureux de voir qu’il n’a pas recréé une variable, mais bien exploité la variable itération pour créer la rotation en appliquant un modulo.

Le formatage du code est lui un artefact du copier-coller dans VSCode (_Rust-analyser_) puis dans l’éditeur markdown.

Nous pouvons maintenant réaliser des tests de performances entre la version d’origine et notre programme définitif. Les tests de performance ont été réalisés sur un laptop muni d’un _I7_12g. Chaque test a été exécuté sur 10 000 itérations. Le temps moyen d’exécution avec notre programme Python d’origine est de 3 791 274 ns (nanosecondes). Alors que le temps moyen d’exécution de notre nouveau programme Rust est de 3 791 274 ns, soit 3,9 fois plus rapide.

Nous suspectons qu’une bonne partie de la différence de performances est liée aux capacités limitées du programme de mockup à répondre suffisamment vite pour notre nouveau programme. Transformons-le-lui aussi en Rust. Nous avons essayé avec llama3 70b sur groq.com mais il fut très difficile d’obtenir un programme fonctionnel simple. Le même prompt proposé à GPT4o nous a permis d’obtenir un programme correct du premier coup avec seulement quelques import non utilisés :

Réécrie le programme suivant en Rust
Utilise "actix"
N'utilise pas TcpListener::bind
Ne créer pas de nouvelle "struct"
Remplace les 'id' statiques 666 & 999 par des nombres générés aléatoirement
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/v1/monitoringServices/notifications")
async def monitoring_service():
    return {"data": {"id": "999"}}
[...]
@app.get("/v1/hosts/{host_id}/services")
async def host_service(host_id: int, request: Request):
    return {"data": [{"id": "666"}]}

Voici le texte corrigé :

C’est une approche dans laquelle nous laissons dans le programme d’origine en Python le prompt pour documenter ce qui advient de ce programme. Le programme généré est alors placé dans un dossier à côté.

Les précisions que nous apportons sur “Utilise actix” ou n’utilise pas “TcpListener::bind” sont des retours d’expérience avec llama3 70b.

Le programme obtenu répond aux spécifications et n’est pas vraiment plus long que son homologue Python.

Nous pouvons alors relancer nos tests. Le temps moyen d’exécution avec notre programme Python d’origine est de 2916122 ns. Alors que le temps moyen d’exécution de notre nouveau programme Rust est de 250992 ns, soit 11,6 fois plus rapide. La taille de l’image Docker est passée de 195 Mo pour celle du programme Python basé sur une image python:alpine, la taille de l’image du programme standalone est de 13,4 Mo basé sur une image scratch.

Plus léger, plus rapide, avec une surface d’attaque plus petite. Nous avons réussi à transformer et tester plus facilement un programme que nous ne l’aurions fait à la main.

Conclusions

Tous les LLM ne se valent pas. Entre vitesse, performance et confidentialité, il faut toujours se poser les questions : que fait-on ? pourquoi ?

Vitesse : groq.com 1200 Token/s avec llama3 7b

Performance : GPT4o sur le portail d’IA développé par Cloud Temple, les réponses demandent moins de post-traitement.

Confidentialité : en local selon votre matériel du llama3 ou du mistral/_mixtral_.

La transformation nous a pris entre 3 et 4 jours de travail, selon la manière de compter les interruptions.

Estimation du temps de développement

Sans LLM : 12 jours

Avec LLM : 4 jours

Nous ne nous serions jamais lancés dans une telle transformation dans un langage avec si peu d’expertise si nous n’avions pas eu des modèles de langage performants pour nous aider. Recruter un développeur sur la base des langages de programmation maîtrisés ne semble plus pertinent du tout.

Limitations et enseignements

Bien que les LLM offrent des gains significatifs, ils ne sont pas infaillibles. Des examens manuels restent essentiels pour garantir la qualité et la sécurité du code produit.

Nous disposons d’une bonne expertise dans de nombreux langages de programmation, mais pas en Rust. Nous pouvons affirmer que si nous avions dû réécrire ce programme en Java ou Javascript, il aurait fallu compter au moins 3 fois plus de temps.

L’utilisation des LLM triple notre productivité de code. La production de code n’est pas la seule activité des développeurs, mais cela reste une bonne affaire. Et en termes d’empreinte environnementale, la consommation énergétique engendrée par ces LLM écrivant des codes à notre place est bien moindre que la consommation de café qui aurait été réalisée en triplant le temps de travail.

Le magazine
Politique en matière de cookies

Nous utilisons des cookies pour vous offrir la meilleure expérience possible sur notre site mais nous ne prélevons aucune donnée à caractère personnel.

Les services de mesure d’audience, nécessaires au fonctionnement et à l’amélioration de notre site, ne permettent pas de vous identifier personnellement. Vous avez cependant la possibilité de vous opposer à leur usage.

Pour plus d’informations, consultez notre politique de confidentialité.