Bei Experience One schreiben wir Sicherheit im Umgang mit KI groß: Die AG ist nach ISO 27001 zertifiziert, der AI Act wird intern geschult, und in unseren Projekten wird Red-Teaming von KI-Anwendungen betrieben – gegen Jailbreaks, Prompt Injection und Prompt Obfuscation. Auch die DSGVO spielt dabei immer eine Rolle.
Gerade im Gesundheitssektor ist besondere Vorsicht geboten, denn hier wird mit besonders schützenswerten Daten gearbeitet. Und ein Chat wird aus Datenschutzsicht schnell sensibel.
Genau deshalb habe ich ein Experiment gewagt: ein Chat auf Basis lokaler Modelle, die ausschließlich im Browser laufen. Personenbezogene Daten verlassen den Browser nicht; das Journal des Users wird im Local Storage gespeichert.
Ermöglicht wird dies durch Transformers.js, das Modelle im Browser via ONNX Runtime ausführt – mit CPU oder WebGPU. Hugging Face bietet speziell zugeschnittene Modelle:
Unter der ID
mistralai/Ministral-3-3B-Instruct-2512-ONNXerhält man Ministral vom Hersteller. Es ist zwar ein Vision-Language-Modell (VLM), welches allerdings auch rein textbasiert über Tensoren aufgerufen werden kann, indem die Bildkomponente ignoriert wird.Die ONNX Community bringt zudem
onnx-community/Llama-3.2-3B-Instructhervor.
// Import Transformers.js for browser-based ML
import {
AutoTokenizer, AutoModelForImageTextToText
} from 'https://cdn.jsdelivr.net/npm/@huggingface/[email protected]';
async function run() {
// Get DOM elements for progress visualization
const progressBar = document.getElementById('progress-bar');
let maxProgress = 0;
// Model configuration - Ministral-3-3B is a VLM but works for text-only
const modelId = 'mistralai/Ministral-3-3B-Instruct-2512-ONNX';
// Step 1: Load tokenizer (converts text to tokens)
const tokenizer = await AutoTokenizer.from_pretrained(modelId);
// Step 2: Load model with WebGPU acceleration
const model = await AutoModelForImageTextToText.from_pretrained(modelId, {
device: 'webgpu', // Enable GPU acceleration in browser
dtype: {
embed_tokens: 'fp16',
vision_encoder: 'q4',
decoder_model_merged: 'q4f16'
}, // Mixed precision for optimal performance
progress_callback: (e) => {
// Update progress bar during model loading
if (e.progress !== undefined) {
maxProgress = Math.max(maxProgress, e.progress);
progressBar.style.width = `${maxProgress}%`;
}
}
});
// Step 3: Prepare input - chat template adds model-specific formatting
const messages = [{ role: 'user', content: 'Erkläre WebGPU in einem Satz.' }];
const inputs = tokenizer.apply_chat_template(messages, {
add_generation_prompt: true, // Add model's generation prompt
return_dict: true, // Return as dictionary for easy access
});
// Step 4: Generate text with the model
const outputs = await model.generate({
input_ids: inputs.input_ids, // Tokenized input
attention_mask: inputs.attention_mask, // Attention mask for input
max_new_tokens: 50 // Limit response length
});
// Step 5: Extract only the generated tokens (skip input tokens)
const newTokens = outputs.slice(
null,
[inputs.input_ids.dims.at(-1) ?? 0, null] // Slice from end of input to end
);
// Step 6: Decode tokens back to human-readable text
const text = tokenizer.batch_decode(newTokens, {
skip_special_tokens: true
})[0].trim();
// Display the result
document.body.innerHTML = `<h1>Antwort:</h1><pre>${text}</pre>`;
}
// Start the demo and handle errors
run().catch(err => {
console.error(err);
document.body.innerHTML = `<h1>Fehler:</h1><pre>${err.stack || err}</pre>`;
});ReAct Prompting
Die Modelle prompte ich „ReActive“. Es ist ein Text-Protokoll, das bewusst nicht natives JSON Tool-Calling verwendet, um eine gewisse Modell-Unabhängigkeit zu erreichen.
In a nutshell, weist man das LLM an, strikt nach dem Pattern:
Reason
Act
Observe
vorzugehen und schreibt die Agent-Loop darum herum – eine simple for-Schleife mit maximaler Anzahl Iteration/Tiefe.
Hier der Prompt, den ich verwende:
[persona, if any]
You solve the user's task with a strict Reason → Act → Observe loop.
You have access to the following tools:
- <toolname>: <description>
Arguments (JSON): { "arg": <type>, ... }
...
Respond with ONE step per reply, in EXACTLY this format:
Thought: <your reasoning about what to do next>
Action: <one of: tool1, tool2, ...>
Action Input: <a single-line JSON object of arguments, or {} if the tool takes none>
After each Action, you will be given:
Observation: <the tool's result>
Repeat the Thought / Action / Action Input cycle as needed. When you have enough
information, reply instead with:
Thought: <why you can now answer>
Final Answer: <the answer for the user>
Rules:
- ...Wie viele Minuten seit Mitternacht?
Eine Frage, die ein 3B Modell alleine nicht beantworten kann.
Es muss die Uhrzeit wissen,
Wissen, wie viele Minuten eine Stunde hat,
und, dass Mitternacht 00:00 Uhr ist,
Abschließend ausrechnen: Stunde × 60 + Minuten
Also verschalte ich mehrere Modelle als „ReActive“ Agenten verpackt mittels Sub-Agents-Pattern. Der Main-Agent bekommt weitere Agenten als Tools angebunden und kann diese „ReActive“ aufrufen. Diese wiederum sind mit Agenten verschaltet usw. – jeder Agent kann genau eine Sache. Atomar. Blatt-Agenten am Ende eines Astes sind keine ReActAgents mehr – sondern SimpleAgents.
Neben dem Datetime-Agenten gibt es weitere atomare Agenten – welche für rewrite, search, answer, summary, entities, tone, write und journal. Der general Agent soll alles andere einfach abfangen und soweit möglich aus trainiertem Weltwissen antworten.
Der Ameisen-Haufen ist komplett und beantwortet schlussendlich die Frage korrekt: um 13:29 sind 809 Minuten vergangen. Ich hätte mir noch den Tone-of-Voice-Agenten gewünscht aber ein simples „809“ ist zumindest korrekt.

Es ist „work-in-progress“ und ich bin gespannt, wie weit ich damit komme. Meine nächsten Schritte sind ganz klar RAG – bin ich schon dran mit einer BM25 Textsuche und TXT-Dateien. Erste Tests laufen gut. I keep you posted.
Auch spannend, was passiert, wenn man dann mal ein Mistral Medium oder so anbindet. Also auch kein Schwergewicht aber deutlich fähiger als 3B-Modelle.
🐜🐜🐜

