Conversations Gemini multi-tour avec état (@google/genai)

Utilisez cette compétence pour bâtir ou améliorer des assistants de chat, codage ou édition de documents/espaces de travail avec Google Gemini (@google/genai) afin d'utiliser des conversations multi-tours avec état au lieu de renvoyer l'intégralité du code, des documents ou du contexte à chaque requête.

Qu'est-ce que genai) ?

Cette compétence résout le problème courant des assistants IA qui traitent chaque requête comme une demande sans état, gaspillant des tokens et de la latence. Elle propose deux patrons de conception pour maintenir des conversations multi-tours avec état avec Google Gemini. Le premier patron utilise l'API Interactions de Google (@google/genai >= 2.3.0), où le serveur conserve le contexte de la conversation via `previous_interaction_id`. Le second patron est géré par l'application : l'historique des tours est stocké localement sous forme de tableau `contents` avec des rôles `user`/`model`, et l'état de l'espace de travail est synchronisé via un hachage SHA-256 pour éviter de renvoyer les fichiers inchangés. La compétence s'applique à tous les assistants utilisant le SDK `@google/genai` (TypeScript et Python). Elle fournit des règles détaillées pour la gestion de l'état, la compression des tours obsolètes, et une implémentation de référence pour construire un tableau `contents` multi-tours.

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Que fait genai) ?

Problème principal résolu par cette compétence

De nombreux assistants IA traitent accidentellement chaque requête comme une demande sans état — ils téléchargent à nouveau l'intégralité du document, du code ou de l'état de l'espace de travail à chaque question de suivi ou modification séquentielle, ou aplatissent les tours précédents en un seul bloc de texte. Cela gaspille des tokens, augmente la latence et dégrade la qualité du raisonnement.

Au lieu de cela, maintenez une conversation multi-tour avec état où :

  1. Le contexte initial (code, document ou état du projet) est établi au tour 1.
  2. Les tours suivants continuent la même conversation (previous_interaction_id ou tours user/model structurés dans contents) sans renvoyer l'état inchangé.
  3. Le code ou l'état du document complet n'est renvoyé que lorsque l'utilisateur modifie manuellement l'espace de travail en dehors du flux de l'IA (détecté par comparaison de hachage SHA-256).

Patron 1 : État multi-tour géré par le serveur (API Interactions)

Lorsque vous utilisez l'API Interactions de Google (@google/genai >= 2.3.0), Gemini conserve le contexte de la conversation sur les serveurs de Google (store: true par défaut). Enchaînez les tours en passant previous_interaction_id: interaction.id :

TypeScript (@google/genai)

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

const client = new GoogleGenAI({});

// Tour 1 : Établir la conversation et l'état initial du document
const turn1 = await client.interactions.create({
  model: "gemini-3.7-flash",
  system_instruction: "Vous êtes un assistant codage expert. Souvenez-vous du code actif entre les tours.",
  input: `Voici mon fichier actuel :\n\`\`\`ts\n${initialCode}\n\`\`\`\nAjoutez une validation d'entrée.`,
});

console.log(turn1.output_text);

// Tour 2 : Question de suivi ou modification séquentielle — NE renvoyez PAS le fichier s'il n'a pas changé !
// Le serveur de Google conserve déjà l'entrée du tour 1 et la sortie du modèle via previous_interaction_id.
const turn2 = await client.interactions.create({
  model: "gemini-3.7-flash",
  system_instruction: "Vous êtes un assistant codage expert. Souvenez-vous du code actif entre les tours.",
  previous_interaction_id: turn1.id,
  input: "Pourquoi avez-vous choisi ce motif de validation, et pouvons-nous aussi journaliser les entrées rejetées ?",
});

console.log(turn2.output_text);

Python (google-genai)

from google import genai

client = genai.Client()

# Tour 1 : Établir le contexte initial
turn1 = client.interactions.create(
    model="gemini-3.7-flash",
    system_instruction="Vous êtes un assistant codage expert. Souvenez-vous du code actif entre les tours.",
    input=f"Voici mon fichier actuel :\n```py\n{initial_code}\n```\nRefactorisez ceci en une classe.",
)

# Tour 2 : Enchaîner avec previous_interaction_id sans retélécharger le fichier
turn2 = client.interactions.create(
    model="gemini-3.7-flash",
    system_instruction="Vous êtes un assistant codage expert. Souvenez-vous du code actif entre les tours.",
    previous_interaction_id=turn1.id,
    input="Ajoutez maintenant une méthode de test unitaire pour les cas limites.",
)

Règles importantes pour interactions.create :

  • system_instruction, tools et generation_config sont propres à l'interaction — spécifiez-les à nouveau à chaque tour.
  • Gardez store: true (valeur par défaut) ; store: false désactive previous_interaction_id.

Patron 2 : contents multi-tour géré par l'application avec état de l'espace de travail synchronisé par hachage

Lorsque votre backend stocke les tours de conversation dans sa propre base de données (ou utilise ai.models.generateContent / ai.chats.create avec des schémas JSON structurés et des workflows de diff/apply), suivez ces cinq règles pour ne jamais renvoyer redondamment des fichiers ou sections d'espace de travail inchangés :

1. Utilisez des tableaux de tours natifs role: 'user' | 'model'

N'aplatissez jamais les tours précédents en une seule chaîne dans un seul message utilisateur. Construisez toujours un tableau contents multi-tour correct :

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

2. Suivez la synchronisation de l'espace de travail par hachage SHA-256

Maintenez un enregistrement de synchronisation par conversation sur le client :

interface SyncedConversationState {
  activeDocumentSha256: string | null;
  secondaryContextHash: string | null; // ex. : autres onglets ouverts, scènes, ou résumé de l'arborescence du projet
}
  • Lorsqu'un tour ask (Q&R) se termine : activeDocumentSha256 reste le SHA-256 du document qui a été envoyé.
  • Lorsqu'un tour edit se termine avec une proposition : activeDocumentSha256 se met à jour avec proposal.sha256 (car le tour model dans l'historique de la conversation contient maintenant le code proposé).
  • Lorsque l'utilisateur clique sur Appliquer : activeDocumentSha256 correspond au proposal.sha256 appliqué.
  • Lorsque l'utilisateur clique sur Ignorer ou Annuler : activeDocumentSha256 revient au base.sha256 d'avant la proposition.

3. Omettez le code d'espace de travail inchangé lors des tours de suivi (sourceSynchronized: true)

Avant d'envoyer un nouveau tour, comparez sha256(currentEditorCode) avec syncedState.activeDocumentSha256 :

  • Si sha256(currentEditorCode) === syncedState.activeDocumentSha256 (sourceSynchronized: true) :
    • En mode Ask (Q&R) : Ne téléchargez pas le fichier du tout (source: null). Le modèle a déjà le code actif exact dans son historique de conversation.
    • En mode Edit : Ne dupliquez pas currentSource à l'intérieur du nouveau tour user envoyé à Gemini ({ mode: "edit", prompt, sourceSynchronized: true }). Conservez l'instantané local de base uniquement pour le diff côté client/Annulation et la validation côté serveur.
  • Si sha256(currentEditorCode) !== syncedState.activeDocumentSha256 (sourceSynchronized: false) :
    • L'utilisateur a modifié manuellement le fichier ou changé de fichier. Joignez le currentSource mis à jour à ce tour pour resynchroniser la conversation, et mettez à jour activeDocumentSha256.

4. Conservez la source complète une seule fois dans l'historique (compacter les tours obsolètes)

Dans une session d'édition de 10 tours, ne conservez pas 10 copies complètes du fichier dans les 10 tours historiques.

  • Conservez le code complet uniquement sur le tour le plus récent qui a introduit ou modifié le code actif (soit le tour model qui a proposé la dernière édition active, soit le tour user qui a joint pour la dernière fois du code modifié manuellement).
  • Pour tous les tours plus anciens dont le code a été remplacé, remplacez la chaîne de code en ligne par des métadonnées compactes ({ supersededSourceSha256: "..." }). Cela maintient l'utilisation des tokens stable sur les longues sessions tout en préservant le fil de raisonnement conversationnel complet.

5. Dédupliquez le contexte secondaire en lecture seule (autres fichiers / onglets / sections)

Si votre application inclut un résumé en lecture seule de fichiers ou sections non actifs :

  • Envoyez ce résumé au tour 1 de la conversation.
  • Aux tours 2+, ne renvoyez le résumé que si son hachage a changé (secondaryContextHash !== currentSecondaryContextHash).

Implémentation de référence : Constructeur de requêtes multi-tour (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;

  // Identifier quel tour historique contient la source actuellement active
  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";

    // Tour utilisateur dans l'historique
    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) }] });

    // Tour modèle dans l'historique
    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) }] });
  }

  // Tour utilisateur final (actuel)
  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;
}

Liste de contrôle lors de la révision ou de la mise en œuvre de l'IA multi-tour

  • Utilisons-nous @google/genai (GoogleGenAI) et jamais l'ancien @google/generative-ai ?
  • Le tour 2+ préserve-t-il les rôles multi-tour user / model (ou previous_interaction_id) au lieu d'aplatir l'historique en une seule chaîne de requête ?
  • Lors des questions de suivi (mode Ask) lorsque le code de l'éditeur n'a pas changé, omettons-nous de retélécharger le code depuis le client ?
  • Lors des modifications séquentielles de l'IA (mode Edit) lorsque le code de l'éditeur correspond au dernier tour, omettons-nous le currentSource dupliqué dans la nouvelle requête utilisateur dans contents ?
  • Les anciennes versions de code obsolètes sont-elles compactées dans l'historique de sorte que seule la version active du code soit envoyée en entier ?
  • Si l'utilisateur modifie manuellement le document ou annule une modification, la différence de hachage SHA-256 déclenche-t-elle automatiquement une resynchronisation au tour suivant ?

Skills associés