Ollama : Intégrer une IA Gratuite dans vos Applications
Installez Ollama, lancez Gemma3 en local et intégrez une IA gratuite dans vos apps, sans API key, sans abonnement. Guide complet avec exemple chatbot.
OpenAI, Anthropic, Mistral API… Ajouter de l’IA à une application web, ça commence souvent par sortir la carte bleue. Pourtant, depuis qu’Ollama s’est imposé comme le standard de facto pour faire tourner des LLM en local, l’équation a complètement changé.
Dans cet article, je vous montre comment intégrer un modèle de langage dans une vraie application web (un chatbot FAQ pour un site e-commerce) en partant de zéro : installation d’Ollama, pull du modèle, serveur Express, frontend Vue 3. Pas d’API key, pas d’abonnement, pas de données qui quittent votre machine.
Le modèle utilisé est gemma3:1b de Google : environ 800 Mo, il tourne sur CPU sans GPU dédié. C’est le compromis idéal pour un projet personnel ou un prototype.
Qu’est-ce qu’Ollama ?
Ollama est un outil open source qui permet de télécharger et d’exécuter des LLM (Large Language Models) directement sur votre machine. Il expose une API REST locale (par défaut sur http://localhost:11434) que vous pouvez appeler depuis n’importe quel langage ou framework.
Ce qui distingue Ollama des autres solutions locales :
- Un catalogue de modèles prêts à l’emploi (Llama 3, Gemma, Mistral, Phi, Qwen…)
- Une interface en ligne de commande unique (CLI) pour tout :
ollama pull,ollama run - Un package officiel (
ollama) qui simplifie l’intégration JavaScript ou l’intégration Python - Zéro configuration réseau : tout reste en local
Si vous voulez aller plus loin sur l’outil en lui-même, j’ai écrit un article dédié à la prise en main d’Ollama qui couvre les commandes essentielles et la gestion des modèles.
La vraie force d’Ollama c’est qu’il permet d’installer et exécuter un modèle en local. Autrement dit, il vous suffira d’investir dans un serveur pour faire tourner une IA sans jamais payer un centime à OpenAI, Google ou Mistral. Hostinger propose des VPS très abordables pour cela.
Adieu le piège des factures à l’usage, welcome le coût fixe du serveur !
Des modèles légers comme gemma3:1b tournent confortablement sur un CPU décent. Pour des modèles plus lourds (> 4-7B), un GPU avec au moins 8 Go de VRAM est recommandé, mais certains tournent aussi sur CPU avec des temps de réponse plus longs.
C’est une révolution pour les développeurs qui veulent intégrer de l’IA dans leurs projets sans dépendre d’une API externe.
Hostinger propose un tutoriel complet pour installer Ollama sur un VPS.
Installer Ollama sur sa machine
Rendez-vous sur ollama.com et téléchargez l’installateur pour votre système : Windows, macOS ou Linux.
L’installation est classique : suivez l’assistant, Ollama se lance en tâche de fond et démarre automatiquement au démarrage.
Pour vérifier que tout fonctionne, tapez la commande suivante :
ollama --versionUtiliser un modèle en local avec Ollama
Télécharger un modèle
On va utiliser gemma3:1b, le modèle le plus léger de la famille Gemma 3 de Google. Avec ~800 Mo, il tourne correctement sur CPU et répond en quelques secondes :
ollama pull gemma3:1bLe téléchargement se fait en une fois, puis le modèle est mis en cache localement.
Exécuter le modèle
Pour tester rapidement en mode interactif :
ollama run gemma3:1bTapez une question, observez la réponse, puis Ctrl+D pour quitter. Si ça répond, vous êtes prêt.
Cas pratique : un chatbot FAQ pour un site e-commerce
Pour prendre en main l’intégration d’Ollama dans une application web, on va construire un projet simple : une route API de chatbot FAQ pour un site e-commerce fictif nommé ShopExpress.
Pour simplifier le code source, nous nous appuierons d’un simple serveur Express.
L’utilisateur pose une question en langage naturel (“Est-ce que je peux retourner un article ?”), et le chatbot répond en s’appuyant sur une FAQ en Markdown.
L’architecture est volontairement simple :
📄 faq.md— la base de connaissances, en Markdown📄 server.js— serveur Express qui parse la FAQ, fait une recherche par mots-clés, construit un prompt contextualisé et interroge Ollama
C’est une forme de RAG simplifié (Retrieval-Augmented Generation) : au lieu d’envoyer toute la FAQ au modèle à chaque requête, on extrait d’abord les 2-3 entrées les plus pertinentes, puis on les injecte dans le prompt. Le modèle répond avec du contexte ciblé.
Structure finale du projet :
📁 shop-express
├── 📁 node_modules
├── 📄 faq.md
├── 📄 server.js
├── 📄 package-lock.json
└── 📄 package.json1. Initialisation du projet
Créer le projet
On commence par initialiser un projet Node.js classique au sein du dossier 📁 shop-express :
npm init -yInstaller les dépendances
Ensuite, on installe Express pour le serveur web et le client officiel ollama pour interagir avec l’API locale d’Ollama :
npm install express ollamaConfiguration de l’environnement Node.js
Activer les ES modules
Ensuite, éditez votre package.json pour activer les ES modules (nécessaire pour utiliser import côté Node) :
{
...
"type": "module",
...
}Créer un script de démarrage
Enfin, ajoutez un script de démarrage dans package.json :
{
...
"scripts": {
"start": "node --watch server.js"
},
...
}2. Créer la FAQ en Markdown
La FAQ est le cœur de la base de connaissances du chatbot. Chaque section ## Titre devient une entrée indépendante que le serveur pourra retrouver et injecter dans le prompt.
## Quels sont les délais de livraison ?
Les livraisons standard sont effectuées sous 3 à 5 jours ouvrés. La livraison express (24h) est disponible pour un supplément de 4,99 €.
## Comment retourner un article ?
Vous disposez de 30 jours après réception pour retourner un article non utilisé dans son emballage d'origine. Le retour est gratuit via notre bon de retour téléchargeable depuis votre espace client.
## Quels modes de paiement acceptez-vous ?
Nous acceptons les cartes Visa, Mastercard, American Express, PayPal et le virement bancaire. Les paiements sont sécurisés par 3D Secure.
## Mon colis est perdu, que faire ?
Si votre colis n'est pas arrivé après 7 jours ouvrés, contactez notre service client via le formulaire de contact. Nous ouvrons une enquête auprès du transporteur sous 24h.
## Comment modifier ou annuler ma commande ?
Vous pouvez modifier ou annuler votre commande dans les 2 heures suivant sa validation, depuis votre espace client. Passé ce délai, la commande est déjà en préparation.Libre à vous d’enrichir cette FAQ avec autant d’entrées que nécessaire. Plus elle est complète, plus le chatbot sera pertinent — sans toucher au code.
3. Création du serveur Express
Si vous débutez avec Express ou souhaitez consolider vos bases, la formation Express sur laconsole.dev couvre tout ce dont vous avez besoin.
Initialiser Express
On commence par importer Express et créer une instance de serveur :
import express from "express";
const app = express();
app.listen(3000, () => {
console.log("Serveur démarré sur http://localhost:3000");
});Lancer le serveur
Assurez-vous qu’Ollama tourne en arrière-plan, puis tapez la commande suivante pour lancer le serveur Express :
npm run start4. Route API de chat POST /api/chat
Il est temps de créer le endpoint API qui recevra les questions du frontend, fera la recherche dans la FAQ et interrogera Ollama pour générer la réponse.
Créons un endpoint POST /api/chat qui :
- Récupère la question de l’utilisateur depuis
req.body - Appelle une fonction de recherche pour trouver les entrées les plus pertinentes dans le fichier
📄 faq.md - Construit un prompt contextualisé avec les entrées FAQ
Réception de la question utilisateur
Commençons par ajouter le middleware pour parser les requêtes contenant du JSON dans le corps de la requête :
import express from "express";
const app = express();
app.use(express.json()); // pour parser les requêtes JSON
app.listen(3000, () => {
console.log("Serveur démarré sur http://localhost:3000");
});Maintenant, définissons la route POST /api/chat qui reçoit une question dans le corps de la requête :
// ...
app.use(express.json()); // pour parser les requêtes JSON
app.post("/api/chat", async (req, res) => {
if (!req.body || typeof req.body.question !== "string") {
return res.status(400).json({ error: "Requête invalide : 'question' manquante" });
}
const { question } = req.body;
if (!question?.trim()) {
return res.status(400).json({ error: "Question manquante" });
}
// TODO : rechercher dans la FAQ et interroger Ollama
res.json({ answer: "Réponse du chatbot" });
});
app.listen(3000, () => {
console.log("Serveur démarré sur http://localhost:3000");
});Maintenant, chaque fois que vous enverrez une requête POST à /api/chat avec un JSON du type { "question": "Votre question ici" }, le serveur répondra avec { "answer": "Réponse du chatbot" }.
Parsing de la FAQ Markdown
La première chose que doit faire notre endpoint est de charger et parser le fichier 📄 faq.md pour en extraire les questions et réponses structurées.
Chaque bloc ## Question devient un objet { question, answer }.
import express from "express";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const faqMd = readFileSync(join(__dirname, "faq.md"), "utf-8");
const app = express();
// ...readFileSync: lit le contenu du fichier📄 faq.mdde manière synchronefileURLToPathetdirname: permettent d’obtenir le chemin absolu du fichier courant, ce qui est nécessaire pour lire📄 faq.mdmême si le script est lancé depuis un autre dossier.
À ce stade, faqMd contient la string entière du fichier Markdown.
## Quels sont les délais de livraison ?
Les livraisons standard sont effectuées sous 3 à 5 jours ouvrés. La livraison express (24h) est disponible pour un supplément de 4,99 €.
## Comment retourner un article ?
Vous disposez de 30 jours après réception pour retourner un article non utilisé dans son emballage d'origine. Le retour est gratuit via notre bon de retour téléchargeable depuis votre espace client.
...On va la transformer en un tableau d’objets { question, answer } :
// ...
const faqMd = readFileSync(join(__dirname, "faq.md"), "utf-8");
const faq = faqMd
.split(/^## /m) // découpe à chaque "## " placé en début de ligne
.filter(Boolean) // supprime le fragment vide avant la première question
.map((block) => {
const [question, ...rest] = block.trim().split("\n"); // sépare la première ligne (question) du reste (réponse)
return { question: question.trim(), answer: rest.join("\n").trim() }; // retourne un objet structuré
});
const app = express();
// ...À ce stade, faq contient un tableau d’objets structurés :
[
{
question: "Quels sont les délais de livraison ?",
answer: "Les livraisons standard sont effectuées sous 3 à 5 jours ouvrés. La livraison express (24h) est disponible pour un supplément de 4,99 €."
},
{
question: "Comment retourner un article ?",
answer: "Vous disposez de 30 jours après réception pour retourner un article non utilisé dans son emballage d'origine. Le retour est gratuit via notre bon de retour téléchargeable depuis votre espace client."
},
...
]Recherche par mots-clés dans la FAQ
Avant d’appeler le modèle, on cherche les entrées de la FAQ les plus pertinentes par rapport à la question de l’utilisateur.
Créons une fonction searchFaq(question) qui prend la question de l’utilisateur et retourne les entrées FAQ les plus pertinentes.
Pour cela, on utilise une approche très simple :
- On tokenise la question (on découpe en mots)
- On met tous les mots en minuscules
- On retire les mots courts (≤ 3 caractères)
- On retire les accents pour éviter les problèmes de correspondance (ex : “délai” vs “delai”).
Enfin, on compte les occurrences de ces mots dans chaque entrée FAQ (question + réponse). Les entrées avec le score le plus élevé sont considérées comme les plus pertinentes.
const faq = ...;
function searchFaq(question) {
const words = question
.toLowerCase()
.normalize("NFD") // sépare les lettres des accents (e + ´ au lieu de é)
.replace(/[\u0300-\u036f]/g, "") // supprime les accents
.split(/\s+/) // découpe en mots
.filter((w) => w.length > 3); // ignore "de", "la", "un"…
}Ensuite, on calcule un score de pertinence pour chaque entrée FAQ en comptant combien de mots de la question apparaissent dans l’entrée (question + réponse).
// ...
function searchFaq(question) {
const words = question
.toLowerCase()
.normalize("NFD") // sépare les lettres des accents (e + ´ au lieu de é)
.replace(/[\u0300-\u036f]/g, "") // supprime les accents
.split(/\s+/) // découpe en mots
.filter((w) => w.length > 3); // ignore "de", "la", "un"…
const scored = faq.map((entry) => {
const target = (entry.question + " " + entry.answer)
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
const score = words.reduce(
(acc, word) => acc + (target.includes(word) ? 1 : 0),
0
);
return { ...entry, score };
});
}Enfin, on filtre les entrées avec un score > 0, on les trie par score décroissant et on garde les 3 meilleures :
// ...
function searchFaq(question) {
const words = question
.toLowerCase()
.normalize("NFD") // sépare les lettres des accents (e + ´ au lieu de é)
.replace(/[\u0300-\u036f]/g, "") // supprime les accents
.split(/\s+/) // découpe en mots
.filter((w) => w.length > 3); // ignore "de", "la", "un"…
const scored = faq.map((entry) => {
const target = (entry.question + " " + entry.answer)
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
const score = words.reduce(
(acc, word) => acc + (target.includes(word) ? 1 : 0),
0
);
return { ...entry, score };
});
return scored
.filter((e) => e.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 3); // on garde les 3 meilleures entrées
}Par exemple, ici : searchFaq("Quels sont vos délais de livraison ?") retournerait :
[
{
question: 'Quels sont les délais de livraison ?',
answer: 'Les livraisons standard sont effectuées sous 3 à 5 jours ouvrés. La livraison express (24h) est disponible pour un supplément de 4,99 €.',
score: 3
},
{
question: 'Quels modes de paiement acceptez-vous ?',
answer: 'Nous acceptons les cartes Visa, Mastercard, American Express, PayPal et le virement bancaire. Les paiements sont sécurisés par 3D Secure.',
score: 2
},
{
question: 'Comment retourner un article ?',
answer: "Vous disposez de 30 jours après réception pour retourner un article non utilisé dans son emballage d'origine. Le retour est gratuit via notre bon de retour téléchargeable depuis votre espace client.",
score: 1
}
]- Item 1 : on retrouve les mots
quels,sontetdélais=> score 3 - Item 2 : on retrouve
quelsetsont=> score 2 - Item 3 : on retrouve
réception=> score 1
C’est le principe du RAG en version minimaliste : récupérer (Retrieve) les informations pertinentes avant de générer (Generate) la réponse.
Aller plus loin avec des embeddings vectoriels
Si notre utilisateur écrit “temps de réception”, cette approche par mots-clés ne trouvera pas la section “Quels sont les délais de livraison ?”, pourtant très pertinente.
Pour dépasser ça, on utilisera sur un projet plus ambitieux des embeddings vectoriels.
Un embedding vectoriel transforme les mots ou phrases en suites de nombres dans un espace mathématique. Exemple :
- “chat” :
[0.12, -0.87, 0.33, ...] - “chien” :
[0.10, -0.82, 0.30, ...]
👉 Ces vecteurs seront proches car les mots ont un sens similaire.
Les mots proches en sens auront des vecteurs proches. Cela sert à mesurer la similarité pour la recherche intelligente, les chatbots, les recommandations ou la détection de doublons.
Par exemple, dans ce cas précis, on peut comparer la question d’un utilisateur à une FAQ pour retrouver la réponse la plus pertinente, même si les mots diffèrent.
Générer la réponse avec Ollama
Il est temps de faire le lien entre la question de l’utilisateur, les entrées FAQ pertinentes et le modèle Ollama pour générer une réponse contextualisée.
Pour cela, nous aurons besoin d’interroger Ollama via le client officiel ollama.
On va d’abord importer le client Ollama et définir une constante MODEL pour choisir le modèle à utiliser :
// imports...
import ollama from "ollama";
const MODEL = "gemma3:1b";
// ...Maintenant, dans la route POST /api/chat, après avoir récupéré les entrées FAQ pertinentes, on construit un prompt complet pour le modèle :
// ...
app.post("/api/chat", async (req, res) => {
if (!req.body || typeof req.body.question !== "string") {
return res
.status(400)
.json({ error: "Requête invalide : 'question' manquante" });
}
const { question } = req.body;
if (!question?.trim()) {
return res.status(400).json({ error: "Question manquante" });
}
const matches = searchFaq(question);
const context =
matches.length > 0
? matches.map((m) => `Q: ${m.question}\nR: ${m.answer}`).join("\n\n")
: "Aucune information trouvée dans la FAQ.";
const prompt = `Tu es un assistant service client pour un site e-commerce appelé ShopExpress. Réponds en français de manière concise et utile en te basant sur la FAQ ci-dessous.
FAQ pertinente :
${context}
Question du client : ${question}
Réponse :`;
try {
const data = await ollama.generate({ model: MODEL, prompt, stream: false });
res.json({ answer: data.response });
} catch (err) {
console.error(err);
res.status(500).json({
error: "Impossible de contacter Ollama. Vérifiez qu'il est lancé.",
});
}
});
app.listen(3000, () => {
console.log("Serveur démarré sur http://localhost:3000");
});Ici, context contient les 3 entrées FAQ les plus pertinentes formatées de manière claire. Le prompt demande au modèle de répondre en français de manière concise et utile, en se basant sur ce contexte.
Avec stream: false, on attend la réponse complète avant de la renvoyer.
5. Tester le chatbot
Ouvrez votre client API et envoyez dans le body de la reqête la question suivante :
{
"question": "Quels sont vos délais de livraison ?"
}Observez la réponse générée.
Changer de modèle
Modifiez simplement la constante MODEL dans server.js :
const MODEL = "llama3.2:3b"; // ~2 Go, meilleure compréhension
const MODEL = "mistral:7b"; // ~4 Go, excellent pour le françaisPensez à faire ollama pull nom-du-modele avant. Les modèles 7B nécessitent généralement 8 Go de RAM minimum pour tourner confortablement sur CPU.
Code complet
Voici le code complet du fichier 📄 server.js :
import express from "express";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import ollama from "ollama";
const MODEL = "gemma3:1b";
const __dirname = dirname(fileURLToPath(import.meta.url));
const faqMd = readFileSync(join(__dirname, "faq.md"), "utf-8");
const faq = faqMd
.split(/^## /m) // découpe à chaque "## " placé en début de ligne
.filter(Boolean) // supprime le fragment vide avant la première question
.map((block) => {
const [question, ...rest] = block.trim().split("\n"); // sépare la première ligne (question) du reste (réponse)
return { question: question.trim(), answer: rest.join("\n").trim() }; // retourne un objet structuré
});
function searchFaq(question) {
const words = question
.toLowerCase()
.normalize("NFD") // sépare les lettres des accents (e + ´ au lieu de é)
.replace(/[\u0300-\u036f]/g, "") // supprime les accents
.split(/\s+/) // découpe en mots
.filter((w) => w.length > 3); // ignore "de", "la", "un"…
const scored = faq.map((entry) => {
const target = (entry.question + " " + entry.answer)
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "");
const score = words.reduce(
(acc, word) => acc + (target.includes(word) ? 1 : 0),
0
);
return { ...entry, score };
});
return scored
.filter((e) => e.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 3); // on garde les 3 meilleures entrées
}
const app = express();
app.use(express.json());
app.post("/api/chat", async (req, res) => {
if (!req.body || typeof req.body.question !== "string") {
return res
.status(400)
.json({ error: "Requête invalide : 'question' manquante" });
}
const { question } = req.body;
if (!question?.trim()) {
return res.status(400).json({ error: "Question manquante" });
}
const matches = searchFaq(question);
const context =
matches.length > 0
? matches.map((m) => `Q: ${m.question}\nR: ${m.answer}`).join("\n\n")
: "Aucune information trouvée dans la FAQ.";
const prompt = `Tu es un assistant service client pour un site e-commerce appelé ShopExpress. Réponds en français de manière concise et utile en te basant sur la FAQ ci-dessous.
FAQ pertinente :
${context}
Question du client : ${question}
Réponse :`;
try {
const data = await ollama.generate({ model: MODEL, prompt, stream: false });
res.json({ answer: data.response });
} catch (err) {
console.error(err);
res.status(500).json({
error: "Impossible de contacter Ollama. Vérifiez qu'il est lancé.",
});
}
});
app.listen(3000, () => {
console.log("Serveur démarré sur http://localhost:3000");
});Aller plus loin
Ce projet est délibérément minimaliste pour rester lisible. Voici les évolutions naturelles :
-
Améliorer la recherche : L’approche par mots-clés a ses limites : “délai de réception” ne matchera pas “délais de livraison”. Pour dépasser ça, il faut des embeddings vectoriels : chaque entrée FAQ est convertie en vecteur, et on cherche par similarité cosinus. Des bibliothèques comme
@xenova/transformerspermettent de le faire entièrement en local. -
Ajouter le streaming : Avec
stream: truecôtéollama.generate()et les Server-Sent Events côté Express, vous pouvez afficher les tokens au fur et à mesure — bien plus agréable visuellement.
Vous avez maintenant un chatbot IA fonctionnel, entièrement local, sans dépendance externe payante. L’architecture — un fichier Markdown comme base de connaissances, une recherche par mots-clés pour le contexte, un modèle Ollama pour la génération — est reproductible sur n’importe quel cas d’usage : documentation interne, support technique, assistant de code…
Le plus important : vous contrôlez tout. Les données ne quittent pas votre machine, le coût n’est pas lié à la consommation de votre API mais bien fixe et assez faible (coût du VPS), et vous pouvez swapper le modèle en une ligne. C’est ça, la promesse d’Ollama. 🚀