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:
- Der anfängliche Kontext (Code, Dokument oder Projektzustand) in Turn 1 festgelegt wird.
- Nachfolgende Turns dieselbe Unterhaltung fortsetzen (
previous_interaction_idoder strukturierteuser/model-Turns incontents), ohne den unveränderten Zustand erneut zu senden. - 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,toolsundgeneration_configsind interaktionsbezogen – gib sie bei jedem Turn erneut an.- Behalte
store: true(Standard); die Einstellungstore: falsedeaktiviertprevious_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:activeDocumentSha256bleibt der SHA-256 des gesendeten Dokuments. - Wenn ein
edit-Turn mit einem Vorschlag abgeschlossen ist:activeDocumentSha256wird aufproposal.sha256aktualisiert (da dermodel-Turn im Gesprächsverlauf nun den vorgeschlagenen Code enthält). - Wenn der Benutzer auf „Übernehmen“ klickt:
activeDocumentSha256stimmt mit dem übernommenenproposal.sha256überein. - Wenn der Benutzer auf „Verwerfen“ oder „Rückgängig“ klickt:
activeDocumentSha256wird auf das vorherigebase.sha256zurü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: DuplizierecurrentSourcenicht innerhalb des neuenuser-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.
- Im
- Wenn
sha256(currentEditorCode) !== syncedState.activeDocumentSha256(sourceSynchronized: false) :- Der Benutzer hat die Datei manuell bearbeitet oder die Datei gewechselt. Füge die aktualisierte
currentSourcein diesem Turn an, um die Unterhaltung neu zu synchronisieren, und aktualisiereactiveDocumentSha256.
- Der Benutzer hat die Datei manuell bearbeitet oder die Datei gewechselt. Füge die aktualisierte
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 deruser-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 (oderprevious_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 voncurrentSourceim neuen User-Prompt incontentsweg? - 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?