Conversas Multi-Turno com Estado no Gemini (@google/genai)
Problema Central que Esta Habilidade Resolve
Muitos assistentes de IA tratam acidentalmente cada solicitação como uma requisição única e sem estado—reenviando o documento, a base de código ou o estado do espaço de trabalho completo a cada pergunta de acompanhamento ou edição sequencial, ou achatando turnos anteriores em um único bloco de texto. Isso desperdiça tokens, aumenta a latência e degrada a qualidade do raciocínio.
Em vez disso, mantenha uma conversa multi-turno com estado onde:
- O contexto inicial (código, documento ou estado do projeto) é estabelecido no Turno 1.
- Turnos subsequentes continuam a mesma conversa (
previous_interaction_idou turnos estruturadosuser/modelemcontents) sem reenviar o estado inalterado. - O código completo ou estado do documento é reenviado apenas quando o usuário edita manualmente o espaço de trabalho fora do fluxo da IA (detectado via comparação de hash SHA-256).
Padrão 1: Estado Multi-Turno Gerenciado pelo Servidor (API de Interações)
Ao usar a API de Interações do Google (@google/genai >= 2.3.0), o Gemini mantém o contexto da conversa nos servidores do Google (store: true por padrão). Encadeie os turnos passando previous_interaction_id: interaction.id:
TypeScript (@google/genai)
import { GoogleGenAI } from "@google/genai";
const client = new GoogleGenAI({});
// Turno 1: Estabelece a conversa e o estado inicial do espaço de trabalho/documento
const turn1 = await client.interactions.create({
model: "gemini-3.7-flash",
system_instruction: "Você é um assistente de codificação especialista. Lembre-se do código ativo entre os turnos.",
input: `Aqui está meu arquivo atual:\n\`\`\`ts\n${initialCode}\n\`\`\`\nAdicione validação de entrada.`,
});
console.log(turn1.output_text);
// Turno 2: Pergunta de acompanhamento ou edição sequencial — NÃO reenvie o arquivo se inalterado!
// O servidor do Google já mantém a entrada do Turno 1 e a saída do modelo via previous_interaction_id.
const turn2 = await client.interactions.create({
model: "gemini-3.7-flash",
system_instruction: "Você é um assistente de codificação especialista. Lembre-se do código ativo entre os turnos.",
previous_interaction_id: turn1.id,
input: "Por que você escolheu esse padrão de validação, e podemos também registrar entradas rejeitadas?",
});
console.log(turn2.output_text);
Python (google-genai)
from google import genai
client = genai.Client()
# Turno 1: Estabelece o contexto inicial
turn1 = client.interactions.create(
model="gemini-3.7-flash",
system_instruction="Você é um assistente de codificação especialista. Lembre-se do código ativo entre os turnos.",
input=f"Aqui está meu arquivo atual:\n```py\n{initial_code}\n```\nRefatore isso em uma classe.",
)
# Turno 2: Encadeia com previous_interaction_id sem reenviar o arquivo
turn2 = client.interactions.create(
model="gemini-3.7-flash",
system_instruction="Você é um assistente de codificação especialista. Lembre-se do código ativo entre os turnos.",
previous_interaction_id=turn1.id,
input="Agora adicione um método de teste unitário para casos extremos.",
)
Regras importantes para
interactions.create:
system_instruction,toolsegeneration_configtêm escopo de interação—reespecifique-os a cada turno.- Mantenha
store: true(o padrão); definirstore: falsedesabilitaprevious_interaction_id.
Padrão 2: contents Multi-Turno Gerenciado pelo Aplicativo com Estado do Espaço de Trabalho Sincronizado por Hash
Quando seu backend armazena turnos de conversa em seu próprio banco de dados (ou usa ai.models.generateContent / ai.chats.create com esquemas JSON estruturados e fluxos de trabalho de diff/aplicação), siga estas cinco regras para nunca reenviar arquivos ou seções do espaço de trabalho inalterados de forma redundante:
1. Use Arrays de Turnos Nativos role: 'user' | 'model'
Nunca achate turnos anteriores em uma única string dentro de uma mensagem de usuário. Sempre construa um array contents multi-turno adequado:
type ContentTurn = {
role: "user" | "model";
parts: Array<{ text: string }>;
};
2. Rastreie a Sincronização do Espaço de Trabalho por SHA-256 Hash
Mantenha um registro de sincronização por conversa no cliente:
interface SyncedConversationState {
activeDocumentSha256: string | null;
secondaryContextHash: string | null; // ex.: outras abas abertas, cenas ou resumo da árvore do projeto
}
- Quando um turno
ask(P&R) é concluído:activeDocumentSha256permanece o SHA-256 do documento que foi enviado. - Quando um turno
edité concluído com uma proposta:activeDocumentSha256é atualizado paraproposal.sha256(porque o turnomodelno histórico da conversa agora contém o código proposto). - Quando o usuário clica em Aplicar:
activeDocumentSha256corresponde aoproposal.sha256aplicado. - Quando o usuário clica em Descartar ou Desfazer:
activeDocumentSha256reverte para obase.sha256anterior à proposta.
3. Omita Código do Espaço de Trabalho Inalterado em Turnos de Acompanhamento (sourceSynchronized: true)
Antes de enviar um novo turno, compare sha256(currentEditorCode) com syncedState.activeDocumentSha256:
- Se
sha256(currentEditorCode) === syncedState.activeDocumentSha256(sourceSynchronized: true) :- No modo
Ask(P&R) : Não envie o arquivo (source: null). O modelo já tem o código ativo exato no histórico da conversa. - No modo
Edit: Não dupliquecurrentSourcedentro do novo turnouserenviado ao Gemini ({ mode: "edit", prompt, sourceSynchronized: true }). Mantenha o snapshot base local apenas para diff/Desfazer no lado do cliente e validação no backend.
- No modo
- Se
sha256(currentEditorCode) !== syncedState.activeDocumentSha256(sourceSynchronized: false) :- O usuário editou manualmente o arquivo ou mudou de arquivo. Anexe o
currentSourceatualizado neste turno para ressincronizar a conversa e atualizeactiveDocumentSha256.
- O usuário editou manualmente o arquivo ou mudou de arquivo. Anexe o
4. Mantenha a Fonte Completa Apenas Uma Vez no Histórico (Turnos Obsoletos Compactados)
Em uma sessão de edição de 10 turnos, não mantenha 10 cópias completas do arquivo em todos os 10 turnos históricos.
- Mantenha o código completo apenas no turno mais recente que introduziu ou modificou o código ativo (seja o turno
modelque propôs a edição ativa mais recente, ou o turnouserque anexou o código editado manualmente pela última vez). - Para todos os turnos mais antigos cujo código foi substituído, substitua a string de código inline por metadados compactos (
{ supersededSourceSha256: "..." }). Isso mantém o uso de tokens estável em sessões longas, preservando ao mesmo tempo o encadeamento completo do raciocínio conversacional.
5. Desduplique Contexto Secundário Somente Leitura (Outros Arquivos / Abas / Seções)
Se seu aplicativo inclui um resumo somente leitura de arquivos ou seções não ativos:
- Envie esse resumo no Turno 1 da conversa.
- No Turno 2+, reenvie o resumo apenas se seu hash mudou (
secondaryContextHash !== currentSecondaryContextHash).
Implementação de Referência: Construtor de Requisição 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 qual turno histórico contém a fonte atualmente ativa
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 do usuário no histórico
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 do modelo no histórico
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 (atual) do usuário
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 Verificação ao Revisar ou Implementar IA Multi-Turno
- Estamos usando
@google/genai(GoogleGenAI) e nunca o obsoleto@google/generative-ai? - O Turno 2+ preserva funções multi-turno
user/model(ouprevious_interaction_id) em vez de achatar o histórico em uma única string de solicitação? - Em perguntas de acompanhamento (modo
Ask) quando o código do editor não mudou, omitimos o reenvio do código do cliente? - Em edições sequenciais de IA (modo
Edit) quando o código do editor corresponde ao último turno, omitimos ocurrentSourceduplicado do novo prompt do usuário emcontents? - Versões de código obsoletas mais antigas são compactadas no histórico, de modo que apenas a versão ativa do código é enviada por completo?
- Se o usuário editar manualmente o documento ou desfizer uma alteração, a incompatibilidade de SHA-256 aciona automaticamente a ressincronização no próximo turno?