Aller au contenu principal
Logo GiwiSoft
localclaw — construire un agent IA autonome avec Ollama et OpenCode

localclaw — construire un agent IA autonome avec Ollama et OpenCode

Xavier MARIN Xavier MARIN Non classé

J’ai conçu et développé localclaw, un agent IA autonome qui tourne entièrement en local. Il utilise Ollama pour l’inférence, expose une interface Angular 20, et orchestre lui-même l’exécution d’outils (recherche web, bash, fichiers, emails, Telegram, etc.) via du function calling. L’agent peut prendre des décisions, enchaîner des actions, et même créer dynamiquement de nouveaux outils à la volée.

Stack : Node.js/Express + SQLite + Angular 20 + Ollama + ws (WebSocket)


Pourquoi un agent local ?

Les assistants IA cloud (ChatGPT, Claude, etc.) sont puissants, mais ils soulèvent des questions de confidentialité, de coût et de dépendance réseau. L’idée de localclaw est de proposer une alternative :

  • 100 % local — aucune donnée ne quitte la machine
  • Sans abonnement — juste le coût du matériel
  • Extensible — l’agent peut créer ses propres outils
  • Autonome — il planifie, exécute et vérifie sans intervention humaine

Le nom « localclaw » vient de la contraction de local et claw (griffe) — l’agent attrape et manipule les ressources locales.

Prérequis système

Avant de vous lancer, voici ce dont vous avez besoin :

ComposantMinimumRecommandé
RAM8 Go16 Go+
VRAM4 Go8 Go+ (GPU NVIDIA/AMD)
Disque10 Go libres30 Go+ (modèles Ollama)
GPUNVIDIA (CUDA) ou AMD (Vulkan)NVIDIA RTX 3060+ / AMD RX 6700+
CPU4 cœurs8 cœurs+
OSLinux (recommandé), macOS, Windows (WSL2)Linux
DockerDocker Engine 24+Docker Compose V2
Node.js18 LTS22 LTS

Modèles Ollama testés :

ModèleTailleVRAMtok/s (RX 580)
qwen2.5:3b1,9 Go2 Go~30 tok/s
qwen2.5:7b-instruct-q3_K_M3,6 Go4 Go~15 tok/s
qwen2.5:7b-instruct-q4_K_M4,4 Go6 Go~10 tok/s
nomic-embed-text274 Mo(embedding)

Architecture générale

Browser (Angular)  ←→  WebSocket  ←→  Express Server  ←→  Ollama

├── SQLite (sessions, messages, RAG, FTS5)
├── Plugin loader (JS/ESM)
└── Background scheduler (30s)

Le point d’entrée est un serveur Express qui sert à la fois l’API REST, les fichiers statiques du frontend Angular, et le serveur WebSocket (port 4173).

Pourquoi WebSocket plutôt que SSE ?

La version initiale utilisait Server-Sent Events (SSE) pour streamer les réponses. C’est simple à mettre en œuvre, mais cela pose un problème fondamental : le client ne peut pas interagir avec le flux une fois démarré. Avec le WebSocket, nous avons :

  • Bidirectionnel — le client peut envoyer des messages pendant la génération (stop, nouvelle session, etc.)
  • Pas de limite de connexion — SSE est limité à 6 connexions simultanées par domaine sous Chrome
  • Framing natif — pas besoin de parser des lignes data: ou de gérer les timeouts HTTP

Le handshake WebSocket est proxyé via le dev server Angular (proxy.conf.js avec ws: true) et passe par l’upgrade HTTP standard côté serveur.

Le cœur : la boucle d’agent

L’agent loop (src/agent.ts) est le cerveau du système. Voici comment il fonctionne :

  1. Récupération du contexte RAG (memories + knowledge base)
  2. Summarisation du contexte si > 8000 caractères
  3. Appel à Ollama avec les outils disponibles
  4. Si Ollama retourne du texte → yield, fin
  5. Si Ollama retourne des tool_calls → exécution
  6. Injection du résultat, retour en 3 (max 15 itérations)
  7. Persistance en mémoire vectorielle du résultat

Function calling avec Ollama

Ollama expose une API compatible OpenAI pour le function calling. Chaque outil est défini par un schéma JSON :

interface ToolDefinition {
name: string
description: string
parameters: {
type: 'object'
properties: Record<string, { type: string; description: string }>
required: string[]
}
}

Ces définitions sont envoyées au modèle dans le champ tools du payload /api/chat. Ollama répond avec tool_calls contenant le nom de la fonction et ses arguments en JSON. L’agent parse cette réponse, exécute l’outil, et réinjecte le résultat dans la conversation.

Gestion des erreurs et re-prompting

Un des défis majeurs est la fiabilité des modèles small (qwen2.5:3b, qwen3:1.7b). Ils ont tendance à :

  • Répondre par des conseils au lieu d’utiliser des outils
  • Renvoyer des réponses vides
  • Abandonner après un échec
  • Envoyer des placeholders au lieu de données réelles

L’agent implémente plusieurs stratégies de re-prompting :

SituationAction
Réponse videNouvel appel avec FORCE_TOOL_MSG
Résultat faible (< 30 chars, « No results », « Error: »)Nouvel essai avec approche différente
Réponse de type conseil (« you can », « i suggest »)Injection d’un message système forçant l’utilisation d’outils
Abandon explicite (« cannot find », « pas trouvé »)Relance avec PERSIST_MSG

Le système inclut aussi un timeout de sécurité de 120s côté frontend pour éviter un état de chargement bloqué si l’événement done n’arrive jamais.

Le système d’outils

Architecture des plugins

Chaque outil est un module ESM exportant une interface ToolModule :

type ToolModule = {
definition: ToolDefinition
execute: (args: Record<string, any>, onChunk?: (chunk: string) => void) => Promise<string>
}

Le onChunk permet aux outils de streamer leur sortie en temps réel (utile pour run_bash, web_fetch, etc.). L’agent collecte ces chunks et les balance côté frontend via WebSocket toutes les 150ms.

Outils disponibles

  • web_fetch — Recherche web (SearXNG ou DuckDuckGo) + fetch d’URL
  • read_file / write_file — Lecture/écriture de fichiers (avec protection contre le path traversal)
  • run_bash — Exécution de commandes bash (streaming, 120s timeout, sandbox Docker optionnel)
  • send_email — Email via Mailgun API
  • send_telegram — Messages Telegram (avec fallback env var pour le chat_id)
  • weather — Météo via wttr.in / Open-Meteo
  • browser_automation — Contrôle Chromium headless (captures, formulaires, clics)
  • generate_image — Génération d’images via Ollama
  • opencode_task — Délégation de tâches complexes à OpenCode
  • create_tool — Création dynamique de nouveaux outils (JS/Python/Bash)

Protection contre le path traversal

Les outils read_file et write_file vérifient que le chemin résolu se trouve bien dans un répertoire autorisé (CWD ou DATA_DIR). Tentative de ../../etc/passwd ? Bloquée.

const resolved = path.resolve(args.path)
const allowed = [CWD, DATA_DIR].map(d => path.resolve(d))
if (!allowed.some(dir => resolved.startsWith(dir))) {
return 'Error: Path traversal blocked'
}

RAG et mémoire vectorielle

Embeddings

À chaque exécution d’outil, le résultat est encodé vectoriellement via Ollama (/api/embed avec nomic-embed-text) et stocké dans SQLite. Au début de chaque tour de conversation, l’agent recherche les résultats d’outils précédents les plus pertinents et les injecte dans le prompt système.

Base de connaissance

Les documents uploadés (PDF, DOCX, TXT) sont découpés en chunks et indexés de la même manière. L’agent peut les interroger via l’outil search_knowledge. En complément, des tables virtuelles FTS5 (Full-Text Search) sont créées pour le fallback par mots-clés quand la recherche sémantique ne donne pas assez de résultats.

Backfill au démarrage

Au démarrage du serveur, un backfillMissingEmbeddings() scanne les entrées qui n’ont pas encore d’embedding et les encode — évitant les problèmes de migration entre versions.

Fonctionnalités

  • Streaming temps réel via WebSocket avec matching des événements par toolRunId
  • Bouton Stop — ferme la WebSocket, l’agent détecte la fermeture et stoppe la boucle
  • Édition de messages — cliquer sur « edit » sur un message utilisateur, la conversation est tronquée après ce point
  • Upload de fichiers — pièce jointe pour TXT/PDF/DOCX
  • Thème clair/sombre — auto-détection via prefers-color-scheme
  • Timeout de sécurité — 120s, réinitialise l’état de chargement si aucune réponse

Matching des événements d’outils

Un détail d’implémentation intéressant : chaque invocation d’outil reçoit un toolRunId (UUID) unique. Côté frontend, les événements tool_start, tool_chunk, tool_end sont appariés par cet ID. Sans cela, si le même outil est appelé deux fois, les chunks se mélangent dans l’interface.

Sécurité

Sandbox Docker

Quand LOCALCLAW_SANDBOX_ENABLED=true, les outils run_bash et create_tool s’exécutent dans des conteneurs Docker isolés :

--network none
--cap-drop ALL
--security-opt no-new-privileges

Pas de réseau, pas de capabilities, pas d’escalade de privilèges. Le code malveillant reste confiné.

Tokens et secrets

Tous les secrets (API keys, tokens Telegram, clés Mailgun) sont dans .env — qui est dans .gitignore. Le README documente les variables nécessaires mais avec des valeurs factices.

Déploiement

Docker Compose (recommandé)

services:
ollama:
image: ollama/ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]

searxng:
image: searxng/searxng
ports: ["8888:8080"]

localclaw:
build: .
ports: ["4173:4173"]
env_file: .env

Le stack complet démarre avec docker compose up -d. Voir la documentation Docker pour plus de détails.

GPU AMD (RX 580 4GB)

Sur ma machine de développement (RX 580, 4 Go VRAM), j’utilise le backend Vulkan via RADV (pilote Mesa) :

OLLAMA_VULKAN=1

Le modèle qwen2.5:7b-instruct-q3_K_M (~3,6 Go) tient dans la VRAM et tourne à ~15 tok/s. Le portage ROCm a un bug de doorbell connu avec le kernel 7.0 — Vulkan est le workaround stable.

OpenCode — l’agent de codage IA open source

OpenCode est un agent de codage IA open source qui s’exécute dans le terminal, en IDE ou en application de bureau. Il est utilisé par plus de 7,5 millions de développeurs chaque mois et compte plus de 160 000 étoiles sur GitHub.

Contrairement à localclaw qui est un agent autonome généraliste, OpenCode est spécialisé dans l’ingénierie logicielle : il peut lire et écrire du code, exécuter des commandes, créer des fichiers, et orchestrer des modifications complexes sur un projet. Il intègre nativement le LSP (Language Server Protocol) pour comprendre la structure du code, et supporte plus de 75 fournisseurs de modèles de langage.

Installation

OpenCode s’installe en une commande :

curl -fsSL https://opencode.ai/install | bash

Ou via npm (nécessite Node.js) :

npm install -g opencode-ai

Alternativement, sur macOS/Linux avec Homebrew :

brew install anomalyco/tap/opencode

Configuration d’une clé API

OpenCode supporte plus de 75 fournisseurs de LLM. La configuration se fait dans le terminal via la commande /connect :

opencode
/connect

Choisissez votre fournisseur (OpenAI, Anthropic, Ollama, OpenCode Zen, etc.) et collez votre clé API. OpenCode Zen offre des modèles testés et validés par l’équipe OpenCode avec une facturation simplifiée — idéal pour commencer. Rendez-vous sur opencode.ai/auth pour créer une clé.

Pour utiliser Ollama en local, ajoutez cette configuration dans opencode.json :

{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama (local)",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"qwen2.5-coder:7b": {
"name": "Qwen 2.5 Coder 7B (local)"
}
}
}
}
}

Intégration avec localclaw

localclaw expose un outil opencode_task qui permet de déléguer des tâches complexes de développement à OpenCode. L’agent localclaw peut ainsi analyser un problème, décider qu’une tâche nécessite de l’ingénierie logicielle, et sous-traiter son exécution à OpenCode — qui dispose d’une compréhension fine du projet, du LSP, et de la capacité à modifier le codebase de manière cohérente.

Liens utiles

RessourceURL
Site officielhttps://opencode.ai
Documentationhttps://opencode.ai/docs
GitHubhttps://github.com/anomalyco/opencode
Installationhttps://opencode.ai/install
Desktop (beta)https://opencode.ai/download
API key / Zenhttps://opencode.ai/auth
Zen (modèles testés)https://opencode.ai/zen
Go (abonnement low-cost)https://opencode.ai/go
Ollama + OpenCodehttps://docs.ollama.com/integrations/opencode

Défis techniques rencontrés

1. La fiabilité des modèles small

Le plus gros défi est sans doute la qualité des modèles en < 7B paramètres. qwen2.5:3b a tendance à :

  • Retourner des réponses vides
  • Ne pas appeler d’outils
  • Abandonner après un premier échec
  • Halluciner des arguments

La solution a été un système de re-prompting agressif avec détection de motifs (advisory, giving-up, weak result) et des messages système de forçage.

Dépannage

ProblèmeCause probableSolution
Ollama ne répond pasService non démarréollama serve ou systemctl start ollama
Modèle introuvableNon téléchargéollama pull qwen2.5:7b-instruct-q3_K_M
Tool call videModèle trop petitPasser à qwen2.5:7b ou augmenter num_ctx (128k)
Erreur CUDA/VulkanGPU non détectéVérifier ollama run --verbose, définir OLLAMA_VULKAN=1
Path traversal bloquéChemin hors CWDUtiliser un chemin dans le répertoire du projet
WebSocket déconnectéProxy Angular manquantVérifier proxy.conf.js avec ws: true
RAG sans résultatsEmbeddings manquantsRedémarrer le serveur (backfill automatique)
OpenCode non trouvéBinaire pas dans $PATHVérifier which opencode ou npm list -g opencode-ai

Améliorations futures

  • Support de modèles plus gros — ajouter une configuration pour utiliser plusieurs GPUs ou le CPU offloading
  • Planification avancée — permettre à l’agent de créer des workflows complexes avec des dépendances entre tâches
  • Tools as Code — faire du create_tool un véritable environnement de développement avec validation de schéma et tests
  • Multi-agents — faire communiquer plusieurs agents spécialisés (recherche, code, analyse) via le scheduler
  • WebUI améliorée — ajouter une vue graphe de l’exécution des tools, un éditeur de prompts, et des presets de sessions

Conclusion

localclaw montre qu’il est possible de construire un agent IA autonome et utile avec des outils open-source et du matériel grand public. Le function calling d’Ollama, combiné à un système d’outils bien conçu et une boucle d’agent robuste, permet des cas d’usage variés : de la simple question-réponse à l’orchestration de workflows complexes.

localclaw — Interface Angular

Essaye-le maintenant :

git clone https://github.com/Giwi/localclaw.git
cd localclaw
cp .env.example .env
docker compose up -d

Rejoins la communauté pour partager tes retours, poser des questions ou proposer des idées :

GitHub


Article écrit par Xavier à l’issue du développement de la version 0.1.0 de localclaw.