Depuis le 20 juin 2024 et la sortie de Claude 3.5 Sonnet, Anthropic a marqué une étape structurante avec la disponibilité générale du tool use (function calling) sur son API, mais aussi sur Amazon Bedrock et Vertex AI. Ce n'est pas une évolution cosmétique : cela transforme un modèle de langage en un vrai agent capable d'appeler des outils externes, d'en lire les résultats, et d'itérer jusqu'à atteindre son objectif — sans intervention humaine entre chaque étape. Pour les équipes PHP, les implications sont concrètes et immédiates. Tu peux construire un agent qui analyse ta codebase comme le ferait un développeur senior : en allant chercher lui-même les données dont il a besoin, pas en travaillant sur ce que tu as bien voulu lui coller dans un prompt. C'est exactement ce qu'on va faire ici. Pas à pas, on va construire un agent de revue de code en PHP 8.3, capable d'appeler git diff pour voir ce qui a changé, de lancer PHPStan pour détecter les erreurs statiques, et de demander à Claude 3.5 Sonnet de synthétiser une revue structurée, directement postée en commentaire de pull request via GitHub Actions.
Tool use en GA depuis juin 2024 : de l'assistant à l'agent
La différence entre assistant et agent tient à une question de contrôle du flux. Avec un prompt classique, tu choisis quelles données passer à Claude et tu interprètes la réponse. Avec le tool use, c'est Claude qui décide quand appeler un outil et avec quels arguments — ton code n'est plus qu'un exécuteur de ses instructions.
Le protocole fonctionne en trois temps. D'abord, tu envoies à l'API une liste de définitions d'outils — chacune avec un nom, une description en langage naturel, et un schéma JSON des paramètres — en même temps que tes messages. Claude analyse la demande et répond soit avec une réponse finale (stop_reason: end_turn), soit avec un ou plusieurs blocs tool_use contenant le nom de l'outil et ses arguments calculés. Ton code exécute l'outil localement, renvoie le résultat dans un message tool_result, et la boucle recommence. C'est ce qu'on appelle la boucle agentique. Le modèle maintient un raisonnement cohérent à travers plusieurs appels successifs grâce à l'historique des messages, qui s'allonge à chaque itération. C'est ce qui rend les agents construits sur cette base qualitativement différents d'un simple pipeline de prompts enchaînés à la main.
Architecture de l'agent
L'agent qu'on va construire est un outil CLI autonome, sans dépendance à un framework. Il s'ajoute à n'importe quel projet existant avec trois fichiers PHP et un workflow YAML. Les dépendances sont volontairement légères : guzzlehttp/guzzle pour les appels HTTP, et phpstan/phpstan déjà présent dans la plupart des projets PHP sérieux. Aucune abstraction d'agent, aucun framework IA : juste du PHP 8.3 lisible.
AnthropicClient— wrapper HTTP typé vers l'API Anthropic, gère la sérialisation JSON et les en-têtes d'authentificationToolExecutor— exécutegit diffet PHPStan en local, nettoie et tronque l'output avant de le renvoyer à ClaudeCodeReviewAgent— orchestre la boucle agentique, définit les outils et construit le prompt de revuebin/code-review.php— point d'entrée CLI avec options--base-branch,--path,--output- GitHub Actions workflow — déclenche la revue sur chaque pull request et poste le résultat en commentaire
Le client HTTP vers l'API Anthropic
Il n'existe pas encore de SDK PHP officiel pour l'API Anthropic — un simple client HTTP suffit. Ce qui compte : l'en-tête anthropic-version: 2023-06-01 (requis par l'API), x-api-key pour l'authentification, et une sérialisation JSON correcte du payload. On utilise les constantes de classe typées introduites en PHP 8.3 pour documenter les valeurs fixes et éviter les erreurs de frappe silencieuses.
<?php
declare(strict_types=1);
namespace App\CodeReview;
use GuzzleHttp\Client;
final class AnthropicClient
{
private const string API_URL = 'https://api.anthropic.com/v1/messages';
private const string MODEL = 'claude-3-5-sonnet-20240620';
private const string API_VERSION = '2023-06-01';
private readonly Client $http;
public function __construct(private readonly string $apiKey)
{
$this->http = new Client(['timeout' => 120]);
}
/**
* @param list<array<string, mixed>> $messages
* @param list<array<string, mixed>> $tools
* @return array<string, mixed>
*/
public function chat(array $messages, array $tools = [], int $maxTokens = 4096): array
{
$payload = [
'model' => self::MODEL,
'max_tokens' => $maxTokens,
'messages' => $messages,
];
if ($tools !== []) {
$payload['tools'] = $tools;
}
$response = $this->http->post(self::API_URL, [
'headers' => [
'x-api-key' => $this->apiKey,
'anthropic-version' => self::API_VERSION,
'content-type' => 'application/json',
],
'json' => $payload,
]);
/** @var array<string, mixed> */
return json_decode(
$response->getBody()->getContents(),
associative: true,
flags: JSON_THROW_ON_ERROR
);
}
}L'exécuteur d'outils : git diff et PHPStan
Le choix de ces deux outils n'est pas arbitraire. git diff donne à Claude le contexte exact de ce qui a changé — il ne relit pas toute la codebase, seulement le delta, ce qui est plus rapide, moins coûteux en tokens, et plus pertinent pour une revue ciblée. PHPStan apporte les erreurs de types et les incohérences structurelles que Claude ne pourrait pas détecter sans exécuter le code. Ensemble, ils couvrent la sémantique du changement et la rigueur statique.
Point critique : il faut tronquer l'output de git diff avant de l'envoyer à Claude. Un diff de 50 000 caractères sur une grosse PR consomme une quantité massive de tokens et dépasse vite la zone de raisonnement utile du modèle. On limite à 8 000 caractères et on signale la troncature explicitement, pour que Claude puisse en tenir compte dans son analyse plutôt que de travailler sur un contexte incomplet sans le savoir.
<?php
declare(strict_types=1);
namespace App\CodeReview;
final class ToolExecutor
{
private const int MAX_DIFF_CHARS = 8_000;
/**
* @param array<string, mixed> $input
*/
public function execute(string $toolName, array $input): string
{
return match ($toolName) {
'run_git_diff' => $this->runGitDiff(
baseBranch: (string) $input['base_branch'],
path: (string) ($input['path'] ?? 'src'),
),
'run_phpstan' => $this->runPhpstan(
path: (string) ($input['path'] ?? 'src'),
level: (int) ($input['level'] ?? 5),
),
default => throw new \InvalidArgumentException("Outil inconnu : {$toolName}"),
};
}
private function runGitDiff(string $baseBranch, string $path): string
{
$cmd = sprintf(
'git diff %s HEAD -- %s 2>&1',
escapeshellarg($baseBranch),
escapeshellarg($path)
);
$output = (string) shell_exec($cmd);
if ($output === '') {
return "Aucune modification détectée par rapport à \u{ab} {$baseBranch} \u{bb}.";
}
if (strlen($output) > self::MAX_DIFF_CHARS) {
$limit = self::MAX_DIFF_CHARS;
$output = substr($output, 0, $limit)
. "\n\n[...diff tronqué — seuls les {$limit} premiers caractères sont fournis]";
}
return $output;
}
private function runPhpstan(string $path, int $level): string
{
$cmd = sprintf(
'./vendor/bin/phpstan analyse %s --level=%d --no-progress --error-format=json 2>/dev/null',
escapeshellarg($path),
$level
);
$raw = (string) shell_exec($cmd);
$data = json_decode($raw, associative: true);
if (!is_array($data) || empty($data['files'])) {
return "PHPStan niveau {$level} : aucune erreur détectée.";
}
$lines = ["PHPStan niveau {$level} — problèmes trouvés :"];
foreach ($data['files'] as $file => $fileData) {
foreach ((array) $fileData['messages'] as $err) {
$lines[] = sprintf('- %s:%d — %s', $file, $err['line'], $err['message']);
}
}
return implode("\n", $lines);
}
}Votre équipe utilise Claude Code ?
Découvrir le workshop →La boucle agentique : CodeReviewAgent
Le cœur de l'agent est une boucle for bornée à 10 itérations — un garde-fou indispensable pour éviter une boucle infinie si Claude décidait d'appeler des outils en continu pour une raison quelconque. À chaque itération, la réponse de l'assistant est intégralement ajoutée à l'historique des messages. C'est ce mécanisme qui permet au modèle de se souvenir de ses appels précédents et de construire son raisonnement de manière cohérente sur plusieurs tours de boucle.
Quand stop_reason vaut tool_use, on extrait tous les blocs de type tool_use de la réponse, on les exécute, et on renvoie les résultats dans un seul message user contenant plusieurs tool_result. Claude 3.5 Sonnet peut appeler plusieurs outils en parallèle dans une même réponse — c'est exactement ce qui se passe ici : il appellera run_git_diff et run_phpstan lors de la même itération, ce qui réduit le nombre de tours de boucle à deux au lieu de quatre. Les définitions d'outils vivent dans une méthode privée : la description en langage naturel est ce que Claude lit pour décider quand appeler chaque outil, elle doit être précise et sans ambiguïté.
<?php
declare(strict_types=1);
namespace App\CodeReview;
final class CodeReviewAgent
{
private const int MAX_ITERATIONS = 10;
public function __construct(
private readonly AnthropicClient $client,
private readonly ToolExecutor $executor,
) {}
public function run(string $baseBranch, string $path): string
{
$tools = $this->toolDefinitions();
$messages = [['role' => 'user', 'content' => $this->buildPrompt($baseBranch, $path)]];
for ($i = 0; $i < self::MAX_ITERATIONS; $i++) {
$response = $this->client->chat($messages, $tools);
$messages[] = ['role' => 'assistant', 'content' => $response['content']];
if ($response['stop_reason'] === 'end_turn') {
foreach ($response['content'] as $block) {
if ($block['type'] === 'text') {
return $block['text'];
}
}
}
if ($response['stop_reason'] !== 'tool_use') {
break;
}
$toolResults = [];
foreach ($response['content'] as $block) {
if ($block['type'] !== 'tool_use') {
continue;
}
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block['id'],
'content' => $this->executor->execute($block['name'], $block['input']),
];
}
$messages[] = ['role' => 'user', 'content' => $toolResults];
}
$max = self::MAX_ITERATIONS;
return "La revue n'a pas pu être complétée après {$max} itérations.";
}
private function buildPrompt(string $baseBranch, string $path): string
{
return <<<PROMPT
Tu es un développeur senior PHP chargé d'effectuer une revue de code rigoureuse.
Utilise les outils disponibles pour :
1. Récupérer le diff Git entre la branche courante et `{$baseBranch}` sur le chemin `{$path}`
2. Lancer PHPStan sur le même chemin pour détecter les erreurs statiques
Produis ensuite une revue structurée en Markdown avec les sections suivantes :
- **Résumé** : ce que fait ce changement en 2-3 phrases
- **Points positifs** : ce qui est bien fait
- **Problèmes critiques** : erreurs bloquantes (bugs, sécurité, ruptures d'API)
- **Améliorations suggérées** : qualité, lisibilité, performance
- **Verdict** : APPROUVÉ / À REVOIR / BLOQUÉ
PROMPT;
}
/**
* @return list<array<string, mixed>>
*/
private function toolDefinitions(): array
{
return [
[
'name' => 'run_git_diff',
'description' => 'Exécute git diff pour obtenir les modifications entre la branche courante et une branche de base. Utilise cet outil en premier pour comprendre ce qui a changé.',
'input_schema' => [
'type' => 'object',
'properties' => [
'base_branch' => [
'type' => 'string',
'description' => 'La branche de référence (ex : main, develop)',
],
'path' => [
'type' => 'string',
'description' => 'Le chemin du répertoire à analyser (ex : src)',
],
],
'required' => ['base_branch'],
],
],
[
'name' => 'run_phpstan',
'description' => "Lance PHPStan pour détecter les erreurs de types et les incohérences statiques. Utilise cet outil après git diff pour compléter l'analyse.",
'input_schema' => [
'type' => 'object',
'properties' => [
'path' => [
'type' => 'string',
'description' => 'Le chemin à analyser (ex : src)',
],
'level' => [
'type' => 'integer',
'description' => 'Le niveau PHPStan de 0 à 9 (défaut : 5)',
],
],
'required' => ['path'],
],
],
];
}
}Le point d'entrée CLI
Le script d'entrée réunit les trois classes et expose les options --base-branch, --path et --output. L'option --output est utilisée par GitHub Actions pour écrire la revue dans un fichier avant de la poster en commentaire ; en usage local, l'omettre affiche la revue directement dans le terminal. La clé API est lue depuis la variable d'environnement ANTHROPIC_API_KEY — jamais en dur dans le code.
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use App\CodeReview\AnthropicClient;
use App\CodeReview\CodeReviewAgent;
use App\CodeReview\ToolExecutor;
$options = getopt('', ['base-branch:', 'path:', 'output:']);
$apiKey = getenv('ANTHROPIC_API_KEY')
?: throw new \RuntimeException('Variable d\'environnement ANTHROPIC_API_KEY manquante.');
$baseBranch = (string) ($options['base-branch'] ?? 'main');
$path = (string) ($options['path'] ?? 'src');
$output = (string) ($options['output'] ?? '');
$agent = new CodeReviewAgent(new AnthropicClient($apiKey), new ToolExecutor());
$review = $agent->run($baseBranch, $path);
if ($output !== '') {
file_put_contents($output, $review);
echo "Revue écrite dans {$output}\n";
} else {
echo $review . "\n";
}Intégration GitHub Actions
Le workflow se déclenche à l'ouverture et à chaque mise à jour d'une pull request. Il installe PHP 8.3, les dépendances Composer, puis exécute l'agent. La permission pull-requests: write est nécessaire pour que github-script puisse créer le commentaire. fetch-depth: 0 est indispensable : sans lui, Git n'a pas accès à l'historique complet et git diff ne peut pas calculer le delta par rapport à la branche de base. La clé API Anthropic se configure une seule fois dans Settings → Secrets and variables → Actions du dépôt.
name: Code Review IA
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Run code review agent
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
php bin/code-review.php \
--base-branch=origin/${{ github.base_ref }} \
--path=src \
--output=review.md
- name: Post review as PR comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const review = fs.readFileSync('review.md', 'utf8');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## Revue de code IA\n\n${review}`,
});Ce que ça change en pratique
Ce que cet agent apporte n'est pas simplement de l'automatisation : c'est un changement dans la boucle de feedback. La revue arrive dans la PR quelques minutes après le push, sans attendre qu'un développeur soit disponible. Elle couvre systématiquement les aspects statiques — PHPStan — et contextuels — le diff — que la revue humaine peut manquer par fatigue ou par habitude. Et elle libère les seniors de la revue de routine pour se concentrer sur les décisions d'architecture.
Quelques pistes pour aller plus loin : ajouter un outil run_phpcs pour les violations de coding standards, brancher un outil check_security sur composer audit, ou passer en mode streaming pour afficher la revue au fil de l'eau sur les gros diffs. La boucle agentique supporte ces extensions sans modification architecturale : il suffit d'ajouter une entrée dans toolDefinitions() et un case dans ToolExecutor::execute().
Le tool use d'Anthropic n'est pas un gadget — c'est l'infrastructure qui transforme Claude d'un assistant réactif en un agent proactif. Cet exemple PHP en est une démonstration directe et opérationnelle : trois classes, un script, un workflow, et ta CI pose des questions intelligentes sur chaque changement de code.
Votre équipe utilise Claude Code ?
Workshop intensif : votre équipe opérationnelle en 1 jour.