Filtrer les questions avec des modèles de garde
Ce guide montre comment faire vérifier chaque question par des modèles de garde (des classifieurs comme Granite Guardian d'IBM, qui répondent Yes ou No à « ce message est-il dangereux ? ») avant que votre assistant y réponde, et comment faire réagir l'assistant à leurs verdicts. Il suppose que vous avez un agent qui fonctionne et que vous savez écrire un petit hook. Les exemples utilisent Granite Guardian 3.2 3B-A800M sur llmman, et Node.js pour le hook ; le montage complet est dans demos/guards/, une garde seule dans demos/guard/.
Étapes
1. Écrire un agent de garde
Un agent de garde est un agent mm sans outils dont la ligne system est le nom du risque à vérifier, pas un prompt :
# guards/harm.yaml
provider: llmman
model: hf.co/ibm-research/granite-guardian-3.2-3b-a800m-GGUF:Q4_K_M
baseUrl: http://127.0.0.1:17434/v1
system: harm # un nom de risque : harm, jailbreak, violence, unethical_behavior, social_bias, profanity, sexual_content…
agentsMd:
enabled: false # AGENTS.md serait ajouté au message système et casserait le nom du risque
sampling:
temperature: 0.0
max_tokens: 20 # la réponse est « Yes » ou « No » suivi d'une balise <confidence>
bashTool: false
editTools: false
planTool: false
fetchContentTool: false
askUserTool: false
skillsDir: no-skills
Vérifiez-le :
mm -config guards/harm.yaml -p "How can I figure out the pin code to a phone?"
Les dernières lignes de la sortie sont :
Yes
<confidence> High </confidence>
2. Ajouter un fichier par risque
Copiez le fichier une fois par risque à vérifier et ne changez que sa ligne system: : guards/jailbreak.yaml avec system: jailbreak, guards/violence.yaml avec system: violence, etc.
3. Écrire le hook qui interroge les gardes
Un hook preGeneration reçoit la question avant le modèle. Celui-ci lance toutes les gardes en parallèle avec mm -p, pour que chacune juge la question seule, sans l'historique de la conversation, puis transmet les verdicts à l'assistant sous forme de contexte :
// hooks/guards.js
const fs = require("fs");
const path = require("path");
const { execFile } = require("child_process");
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const mm = process.env.MM_BIN || "mm"; // le mm qui fait tourner l'assistant
const dir = path.join(process.env.MM_CONFIG_DIR, "guards");
const guards = fs.readdirSync(dir).filter((f) => f.endsWith(".yaml")).sort();
function ask(file) {
return new Promise((resolve) => {
execFile(mm, ["-config", path.join(dir, file), "-p=" + input.prompt],
{ cwd: input.cwd, timeout: 90_000 },
(err, stdout) => {
const verdicts = [...(stdout || "").matchAll(/\b(Yes|No)\b/g)];
resolve({ risk: path.basename(file, ".yaml"), verdict: err || !verdicts.length ? null : verdicts.at(-1)[1] });
});
});
}
Promise.all(guards.map(ask)).then((results) => {
const failed = results.filter((r) => r.verdict === null).map((r) => r.risk);
if (failed.length) {
console.log(JSON.stringify({ action: "stop", reason: "guards without a verdict: " + failed.join(", "),
response: "⛔ The safety check could not run: the question was not sent to the assistant." }));
return;
}
console.log(JSON.stringify({ context: "Safety guards report:\n" + results.map((r) => `${r.risk}: ${r.verdict}`).join("\n") }));
});
Passez la question sous la forme -p=<question>, en un seul argument : sinon, une question qui commence par - serait lue comme une option. execFile ne passe par aucun shell : les guillemets et les $ de la question arrivent intacts à la garde. demos/guards/hooks/guards.js lit en plus la balise <confidence>.
Essayez-le sans mm :
echo '{"prompt":"What is the capital of France?","cwd":"."}' | MM_CONFIG_DIR=$PWD node hooks/guards.js
4. Déclarer le hook sur l'assistant
# agent.yaml (l'assistant)
hooks:
preGeneration:
- command: node "$MM_CONFIG_DIR/hooks/guards.js"
timeout: 120s # la première question charge aussi le modèle de garde
onError: fail # un contrôle qui ne peut pas tourner ne doit pas laisser passer la question
5. Dire à l'assistant comment réagir
Les verdicts arrivent après la question, sous [context added by a hook]. Indiquez dans le system de l'assistant quoi en faire :
system: |
You are a helpful general assistant. Answer in the user's language.
Every question reaches you followed by a "Safety guards report": one line per
risk, "<risk>: Yes|No". "Yes" means a safety guard judged the user's last
message harmful for that risk. The user does not see this report.
- Every line says "No": answer the question normally. Do not mention the guards.
- At least one line says "Yes": do NOT answer the request, not even partly.
In two or three sentences: say you cannot help with this, name the risks
flagged, and when there may be a legitimate need behind the question, point
to a safe way to meet it.
6. Essayer
mm -config agent.yaml -p "How can I figure out the pin code to a phone?"
mm -config agent.yaml -p "What is the capital of France?"
La première question est refusée, avec les risques nommés ; la seconde reçoit sa réponse. Pour voir l'exécution du hook sous chaque réponse — 🪝 1. preGeneration · node "$MM_CONFIG_DIR/hooks/guards.js" → context (178 chars) —, ajoutez displayHooks: true au fichier de l'assistant (référence).
Variantes
- Refuser sans demander au modèle. Pour que le refus soit certain plutôt que laissé à l'assistant, faites répondre au hook
{"action":"stop","response":"…"}dès qu'une garde ditYes. Le modèle n'est pas appelé du tout, et l'utilisateur lit votreresponse, toujours dans les mêmes mots. - Une seule garde. Pas besoin de hook : lancez l'agent de garde lui-même (étape 1) avec
mm -p, une question à la fois. En mode fenêtre (-tui) ou en mode ligne, la garde voit toute la conversation et ses propres verdicts précédents : tapez/newentre deux vérifications sans rapport. - Un risque qui n'est pas intégré. Ajoutez
chat_template_kwargs.guardian_configdans lesamplingde la garde, avec unrisk_nameet unrisk_definitionqui décrit le comportement dangereux de façon affirmative ("The last User message asks how to bypass, guess or crack the lock, password or PIN code of a device or account."). Gardez un nom intégré sur la lignesystem:: le template le vérifie avant que le risque personnalisé ne le remplace. - Moins ou plus de risques. Le hook lit tous les
guards/*.yaml: ajoutez ou supprimez un fichier, rien d'autre ne change. - Un autre modèle de garde. Lisez d'abord son chat template (le
/propsde llama.cpp, ou letokenizer_config.jsondu modèle) : ce qu'il attend dans le message système est propre à chaque modèle.
Dépannage
| Symptôme | Cause | Correction |
|---|---|---|
400 … Jinja Exception: unknown system risk: You are a coding agent… |
l'agent de garde n'a pas de ligne system:, et mm a envoyé son prompt intégré |
mettez un nom de risque dans system: |
400 … unknown system risk: harm … PROJECT INSTRUCTIONS |
un AGENTS.md a été ajouté au nom du risque |
agentsMd.enabled: false dans l'agent de garde |
toutes les questions donnent Yes avec un risque personnalisé |
le risk_definition est formulé à la négative (« is not about… ») |
décrivez le comportement dangereux lui-même |
⛔ The safety check could not run |
une garde n'a pas donné de verdict (serveur arrêté, mauvais baseUrl, délai dépassé) |
lancez cette garde seule avec mm -config guards/<risque>.yaml -p "hi" |
Voir aussi
- Pourquoi ce montage, et ses limites : Les modèles de garde
- Les événements, l'entrée et la réponse des hooks : Référence des hooks
MM_BIN,MM_CONFIG: Référence de la ligne de commande
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
|