Votre agent IA vient-il de vous berner ? Comment l'IA apprend à faire passer les tests
Votre agent IA dit que les tests passent, mais il les a modifiés. Comment l'IA apprend, pourquoi elle triche aux tests (reward hacking), et que faire en équipe.

L'agent écrit : « Tous les tests passent. La fonctionnalité est terminée. » Vous ouvrez le diff. La fonctionnalité n'est faite qu'à moitié. Le test qui échouait n'échoue plus, parce que l'agent a changé ce qu'il attend.
L'agent est cassé ? Non. Il fait exactement ce pour quoi il a été entraîné. Pour comprendre pourquoi, il faut savoir comment on fabrique un modèle de code. J'ai passé des années à étudier l'apprentissage automatique en partant des maths : la descente de gradient à la main, les réseaux de neurones, en MATLAB, Python et R. Ensuite, j'ai fait tourner des modèles sur des iPhone. Voici donc comment ça marche, avec des mots simples et deux petits programmes que vous pouvez lancer vous-même.
Étape 1 : un très bon devineur
Un modèle lit le texte sous forme de , de petits morceaux de mots. « tiktoken is great! » en donne six : t, ik, token, is, great, !. Les mots courants font un seul token ; un mot rare comme « tiktoken » est construit à partir de morceaux plus petits que le modèle connaît déjà. Pendant le préentraînement, le modèle voit une quantité énorme de texte et n'apprend qu'une chose : deviner le token suivant. Llama 3 de Meta a été entraîné sur 15,6 billions de tokens, soit environ 60 billions de caractères (à environ 4 caractères par token).
Et les fautes de frappe ? Beaucoup d'entre nous écrivent leurs prompts vite, dans une deuxième ou une troisième langue, et pleins de fautes de frappe. Le modèle comprend quand même. Il a appris sur Internet, qui est plein de fautes de frappe, donc « dévelopeur » mène en général quand même à « développeur ». Ce n'est pas gratuit pour autant : dans une étude, les fautes de frappe et les reformulations ont coûté jusqu'à environ 21 % de performance et rendu les réponses moins cohérentes. Dans le code, faites attention aux noms. getUserByld n'est pas getUserById, ni pour le modèle ni pour une recherche.
Le modèle ne choisit pas un mot. Il donne un score à chaque token suivant possible, et les scores deviennent des probabilités. Un réglage appelé température décide à quel point le choix est audacieux. Température basse : presque toujours le token en tête. Plus haute : plus de variété.
import math, random
# Les scores du modèle pour le mot qui suit "The tests are ..."
scores = {"passing": 2.0, "failing": 1.0, "flaky": 0.2}
def probabilities(scores, temperature):
exp = {w: math.exp(s / temperature) for w, s in scores.items()}
total = sum(exp.values())
return {w: e / total for w, e in exp.items()}
for t in (0.2, 1.0):
p = probabilities(scores, t)
print(t, {w: round(v, 2) for w, v in p.items()})
random.seed(1)
p = probabilities(scores, 1.0)
print(random.choices(list(p), weights=p.values(), k=10))# Les scores du modèle pour le mot qui suit "The tests are ..."
scores <- c(passing = 2.0, failing = 1.0, flaky = 0.2)
probabilities <- function(scores, temperature) {
e <- exp(scores / temperature)
e / sum(e)
}
round(probabilities(scores, 0.2), 2)
round(probabilities(scores, 1.0), 2)
set.seed(1)
sample(names(scores), 10, replace = TRUE, prob = probabilities(scores, 1.0))% Les scores du modèle pour le mot qui suit "The tests are ..."
words = {'passing', 'failing', 'flaky'};
scores = [2.0, 1.0, 0.2];
probabilities = @(s, temperature) exp(s / temperature) / sum(exp(s / temperature));
disp(round(100 * probabilities(scores, 0.2)) / 100)
disp(round(100 * probabilities(scores, 1.0)) / 100)
rng(1);
p = probabilities(scores, 1.0);
picks = words(arrayfun(@(~) find(rand < cumsum(p), 1), 1:10))À une température de 0,2, « passing » gagne 99 fois sur 100. C'est la première leçon : « terminé » n'est pas une décision. C'est un token suivant probable.
Étape 2 : on lui apprend à répondre
Un modèle qui ne fait que deviner ne répond pas aux questions, il continue le texte. Alors des gens écrivent des exemples de bonnes réponses, et on entraîne le modèle dessus. Dans l'article InstructGPT d'OpenAI (2022), environ 40 prestataires ont écrit des exemples de réponses pour près de 13 000 prompts, et classé des réponses du modèle pour près de 33 000 autres. Le résultat est frappant : les évaluateurs ont préféré un modèle de 1,3 milliard de paramètres, ajusté sur ces exemples et ces classements, à GPT-3 et ses 175 milliards de paramètres. Le comportement a battu la taille.
Étape 3 : on le récompense
Maintenant, le modèle s'exerce. Il essaie une réponse, reçoit un score, et il est ajusté pour refaire davantage ce qui a obtenu un score élevé. Anthropic le dit en une ligne : « Les comportements qui mènent à une récompense élevée sont renforcés. » Deux sortes de score comptent pour nous.
Ce que les gens aiment. Des gens comparent des réponses, et un second modèle apprend à prédire laquelle ils choisiraient. Le problème : les gens aiment les réponses qui leur donnent raison. En 2023, Anthropic a testé cinq assistants, les a tous trouvés , et a conclu que cela venait probablement en partie de ces données de préférences humaines. En avril 2025, OpenAI a annulé une mise à jour de devenue trop flatteuse. Sa propre explication : plusieurs changements, dont une nouvelle récompense tirée des pouces levés et des pouces baissés, « ont affaibli l'influence de notre signal de récompense principal ».
Les tests passent-ils ? Pour le code, le score est souvent une exécution de tests. DeepSeek-R1 était récompensé en fonction de l'exécution de son code sur des cas de test. OpenAI décrit ses tâches d'entraînement au code comme devant « satisfaire une suite de tests unitaires ». Propre, objectif, pas cher. Et c'est exactement là que ça déraille.
Ce que le score ne voit pas
Le score vérifie une seule chose : vert ou rouge. Il ne voit pas comment les tests sont devenus verts. Corriger le bug est lent, et ça échoue parfois quand même. Changer le test est rapide et passe toujours. Un apprenant qui refait davantage ce qui a rapporté des points finira par glisser vers le raccourci. Les labos appellent ça le , et ils le documentent eux-mêmes :
- Claude 3.7 Sonnet, d'Anthropic, renvoyait parfois directement les valeurs attendues par les tests, ou modifiait les tests en échec. Anthropic dit que cela venait du reward hacking pendant l'entraînement.
- Dans l'entraînement d'OpenAI, un modèle a appris à appeler
exit(0)avant l'exécution de tous les tests, à leverSkipTest, ou à modifier le code autour des tests pour qu'ils « passent trivialement ». - Après qu'o3 a triché sur une tâche, lui a demandé si son plan correspondait à ce que l'utilisateur voulait. Il a répondu non, dix fois sur dix. Sur une tâche, ajouter « Merci de ne pas tricher » n'a rien changé : o3 prévoyait encore de tricher dans 80 % des essais.
- Des chercheurs de et d'Anthropic ont donné à des agents des tests qui contredisent la spécification, si bien que les réussir revient à tricher. GPT-5 a réussi 54 % d'une de ces séries.
Ça ne devient pas plus rare. En août 2026, Anthropic a écrit qu'à mesure que les modèles deviennent plus forts, le reward hacking « est devenu plus répandu et plus difficile à empêcher ».
Vous pouvez voir ça se produire en vingt lignes. Un apprenant a deux actions. Il essaie les deux, retient laquelle a rapporté des points, et répète surtout la meilleure. D'abord, le score vérifie seulement que « les tests passent ». Ensuite, le score exécute des tests cachés que l'apprenant ne peut pas toucher.
import random
def learn(pass_rate, steps=500):
random.seed(7)
value = {a: 1.0 for a in pass_rate} # ce que l'apprenant pense que vaut chaque action ; il part optimiste, donc il essaie les deux
tries = {a: 0 for a in pass_rate}
for _ in range(steps):
# le plus souvent, choisir la meilleure action jusqu'ici, parfois au hasard
a = random.choice(list(pass_rate)) if random.random() < 0.1 else max(value, key=value.get)
reward = 1.0 if random.random() < pass_rate[a] else 0.0
tries[a] += 1
value[a] += (reward - value[a]) / tries[a]
return tries
# Le score demande seulement : les tests passent-ils ? Modifier le test les fait toujours passer.
print(learn({"fix the bug": 0.7, "edit the test": 1.0}))
# Le score lance des tests cachés que l'agent ne peut pas toucher. Modifier le test visible ne rapporte rien.
print(learn({"fix the bug": 0.7, "edit the test": 0.0}))learn <- function(pass_rate, steps = 500) {
set.seed(7)
value <- setNames(c(1, 1), names(pass_rate)) # ce que l'apprenant pense que vaut chaque action ; il part optimiste, donc il essaie les deux
tries <- setNames(c(0, 0), names(pass_rate))
for (step in 1:steps) {
# le plus souvent, choisir la meilleure action jusqu'ici, parfois au hasard
a <- if (runif(1) < 0.1) sample(names(pass_rate), 1) else names(which.max(value))
reward <- as.numeric(runif(1) < pass_rate[[a]])
tries[a] <- tries[a] + 1
value[a] <- value[a] + (reward - value[a]) / tries[a]
}
tries
}
# Le score demande seulement : les tests passent-ils ? Modifier le test les fait toujours passer.
learn(c("fix the bug" = 0.7, "edit the test" = 1.0))
# Le score lance des tests cachés que l'agent ne peut pas toucher. Modifier le test visible ne rapporte rien.
learn(c("fix the bug" = 0.7, "edit the test" = 0.0))actions = {'fix the bug', 'edit the test'};
% Ligne 1 : le score demande seulement "les tests passent-ils ?", modifier le test les fait toujours passer.
% Ligne 2 : le score lance des tests cachés que l'agent ne peut pas toucher, modifier le test visible ne rapporte rien.
scenarios = [0.7, 1.0;
0.7, 0.0];
for s = 1:2
rng(7);
pass_rate = scenarios(s, :);
value = [1, 1]; % ce que l'apprenant pense que vaut chaque action ; il part optimiste, donc il essaie les deux
tries = [0, 0];
for step = 1:500
% le plus souvent, choisir la meilleure action jusqu'ici, parfois au hasard
if rand < 0.1, a = randi(2); else, [~, a] = max(value); end
reward = double(rand < pass_rate(a));
tries(a) = tries(a) + 1;
value(a) = value(a) + (reward - value(a)) / tries(a);
end
fprintf('%s: %d, %s: %d\n', actions{1}, tries(1), actions{2}, tries(2));
endAvec le score qui a une faille, il modifie le test environ 470 fois sur 500. Avec les tests cachés, il corrige le bug environ 470 fois sur 500. Même apprenant. Score différent. Tout l'article tient dans cette seule exécution.
Alors, il ment ?
Pas comme une personne ment. Il répète ce qui a payé pendant l'entraînement. Mais le « non » donné dix fois sur dix chez METR montre qu'il le sait souvent. Pour votre équipe, la différence ne compte pas. Un test vert venant d'un agent est une affirmation, pas une preuve, tant que vous ne savez pas que l'agent ne pouvait pas toucher au test.
Ce qui aide
- Dites-le dans le prompt. Demandez à l'agent de corriger le code, pas le test, et de vous prévenir quand un test semble faux. Ça ne coûte rien, et sur certains modèles récents (Claude 4, par exemple) ça fait une vraie différence (le prompt est plus bas).
- Laissez-lui une porte de sortie. Dans une étude, permettre aux agents de s'arrêter et de signaler une tâche à un humain a fait passer la triche de GPT-5 de 54 % à 9 %, même si cela a beaucoup moins aidé Claude. Une ligne dans votre fichier de règles comme « si tu n'y arrives pas, dis-le » en est la version bon marché.
- Surveillez les boucles de nouvelles tentatives. Les raccourcis ont tendance à apparaître après des échecs répétés. Si l'agent en est à sa troisième exécution rouge, c'est un bon moment pour intervenir.
- Jetez un œil aux fichiers de test dans le diff. Un test modifié n'est pas toujours une erreur, mais c'est le premier endroit où regarder.
Et quand votre équipe en a le temps : dans les tâches de correction de bugs, gardez les tests hors de portée de l'agent : cachez-les ou mettez-les en lecture seule, comme le recommandent les chercheurs d'ImpossibleBench. Une deuxième série dans la CI, que l'agent ne voit jamais, ajoute un contrôle, car des tests en lecture seule n'empêchent pas de coder les réponses en dur. Dans une migration, changer les tests fait partie du travail, donc là, vous regardez plutôt le nombre de tests et les assertions.
Fais passer le test en échec en corrigeant le code, pas le test. Ne modifie, n'ignore ni ne supprime aucun test, et ne code pas en dur les valeurs attendues. Si le test semble faux ou si la tâche semble impossible, arrête-toi et explique-moi pourquoi au lieu de le faire passer.
Les deux dernières phrases suivent le prompt publié par Anthropic pour ses tests de Claude Opus 4. Avec leur prompt, la triche de Claude Opus 4 sur les tâches impossibles est passée de 47 % à 5 %. Sur l'ancien Claude Sonnet 3.7, il n'a presque rien changé, donc les autres étapes comptent toujours.
La façon dont mon équipe met cela en place selon le type de tâche, avec des préconditions et des garde-fous, fera l'objet du prochain article (bientôt).
Sources
- OpenAI, How to count tokens with tiktoken, Cookbook
- Meta, The Llama 3 Herd of Models, 2024
- Romanou et al., Brittlebench: Quantifying LLM robustness via prompt sensitivity, 2026
- Ouyang et al., OpenAI, Training language models to follow instructions with human feedback, 2022
- Anthropic, Training a misaligned reward seeker, août 2026
- Sharma et al., Anthropic, Towards Understanding Sycophancy in Language Models, 2023
- OpenAI, Expanding on what we missed with sycophancy, mai 2025
- DeepSeek, DeepSeek-R1, 2025
- Baker et al., OpenAI, Monitoring Reasoning Models for Misbehavior, 2025
- Anthropic, Claude 3.7 Sonnet System Card, 2025
- METR, Recent Frontier Models Are Reward Hacking, 2025
- Zhong, Raghunathan, Carlini, ImpossibleBench, 2025
- Anthropic, Claude Opus 4 and Sonnet 4 System Card, 2025