# Intégration RAG (embeddings) Kazeia-Engine — Guide développeur **Date** : 09/06/2026 **Cible** : exposer `texte → vecteur de phrase poolé` pour le RAG de Kazeia mobile, sans toucher au chemin Speaker/TTS. **Statut** : entrypoint JNI livré + validé (parité bit-identique vs `llama-embedding` du fork). > ⚠️ **Même bloqueur SELinux que LLM/TTS** (cf `REBUILD_CPU_ONLY.md`). L'embedder linke `libllama.so` > qui dépend de `libggml-hexagon.so` ouvrant `/dev/fastrpc-cdsp` au `llama_backend_init()` → SIGABRT > dans un APK `untrusted_app`. `n_gpu_layers=0` ne suffit PAS (c'est l'énumération des devices qui > plante). **L'embedder DOIT rouler sur le `libllama` CPU-only** (`GGML_HEXAGON=OFF`), comme LLM/TTS in-app. > Les tests ci-dessous passent en `adb shell` (uid 2000, SELinux OK) mais c'est un FAUX positif pour l'app. --- ## API ```kotlin // EngineJni (dist/jni/EngineLlmEngine.kt) external fun loadEmbedder(ggufPath: String, nThreads: Int, pooling: Int): Long external fun embedText(handle: Long, text: String): FloatArray? // float[n_embd] L2-normalisé, ou null external fun freeEmbedder(handle: Long) // Wrapper fourni val emb = EmbedderEngine("$dir/multilingual-e5-small.gguf", nThreads = 4, pooling = -1) val v = emb.embed("query: j'ai du mal à dormir") // FloatArray(384), norme 1.0 emb.release() ``` `pooling` : **-1 = défaut du modèle (recommandé)** ; 1 = MEAN (e5) ; 2 = CLS (bge). Handle **séparé** du LLM Speaker/TTS. CPU pur, aucun HTP. Déterministe (pas d'échantillonnage). --- ## Modèle d'embedding — décision critique (point #1 du spec) | | Statut | |---|---| | **Recommandé** | `multilingual-e5-small` (384-dim, FR-capable, pooling MEAN) — **à sourcer/convertir + versionner** | | Testé localement | `nomic-embed-text-v1.5` (768-dim) — **anglo-centré, marge FR fine** | **Mesure FR (nomic, sur tablette)** : `cos("mal à dormir","insomnie")=0.556` vs `cos("mal à dormir","recette de tarte")=0.517` — ordre correct mais marge de **0.04 seulement**. Avec un mauvais préfixe l'ordre s'inverse (0.477 vs 0.611). **Conclusion : nomic n'est pas assez robuste en FR pour un RAG thérapeutique. Shipper `multilingual-e5-small` comme le spec l'exige.** Impératifs (contrat de distribution) : - **Le MÊME GGUF à l'ingestion ET à la requête** (vecteurs incompatibles sinon). Versionner le n° de modèle. - **Préfixes e5 (`query:` / `passage:`) = côté appelant**, jamais dans le moteur. --- ## Validation effectuée (tests d'acceptation du spec) Sur tablette SM8750, `adb shell`, nomic-embed-text-v1.5 Q4_K_M, CPU : | Test | Résultat | |---|---| | Arch BERT/nomic-bert charge end-to-end | ✅ vecteur produit | | `embedText` renvoie `float[n_embd]` | ✅ 768 (nomic) / 384 attendu (e5-small) | | **Déterminisme** (2 runs, même texte) | ✅ **max\|diff\| = 0.0 bit-identique** | | **Norme L2 ≈ 1.0** | ✅ 1.0000 | | Cohérence sémantique (proche > loin) | ✅ avec bons préfixes (model-dépendant, cf ci-dessus) | | **Parité référence** vs `llama-embedding` | ✅ **max\|diff\| = 5e-7, cos = 1.000000** | La parité prouve que le chemin C++ (`tokenize add_special` + batch manuel `logits=1` + `llama_decode` + `llama_get_embeddings_seq` + L2) est **bit-identique** à l'outil de référence du fork. Confirmé : BERT encoder-only est routé via `llama_decode` (pas `llama_encode`) dans ce fork — `examples/embedding/embedding.cpp` ne bascule sur erreur que pour les modèles encoder **ET** decoder (T5). --- ## Contraintes techniques (impératifs) 1. **`n_ctx = n_batch = n_ubatch = 512`** : le pooling MEAN/CLS exige toute la séquence dans un seul ubatch. Cap à 512 tokens (chunker plus court en amont). 2. **CPU only** (`n_gpu_layers=0`) + **build CPU-only** (cf bloqueur SELinux). 3. **Pas de réutilisation** de `prefillEmbeds`/`decodeEmbed` (I/O float du Talker TTS, pas un embedding de phrase poolé). --- ## Build Rebuild `libkazeia_engine.so` (les 3 symboles `loadEmbedder`/`embedText`/`freeEmbedder` y sont exportés). **Aucune nouvelle dépendance** (tout est dans libllama). Pour l'in-app : build CPU-only (`REBUILD_CPU_ONLY.md`). Livrer la `.so` + un GGUF d'embedding **multilingual-e5-small** versionné. --- ## Reste côté app (hors moteur, pour info) Schéma SQLite (`chunk_text`, `source`, `embedding BLOB`) dans SQLCipher → ingestion admin (chunk sémantique + `embedText`) → runtime : matrice en RAM → brute-force cosinus NEON top-k 3-5 + seuil → bloc « CONTEXTE » budgété en tokens dans le system prompt. Pas de vector-DB, pas d'ANN, pas d'ONNX. Le RAG ne porte jamais la gestion de crise.