89 lines
4.5 KiB
Markdown
89 lines
4.5 KiB
Markdown
# 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.
|