CommunityCodierung & Entwicklunggithub.com

EBIKEBUILDER/ai-skills

Verwende diese Fähigkeit, wenn du KI-Chats, Codierungsassistenten oder Dokument-/Workspace-Editoren mit Google Gemini (@google/genai) erstellst oder aktualisierst, um zustandsbehaftete mehrschrittige Unterhaltungen anstelle des erneuten Sendens vollständiger Codebasen, Dokumente oder Kontexte bei jeder Anfrage zu nutzen.

Was ist ai-skills?

Diese Fähigkeit löst das grundlegende Problem, dass viele KI-Assistenten jede Anfrage als zustandslose Einzelanfrage behandeln und bei jeder Folgefrage oder sequenziellen Bearbeitung das gesamte Dokument, die Codebasis oder den Workspace-Zustand erneut hochladen. Dies verschwendet Tokens, erhöht die Latenz und verschlechtert die Denkqualität. Die Fähigkeit stellt zwei Hauptmuster bereit, um echte zustandsbehaftete mehrschrittige Unterhaltungen zu implementieren: 1. **Serververwalteter mehrschrittiger Zustand (Interactions API)**: Mit `@google/genai` `>= 2.3.0` wird der Gesprächskontext auf den Servern von Google gehalten. Durch das Übergeben von `previous_interaction_id` können Folgeturns ohne erneutes Hochladen unveränderter Zustände verarbeitet werden. 2. **Anwendungsverwalteter mehrschrittiger Zustand (contents-Array mit Hash-Synchronisation)**: Die Anwendung speichert die Gesprächsturns in einer eigenen Datenbank und verwendet SHA-256-Hashes, um Workspace-Zustände zu synchronisieren. Dies vermeidet redundantes Senden unveränderter Dateien und komprimiert ältere, überschriebene Codeversionen, um den Tokenverbrauch niedrig zu halten. Die Fähigkeit unterstützt die Plattformen `@google/genai` (TypeScript und Python) sowie generische Generative-AI-APIs, die strukturierte `contents`-Arrays mit „user“- und „model“-Rollen akzeptieren. Sie ist für Entwickler von KI-Chats, Codierungsassistenten und Dokumenteditoren gedacht, die Google Gemini integrieren.

Funktioniert mit~Claude Code~Codex CLI~Cursor✓Gemini CLI
npx skills add https://github.com/EBIKEBUILDER/ai-skills/tree/HEAD/gemini-multiturn-conversation

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Zustandsbehaftete mehrschrittige Gemini-Unterhaltungen (@google/genai)

Kernproblem, das diese Fähigkeit löst

Viele KI-Assistenten behandeln jede Aufforderung versehentlich als zustandslose Einzelanfrage – sie laden das gesamte Dokument, die Codebasis oder den Workspace-Zustand bei jeder Folgefrage oder sequenziellen Bearbeitung erneut hoch oder reduzieren frühere Turns auf einen einzigen Textblock. Dies verschwendet Tokens, erhöht die Latenz und verschlechtert die Denkqualität.

Stattdessen sollte eine zustandsbehaftete mehrschrittige Unterhaltung genutzt werden, bei der:

  1. Der anfängliche Kontext (Code, Dokument oder Projektzustand) in Turn 1 festgelegt wird.
  2. Nachfolgende Turns dieselbe Unterhaltung fortsetzen (previous_interaction_id oder strukturierte user/model-Turns in contents), ohne den unveränderten Zustand erneut zu senden.
  3. Der vollständige Code- oder Dokumentzustand nur dann erneut gesendet wird, wenn der Benutzer den Workspace manuell außerhalb des KI-Flusses bearbeitet (erkannt durch SHA-256-Hash-Vergleich).

Muster 1: Serververwalteter mehrschrittiger Zustand (Interactions API)

Bei Verwendung der Interactions API von Google (@google/genai >= 2.3.0) hält Gemini den Gesprächskontext auf den Servern von Google (store: true standardmäßig). Verkette Turns, indem du previous_interaction_id: interaction.id übergibst:

TypeScript (@google/genai)

import { GoogleGenAI } from "@google/genai";

const client = new GoogleGenAI({});

// Turn 1: Unterhaltung und anfänglichen Workspace-/Dokumentzustand festlegen
const turn1 = await client.interactions.create({
  model: "gemini-3.7-flash",
  system_instruction: "Du bist ein erfahrener Code-Assistent. Merke dir den aktiven Code über die Turns hinweg.",
  input: `Hier ist meine aktuelle Datei:
\`\`\`ts
${initialCode}
\`\`\`
Füge Eingabevalidierung hinzu.`,
});

console.log(turn1.output_text);

// Turn 2: Folgefrage oder sequenzielle Bearbeitung — Datei NICHT erneut senden, wenn unverändert!
// Googles Server hält bereits die Eingabe von Turn 1 und die Modellausgabe über previous_interaction_id.
const turn2 = await client.interactions.create({
  model: "gemini-3.7-flash",
  system_instruction: "Du bist ein erfahrener Code-Assistent. Merke dir den aktiven Code über die Turns hinweg.",
  previous_interaction_id: turn1.id,
  input: "Warum hast du dieses Validierungsmuster gewählt, und können wir auch abgelehnte Eingaben protokollieren?",
});

console.log(turn2.output_text);

Python (google-genai)

from google import genai

client = genai.Client()

# Turn 1: Anfangskontext festlegen
turn1 = client.interactions.create(
    model="gemini-3.7-flash",
    system_instruction="Du bist ein erfahrener Code-Assistent. Merke dir den aktiven Code über die Turns hinweg.",
    input=f"Hier ist meine aktuelle Datei:\n```py\n{initial_code}\n```\nRefaktoriere dies in eine Klasse.",
)

# Turn 2: Mit previous_interaction_id verketten, ohne die Datei erneut hochzuladen
turn2 = client.interactions.create(
    model="gemini-3.7-flash",
    system_instruction="Du bist ein erfahrener Code-Assistent. Merke dir den aktiven Code über die Turns hinweg.",
    previous_interaction_id=turn1.id,
    input="Füge jetzt eine Unit-Test-Methode für Grenzfälle hinzu.",
)

Wichtige Regeln für interactions.create:

  • system_instruction, tools und generation_config sind interaktionsbezogen – gib sie bei jedem Turn erneut an.
  • Behalte store: true (Standard); die Einstellung store: false deaktiviert previous_interaction_id.

Muster 2: Anwendungsverwalteter mehrschrittiger contents mit hash-synchronisiertem Workspace-Zustand

Wenn dein Backend Gesprächsturns in einer eigenen Datenbank speichert (oder ai.models.generateContent / ai.chats.create mit strukturierten JSON-Schemas und Diff/Apply-Workflows verwendet), befolge diese fünf Regeln, um niemals unveränderte Dateien oder Workspace-Abschnitte redundant erneut zu senden:

1. Verwende native role: 'user' | 'model'-Turn-Arrays

Flache vorherige Turns niemals in einen einzelnen String innerhalb einer Benutzernachricht ab. Erstelle immer ein ordnungsgemäßes mehrschrittiges contents-Array:

type ContentTurn = {
  role: "user" | "model";
  parts: Array<{ text: string }>;
};

2. Verfolge die Workspace-Synchronisation per SHA-256-Hash

Führe einen pro-Conversation-Synchronisationsdatensatz auf dem Client:

interface SyncedConversationState {
  activeDocumentSha256: string | null;
  secondaryContextHash: string | null; // z. B. andere offene Tabs, Szenen oder Projektbaumzusammenfassung
}
  • Wenn ein ask (F&A)-Turn abgeschlossen ist: activeDocumentSha256 bleibt der SHA-256 des gesendeten Dokuments.
  • Wenn ein edit-Turn mit einem Vorschlag abgeschlossen ist: activeDocumentSha256 wird auf proposal.sha256 aktualisiert (da der model-Turn im Gesprächsverlauf nun den vorgeschlagenen Code enthält).
  • Wenn der Benutzer auf „Übernehmen“ klickt: activeDocumentSha256 stimmt mit dem übernommenen proposal.sha256 überein.
  • Wenn der Benutzer auf „Verwerfen“ oder „Rückgängig“ klickt: activeDocumentSha256 wird auf das vorherige base.sha256 zurückgesetzt.

3. Lasse unveränderten Workspace-Code bei Folgeturns weg (sourceSynchronized: true)

Vergleiche vor dem Senden eines neuen Turns sha256(currentEditorCode) mit syncedState.activeDocumentSha256:

  • Wenn sha256(currentEditorCode) === syncedState.activeDocumentSha256 (sourceSynchronized: true) :
    • Im Ask-Modus (F&A): Lade die Datei überhaupt nicht hoch (source: null). Das Modell hat den genauen aktiven Code bereits im Gesprächsverlauf.
    • Im Edit-Modus: Dupliziere currentSource nicht innerhalb des neuen user-Turns, der an Gemini gesendet wird ({ mode: "edit", prompt, sourceSynchronized: true }). Behalte den lokalen Basis-Snapshot nur für clientseitiges Diffing/Undo und Backend-Validierung.
  • Wenn sha256(currentEditorCode) !== syncedState.activeDocumentSha256 (sourceSynchronized: false) :
    • Der Benutzer hat die Datei manuell bearbeitet oder die Datei gewechselt. Füge die aktualisierte currentSource in diesem Turn an, um die Unterhaltung neu zu synchronisieren, und aktualisiere activeDocumentSha256.

4. Behalte die vollständige Quelle nur einmal im Verlauf (kompakte überschriebene Turns)

In einer 10-Turn-Bearbeitungssitzung sollten nicht 10 vollständige Kopien der Datei über alle 10 historischen Turns hinweg aufbewahrt werden.

  • Behalte den vollständigen Code nur im aktuellsten Turn, der den aktiven Code eingeführt oder geändert hat (entweder der model-Turn, der die neueste aktive Bearbeitung vorgeschlagen hat, oder der user-Turn, der zuletzt manuell bearbeiteten Code angehängt hat).
  • Ersetze bei allen älteren Turns, deren Code überschrieben wurde, den Inline-Code-String durch kompakte Metadaten ({ supersededSourceSha256: "..." }). Dies hält den Tokenverbrauch über lange Sitzungen flach, während der vollständige konversationelle Denkfaden erhalten bleibt.

5. Dedupliziere schreibgeschützten sekundären Kontext (andere Dateien / Tabs / Abschnitte)

Wenn deine App eine schreibgeschützte Zusammenfassung nicht aktiver Dateien oder Abschnitte enthält:

  • Sende diese Zusammenfassung in Turn 1 der Unterhaltung.
  • Sende die Zusammenfassung ab Turn 2+ nur dann erneut, wenn sich ihr Hash geändert hat (secondaryContextHash !== currentSecondaryContextHash).

Referenzimplementierung: Multi-Turn-Request-Builder (TypeScript)

interface HistoryTurn {
  prompt: string;
  response: string;
  mode: "ask" | "edit";
  baseSource?: string | null;
  baseSha256?: string | null;
  proposalSource?: string | null;
  proposalSha256?: string | null;
  outcomeStatus?: "none" | "proposed" | "applied" | "discarded" | "undone";
}

export function buildMultiTurnContents(params: {
  history: HistoryTurn[];
  mode: "ask" | "edit";
  prompt: string;
  currentSource: string | null;
  sourceSynchronized: boolean;
}) {
  const { history, mode, prompt, currentSource, sourceSynchronized } = params;

  // Identifiziere den historischen Turn, der die aktuell aktive Quelle enthält
  let latestActiveSourceIndex = -1;
  if (sourceSynchronized || currentSource === null) {
    for (let i = history.length - 1; i >= 0; i--) {
      const item = history[i];
      const hasValidProposal =
        item.mode === "edit" &&
        item.proposalSource &&
        item.outcomeStatus !== "discarded" &&
        item.outcomeStatus !== "undone";
      if (hasValidProposal || item.baseSource) {
        latestActiveSourceIndex = i;
        break;
      }
    }
  }

  const contents: Array<{ role: "user" | "model"; parts: Array<{ text: string }> }> = [];

  for (let i = 0; i < history.length; i++) {
    const item = history[i];
    const isLatestSourceTurn = i === latestActiveSourceIndex;
    const hasActiveProposal =
      item.mode === "edit" &&
      Boolean(item.proposalSource) &&
      item.outcomeStatus !== "discarded" &&
      item.outcomeStatus !== "undone";

    // User-Turn im Verlauf
    const userPayload: Record<string, unknown> = {
      mode: item.mode ?? "ask",
      prompt: item.prompt,
    };
    if (isLatestSourceTurn && !hasActiveProposal && item.baseSource) {
      userPayload.currentSource = item.baseSource;
    } else if (item.baseSha256) {
      userPayload.supersededSourceSha256 = item.baseSha256;
    }
    contents.push({ role: "user", parts: [{ text: JSON.stringify(userPayload) }] });

    // Model-Turn im Verlauf
    const modelPayload: Record<string, unknown> = {
      kind: item.mode === "edit" ? "code_proposal" : "answer",
      explanation: item.response,
    };
    if (isLatestSourceTurn && hasActiveProposal && item.proposalSource) {
      modelPayload.proposedSource = item.proposalSource;
    } else if (item.proposalSha256) {
      modelPayload.supersededProposalSha256 = item.proposalSha256;
    }
    contents.push({ role: "model", parts: [{ text: JSON.stringify(modelPayload) }] });
  }

  // Finaler (aktueller) User-Turn
  const omitDuplicateSource = sourceSynchronized && latestActiveSourceIndex !== -1;
  const currentTurnPayload: Record<string, unknown> = { mode, prompt };
  if (omitDuplicateSource) {
    currentTurnPayload.sourceSynchronized = true;
  } else {
    currentTurnPayload.currentSource = currentSource;
  }

  contents.push({ role: "user", parts: [{ text: JSON.stringify(currentTurnPayload) }] });
  return contents;
}

Checkliste bei der Überprüfung oder Implementierung von Multi-Turn-KI

  • Verwenden wir @google/genai (GoogleGenAI) und niemals das veraltete @google/generative-ai?
  • Bewahrt Turn 2+ mehrschrittige user / model-Rollen (oder previous_interaction_id) anstelle des Flattening des Verlaufs in einen einzelnen Prompt-String?
  • Lassen wir bei Folgefragen (Ask-Modus), wenn sich der Editor-Code nicht geändert hat, das erneute Hochladen des Codes vom Client weg?
  • Lassen wir bei sequenziellen KI-Bearbeitungen (Edit-Modus), wenn der Editor-Code mit dem letzten Turn übereinstimmt, das Duplikat von currentSource im neuen User-Prompt in contents weg?
  • Sind ältere überschriebene Code-Versionen im Verlauf komprimiert, sodass nur die aktive Code-Version vollständig gesendet wird?
  • Wenn der Benutzer das Dokument manuell bearbeitet oder eine Änderung rückgängig macht, löst die SHA-256-Nichtübereinstimmung automatisch eine erneute Synchronisation beim nächsten Turn aus?

Verwandte Skills