Exercice 1 : Réponse à des questions
Exercice 1 : L'exaltante intégration d'un LLM
Bienvenue dans ce chapitre, ô brillants étudiants… ou pas. Le programme du jour ? Un exercice ridiculement simple : créer une page permettant de poser UNE et UNE SEULE question à la fois à un modèle d'IA, sans stocker quoi que ce soit en base de données. Oui, vous avez bien lu : même un poulpe enrhumé y arriverait. 😏
Plan de bataille
- Configurer la connexion à OpenRouter avec les variables d'environnement.
- Créer la classe
SimpleAskServicepour communiquer avec l'API. - Mettre en place le controller
AskControllerpour gérer votre question unique et misérable. - Créer les vues Inertia/VueJS qui permettront à l'utilisateur d'entrer sa question et d'afficher la réponse. Un Hello World version LLM, on aura tout vu. 🙄
- Utiliser
markdown-itethighlight.jsavec@tailwindcss/typographypour mettre en forme la réponse, parce que c'est joli… et parce que vous devrez bien impressionner votre correcteur si vous voulez vos points.
Bon, vous avez aussi droit à quelques extraits de code ultra-époustouflants. Encore heureux, sinon vous seriez perdus comme des poulets sans tête.
Exemple d'interface

Affichage de la réponse

Étape 1 : Configuration de l'environnement
Variables d'environnement
Dans votre .env ET .env.example, ajoutez :
OPENROUTER_API_KEY=VotreCélesteCléAPI
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
(Et n'oubliez pas de renseigner votre vraie clé, sinon ce sera aussi utile qu'un parapluie dans un désert.)
Configuration Laravel
Dans config/services.php, ajoutez la configuration pour OpenRouter :
'openrouter' => [
'api_key' => env('OPENROUTER_API_KEY'),
'base_url' => env('OPENROUTER_BASE_URL', 'https://openrouter.ai/api/v1'),
],
Formidable. Vous savez éditer un fichier PHP, vous êtes déjà de futurs architectes… ou presque.
Étape 2 : Création de la classe SimpleAskService
Pour ce premier exercice, on va utiliser un service "tout-en-un" qui gère directement les appels HTTP. C'est plus simple pour démarrer. Dans les exercices suivants, on verra comment structurer ça proprement avec un Client séparé.
Créez le fichier app/Services/SimpleAskService.php :
<?php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Support\Facades\Http;
/**
* Service simplifié pour communiquer avec l'API OpenRouter.
*
* Exemple pédagogique utilisant le client HTTP de Laravel.
*/
class SimpleAskService
{
public const DEFAULT_MODEL = 'openai/gpt-5-mini';
private string $apiKey;
private string $baseUrl;
public function __construct()
{
$this->apiKey = config('services.openrouter.api_key');
$this->baseUrl = rtrim(config('services.openrouter.base_url', 'https://openrouter.ai/api/v1'), '/');
}
/**
* Récupère la liste des modèles disponibles.
*
* @return array<int, array{
* id: string,
* name: string,
* description: string,
* context_length: int,
* max_completion_tokens: int,
* input_modalities: array<string>,
* output_modalities: array<string>,
* supported_parameters: array<string>
* }>
*/
public function getModels(): array
{
return cache()->remember('openrouter.models', now()->addHour(), function (): array {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->get($this->baseUrl . '/models');
return collect($response->json('data', []))
->sortBy('name')
->map(fn (array $model): array => [
'id' => $model['id'],
'name' => $model['name'],
'description' => $model['description'] ?? '',
'context_length' => $model['context_length'] ?? 0,
'max_completion_tokens' => $model['top_provider']['max_completion_tokens'] ?? 0,
'input_modalities' => $model['architecture']['input_modalities'] ?? [],
'output_modalities' => $model['architecture']['output_modalities'] ?? [],
'supported_parameters' => $model['supported_parameters'] ?? [],
])
->values()
->toArray()
;
});
}
/**
* Envoie un message et retourne la réponse du modèle.
*
* @param array<int, array{
* role: 'assistant'|'system'|'tool'|'user',
* content: array<int, array{
* type: 'image_url'|'text',
* text?: string,
* image_url?: array{url: string, detail?: string}
* }>|string
* }> $messages
*/
public function sendMessage(array $messages, ?string $model = null, float $temperature = 1.0): string
{
$model = $model ?? self::DEFAULT_MODEL;
$messages = [$this->getSystemPrompt(), ...$messages];
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
'HTTP-Referer' => config('app.url'),
'X-Title' => config('app.name'),
])
->timeout(120)
->post($this->baseUrl . '/chat/completions', [
'model' => $model,
'messages' => $messages,
'temperature' => $temperature,
])
;
// Gestion des erreurs
if ($response->failed()) {
$error = $response->json('error.message', 'Erreur inconnue');
throw new \RuntimeException("Erreur API: {$error}");
}
return $response->json('choices.0.message.content', '');
}
/**
* Retourne le prompt système.
*
* @return array{role: 'system', content: string}
*/
private function getSystemPrompt(): array
{
$user = auth()->user()?->name ?? 'l\'utilisateur';
$now = now()->locale('fr')->format('l d F Y H:i');
return [
'role' => 'system',
'content' => view('prompts.system', [
'now' => $now,
'user' => $user,
])->render(),
];
}
}
La vue pour le System Prompt
Plutôt que de mettre le prompt système directement dans le code PHP (avec un heredoc moche), on utilise une vue Blade. C'est plus propre, plus maintenable, et ça permet de modifier le prompt sans toucher au code.
Créez le fichier resources/views/prompts/system.blade.php :
Tu es un assistant de chat. La date et l'heure actuelle est le {{ $now }}.
Tu es actuellement utilisé par {{ $user }}.
Merci qui ? Merci le service. Il s'occupe de toute la plomberie. Vous, vous avez juste à l'appeler comme des grands.
Étape 3 : Création du contrôleur AskController
Parce qu'il faut bien un endroit pour orchestrer votre question totalement transcendantale. Ajoutez ces routes dans routes/web.php :
use App\Http\Controllers\AskController;
Route::get('/ask', [AskController::class, 'index'])->name('ask.index');
Route::post('/ask', [AskController::class, 'ask'])->name('ask.post');
🤔 Question sécurité : Ces routes sont-elles accessibles à tout le monde ? Est-ce vraiment ce que vous voulez ? Réfléchissez à ce qui pourrait manquer ici... Un middleware peut-être ? Vous ne voudriez pas que n'importe qui vienne vider vos crédits API, non ? 😏
Puis, créez AskController :
<?php
namespace App\Http\Controllers;
use App\Services\SimpleAskService;
use Illuminate\Http\Request;
use Inertia\Inertia;
class AskController extends Controller
{
public function __construct(private SimpleAskService $askService) {}
public function index()
{
return Inertia::render('Ask/Index', [
'models' => $this->askService->getModels(),
'selectedModel' => $this->askService::DEFAULT_MODEL,
]);
}
public function ask(Request $request)
{
$request->validate([
'message' => 'required|string',
'model' => 'required|string',
]);
$response = null;
$error = null;
$messages = [[
'role' => 'user',
'content' => $request->message,
]];
try {
$response = $this->askService->sendMessage(
messages: $messages,
model: $request->model
);
} catch (\Exception $e) {
$error = $e->getMessage();
}
return Inertia::render('Ask/Index', [
'models' => $this->askService->getModels(),
'selectedModel' => $request->model,
'message' => $request->message,
'response' => $response,
'error' => $error,
]);
}
}
Notez qu'on utilise l'injection de dépendance dans le constructeur (private SimpleAskService $askService). C'est plus propre que de créer une nouvelle instance à chaque méthode, et ça facilite les tests.
On retourne directement les données via Inertia::render(). Simple et efficace.
Étape 4 : Mise en place du Frontend (VueJS + Tailwind + Inertia)
Voilà la partie « magique » où vous devez faire preuve de quelques neurones (si, si…). L'idée : afficher un formulaire pour saisir un message, choisir un modèle dans la liste proposée, puis envoyer le tout au contrôleur AskController.
1. Récupérer les props côté VueJS
Avec Inertia et la Composition API, vous récupérez les props avec defineProps. Voici un exemple de structure :
<script setup>
import { useForm } from '@inertiajs/vue3'
const props = defineProps({
models: Array,
selectedModel: String,
message: String,
response: String,
error: String,
})
const form = useForm({
message: props.message ?? '',
model: props.selectedModel,
})
const submit = () => {
form.post(route('ask.post'))
}
</script>
Les données response et error sont directement disponibles dans props.response et props.error. Simple !
2. Préparer votre formulaire
Vous avez besoin, pêle-mêle, de :
- Un champ texte (ou
textarea) dans lequel l'utilisateur va taper sa question (ou un monologue, selon l'humeur). - Un sélecteur (ou un dropdown, si vous voulez faire "pro") pour le choix du modèle. Utilisez
props.modelspour peupler la liste. - Un bouton pour soumettre le formulaire. Bah oui, vous n'allez pas faire ça à la force de la pensée (quoique…).
Quand vous cliquerez sur « Envoyer », vous ferez un POST vers la route ask.post.
3. Installer les dépendances pour le rendu Markdown
Pour faire un joli rendu de la réponse (histoire de feindre le professionnalisme), vous allez utiliser markdown-it et highlight.js pour la coloration syntaxique du code :
npm install markdown-it highlight.js
4. Rendre la réponse en Markdown
Dans votre composant, initialisez markdown-it avec highlight.js :
<script setup>
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css' // ou un autre thème
const md = new MarkdownIt({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value
} catch (__) {}
}
return '' // use external default escaping
}
})
// Utilisation : md.render(props.response)
</script>
5. Tailwind Typography
Le Starter Kit intègre déjà Tailwind. Il faut installer le plugin typography (@tailwindcss/typography) si ce n'est pas déjà fait :
npm install @tailwindcss/typography
Ensuite, ajoutez le plugin dans votre fichier resources/css/app.css :
@import "tailwindcss";
@plugin "@tailwindcss/typography";
Cette extension vous permet d'avoir de jolies règles CSS adaptées à du contenu "rédactionnel". Quand vous entourerez votre bloc de réponse d'un class="prose", ça fera tout de suite plus sérieux.
Exemple d'utilisation :
<template>
<div
v-if="props.response"
class="prose dark:prose-invert prose-slate max-w-none"
v-html="md.render(props.response)"
/>
</template>
Classes utiles :
prose: Applique le style principaldark:prose-invert: Pour le dark modeprose-slate/prose-lg/ etc. : Variantes de couleur et taillemax-w-none: Retire la largeur maximale par défaut
📚 Documentation : https://github.com/tailwindlabs/tailwindcss-typography
6. Gérer les erreurs
N'oubliez pas d'afficher les erreurs si elles existent :
<template>
<div v-if="props.error" class="text-red-500 p-4 rounded bg-red-50">
Erreur : {{ props.error }}
</div>
</template>
Astuce : si vous êtes paresseux (ou vif d'esprit, selon le point de vue), vous pouvez créer un composant dédié pour le rendu Markdown + Highlight, plutôt que de tout coller dans le même bloc. Mais encore une fois, un minimum de jugeote est requis.
Bref, pas de composant tout fait ici, pas de code complet copié-collé : à vous de faire votre tambouille. Mais franchement, c'est presque un Hello World du chat IA, donc si vous n'y arrivez pas, il est temps de remettre en question vos choix de carrière.
Conclusion
Vous voilà donc avec une page où l'on peut :
- Choisir un modèle.
- Poser une unique question (pas question de faire une conversation complète, hein).
- Afficher la réponse sous forme HTML/Markdown avec de la belle coloration syntaxique.
- Vous vanter que vous maîtrisez l'IA alors que vous venez juste de lire un tuto (mais ne le dites à personne, ça restera notre secret 🦄).
Dans les prochains exercices, on va :
- Ajouter le streaming pour voir les tokens apparaître en temps réel
- Créer un vrai système de conversations avec historique
- Structurer le code avec un Client propre et des DTOs
Si, malgré la clarté éblouissante de ce chapitre, vous rencontrez un souci, demandez-vous si vous avez tout fait correctement. Dans 90 % des cas, non. (╯°□°)╯︵ ┻━┻
Allez, au travail, futurs maîtres de l'IA… ou futurs champions du copier-coller.
Autres exemples :


