Conversaciones Multi-Turno con Estado en Gemini (@google/genai)
Problema Principal que Resuelve Esta Habilidad
Muchos asistentes de IA tratan accidentalmente cada prompt como una solicitud sin estado de un solo turno—re-subiendo el documento completo, la base de código o el estado del espacio de trabajo en cada pregunta de seguimiento o edición secuencial, o aplanando los turnos anteriores en un solo bloque de texto. Esto desperdicia tokens, aumenta la latencia y degrada la calidad del razonamiento.
En su lugar, mantén una conversación multi-turno con estado donde:
- El contexto inicial (código, documento o estado del proyecto) se establece en el Turno 1.
- Los turnos subsiguientes continúan la misma conversación (
previous_interaction_ido turnos estructuradosuser/modelencontents) sin reenviar el estado sin cambios. - El código o documento completo solo se reenvía cuando el usuario edita manualmente el espacio de trabajo fuera del flujo de la IA (detectado mediante comparación de hash SHA-256).
Patrón 1: Estado Multi-Turno Gestionado por el Servidor (API de Interacciones)
Al usar la API de Interacciones de Google (@google/genai >= 2.3.0), Gemini mantiene el contexto de la conversación en los servidores de Google (store: true por defecto). Encadena los turnos pasando previous_interaction_id: interaction.id:
TypeScript (@google/genai)
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI({});
// Turno 1: Establece la conversación y el estado inicial del espacio de trabajo/documento
const turn1 = await client.interactions.create({
model: "gemini-3.7-flash",
system_instruction: "Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
input: `Aquí está mi archivo actual:\n\`\`\`ts\n${initialCode}\n\`\`\`\nAñade validación de entrada.`,
});
console.log(turn1.output_text);
// Turno 2: Pregunta de seguimiento o edición secuencial — ¡NO reenvíes el archivo si no ha cambiado!
// El servidor de Google ya mantiene la entrada del Turno 1 y la salida del modelo a través de previous_interaction_id.
const turn2 = await client.interactions.create({
model: "gemini-3.7-flash",
system_instruction: "Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
previous_interaction_id: turn1.id,
input: "¿Por qué elegiste ese patrón de validación, y podemos también registrar las entradas rechazadas?",
});
console.log(turn2.output_text);
Python (google-genai)
from google import genai
client = genai.Client()
# Turno 1: Establece el contexto inicial
turn1 = client.interactions.create(
model="gemini-3.7-flash",
system_instruction="Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
input=f"Aquí está mi archivo actual:\n```py\n{initial_code}\n```\nRefactoriza esto en una clase.",
)
# Turno 2: Encadena con previous_interaction_id sin re-subir el archivo
turn2 = client.interactions.create(
model="gemini-3.7-flash",
system_instruction="Eres un asistente de codificación experto. Recuerda el código activo entre turnos.",
previous_interaction_id=turn1.id,
input="Ahora añade un método de prueba unitaria para casos extremos.",
)
Reglas importantes para
interactions.create:
system_instruction,toolsygeneration_configtienen alcance de interacción—vuelve a especificarlos en cada turno.- Mantén
store: true(el valor predeterminado); establecerstore: falsedeshabilitaprevious_interaction_id.
Patrón 2: contents Multi-Turno Gestionados por la Aplicación con Estado de Espacio de Trabajo Sincronizado por Hash
Cuando tu backend almacena los turnos de la conversación en su propia base de datos (o usa ai.models.generateContent / ai.chats.create con esquemas JSON estructurados y flujos de diff/aplicar), sigue estas cinco reglas para no reenviar nunca de manera redundante archivos o secciones del espacio de trabajo sin cambios:
1. Usa Arreglos de Turnos Nativos con role: 'user' | 'model'
Nunca aplanes turnos anteriores en una sola cadena dentro de un mensaje de usuario. Siempre construye un arreglo contents multi-turno adecuado:
type ContentTurn = {
role: "user" | "model";
parts: Array<{ text: string }>;
};
2. Rastrea la Sincronización del Espacio de Trabajo mediante SHA-256
Mantén un registro de sincronización por conversación en el cliente:
interface SyncedConversationState {
activeDocumentSha256: string | null;
secondaryContextHash: string | null; // p. ej., otras pestañas abiertas, escenas o resumen del árbol del proyecto
}
- Cuando un turno de
ask(P&R) se completa:activeDocumentSha256permanece como el SHA-256 del documento que se envió. - Cuando un turno de
editse completa con una propuesta:activeDocumentSha256se actualiza aproposal.sha256(porque el turnomodelen el historial de la conversación ahora contiene el código propuesto). - Cuando el usuario hace clic en Aplicar:
activeDocumentSha256coincide con elproposal.sha256aplicado. - Cuando el usuario hace clic en Descartar o Deshacer:
activeDocumentSha256revierte albase.sha256anterior a la propuesta.
3. Omite el Código del Espacio de Trabajo sin Cambios en Turnos Posteriores (sourceSynchronized: true)
Antes de enviar un nuevo turno, compara sha256(currentEditorCode) con syncedState.activeDocumentSha256:
- Si
sha256(currentEditorCode) === syncedState.activeDocumentSha256(sourceSynchronized: true) :- En modo
Ask(P&R): No subas el archivo en absoluto (source: null). El modelo ya tiene el código activo exacto en su historial de conversación. - En modo
Edit: No dupliquescurrentSourcedentro del nuevo turnouserenviado a Gemini ({ mode: "edit", prompt, sourceSynchronized: true }). Mantén la instantánea base local solo para el diff/Undo del lado del cliente y la validación del backend.
- En modo
- Si
sha256(currentEditorCode) !== syncedState.activeDocumentSha256(sourceSynchronized: false) :- El usuario editó manualmente el archivo o cambió de archivo. Adjunta el
currentSourceactualizado en este turno para resincronizar la conversación y actualizaactiveDocumentSha256.
- El usuario editó manualmente el archivo o cambió de archivo. Adjunta el
4. Mantén la Fuente Completa Solo una Vez en el Historial (Compactar Turnos Reemplazados)
En una sesión de edición de 10 turnos, no mantengas 10 copias completas del archivo en los 10 turnos históricos.
- Mantén el código completo solo en el turno más reciente que introdujo o modificó el código activo (ya sea el turno
modelque propuso la última edición activa, o el turnouserque adjuntó por última vez código editado manualmente). - Para todos los turnos anteriores cuyo código fue reemplazado, reemplaza la cadena de código en línea con metadatos compactos (
{ supersededSourceSha256: "..." }). Esto mantiene el uso de tokens plano en sesiones largas mientras preserva el hilo completo de razonamiento conversacional.
5. Desduplica el Contexto Secundario de Solo Lectura (Otros Archivos / Pestañas / Secciones)
Si tu aplicación incluye un resumen de solo lectura de archivos o secciones no activos:
- Envía ese resumen en el Turno 1 de la conversación.
- En el Turno 2+, solo reenvía el resumen si su hash cambió (
secondaryContextHash !== currentSecondaryContextHash).
Implementación de Referencia: Constructor de Solicitudes Multi-Turno (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;
// Identifica qué turno histórico contiene la fuente actualmente activa
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";
// Turno de usuario en el historial
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) }] });
// Turno del modelo en el historial
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) }] });
}
// Turno final (actual) del usuario
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;
}
Lista de Verificación al Revisar o Implementar IA Multi-Turno
- ¿Estamos usando
@google/genai(GoogleGenAI) y nunca el obsoleto@google/generative-ai? - ¿El Turno 2+ preserva los roles multi-turno
user/model(oprevious_interaction_id) en lugar de aplanar el historial en una sola cadena de prompt? - En preguntas de seguimiento (modo
Ask) cuando el código del editor no ha cambiado, ¿omitimos re-subir el código desde el cliente? - En ediciones secuenciales de IA (modo
Edit) cuando el código del editor coincide con el último turno, ¿omitimos elcurrentSourceduplicado del nuevo prompt de usuario encontents? - ¿Las versiones de código antiguas reemplazadas están compactadas en el historial para que solo la versión de código activa se envíe completa?
- Si el usuario edita manualmente el documento o deshace un cambio, ¿la discrepancia de SHA-256 activa automáticamente la resincronización en el siguiente turno?