4.8 KiB
| title | slug | type | source | domains | tags | aliases | answers | priority | linked | updated | visibility | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GrasBot — pipeline de retrieval (graph + BM25) | grasbot-retrieval | technique | manual |
|
|
|
|
6 |
|
2026-04-23 | public |
GrasBot — pipeline de retrieval (graph + BM25)
GrasBot est l'assistant conversationnel du site fernandgrascalvet.com. Il répond aux visiteurs en s'appuyant sur le vault Obsidian personnel vault-structure et sur un LLM local qui tourne sur la machine de Fernand.
Version actuelle : 3.0 (avril 2026). Remplace l'ancien pipeline RAG vectoriel (ChromaDB + embeddings Ollama) par une recherche déterministe sur graphe de notes + BM25. Plus simple, plus précis pour un vault de cette taille, zéro dépendance lourde.
Matériel
- GPU : NVIDIA RTX 2080 Ti (11 Go VRAM).
- Modèle chat : Qwen3 8B en quantification Q4_K_M via Ollama (≈ 5 Go VRAM), laissant une marge confortable pour le contexte.
- Pas d'embeddings : plus besoin de
nomic-embed-texten VRAM. Le retrieval tourne sur CPU en pur Python, ~50 ms par requête.
Pipeline d'une question
question utilisateur
│
▼
[ tokenize_fr ] ◄── stop-words FR, c++→cpp, split sur -/_
│
▼
[ score_note pour chaque note ]
- alias match (+10)
- title / slug match (+8)
- answers overlap (+5 à +12)
- domains / tags match (+3 à +5 par hit)
- BM25 sur body (+0 à +5)
- priorité + MOC-hub (+0.3 à +1.3 si déjà scoré)
│
▼
[ expand_by_graph ] ◄── voisins via linked / related / wikilinks
│
▼
[ build_prompt ] ◄── notes entières en contexte (top-5)
│
▼
[ Ollama /api/chat ] ◄── Qwen3 8B, temperature 0.4, keep_alive 30m
│
▼
{ response: "...",
sources: [{slug, title, type, score, reasons, url}],
grounded: true|false,
model: "qwen3:8b",
vault_size: 41 }
Implémentation
Deux modules Python dans llm-api/ :
api.py— FastAPI. EndpointsGET /ask?q=...,GET /health,POST /reload-vault.search.py— primitivesload_vault,tokenize_fr,score_note,expand_by_graph,search,build_prompt,generate,answer. Configuration via variables d'environnement (LLM_MODEL,VAULT_DIR,SEARCH_TOP_K,SEARCH_MIN_SCORE…).
Le vault est chargé une seule fois par process FastAPI via
@lru_cache sur load_vault(). Après édition d'une note, l'endpoint
POST /reload-vault force la relecture sans redémarrer l'API.
Prompt système
Qwen3 reçoit un prompt qui le force à :
- Répondre en français, ton sobre, sans emojis.
- Citer ses sources entre crochets (par slug :
[push-swap],[ia]). - Avouer quand l'info n'est pas dans les notes, orienter vers le site (/portfolio, /competences, /contact).
- Rester concis (3 à 6 phrases).
- Réorienter poliment si la question est totalement hors-sujet.
Quand grounded=false (score max < SEARCH_MIN_SCORE), le prompt user
bascule en mode sans contexte : le LLM sait qu'il ne doit pas inventer
de faits sur Fernand.
Compatibilité front
Le champ response de la réponse JSON est conservé. ChatBot.js et
askAI.js peuvent exploiter en plus :
sources[]→ vignettes cliquables (url/portfolio/<slug>ou/competences/<slug>selontype).grounded→ afficher un badge « basé sur X notes » ou « réponse généraliste » selon la valeur.
Limites connues
- Pas de mémoire conversationnelle : chaque question est indépendante.
Ajouter un historique court (3-4 derniers tours) passerait par
ChatBot.js(envoi du contexte) etsearch.answer()(intégration). - Pas de streaming : la réponse arrive en un bloc après 2-10 s selon
la question. Passage à
stream: truepossible (modifiergenerate()et exposer un endpoint SSE). - Taxonomie à entretenir à la main : voir vault-structure et
vault-grasbot/TAXONOMIE.md. Les aliases/answers/priority générés parbuild-vault.pysont une base, mais les notes stratégiques méritent un enrichissement manuel (voir le CV cv-grascalvet-fernand). - Pas encore de hybrid scoring : on pourrait composer avec un embedding optionnel si le vault grossit > ~500 notes. Sur le périmètre actuel, superflu.