llamacppctl/scripts/run_prompt_suite.sh
dschlueter b0c215abbb feat(scripts): detect how a model evades, not just whether
classify_refusal() separates refusal (head), preamble, appended disclaimer
(tail) and moralising insert (inline). Only the first three are meaningful, and
only where a prompt forbids them: a preamble on a coding answer is normal, so it
no longer counts as evasion.

Two sources of false positives had to be removed first, both found by checking
the detector's hits against the archive rather than trusting them:

  * llamacppctl's own truncation warning on stderr had leaked into an archived
    output and was read as a model disclaimer. It also inflated that run's word
    count, so count_words() strips it too.
  * An AI character saying "Bitte beachten Sie:" inside a dystopian story is
    plot, not distancing. Quoted speech is removed before markers are matched.

run_prompt_suite.sh gains a CASES filter so a single prompt can be re-run.

Measured result, recorded in KI_TOOLS_PROFILES.md: across seven prose prompts
neither the abliterated model nor the aligned base model evaded once. On
literary prose the abliteration buys nothing measurable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 17:10:39 +02:00

102 lines
3.3 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Faehrt eine Domaene der Beispiel-Prompts gegen ein laufendes Modell-Profil und
# legt die Ausgaben unter der etablierten Namenskonvention im Test-Archiv ab:
#
# <label>_<domaene>_<nr>.out.txt
#
# Der Server fuer <profil> muss bereits laufen (llamacppctl --start --profile ...).
# Ausgewertet wird anschliessend mit scripts/eval_prompt_tests.py.
#
# Verwendung:
# scripts/run_prompt_suite.sh <profil> <label> <domaene> [<domaene>...]
#
# Beispiel (Kontrolllauf des nicht-abliterierten Basismodells auf Prosa):
# scripts/run_prompt_suite.sh qwen35base qwen35base prosa
#
# Environment:
# OUT_DIR Zielverzeichnis (Default: ~/llamacppctl_prompt_tests)
# CONFIG Config-Datei (Default: llama.cpp.config)
# MAX_TOKENS Antwortbudget (Default: 20000 -- Reasoning frisst viel; unter
# 16000 brechen Prosa/Reden mitten im Denken ab)
# CLI Pfad zum CLI (Default: ./.venv/bin/llamacppctl)
# OVERWRITE 1 = vorhandene Ausgaben ueberschreiben (Default: 0 = ueberspringen)
# CASES Leerzeichenliste von Fallnummern, z. B. "06 07 08" (Default: alle)
#
set -uo pipefail
cd "$(git rev-parse --show-toplevel)" || exit 1
if [ "$#" -lt 3 ]; then
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
exit 2
fi
PROFILE=$1; LABEL=$2; shift 2
OUT_DIR=${OUT_DIR:-"$HOME/llamacppctl_prompt_tests"}
CONFIG=${CONFIG:-llama.cpp.config}
MAX_TOKENS=${MAX_TOKENS:-20000}
CLI=${CLI:-./.venv/bin/llamacppctl}
OVERWRITE=${OVERWRITE:-0}
if [ ! -x "$CLI" ]; then
echo "CLI nicht ausfuehrbar: $CLI" >&2
exit 1
fi
# Ohne laufenden Server produziert jeder Prompt nur eine Fehlermeldung als
# "Ergebnis" -- lieber sofort abbrechen als 4 Muelldateien schreiben.
if ! "$CLI" --check --config "$CONFIG" --profile "$PROFILE" >/dev/null 2>&1; then
echo "Server fuer Profil '$PROFILE' ist nicht erreichbar (--check schlug fehl)." >&2
echo "Zuerst starten: $CLI --start --config $CONFIG --profile $PROFILE" >&2
exit 5
fi
mkdir -p "$OUT_DIR"
fail=0
for domain in "$@"; do
system_prompt="example_system_prompts/system_prompt_${domain}.md"
if [ ! -f "$system_prompt" ]; then
echo "System-Prompt fehlt: $system_prompt" >&2
fail=1
continue
fi
for prompt in example_user_prompts/"${domain}"_[0-9][0-9]_*.md; do
[ -e "$prompt" ] || continue
base=$(basename "$prompt" .md) # z. B. prosa_01_werkstatt
number=$(echo "$base" | cut -d_ -f2) # z. B. 01
out="$OUT_DIR/${LABEL}_${domain}_${number}.out.txt"
if [ -n "${CASES:-}" ] && ! echo " $CASES " | grep -q " $number "; then
continue
fi
if [ -e "$out" ] && [ "$OVERWRITE" != "1" ]; then
echo "== $base -> vorhanden, uebersprungen (OVERWRITE=1 erzwingt)"
continue
fi
echo "== $base -> $out"
if "$CLI" --chat --config "$CONFIG" --profile "$PROFILE" \
--system-file "$system_prompt" \
--prompt-file "$prompt" \
--stream --max-tokens "$MAX_TOKENS" > "$out.part" 2>"$out.err"; then
mv "$out.part" "$out"
rm -f "$out.err"
printf ' %s Woerter\n' "$(wc -w < "$out")"
else
echo " FEHLGESCHLAGEN -- siehe $out.err" >&2
rm -f "$out.part"
fail=1
fi
done
done
echo
if [ "$fail" -ne 0 ]; then
echo "Mindestens ein Lauf schlug fehl."
exit 1
fi
echo "Fertig. Auswerten mit: scripts/eval_prompt_tests.py"