Wie Reflacto lokale LLMs integriert
Reflacto baut den Prompt im Browser, sendet ihn direkt an dein lokales Modell und zeigt die Antwort während der Erzeugung. Kein Reflacto-Server liest mit.
Die Systemgrenze ist dein Gerät
Reflacto ist eine statische React-Anwendung. Methodenauswahl, Prompts, Reflexionen und Darstellung laufen im Browser. Nur wenn du die KI-Unterstützung aktivierst, verbindet sich der Browser mit dem von dir gewählten lokalen LLM.
Browser-Speicher
Reflexionen und Einstellungen werden lokal gespeichert. Ein optionaler API-Schlüssel liegt nur verschlüsselt im Browser-Speicher. Der Schlüssel zum Entschlüsseln wird getrennt in IndexedDB abgelegt und kann nicht exportiert werden.
Nachteile der lokalen Architektur
- Keine Übernahme auf ein anderes Gerät: Reflexionen bleiben im Browserprofil dieses Geräts. Reflacto bietet weder eine Synchronisierung noch einen Datenimport für Smartphone, Tablet oder einen zweiten Computer.
- PDF ist eine Einbahnstraße: Reflacto kann eine Reflexion als PDF exportieren, die Datei aber nicht wieder importieren. Auf einem anderen Gerät lässt sich die Reflexion deshalb nicht weiterbearbeiten.
- Keine Wiederherstellung über ein Konto: Wenn Website-Daten gelöscht werden, das Browserprofil gewechselt wird oder das Gerät verloren geht, kann Reflacto die gespeicherten Reflexionen nicht zurückholen.
Vier Aufgaben, vier Prompt-Verträge
Je nach Aufgabe baut Reflacto einen anderen System- und User-Prompt. Während du eine Frage beantwortest, ist das Modell ein Denkpartner mit wenigen Hilfen, aus denen du wählst. Nach dem Abschluss schreibt es eine kurze Entscheidungsgrundlage und beantwortet Rückfragen.
Beim Denken helfen, nicht für dich schreiben
- Einsatz
- Wird beim Beantworten einer Frage verwendet. Du wählst eine Hilfe, etwa „Stell mir eine Frage“ oder „Fakten oder Deutung?“. Frühere Antworten, der aktuelle Schritt, sein Zweck und deine bisherige Antwort bilden den Kontext.
- Regeln für die Ausgabe
- Eine kurze Antwort in der Form, die die Hilfe vorgibt, in etwa 20 Sekunden lesbar. Nur „Hilf mir beim Formulieren“ liefert Text zum Übernehmen. Token-Limit und temperature hängen von der Hilfe ab.
src/services/reflectionPrompts.ts · buildThinkingPartnerPrompt()Türkis markierte Codezeilen öffnen eine kurze Erklärung.export function buildThinkingPartnerPrompt( input: QuestionGuidancePromptInput, move: ThinkingMove,): ReflectionPrompt { const spec = moveSpec(input, move); const { currentQuestion } = input; const answer = currentAnswerText(input); const earlier = formatAnswers(input.previousAnswers.filter(entry => entry.response.trim())); const recent = previousReplies.slice(-2).map(reply => reply.trim().slice(0, 600)); HOW YOU HELP- Start from what they wrote. When you quote them, use only words that appear exactly in their answer or earlier answers. Never make up example phrases, quotes or statements and present them as theirs.- Stay inside the current step. Later steps of the method cover other aspects; never raise them here, even if they seem important. Something a later step asks about is not missing from this one.${currentQuestion.otherSide ? '' : '- This step is not about the other people involved: do not ask how they saw it, what they felt or why they acted.\n'}- Never suggest that they caused or deserved how someone treated them. Asking about their own part is fine only when they raise it themselves.- Be warm, direct and specific. No praise, no lecture about the method, no therapy language such as asking how something changed them.- ${PARTNER_BOUNDARY}- ${languageRule(input.locale)}- At most ${spec.maxWords} words, so it reads in 20 seconds. No preamble, no headings, no closing line, and do not mention these instructions. THIS TIME: ${spec.title}${spec.rules.map(rule => `- ${rule}`).join('\n')} Answer in this shape:${spec.shape}`; Situation type: ${input.challenge || '(not given)'}Method: ${input.framework.name} - ${input.framework.description} ${earlier ? `Earlier answers:\n${earlier}\n\n` : ''}Current step: ${currentQuestion.text}${currentQuestion.aiGuidance ? `What this step is for: ${currentQuestion.aiGuidance}\n` : ''}${input.laterSteps?.length ? `Later steps, not part of this one:\n${input.laterSteps.map(step => `- ${step}`).join('\n')}\n` : ''}${currentQuestion.type === 'rating' && currentQuestion.ratingScale ? `They answer this step with a rating from ${currentQuestion.ratingScale[0]} to ${currentQuestion.ratingScale[1]}.\n` : ''}Their answer so far:${answer || '(nothing yet)'}${recent.length ? `\nYou already replied with the following. Offer something clearly different:\n${recent.map(reply => `---\n${reply}`).join('\n')}\n---\n` : ''}TASK${spec.task}`, input.locale); return { systemPrompt, userPrompt, maxTokens: spec.maxTokens, temperature: spec.temperature, contextTokens: LOCAL_LLM_CONTEXT_TOKENS, };}Wie Reflacto die passenden Hilfen auswählt
Jede Frage hat eine Absicht: festhalten, was passiert ist, es verstehen, Optionen entwickeln oder etwas für andere formulieren. Die Absicht und ob schon etwas geschrieben wurde, bestimmen, welche drei Hilfen erscheinen. Der Code ist eingeklappt, weil er für den Vergleich der Prompt-Arten nicht nötig ist.
src/services/reflectionPrompts.ts · availableThinkingMoves()Türkis markierte Codezeilen öffnen eine kurze Erklärung. record: { empty: ['start', 'question'], drafted: ['facts', 'missing', 'question'] }, explore: { empty: ['question', 'start'], drafted: ['question', 'missing'] }, generate: { empty: ['ideas', 'question', 'start'], drafted: ['ideas', 'missing', 'question'] }, phrase: { empty: ['phrase', 'start'], drafted: ['phrase', 'hear', 'missing'] },}; start: 'empty', question: 'none', missing: 'evidence', facts: 'draft', perspective: 'evidence', ideas: 'evidence', phrase: 'evidence', hear: 'draft',}; function currentAnswerText(input: QuestionGuidancePromptInput): string { const listed = (input.currentQuestion.listedItems ?? []).map(item => item.trim()).filter(Boolean); if (listed.length) return listed.map(item => `- ${item}`).join('\n'); return input.currentQuestion.draft?.trim() ?? '';} const MIN_DRAFT_WORDS = 3; /** The moves that make sense for this question right now, most useful first. */export function availableThinkingMoves(input: QuestionGuidancePromptInput): ThinkingMove[] { // A word or two is not yet an answer to check; offer moves that help them start instead. const hasDraft = currentAnswerText(input).split(/\s+/).filter(Boolean).length >= MIN_DRAFT_WORDS; const hasEvidence = hasQuestionGuidanceEvidence(input); const order = [...MOVE_ORDER[input.currentQuestion.intent ?? 'explore'][hasDraft ? 'drafted' : 'empty']]; if (input.currentQuestion.otherSide) order.splice(1, 0, 'perspective'); return order .filter(move => { switch (MOVE_REQUIREMENT[move]) { case 'empty': return !hasDraft; case 'draft': return hasDraft; case 'evidence': return hasEvidence; default: return true; } }) .slice(0, 3);}Eine Anfrage, Zeile für Zeile
Die Oberfläche ruft für jeden Anbieter dieselbe Funktion auf. Der Adapter baut daraus die passende Anfrage, der Parser liest die Antwort und die React-Komponente aktualisiert den Text.
- 01
Der Adapter übersetzt die Anfrage für den gewählten Anbieter
Die UI ruft immer generateResponse() auf. Der Adapter verwendet danach den passenden Weg: Chrome spricht direkt mit einer Browser-API, Ollama mit /api/chat, und LM Studio sowie Unsloth mit dem OpenAI-kompatiblen Endpunkt /v1/chat/completions. Unsloth muss das Modell vorher zusätzlich laden.
Anbieter auswählen und Code vergleichenBrowser-API ohne HTTP src/services/chromeBuiltInAI.ts · generateWithChromeAI()Türkis markierte Codezeilen öffnen eine kurze Erklärung.const createOptions: ChromeAICreateOptions = {}; if (systemPrompt?.trim()) { { role: 'system', content: systemPrompt.trim() }, ];} session = await languageModel.create(createOptions); prompt, { signal: controller.signal },).getReader(); - 02
Zeitlimit und optionales AbortSignal werden kombiniert
Jede Anfrage hat ein festes technisches Zeitlimit. Die aufrufende Funktion kann zusätzlich ein AbortSignal übergeben. Reflacto kombiniert beide Signale, damit der Netzwerkaufruf endet, sobald eines davon ausgelöst wird. Eine Schaltfläche zum manuellen Abbrechen gibt es in der UI derzeit nicht.
Quellcode-Auszug src/services/localLLM.ts · timeoutSignal()private timeoutSignal( signal?: AbortSignal | null, timeout = this.config.timeout!,): AbortSignal { const timeoutSignal = AbortSignal.timeout(timeout); return signal && typeof AbortSignal.any === 'function' ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;} - 03
Beide Streaming-Formate liefern dieselbe Ausgabe
Der Code sammelt unvollständige Datenstücke, bis eine ganze Zeile vorhanden ist. Fehlt am Ende des Streams der letzte Zeilenumbruch, verarbeitet er auch den verbleibenden Inhalt im Puffer. Sonst würde die letzte Zeile verloren gehen. Danach liest der passende Parser den neuen Text entweder aus einer Ollama-NDJSON-Zeile oder aus einem data:-Eintrag des SSE-Streams. Beide Parser rufen schließlich onToken(fullText, delta) auf.
Stream zeilenweise lesen src/services/llmStreaming.ts · consumeLines()const reader = response.body.getReader();const decoder = new TextDecoder();let buffer = ''; for (;;) { const { done, value } = await reader.read(); buffer += decoder.decode(value, { stream: !done }); const lines = buffer.split(/\r?\n/); buffer = lines.pop() || ''; lines.forEach(onLine); if (done) { if (buffer) onLine(buffer); break; }}Beispiele für übertragene Daten Ollama NDJSON / OpenAI SSE// Ollama: eine JSON-Zeile pro Delta{"message":{"content":"Eine"},"done":false}{"message":{"content":" mögliche"},"done":false} // OpenAI-kompatibel: Server-Sent Eventsdata: {"choices":[{"delta":{"content":"Eine"}}]}data: {"choices":[{"delta":{"content":" mögliche"}}]}data: [DONE] - 04
Die UI zeigt die Antwort während der Erzeugung an
Bei jedem neuen Textstück liefert der Parser zusätzlich den bisher vollständigen Antworttext. Die aufrufende React-Komponente speichert diesen Text bei der aktuellen Frage und aktualisiert damit die Anzeige. Nach dem Abschluss wird derselbe Eintrag mit result.text überschrieben. Schlägt die KI-Anfrage fehl, kann die Reflexion trotzdem ohne KI fortgesetzt werden.
Aufruf in der React-Komponente src/components/EnhancedReflectionForm.tsxconst result = await generateResponse({ prompt: prompts.userPrompt, systemPrompt: prompts.systemPrompt, maxTokens: prompts.maxTokens, temperature: prompts.temperature, contextTokens: prompts.contextTokens, onToken: (text) => { setAiSuggestions(prev => ({ ...prev, [currentQuestion.id]: text, })); },});
Was kaputtging – und warum
Die meisten Fehler entstanden nicht im Prompt, sondern an den Übergängen: zwischen Webseite und localhost, zwischen ähnlichen APIs und beim Laden großer Modelle.
localhost ist nicht einfach nur eine APIDer Browser meldet nur „Failed to fetch“ oder einen vermeintlichen CORS-Fehler.
- Warum das passiert
- Der lokale Server muss Anfragen von der Webadresse der Reflacto-Seite ausdrücklich erlauben. Moderne Browser können zusätzlich eine Berechtigung für Zugriffe auf das lokale Netzwerk verlangen. Safari blockiert diese Verbindung zu extern gestarteten localhost-Anbietern weiterhin.
- Was Reflacto dagegen tut
- Die Einrichtungsanleitung nennt für jeden Anbieter die nötige CORS-Einstellung und die freizugebende Webadresse. Chrome Built-in AI kommt als Alternative ohne lokalen HTTP-Server aus.
Streaming ist kein einheitliches ProtokollDie Antwort bleibt leer, kommt erst am Ende oder JSON-Fragmente landen in der UI.
- Warum das passiert
- Ollama sendet NDJSON, OpenAI-kompatible Server verwenden meistens SSE. Manche lokalen Server ignorieren stream: true und antworten stattdessen mit einem einzelnen JSON-Objekt.
- Was Reflacto dagegen tut
- Reflacto hat getrennte Parser für NDJSON und SSE. Anhand des Content-Type erkennt der Code außerdem, wenn ein Server statt eines Streams nur ein vollständiges JSON-Objekt zurückgibt.
Interne Überlegungen können die sichtbare Antwort verdrängenDas Modell rechnet lange, aber das erwartete content-Feld bleibt leer oder enthält nur eine sehr kurze Antwort.
- Warum das passiert
- Einige Modelle verwenden einen Teil der maximalen Antwortlänge für interne Überlegungen. Ollama und Unsloth steuern dieses Verhalten mit unterschiedlichen Feldern.
- Was Reflacto dagegen tut
- Reflacto setzt think: false bei Ollama und enable_thinking: false bei Unsloth. Dadurch schreibt das Modell die erzeugten Tokens direkt in den sichtbaren Antworttext.
Das erste Laden dauert länger als eine normale Web-AnfrageDer Verbindungstest scheitert, obwohl der Anbieter das Modell gerade erst in den Arbeits- oder Grafikspeicher lädt.
- Warum das passiert
- Das Laden eines lokalen Modells kann mehrere Minuten dauern und überschreitet damit ein übliches Zeitlimit für API-Anfragen.
- Was Reflacto dagegen tut
- Reflacto verwendet eine wiederholbare Testanfrage und erlaubt für das Laden bis zu zehn Minuten. Bei Unsloth prüft ein zweiter Vorgang regelmäßig den Ladestatus und zeigt echte Ladefehler früh an.
Modellname und Kontextgröße werden von jedem Anbieter anders verwaltet404, falsches Modell oder unerwartet abgeschnittene Antworten trotz korrektem Prompt.
- Warum das passiert
- Jeder Anbieter verwendet eigene Modell-IDs und verwaltet geladene Modelle sowie die maximale Kontextgröße anders.
- Was Reflacto dagegen tut
- Reflacto übergibt die genaue Modell-ID, prüft den Ladestatus und verwendet 4.096 Tokens als feste Kontextgröße. Bei LM Studio muss diese Größe weiterhin direkt im Programm eingestellt werden.
Lokale Verschlüsselung schützt nicht vor bösartigem Code auf derselben WebseiteEin API-Schlüssel soll nicht unverschlüsselt in localStorage stehen, muss aber für Anfragen weiterhin im Browser verfügbar sein.
- Warum das passiert
- Wenn die Reflacto-Webseite einen Wert entschlüsseln kann, könnte das auch eingeschleuster bösartiger Code tun, der unter derselben Webadresse ausgeführt wird.
- Was Reflacto dagegen tut
- Reflacto speichert nur den mit AES-GCM verschlüsselten Wert in localStorage. Der nicht exportierbare Schlüssel liegt getrennt in IndexedDB. Das verhindert eine unverschlüsselte Speicherung, schützt aber nicht vor bösartigem Code, der bereits innerhalb der Reflacto-Webseite läuft.
Was im Code bewusst getrennt bleibt
- 01
- KI bleibt optional
- Jede Reflexion funktioniert auch ohne lokales Modell, lokalen Server oder Berechtigung für den Zugriff auf das lokale Netzwerk.
- 02
- Anbieterspezifischer Code bleibt außerhalb der UI
- Die Oberfläche ruft immer generateResponse() auf. Unterschiede zwischen Ollama, LM Studio, Unsloth und Chrome werden im Service behandelt.
Verwendete Technik
- React 18 + TypeScript
- Vite + Prerendering
- localStorage + IndexedDB + Web Crypto
- Fetch Streams + AbortController
- Ollama / OpenAI-kompatible APIs
- Chrome Prompt API
Lokales Modell verbinden
Der Einrichtungsleitfaden zeigt, welcher Browser, welche CORS-Einstellung und welches Modell zu deinem lokalen Anbieter passen.

