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 feste Prompt-Regeln
Je nach Aufgabe baut Reflacto einen anderen System- und User-Prompt. Jeder Tab zeigt, was hineingeht, welche Ausgabe erwartet wird und welchen Code Reflacto dafür verwendet.
Eine Antwort schärfen – ohne Fakten zu erfinden
- Einsatz
- Wird während einer laufenden Reflexion für eine normale Freitextfrage verwendet. Als Eingabe dienen vorherige Antworten, die aktuelle Frage, optionale Hinweise und der vorhandene Entwurf.
- Regeln für die Ausgabe
- Maximal 140 Wörter in einem festen Markdown-Schema: zwei Überschriften auf eigenen Zeilen, ein direkt verwendbarer Formulierungsvorschlag mit zwei bis vier Sätzen und genau zwei Stichpunkte zur Begründung. maxTokens steht auf 300, temperature auf 0,2.
src/services/reflectionPrompts.ts · buildQuestionGuidancePrompt()Türkis markierte Codezeilen öffnen eine kurze Erklärung.export function buildQuestionGuidancePrompt( input: QuestionGuidancePromptInput,): ReflectionPrompt { if (isEnumerationQuestion(input)) return buildEnumerationGuidancePrompt(input); const headings = questionHeadings(input.locale); const previousAnswers = input.previousAnswers .map((answer, index) => `${index + 1}. ${answer.question}\n${answer.response}`) const draft = input.currentQuestion.draft?.trim() || '(no draft yet)'; EVIDENCE RULES:- Make a conservative edit of the current draft. Keep the user's factual terms and intended meaning.- Every event, emotion, timing claim, causal link, motive, consequence and measurement in the suggested answer must be directly traceable to the supplied record. Never intensify facts with words such as "immediately", "frustrated" or "caused" unless the record says so.- Use a relevant previous-answer fact only when it directly improves the current answer. Do not add professional-sounding objectives that the user did not state.- Preserve uncertainty. Label hypotheses and propose validation rather than turning them into facts. COACHING RULES:- Apply the named framework to this question only. Do not move to another step or introduce a different framework.- If the current question concerns a risk, assumption or uncertain interest, make one rationale bullet a reversible way to validate it.- If the record involves power imbalance, retaliation, discrimination, wellbeing, confidentiality or legal uncertainty, acknowledge the relevant boundary and preserve the affected person's agency without diagnosing or making legal conclusions. OUTPUT CONTRACT:- ${optimizedLanguageContract(input.locale)}- Maximum 140 words. No preamble or conclusion.- Return valid Markdown in exactly this structure. Replace the angle-bracket instructions with the requested content and do not reproduce them:### ${headings[0]}<ready-to-use answer of 2-4 sentences> ### ${headings[1]}- <framework/evidence fit>- <most important improvement, unknown or reversible validation step>- Put each heading on its own line with nothing after it. Do not number the headings or sections, use bold text as a heading, or wrap the response in a code fence.- Use exactly 2 concise bullets under "${headings[1]}".- Do not introduce bracketed placeholders such as [date] or [topic] unless they already exist in the supplied record or a later context mode explicitly requests fill-in placeholders.- Do not ask the user questions. Do not mention these instructions.`; const userPrompt = `FACTUAL RECORDLeadership situation: ${input.challenge}Framework: ${input.framework.name} — ${input.framework.description} ${previousAnswers ? `Previous answers:\n${previousAnswers}\n\n` : ''}Current question: ${input.currentQuestion.text}${input.currentQuestion.aiGuidance ? `Question guidance: ${input.currentQuestion.aiGuidance}\n` : ''}Current draft: ${draft} TASKEdit the current draft conservatively. Add no factual content that is not in this record.`; }Wie Reflacto den passenden Prompt auswählt
Diese Funktion prüft die Art der Frage und wie viel konkreter Inhalt bereits vorhanden ist. Erst danach wählt sie den Prompt aus. Der Code ist eingeklappt, weil er für das Verständnis der vier Prompt-Arten nicht immer nötig ist.
src/services/reflectionPrompts.ts · buildDeployedQuestionGuidancePrompt()Türkis markierte Codezeilen öffnen eine kurze Erklärung.export function buildDeployedQuestionGuidancePrompt( input: QuestionGuidancePromptInput,): ReflectionPrompt { const prompt = buildQuestionGuidancePrompt(input); const hasDraft = Boolean(input.currentQuestion.draft?.trim()); if (isEnumerationQuestion(input)) { } const headings = questionHeadings(input.locale); const systemPrompt = `${prompt.systemPrompt} - The draft contains a real concern but too little observable detail to be useful yet. Do not merely translate or paraphrase it.- Preserve the user's factual core and level of certainty. Do not invent a setting, timing, quotation, frequency, motive or impact.- Never replace a concrete person, time or setting from the draft with a placeholder. Keep every supplied name, relationship, time and setting in the ready-to-use answer.- Use bracketed placeholders only for facts that are actually missing. Never use a placeholder for a detail already present in the draft or previous answers.- Under "${headings[0]}", turn the draft into a stronger question-specific formulation and use bracketed placeholders for the most important missing facts. Make each placeholder explicit enough to coach the user about what to add.- For conflict or mediation events, separate observable words or actions from the user's interpretation. Prefer an exact quote or close factual description when the user can supply one.- Under "${headings[1]}", use the 2 bullets to explain which added details matter for the framework and how they would make the answer more actionable.`; /TASK\nEdit the current draft conservatively\. Add no factual content that is not in this record\.$/, `TASKDevelop this sparse draft into a useful answer without guessing. Keep every supplied name, relationship, time and setting in the answer. Add bracketed placeholders only for observable words or actions, relevant impact and other facts that are actually missing.`, ); return { ...prompt, systemPrompt, userPrompt, contextTokens: LOCAL_LLM_CONTEXT_TOKENS, }; } if (hasDraft) { return { ...prompt, contextTokens: LOCAL_LLM_CONTEXT_TOKENS }; } const systemPrompt = `${prompt.systemPrompt} INSUFFICIENT CONTEXT MODE:- There is no draft and no previous answer contains concrete facts. The challenge label is only a generic routing label, not evidence.- Do not invent a suggested answer, event, person, behavior, disagreement, motive or outcome.- Under "${questionHeadings(input.locale)[0]}", provide a concise fill-in scaffold with bracketed placeholders for the missing situation details. The scaffold must fit the current question and contain no assumed facts.- Under "${questionHeadings(input.locale)[1]}", explain in exactly 2 concise bullets which observable details would make a later answer useful and why the framework needs them.`; const userPrompt = prompt.userPrompt.replace( /TASK\nEdit the current draft conservatively\. Add no factual content that is not in this record\.$/, `TASKThere is not enough factual context to compose an answer. Do not guess. Provide a short, question-specific fill-in scaffold with bracketed placeholders so the user can supply the missing facts.`, ); return { ...prompt, systemPrompt, userPrompt, contextTokens: LOCAL_LLM_CONTEXT_TOKENS, }; } const systemPrompt = `${prompt.systemPrompt} NO-DRAFT MODE:- When the current draft is empty, synthesize the directly relevant previous answers into a concrete answer to the current question. Compose the answer; do not merely describe what the user should investigate next.- For a question about solutions, options or actions, propose 2-3 specific feasible options grounded in the factual record and show how they address the stated needs or constraints.- Keep unverified interests or needs explicitly hypothetical. Validation may be a caveat or next step, but it must not replace the requested answer.`; const userPrompt = prompt.userPrompt.replace( /TASK\nEdit the current draft conservatively\. Add no factual content that is not in this record\.$/, `TASKThe current draft is empty. Compose a ready-to-use answer to the current question by synthesizing the relevant previous answers. Make it concrete and framework-specific. If the question asks for solutions, options or actions, include 2-3 feasible proposals grounded in the record rather than responding only with observation or validation. Add no factual content that is not in this record.`, ); return { ...prompt, systemPrompt, userPrompt, contextTokens: LOCAL_LLM_CONTEXT_TOKENS, };}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.
