Un LLM local utile en 30 minutes : programmation, questions-réponses sur des documents et reconnaissance vocale
Un LLM local utile en 30 minutes avec Ollama, Qwen3 et Whisper.cpp : un assistant de codage, un système RAG pour vos notes et un boucle vocale hors ligne, adapté à votre mémoire.
Sources: Run a Useful Local LLM in 30 Minutes (Coding, RAG, Voice)
Un modèle local peut répondre à une vraie question de programmation même sans réseau, sur un ordinateur portable déjà posé sur le bureau. La configuration ne prend qu’environ huit minutes et la facture mensuelle est nulle. Un prompt comme celui ci-dessous suffit pour voir comment il fonctionne.
$ ollama run qwen3:8b
>>> rewrite this function to use async/await
La qualité est la partie surprenante. Il ne rivalise pas avec les meilleurs modèles cloud en termes de raisonnement complexe à plusieurs étapes, mais il gère les tâches d’ingénierie ordinaires de manière suffisamment fiable pour que l’écart entre un jouet de week-end et un outil fonctionnel se soit considérablement réduit.
Trois mises à jour au cours des douze derniers mois ont rendu cela pratique. Les fichiers des modèles se sont réduits pour tenir dans une mémoire RAM ordinaire : Qwen 3 propose une version à 8 milliards de paramètres d’environ 5 GB. Qwen3-Coder, sorti en février 2026, atteint 70,6 % de performance sur SWE-bench. Il a été testé avec un modèle de type mixture-of-experts de 30 milliards de paramètres qui n’active que 3 milliards de paramètres par token. De plus, grâce à Ollama, l’installation est passée d’un enchevêtrement d’environnements Python à un seul fichier binaire.
Voici trois versions distinctes, chacune d’environ une demi-heure : un assistant de codage intégré à l’éditeur, un script Python qui répond aux questions concernant un dossier de documents, et un boucle vocale qui écoute et répond. Toutes partagent une base de cinq minutes avant de diverger, il est donc judicieux de choisir celle qui correspond le mieux au travail quotidien et de reporter les autres à plus tard.
La base commune
Chaque solution nécessite un moteur d’inférence ainsi qu’un ensemble de poids de modèle.
Ollama remplit le rôle de ce moteur. Il gère les différences matérielles, en détectant si la machine dispose d’une GPU NVIDIA, d’un chip Apple Silicon ou seulement d’un CPU, et prépare le graphe d’exécution en conséquence. Il fonctionne en arrière-plan et propose une API HTTP locale dont le fonctionnement est similaire à celui des endpoints des grands fournisseurs cloud.
L’installation dépend de la plateforme. Sur macOS, c’est Homebrew qui s’en charge :
brew install ollama
Linux dispose d’un script d’installation :
curl -fsSL https://ollama.com/install.sh | sh
Les utilisateurs de Windows peuvent télécharger l’installateur MSI depuis le site web d’Ollama.
Une fois le moteur en place, il reste à installer un modèle. Le modèle de référence est qwen3:8b, qui nécessite environ 5 Go d’espace disque et fonctionne parfaitement avec 16 Go de RAM. En l’absence de GPU dédié, Ollama recourt à l’inference sur CPU, à un rythme d’environ 8 à 12 tokens par seconde, ce qui reste tout à fait lisible.
Téléchargez-le :
ollama pull qwen3:8b
Ensuite, confirmez que tout fonctionne bien en effectuant un essai rapide :
ollama run qwen3:8b "Write a python script to reverse a string."
La réponse s’affiche en continu dans le terminal. En arrière-plan, Ollama charge un fichier GGUF, un format qui mape mémoire par mémoire les poids depuis le disque directement dans la RAM, de sorte qu’un coup d’œil au moniteur système montre la mémoire augmenter d’environ la taille du fichier.
Une machine disposant de 8 GB de RAM ou moins commencera à utiliser le disque pour le stockage temporaire, ce qui ralentit considérablement les calculs. Pour ces cas, qwen3:4b ou gemma3:4b sont de meilleurs choix. Avec une GPU de 24 GB, on peut passer à qwen3:14b comme étape logique suivante.
Voie A : un assistant de codage dans l’éditeur
Tout utilisateur régulier d’un éditeur de code bénéficiera ici des meilleurs résultats. L’objectif est de connecter un modèle local à VS Code pour effectuer des refactorisations, des documents de description et des templates.
Le modèle général Qwen 3 est compétent, mais le travail sur du code bénéficie d’un entraînement spécialisé. En mai 2026, Qwen3-Coder est le meilleur codeur local polyvalent, obtenant 70,6 % sur SWE-bench Verified et 44,3 % sur SWE-Bench Pro. Sa taille totale est de 30 milliards de paramètres, mais seuls 3 milliards sont actifs par token, ce qui lui permet de tenir dans 24 Go de VRAM ou de fonctionner correctement sur un Mac de la série M disposant de 32 Go de mémoire unifiée.
Un ordinateur portable de 16 Go sans GPU dédié aura probablement du mal à gérer le modèle de 30 milliards de paramètres. Une alternative est qwen2.5-coder:7b, qui nécessite 4,7 Go et atteint néanmoins environ 92 % sur HumanEval.
# For 24GB+ VRAM or 32GB+ Mac Unified Memory
ollama pull qwen3-coder:30b
# For 16GB RAM laptops
ollama pull qwen2.5-coder:7b
L’éditeur a besoin d’une interface pour communiquer avec le modèle. Cline, une extension agente pour VS Code comptant plus de 5 millions d’installations, permet de lire des fichiers, de proposer des différences et d’exécuter des commandes de terminal. On peut l’obtenir en recherchant son nom sur le marché.
Un piège mérite d’être souligné. Cline intègre un long prompt système qui explique comment formater les différences, lire l’arborescence des fichiers et utiliser les outils, et ce prompt dépasse souvent 25 000 tokens avant que du code ne soit ajouté. La fenêtre de contexte par défaut d’Ollama pour les modèles Qwen 3 est de 40 000 tokens. En ouvrant plusieurs fichiers, la somme dépasse facilement ce chiffre, ce qui amène Ollama à tronquer le prompt sans avertissement. Le modèle perd alors les instructions de formatage, ignore les règles et génère du texte brut au lieu de différences valides.
La solution consiste à utiliser un Modelfile personnalisé qui force l’utilisation d’une fenêtre plus grande.
Un Modelfile adapté à Cline
Enregistrez ce qui suit dans le répertoire de travail sous le nom de fichier Modelfile.
# Modelfile — cline-tuned qwen3-coder
# Save as: ./Modelfile
FROM qwen3-coder:30b
# Cline's system prompt is ~25-30K tokens before your code is added.
# Ollama's default num_ctx for Qwen 3 is 40K, but Cline ships an
# expanded system prompt in v3+ that comfortably exceeds it on any
# real task. 65536 is the safe floor for serious use; 131072 if
# you have RAM/VRAM to spare.
PARAMETER num_ctx 65536
# Code edits want low-variance output. The default 0.7 is too loose.
PARAMETER temperature 0.2
# Stop after the model's natural turn. Cline parses this.
PARAMETER stop "<|im_end|>"
Construisez le modèle à partir de celui-ci à l’aide de la CLI Ollama, puis vérifiez que la configuration du contexte a pris effet :
# Build the Cline-tuned model from the Modelfile in the current dir
ollama create qwen3-coder-cline -f ./Modelfile
# Verify the context window actually took
ollama show qwen3-coder-cline --modelfile | grep num_ctx
# Expected output: PARAMETER num_ctx 65536
# Sanity-check it answers
ollama run qwen3-coder-cline "write a python one-liner to read /etc/hostname"
Dans VS Code, ouvrez la palette de commandes avec Cmd+Shift+P et sélectionnez l’entrée « Cline settings ». Pointez-la vers le moteur local :
API Provider: Ollama
Base URL: http://localhost:11434
Model: qwen3-coder-cline
Context Window: 65536
La valeur de la fenêtre de contexte dans l’interface graphique doit correspondre exactement à num_ctx dans le fichier du modèle. Si elle est plus faible, Cline suppose qu’il s’agit d’une fenêtre plus petite et limite discrètement les fichiers qu’il envoie.
Pour essayer, ouvrez un fichier Python contenant une fonction synchrone, affichez le panneau de chat de Cline et demandez une refonte en mode async/await. Cline lit le fichier, le envoie au modèle local Qwen3-Coder, reçoit la réponse et affiche une différence nette dans l’éditeur. Accepter cette modification ne nécessite qu’un seul clic, et tout le processus s’effectue hors ligne.
Cette configuration permet de gérer les refontes de fichiers uniques, les tests de fonctions isolées ainsi que la génération de code de base. Les tests menés par la communauté lui attribuent une qualité équivalente à 70 à 85 % de celle des solutions cloud de pointe pour des tâches à portée restreinte.
Cela échoue face à de grandes modifications de l’architecture impliquant de nombreux fichiers. Le transfert d’un projet Flask composé de vingt fichiers vers FastAPI est précisément le moment où cela se produit : les threads se perdent, des importations fictives apparaissent et la base de code finit par être endommagée. Un projet de cette envergure convient encore mieux à un modèle cloud avancé. Cependant, pour les dizaines de petites modifications fastidieuses effectuées chaque jour, un environnement local suffit.
Option B : récupération à partir de vos propres documents
Cette option concerne un dossier contenant des notes en markdown, des transcriptions de réunions ou de la documentation de projet que l’on recherche manuellement. Tout le processus tient dans environ 60 lignes de Python standard, en utilisant uniquement le client Ollama et numpy.
Un modèle d’embedding convertit du texte en vecteurs. Nomic Embed Text v1.5 dispose de 137 millions de paramètres, génère des vecteurs de 768 dimensions, occupe moins de 300 MB, gère un contexte de 8192 tokens et obtient de bons résultats dans les benchmarks de récupération MTEB.
Récupérez le modèle d’embedding et installez les paquets Python :
llama pull nomic-embed-text
pip install ollama numpy
Ensuite, enregistrez le script sous le nom rag.py.
"""Local RAG over a folder of notes — Ollama embeddings + numpy cosine.
No vector DB. For a few hundred docs, an in-memory list + numpy is
faster to set up and fast enough to run. Swap in sqlite-vec when
the corpus outgrows it (typically past ~5000 chunks).
Setup:
ollama pull qwen3:8b
ollama pull nomic-embed-text
pip install ollama numpy
Run:
python rag.py ./notes "what did the team decide about pricing?"
"""
from __future__ import annotations
import glob
import os
import sys
import numpy as np
import ollama
EMBED_MODEL = "nomic-embed-text"
CHAT_MODEL = "qwen3:8b"
CHUNK_WORDS = 400 # ~500 tokens; fits 4 chunks in an 8K context
TOP_K = 3
def load_chunks(folder: str) -> list[str]:
"""Read every .md / .txt under folder, split into ~CHUNK_WORDS chunks."""
chunks: list[str] = []
for path in glob.glob(os.path.join(folder, "**/*"), recursive=True):
if not path.endswith((".md", ".txt")):
continue
with open(path, encoding="utf-8") as f:
words = f.read().split()
for i in range(0, len(words), CHUNK_WORDS):
chunk = " ".join(words[i : i + CHUNK_WORDS])
if chunk.strip():
chunks.append(chunk)
return chunks
def embed(texts: list[str]) -> np.ndarray:
"""Embed a batch of texts via Ollama. Returns an (N, D) matrix."""
resp = ollama.embed(model=EMBED_MODEL, input=texts)
return np.array(resp["embeddings"], dtype=np.float32)
def top_k_indices(
query_vec: np.ndarray, doc_mat: np.ndarray, k: int
) -> list[int]:
"""Return indices of the k most cosine-similar rows in doc_mat.
The trick: if both vectors are unit-length (norm == 1), their
dot product equals their cosine similarity. So we normalize
once, then one matrix multiply gives a similarity score for
every chunk against the query — no Python loop needed.
"""
# Normalize query and every chunk to unit length.
# The 1e-8 prevents division by zero on a zero vector.
q = query_vec / (np.linalg.norm(query_vec) + 1e-8)
d = doc_mat / (np.linalg.norm(doc_mat, axis=1, keepdims=True) + 1e-8)
# One matmul across all chunks: scores[i] = cos(query, chunk_i).
scores = d @ q
# Sort descending, take the first k indices.
return np.argsort(scores)[::-1][:k].tolist()
def answer(query: str, chunks: list[str], doc_mat: np.ndarray) -> str:
"""Retrieve top-K chunks, stuff them into a prompt, generate."""
q_vec = embed([query])[0]
idx = top_k_indices(q_vec, doc_mat, k=TOP_K)
context = "\n\n---\n\n".join(chunks[i] for i in idx)
prompt = (
"Answer the question using ONLY the context below. "
"If the context does not contain the answer, say so plainly.\n\n"
f"CONTEXT:\n{context}\n\nQUESTION: {query}"
)
resp = ollama.chat(
model=CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
)
return resp["message"]["content"]
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit('usage: python rag.py <folder> "<question>"')
folder, query = sys.argv[1], sys.argv[2]
chunks = load_chunks(folder)
if not chunks:
sys.exit(f"no .md or .txt files found under {folder}")
print(f"indexing {len(chunks)} chunks...")
doc_mat = embed(chunks)
print(answer(query, chunks, doc_mat))
Pointez-le vers un répertoire de notes et posez une question :
python rag.py ~/Documents/meeting_notes "what did the team decide about pricing?"
Le script parcourt le dossier, lit les fichiers et les divise en blocs de 400 mots. La liste complète des blocs est envoyée en une seule fois à l’endpoint d’embedding d’Ollama. Pour chaque question, le vecteur de requête est comparé à chaque vecteur de document à l’aide de la similarité cosinus.
Les trois meilleurs blocs sont intégrés dans le prompt de génération et qwen3:8b rédige la réponse. Une instruction spécifique dans ce prompt exige de répondre uniquement en s’appuyant sur le contexte fourni, et d’indiquer clairement si ce dernier ne contient pas la réponse. Les modèles locaux ont tendance à produire des informations inventées qui semblent utiles lorsque la recherche ne renvoie rien, et cette instruction les pousse à admettre leur ignorance.
Ce n’est pas une récupération en environnement de production. Le découpage en segments est une méthode grossière de comptage des mots qui divise parfois une phrase en deux. Les embeddings résident en mémoire et disparaissent à la fin de l’exécution, ce qui oblige à les re-générer à chaque lancement. Lorsqu’une collection dépasse quelques milliers de documents et que le temps de démarrage devient gênant, remplacer la matrice numpy par sqlite-vec permet de stocker les vecteurs dans un fichier SQLite local. Pour des notes personnelles, ce petit script suffit amplement.
Voie C : un boucle vocale
Pour communiquer avec un modèle sans taper et sans envoyer d’audio vers un service cloud, cette voie crée une boucle complète voix-à-voix sur du matériel local.
Le processus se compose de trois étapes : la transcription du son en texte à partir du micro, l’utilisation d’un LLM pour rédiger une réponse, et la lecture à haute voix du texte généré.
La transcription utilise Whisper.cpp, une version en C++ fortement optimisée de Whisper d’OpenAI qui fonctionne sur le CPU, Apple Metal et NVIDIA CUDA. Elle a besoin de la dépendance ffmpeg pour la conversion audio. Sur macOS :
brew install whisper-cpp ffmpeg
Sur Linux, installez ffmpeg via le gestionnaire de paquets, clonez le répertoire whisper.cpp et exécutez make.
Whisper.cpp nécessite des poids au format GGML. Le fichier ggml-base.en.bin, d’environ 150 MB, est disponible dans le répertoire officiel de Hugging Face et offre une synthèse rapide et précise pour l’anglais. Pour d’autres langues ou une plus grande précision, ggml-large-v3.bin constitue une alternative. Placez ce fichier dans un dossier connu comme ~/models/.
La sortie audio provient de Kokoro ONNX, un moteur de TTS léger dont les poids totalisent environ 300 MB et qui permet une synthèse quasi en temps réel sur un CPU ordinaire. Il exige actuellement Python 3.10 à 3.13 et ne peut pas être installé sous 3.14.
pip install -U sounddevice ollama kokoro-onnx
Le script de pipeline
Enregistrez ce qui suit sous le nom de voice.py et modifiez le chemin WHISPER_MODEL pour qu’il pointe vers le fichier GGML que vous venez de télécharger.
"""Local voice loop — mic → whisper.cpp → Ollama → Kokoro → speaker.
Everything runs offline. Nothing leaves the machine.
Setup:
brew install whisper-cpp ffmpeg # macOS
# (Linux: apt install ffmpeg; build whisper.cpp from source)
pip install -U sounddevice ollama kokoro-onnx
# Whisper GGML model — pick one:
# ggml-base.en.bin (~150MB, fast, English-only)
# ggml-large-v3.bin (~3GB, accurate, multilingual)
# Download from https://huggingface.co/ggerganov/whisper.cpp
# Kokoro model files auto-download to ~/.cache/kokoro-onnx/
# on first run, or grab them manually from
# https://github.com/thewh1teagle/kokoro-onnx/releases
Run:
python voice.py
"""
from __future__ import annotations
import subprocess
import tempfile
import wave
from pathlib import Path
import sounddevice as sd
import ollama
from kokoro_onnx import Kokoro
WHISPER_MODEL = Path.home() / "models" / "ggml-base.en.bin"
CHAT_MODEL = "qwen3:8b"
KOKORO_VOICE = "af_sarah" # see kokoro voices.json for all options
SAMPLE_RATE = 16000 # whisper.cpp requires 16kHz mono
RECORD_SECONDS = 5
# Kokoro auto-downloads the model files on first instantiation.
kokoro = Kokoro("kokoro-v1.0.onnx", "voices-v1.0.bin")
def record() -> Path:
"""Record from the default mic, write a 16kHz mono WAV, return its path."""
print(f"listening for {RECORD_SECONDS}s...")
audio = sd.rec(
int(RECORD_SECONDS * SAMPLE_RATE),
samplerate=SAMPLE_RATE,
channels=1, # mono
dtype="int16", # 16-bit PCM is what wave.open expects
)
sd.wait() # block until recording finishes
# Fixed path in the system tempdir — overwritten each invocation.
wav_path = Path(tempfile.gettempdir()) / "voice_in.wav"
with wave.open(str(wav_path), "wb") as w:
w.setnchannels(1) # mono
w.setsampwidth(2) # 2 bytes per sample == int16
w.setframerate(SAMPLE_RATE) # 16 kHz — whisper.cpp's required rate
w.writeframes(audio.tobytes())
return wav_path
def transcribe(wav_path: Path) -> str:
"""Run whisper.cpp on a WAV file, return the transcript text."""
result = subprocess.run(
[
"whisper-cli",
"-m", str(WHISPER_MODEL),
"-f", str(wav_path),
"-otxt", # write the transcript to <input>.txt
"-np", # suppress progress prints on stdout
],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
raise RuntimeError(f"whisper-cli failed: {result.stderr}")
# -otxt writes the transcript next to the input file, with .txt
# tacked onto the existing name — so /tmp/voice_in.wav becomes
# /tmp/voice_in.wav.txt (same directory, same stem, extra suffix).
txt_path = wav_path.with_suffix(wav_path.suffix + ".txt")
return txt_path.read_text(encoding="utf-8").strip()
def respond(text: str) -> str:
"""Send the transcript to the local LLM, return the reply."""
resp = ollama.chat(
model=CHAT_MODEL,
messages=[
{
"role": "system",
"content": (
"You are a voice assistant. Keep replies to one or "
"two short sentences. No markdown, no lists."
),
},
{"role": "user", "content": text},
],
)
return resp["message"]["content"]
def speak(text: str) -> None:
"""Synthesize the reply with Kokoro and play it back."""
samples, sample_rate = kokoro.create(
text, voice=KOKORO_VOICE, speed=1.0, lang="en-us"
)
sd.play(samples, sample_rate)
sd.wait()
if __name__ == "__main__":
wav = record()
heard = transcribe(wav)
if not heard:
print("nothing heard. exiting.")
raise SystemExit(0)
print(f"you said: {heard}")
reply = respond(heard)
print(f"model: {reply}")
speak(reply)
Lorsqu’on le lance, le script écoute pendant cinq secondes, transcrit le son, génère une réponse et la prononce. La première exécution s’arrête le temps que Kokoro télécharge ses fichiers ONNX dans ~/.cache/kokoro-onnx/.
Trois choix de conception ont une importance plus grande qu’il n’y paraît. L’enregistrement en mono à 16 kHz est obligatoire : Whisper a été entraîné sur des fichiers audio à 16 kHz, et un microphone par défaut à 44,1 kHz produit du texte inventé ou aucune sortie. L’exécution de whisper-cli en tant que sous-processus distinct plutôt qu’en tant qu’extension Python permet d’éviter les conflits de verrouillage et les extensions C problématiques, et son paramètre de sortie de texte enregistre la transcription dans un fichier, de sorte que personne n’a besoin de récupérer la sortie standard. L’invite système interdit également explicitement le markdown, car Kokoro lit les astérisques à haute voix et une réponse formatée gâche l’expérience utilisateur.
La boucle ne gère qu’un seul tour. Elle ne diffuse pas les fragments audio en temps réel ni ne conserve l’historique des conversations. Un véritable agent conversationnel intégrerait une détection d’activité vocale pour savoir quand le discours se termine, au lieu d’utiliser une fenêtre fixe de cinq secondes. En tant que référence, cela montre que du matériel local peut exécuter l’ensemble de la chaîne de conversion parole en texte, du modèle à la synthèse vocale, en quelques secondes.
Ce que le matériel permet
Les modèles locaux sont limités bien plus par la bande passante mémoire que par la puissance de calcul brute. La vitesse à laquelle les poids sont transférés de la RAM vers les cœurs de calcul est plus importante que la rapidité avec laquelle le processeur effectue des calculs, et c’est ce facteur qui détermine quel modèle convient à une machine.
Un ordinateur portable de 16 Go sans GPU dédié atteint une bande passante mémoire système allant de 50 à 70 Go/s. Il peut exécuter qwen3:8b ou qwen2.5-coder:7b sur le CPU à un rythme de 8 à 12 tokens par seconde, ce qui est suffisant pour la récupération d’informations et les tâches vocales, mais trop lent pour le codage. Apple Silicon inverse la situation grâce à une mémoire unifiée offrant une bande passante de 200 à 400 Go/s, de sorte qu’un modèle Pro ou Max de la série M disposant de 24 à 36 Go peut gérer aisément qwen3-coder:30b, le design MoE ne activant que 3 milliards de paramètres par token.
Les cartes NVIDIA échangent de la capacité contre de la bande passante. Avec 12 à 16 Go de VRAM, qwen3:8b s’exécute instantanément et qwen3-coder:30b quantifié peut également être utilisé. Avec 24 Go ou plus, comme sur une RTX 3090 ou 4090, le modèle de 30 milliards de paramètres fonctionne à pleine vitesse, ce qui revient presque à utiliser un abonnement payant pour des travaux sur un seul fichier. Avec moins de 8 Go de RAM, le codage reste en grande partie hors de portée ; dans ce cas, qwen3:4b ou phi-4-mini, associé à des fonctions vocales et à de petites tâches de récupération d’informations, constitue la solution réaliste.
Lectures complémentaires
- Notes pratiques : Construction d’un agent de codage en cloud privé : OpenCode + un LLM local — Guide pas à pas des Notes pratiques : Construction d’un agent de codage en cloud privé : OpenCode + un LLM local : contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes souhaitant mettre en œuvre ce modèle.
- Traçage d’un agent RAG vocal avec LiveKit, Qdrant et MLflow — Créez un assistant hypothécaire de parole à parole en utilisant LiveKit, LlamaIndex et Qdrant, avec des traces OpenTelemetry dans MLflow montrant chaque opération de récupération et chaque étape du LLM.