Démarrer
D'un clone à une réponse calibrée : installer Kahn1, le servir, envoyer des questions typées, lire ce qui revient et l'ajuster à vos données. Tout tourne sur votre propre machine.
01 · Laisser un agent IA s'en charger
Collez ce prompt dans un agent de code qui peut lancer des commandes sur votre machine (Claude Code, Codex, Cursor, Gemini CLI…) : il examine votre matériel, choisit le backend GPU ou CPU, installe, démarre le serveur et envoie une requête de test. Il demande avant sudo ou tout paquet système. Un assistant de chat sans terminal (ChatGPT ou Claude dans le navigateur) peut vous guider pas à pas, mais pas exécuter.
Installe Kahn1 sur cette machine et prouve qu'il fonctionne avec une vraie requête.
Kahn1 (https://github.com/Okura66/kahn1, code sous MIT) est un moteur de décision open source : un modèle ouvert fine-tuné qui répond à des questions typées (choice, score, noul) sur un texte en lisant les probabilités des tokens d'option. Il existe en deux tailles, qui tournent toutes deux sur GPU (vLLM) comme sur CPU (transformers) : Kahn1 4B (Okura66/Kahn1-Qwen3.5-4B, poids sous Apache 2.0, 8,4 Go) et Kahn1 3B (Okura66/Kahn1-Qwen2.5-3B, poids sous licence de recherche Qwen, voir ses conditions, 6,17 Go). Le 3B est plus petit et plus rapide (médiane de 36 ms contre 89 ms sur un GPU) ; le 4B est nettement plus fort sur les décisions difficiles. Prends le 4B sauf si je demande le 3B. Il est servi par une application FastAPI, `sysone`. Guide : https://kahn1.com/fr/demarrer/
Règles : avance étape par étape et montre chaque commande avant de la lancer. Demande-moi avant d'utiliser sudo, d'installer des paquets système ou de modifier quoi que ce soit hors du dossier du projet. Si une étape échoue, montre l'erreur exacte, explique la cause probable et propose une correction avant de continuer. N'invente jamais de sortie : si le modèle n'a pas pu tourner, dis-le.
1. Inspecte la machine et dis-moi ce que tu trouves : système (Linux, macOS, Windows, WSL2), version de Python (3.11+ requise), RAM et disque libres, et si un GPU NVIDIA est utilisable (`nvidia-smi`). Choisis ensuite le backend et explique pourquoi :
- GPU (vLLM) : seulement sous Linux ou WSL2 avec un GPU NVIDIA et CUDA. Kahn1 4B demande 12 Go de VRAM ou plus (il a tourné sur 16 Go), Kahn1 3B 8 Go ou plus.
- CPU (transformers) sinon : quelques secondes par question. En float32, environ 13 Go de RAM libre pour le 3B, environ 17 Go pour le 4B.
Le téléchargement fait 8,4 Go pour le 4B, 6,17 Go pour le 3B. Si la taille choisie ne tient pas sur cette machine, dis-le-moi avant de continuer.
2. Installe uv s'il manque (https://docs.astral.sh/uv/). Si ce dossier n'est pas déjà un clone du dépôt, clone-le :
git clone https://github.com/Okura66/kahn1 && cd kahn1
uv venv --python 3.11
GPU : uv pip install -e ".[gpu]"
CPU : uv pip install -e ".[cpu]" --extra-index-url https://download.pytorch.org/whl/cpu
Le tokenizer du modèle demande transformers 5 ou plus. Vérifie avec
uv run python -c "import transformers; print(transformers.__version__)"
et lance `uv pip install -U transformers` s'il affiche 4.x.
3. Démarre le serveur en arrière-plan et garde son journal :
GPU : SYSONE_MODEL=Okura66/Kahn1-Qwen3.5-4B uv run sysone serve --port 8000
CPU : SYSONE_BACKEND=cpu SYSONE_MODEL=Okura66/Kahn1-Qwen3.5-4B uv run sysone serve --port 8000
Pour le 3B, mets SYSONE_MODEL=Okura66/Kahn1-Qwen2.5-3B à la place, sur l'un ou l'autre backend. Le moteur choisit seul le format de prompt de chaque modèle.
Sous Windows PowerShell, définis d'abord chaque variable avec $env:NOM = "valeur". Interroge GET http://127.0.0.1:8000/health jusqu'à obtenir {"status": "ok"}.
4. Envoie ce JSON en POST à http://127.0.0.1:8000/v1/evaluate/jev (écris-le dans un fichier et envoie-le avec curl -d @fichier, ou utilise Python). Le premier appel charge le modèle et peut prendre des minutes :
{"state": "Bonjour, j'ai été débité deux fois pour ma commande #48213. Je veux être remboursé, sinon je fais opposition auprès de ma banque.",
"schema": {
"intent": {"type": "choice", "instructions": "Quelle est la demande principale du client ?",
"criteria": {"remboursement": "le client veut être remboursé", "livraison": "question sur une livraison", "compte": "problème d'accès au compte"}},
"urgency": {"type": "score", "instructions": "Quel est le niveau d'urgence de ce message ?", "criteria": ["faible", "moyen", "élevé", "critique"]},
"churn_risk": {"type": "noul", "instructions": "Le client menace de quitter le service ou d'escalader."}},
"n_permutations": 1}
Montre-moi la réponse JSON et vérifie-la : intent a un choice et des probabilités dont la somme vaut 1, urgency a un level et un score, churn_risk est un nombre entre 0 et 1. Donne latency_ms, puis renvoie la même requête pour montrer la latence à chaud.
5. Termine par un court résumé : backend utilisé, chemin d'installation, comment arrêter et relancer le serveur, et où aller ensuite (https://kahn1.com/fr/demarrer/ pour l'API, le format de réponse, les réglages et la calibration sur mes propres données).
Chaque lien remplit le prompt et attend : rien ne s'exécute tant
que vous ne l'avez pas lu et validé. Si un lien ne fait rien, l'application n'est pas installée (Claude Code
enregistre ses liens après sa première session). Depuis un terminal bash ou zsh :
claude "$(curl -s https://kahn1.com/fr/demarrer/setup-prompt.md)". Le prompt est aussi sur https://kahn1.com/fr/demarrer/setup-prompt.md.
02 · Installer
Python 3.11 ou plus et uv. Le backend GPU est vLLM, qui demande Linux ou WSL2 avec CUDA ; le backend CPU tourne partout.
$ git clone https://github.com/Okura66/kahn1 && cd kahn1 $ uv venv --python 3.11 $ source .venv/bin/activate # .venv\Scripts\activate sous Windows $ uv pip install -e ".[gpu]" # vLLM, GPU CUDA $ uv pip install -e ".[cpu]" --extra-index-url https://download.pytorch.org/whl/cpu # ou CPU seul
Les poids viennent de Hugging Face au premier usage, en modèle fusionné (celui que sert vLLM) ou en adaptateur LoRA. Kahn1 existe en deux tailles, et toutes deux tournent sur les backends GPU et CPU. Kahn1 4B, bien meilleur sur les décisions difficiles : le modèle fusionné ou l'adaptateur LoRA (57 Mo, par-dessus Qwen/Qwen3.5-4B) ; son modèle fusionné pèse 8,4 Go et demande 12 Go de VRAM ou plus avec vLLM. Kahn1 3B, plus petit et plus rapide (médiane de 36 ms contre 89 ms) : le modèle fusionné (6,17 Go) ou l'adaptateur LoRA (239 Mo, par-dessus Qwen2.5-3B-Instruct). Code sous MIT ; poids de Kahn1 4B sous Apache 2.0 ; poids de Kahn1 3B sous la licence de recherche Qwen de son modèle de base : voir ses conditions.
03 · Servir
$ SYSONE_MODEL=Okura66/Kahn1-Qwen3.5-4B uv run sysone serve --port 8000 $ SYSONE_BACKEND=cpu SYSONE_MODEL=Okura66/Kahn1-Qwen3.5-4B uv run sysone serve --port 8000 # sans GPU $ curl http://127.0.0.1:8000/health
Le modèle se charge à la première requête (SYSONE_EAGER=1 le charge au démarrage). Le serveur
écoute sur 127.0.0.1 par défaut ; --host 0.0.0.0 l'expose. Okura66/Kahn1-Qwen2.5-3B
s'utilise de la même façon sur l'un ou l'autre backend. Le moteur choisit le format de
prompt d'après le modèle (EngineConfig.prompt_format vaut "auto" par défaut) : le
template de chat natif pour le 4B Qwen3.5, la mise en forme à balises pour le 3B.
| Variable | Défaut | Rôle |
|---|---|---|
SYSONE_MODEL | checkpoints/qwen_merged s'il existe | identifiant Hugging Face ou chemin local |
SYSONE_BACKEND | vllm | vllm (GPU) ou cpu (transformers) |
SYSONE_CALIBRATION | calibration.json | fichier de températures, appliqué s'il existe |
SYSONE_DTYPE | float32 | CPU seulement ; gardez float32, bfloat16 est ~7× plus lent sans AMX |
SYSONE_THREADS | tous les cœurs | CPU seulement : threads torch |
SYSONE_EAGER | désactivé | 1 charge le modèle au démarrage |
SYSONE_CORS_ORIGIN_REGEX | localhost, kahn1.com | origines navigateur autorisées à appeler l'API |
04 · Envoyer une requête
POST /v1/evaluate prend un état et une liste de questions. Chaque question a un
kind et une key qui nomme sa réponse.
{
"state": "Bonjour, j'ai été débité deux fois pour ma commande #48213 passée le 12 septembre. Cela fait une semaine que j'attends une réponse du service client. Je veux être remboursé au plus vite, sinon je fais opposition auprès de ma banque et je ferme mon compte.",
"questions": [
{
"kind": "choice",
"key": "intent",
"prompt": "Quelle est la demande principale du client ?",
"options": ["remboursement: le client veut être remboursé", "livraison: question sur une livraison", "compte: problème d'accès au compte", "information: demande d'information produit"],
"allow_other": true
},
{
"kind": "score",
"key": "urgency",
"prompt": "Quel est le niveau d'urgence de ce message ?",
"levels": ["faible", "moyen", "élevé", "critique"]
},
{
"kind": "noul",
"key": "churn_risk",
"statement": "Le client menace de quitter le service ou d'escalader."
}
],
"n_permutations": 1
}
$ curl -X POST http://127.0.0.1:8000/v1/evaluate \
-H "Content-Type: application/json" -d @request.json
Vous avez déjà des schémas JEV / TypeSafe ? POST /v1/evaluate/jev prend le même dictionnaire
(type, instructions, criteria). /v1/evaluate l'accepte aussi
dans un champ schema.
$ curl -X POST http://127.0.0.1:8000/v1/evaluate/jev \
-H "Content-Type: application/json" \
-d '{
"state": "Impossible de me connecter à mon compte depuis ce matin.",
"schema": {
"categorie": {"type": "choice", "instructions": "Catégorie du ticket",
"criteria": {"bug": "Quelque chose est cassé", "facturation": "Factures, remboursements", "compte": "Connexion, droits"}},
"urgence": {"type": "score", "instructions": "Niveau d’urgence", "criteria": ["faible", "moyen", "critique"]},
"menace_juridique": {"type": "noul", "instructions": "Le client menace d’une action en justice"}
}
}'
05 · Lire la réponse
La réponse à la requête ci-dessus, enregistrée depuis le 3B publié sur CPU (d'où la latence : sur GPU avec vLLM, comptez une médiane d'environ 36 ms pour le 3B et 89 ms pour le 4B à k = 3).
{
"answers": {
"intent": {
"choice": "remboursement: le client veut être remboursé",
"probabilities": {
"remboursement: le client veut être remboursé": 0.9411,
"livraison: question sur une livraison": 0.0026,
"compte: problème d'accès au compte": 0.0005,
"information: demande d'information produit": 0.0004,
"None of these answers": 0.0552
},
"confidence": 0.9264
},
"urgency": {
"score": 2.2562,
"level": "élevé",
"probabilities": {
"faible": 0.0065,
"moyen": 0.0125,
"élevé": 0.6992,
"critique": 0.2818
},
"confidence": 0.5989,
"expected_score": 3.2562
},
"churn_risk": {
"noul": 0.8614
}
},
"latency_ms": 21192.1,
"cache_hit_rate": 0.0
}
| Champ | Sens |
|---|---|
choice | l'option la plus probable, telle qu'écrite dans la requête |
probabilities | la distribution complète, de somme 1. Choice ajoute None of these answers quand allow_other vaut true |
confidence | (p_max − 1/n) / (1 − 1/n) : l'écart à un tirage uniforme, pas la probabilité d'avoir raison |
score / expected_score | Σ i·pi, à partir de 0 / de 1 : une position continue sur l'échelle |
level | le niveau le plus probable |
noul | p(oui), entre 0 et 1 |
cache_hit_rate | part des tokens du prompt servis par le cache de préfixe (vLLM) |
06 · Depuis Python
from sysone.calibrate import CalibratedEngine, TemperatureConfig
from sysone.engine import Engine, EngineConfig
from sysone.types import ChoiceQuestion, NoulQuestion, Query, ScoreQuestion
engine = Engine(EngineConfig(model="Okura66/Kahn1-Qwen3.5-4B")) # vLLM, GPU
# from sysone.cpu import CPUEngine
# engine = CPUEngine("Okura66/Kahn1-Qwen3.5-4B", dtype="float32") # or a CPU
# "Okura66/Kahn1-Qwen2.5-3B" works the same way in both
engine = CalibratedEngine(engine, TemperatureConfig.load("calibration.json"))
query = Query(state="Hi, I was charged twice for order #48213 ...", questions=[
ChoiceQuestion(key="intent", prompt="What is the customer's main request?",
options=["refund", "delivery", "account", "information"]),
ScoreQuestion(key="urgency", prompt="How urgent is this message?",
levels=["low", "medium", "high", "critical"]),
NoulQuestion(key="churn_risk", statement="The customer threatens to leave."),
])
# Or from a JEV dictionary: Query.from_jev(state=..., schema={...})
resp = engine.evaluate(query, n_permutations=3)
print(resp.answers["intent"].choice, resp.answers["intent"].probabilities)
print(resp.answers["urgency"].expected_score, resp.answers["churn_risk"].noul)
Engine.evaluate_batch(queries) traite plusieurs états en un appel (le benchmark Snake fait
avancer 20 parties de cette façon).
07 · Régler la réponse
n_permutations(k, 3 par défaut). Choice est posée k fois avec les options mélangées, et les distributions sont moyennées, ce qui annule le biais de position. Score, à partir de k = 2, est posée dans l'ordre croissant puis décroissant. Noul est posée une fois. Chaque permutation est un prompt de plus : sur un état court, k = 3 coûte près de 3×, moins sur un état long dont le moteur met le préfixe en cache ; k = 1 est le plus rapide.allow_other(Choice, true par défaut) ajoute None of these answers en dernière option. Mettez false quand une option s'applique toujours.- Plus de 26 options. La voie directe lit les lettres A à Z. Au-delà, appelez
engine.evaluate_two_stage(query)depuis Python : un Noul par option retient 10 candidats, puis un Choice tranche entre eux. L'API HTTP ne fait pas ce routage. - Écrivez les options comme une étiquette courte suivie d'une description
(
"remboursement: le client veut être remboursé") : le modèle lit le texte, et les dictionnairescriteriade JEV sont convertis sous cette forme.
08 · Calibrer sur vos données
Les températures sont propres à un modèle, et celles de Kahn1 ont été ajustées sur une partie de la distribution d'entraînement. Sur votre domaine, ajustez les vôtres pour le modèle que vous servez avant de vous fier à un seuil. Il faut des exemples annotés en JSONL :
# un exemple annoté par ligne ; label = indice de la bonne option ou du bon niveau,
# et pour noul 1 = oui, 0 = non
{"state": "...", "kind": "choice", "prompt": "...", "options": ["a", "b", "c"], "label": 2}
{"state": "...", "kind": "score", "prompt": "...", "levels": ["bas", "moyen", "haut"], "label": 0}
{"state": "...", "kind": "noul", "statement": "...", "label": 1}
$ uv run sysone calibrate --data mes_exemples.jsonl --out calibration.json --model Okura66/Kahn1-Qwen3.5-4B $ curl -X POST "http://127.0.0.1:8000/v1/calibrate/load?path=calibration.json" # rechargement à chaud
Choisissez ensuite votre seuil d'automatisation sur des exemples réservés de votre domaine, et envoyez le reste à une personne ou à un modèle plus gros.
10 · Aller plus loin
Pour entraîner sur votre domaine ou sur un autre modèle ouvert (Llama 3.2 3B, Qwen2.5 7B…), suivez le
playbook (en anglais) :
format des données, entraînement LoRA, fusion pour vLLM, calibration. Les rapports d'évaluation et les scripts
sont dans reports/.