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.

Laufzeit und KomponentenDiagramm horizontal verschieben
Laufzeit und KomponentenUML Deployment- und Komponentendiagramm. Reflacto läuft im Browser auf dem persönlichen Gerät. Die LLM-Einstellungen geben den gewählten Provider, das Modell und die Base-URL an den Provider Adapter weiter. Die Reflexionsoberfläche verwendet den Prompt Builder und Provider Adapter. Der Adapter spricht entweder mit Chrome Built-in AI im Browser oder über HTTP mit Ollama auf Port 11434, LM Studio auf Port 1234 oder Unsloth auf Port 8888. Beide Wege streamen Antworten über den Provider Adapter zurück in die Reflexionsoberfläche. Unterstützte Browser blockieren das vertrauenswürdige localhost nicht als Mixed Content, der lokale Server muss die Reflacto-Origin aber per CORS erlauben; Ollama verwendet dafür OLLAMA_ORIGINS=https://www.reflacto.com. Für Prompts, Reflexionen und Modellantworten existiert kein Reflacto-Backend.«device»Persönliches Gerät«executionEnvironment»Browser«component»Reflexions-UIReact-Komponente«component»Prompt BuilderSystem- + User-Prompt«component»Provider AdaptergenerateResponse()«configuration»LLM-EinstellungenProvider · Modell · Base-URL«storage»localStorageReflexionen (Klartext)+ Unsloth-API-Key (AES-GCM)«service»IndexedDB +Web CryptoAES-GCM-Schlüssel«component»Chrome Built-in AILanguageModel APIinputpromptTokenslesen / schreibenver- / entschlüsseln«node»Lokale LLM-Runtimes«component»Ollama:11434/api/chat · NDJSON«component»LM Studio:1234/v1/chat/completions · SSE«component»Unsloth Studio:8888/v1/chat/completions · SSEHTTP POSTNDJSON / SSE«note»HTTPS → http://localhost: CORSKein Mixed-Content-Block; Origin muss erlaubt seinOLLAMA_ORIGINS=https://www.reflacto.comReguläre KomponentenKonfiguration · SpeicherProvider AdapterLeitet Anfragen an das gewählte LLMLokale LLMsChrome · Ollama · LM Studio · UnslothAufruf oder AnfrageGestreamte Antwort

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.

Prompt-Art auswählen

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.
Vollständiger Quellcodesrc/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.

Auswahl der Hilfensrc/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.

  1. 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 vergleichen
    Browser-API ohne HTTPsrc/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();
  2. 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-Auszugsrc/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;}
  3. 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 lesensrc/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 DatenOllama 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]
  4. 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-Komponentesrc/components/EnhancedReflectionForm.tsx
    const 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.