Suspendre la génération à l'aide d'interruptions

Les interruptions sont un type spécial d'outil qui peut suspendre la boucle de génération et d'appel d'outil LLM pour vous rendre le contrôle. Lorsque vous êtes prêt, vous pouvez reprendre la génération en envoyant des réponses que le LLM traite pour une génération ultérieure.

Les utilisations les plus courantes des interruptions se répartissent en plusieurs catégories:

  • Human-in-the-Loop:permet à l'utilisateur d'une IA interactive de clarifier les informations nécessaires ou de confirmer l'action du LLM avant qu'elle ne soit effectuée, ce qui offre une certaine sécurité et confiance.
  • Traitement asynchrone:démarrage d'une tâche asynchrone qui ne peut être effectuée que hors bande, par exemple l'envoi d'une notification d'approbation à un examinateur humain ou le lancement d'un processus en arrière-plan de longue durée.
  • Quitter une tâche autonome:permet au modèle de marquer une tâche comme terminée, dans un workflow qui peut itérer sur une longue série d'appels d'outils.

Avant de commencer

Tous les exemples décrits ici partent du principe que vous avez déjà configuré un projet avec les dépendances Genkit installées. Si vous souhaitez exécuter les exemples de code de cette page, suivez d'abord les étapes du guide Premiers pas.

Avant de vous plonger dans le sujet, vous devez également connaître les concepts suivants:

Présentation des interruptions

De manière générale, voici à quoi ressemble une interruption lors de l'interaction avec un LLM:

  1. L'application appelante envoie une requête au LLM. La requête inclut une liste d'outils, dont au moins un pour une interruption que le LLM peut utiliser pour générer une réponse.
  2. Le LLM génère une réponse complète ou une requête d'appel d'outil dans un format spécifique. Pour le LLM, un appel d'interruption ressemble à n'importe quel autre appel d'outil.
  3. Si le LLM appelle un outil d'interruption, la bibliothèque Genkit met automatiquement la génération en pause plutôt que de renvoyer immédiatement les réponses au modèle pour un traitement supplémentaire.
  4. Le développeur vérifie si un appel d'interruption est effectué et effectue la tâche nécessaire pour collecter les informations nécessaires à la réponse d'interruption.
  5. Le développeur reprend la génération en transmettant une réponse d'interruption au modèle. Cette action déclenche un retour à l'étape 2.

Définir des interruptions de réponse manuelle

Le type d'interruption le plus courant permet au LLM de demander des précisions à l'utilisateur, par exemple en posant une question à choix multiples.

Pour ce cas d'utilisation, utilisez la méthode defineInterrupt() de l'instance Genkit:

import { genkit, z } from 'genkit';
import { googleAI, gemini15Flash } from '@genkitai/google-ai';

const ai = genkit({
  plugins: [googleAI()],
  model: gemini15Flash,
});

const askQuestion = ai.defineInterrupt({
  name: 'askQuestion',
  description: 'use this to ask the user a clarifying question',
  inputSchema: z.object({
    choices: z.array(z.string()).describe('the choices to display to the user'),
    allowOther: z.boolean().optional().describe('when true, allow write-ins')
  }),
  outputSchema: z.string()
});

Notez que le outputSchema d'une interruption correspond aux données de réponse que vous fournirez, par opposition à un élément qui sera automatiquement renseigné par une fonction d'outil.

Utiliser des interruptions

Les interruptions sont transmises au tableau tools lors de la génération de contenu, comme pour les autres types d'outils. Vous pouvez transmettre à la même fonction generate des outils normaux et des interruptions:

générer

const response = await ai.generate({
  prompt: 'Ask me a movie trivia question.',
  tools: [askQuestion],
});

definePrompt

const triviaPrompt = ai.definePrompt(
  {
    name: 'triviaPrompt',
    tools: [askQuestion],
    input: {
      schema: z.object({subject: z.string()})
    },
    prompt: 'Ask me a trivia question about {{subject}}
    .',
  }
);

const response = await triviaPrompt({ subject: 'computer history' });

Fichier de requête

---
tools: [askQuestion]
input:
  schema:
    partyType: string
---
{{role "system"}}
Use the askQuestion tool if you need to clarify something.

{{role "user"}}
Help me plan a {{partyType}} party next week.

Vous pouvez ensuite exécuter l'invite dans votre code comme suit:

```ts
// assuming prompt file is named partyPlanner.prompt
const partyPlanner = ai.prompt('partyPlanner');

const response = await partyPlanner({ partyType: 'birthday' });
```

Chat

const chat = ai.chat({
  system: 'Use the askQuestion tool if you need to clarify something.',
  tools: [askQuestion],
});

const response = await chat.send('make a plan for my birthday party');

Genkit renvoie immédiatement une réponse à la réception d'un appel d'outil d'interruption.

Répondre aux interruptions

Si vous avez transmis une ou plusieurs interruptions à votre appel de génération, vous devez vérifier la réponse pour les interruptions afin de pouvoir les gérer:

// you can check the 'finishReason' of the response
response.finishReason === 'interrupted'
// or you can check to see if any interrupt requests are on the response
response.interrupts.length > 0

La réponse à une interruption s'effectue à l'aide de l'option resume sur un appel generate ultérieur, en veillant à transmettre l'historique existant. Chaque outil dispose d'une méthode .respond() pour vous aider à construire la réponse.

Une fois la reprise effectuée, le modèle réintègre la boucle de génération, y compris l'exécution de l'outil, jusqu'à ce qu'elle soit terminée ou qu'une autre interruption soit déclenchée:

let response = await ai.generate({
  tools: [askQuestion],
  system: 'ask clarifying questions until you have a complete solution',
  prompt: 'help me plan a backyard BBQ',
});

while (response.interrupts.length) {
  const answers = [];
  // multiple interrupts can be called at once, so we handle them all
  for (const question in response.interrupts) {
    answers.push(
      // use the `respond` method on our tool to populate answers
      askQuestion.respond(
        question,
        // send the tool request input to the user to respond
        await askUser(question.toolRequest.input)
      )
    );
  }

  response = await ai.generate({
    tools: [askQuestion],
    messages: response.messages,
    resume: {
      respond: answers
    }
  })
}

// no more interrupts, we can see the final response
console.log(response.text);

Outils avec interruptions redémarrables

Un autre modèle courant pour les interruptions consiste à confirmer une action suggérée par le LLM avant de l'exécuter. Par exemple, une application de paiement peut demander à l'utilisateur de confirmer certains types de transferts.

Pour ce cas d'utilisation, vous pouvez utiliser la méthode defineTool standard pour ajouter une logique personnalisée sur le moment où déclencher une interruption et ce qu'il faut faire lorsqu'une interruption est relancée avec des métadonnées supplémentaires.

Définir un outil pouvant être redémarré

Chaque outil a accès à deux assistants spéciaux dans le deuxième argument de sa définition d'implémentation:

  • interrupt: lorsqu'elle est appelée, cette méthode génère un type d'exception spécial qui est intercepté pour suspendre la boucle de génération. Vous pouvez fournir des métadonnées supplémentaires en tant qu'objet.
  • resumed: lorsqu'une requête d'une génération interrompue est redémarrée à l'aide de l'option {resume: {restart: ...}} (voir ci-dessous), cet assistant contient les métadonnées fournies lors du redémarrage.

Si vous créez une application de paiement, par exemple, vous pouvez demander confirmation à l'utilisateur avant d'effectuer un transfert supérieur à un certain montant:

const transferMoney = ai.defineTool({
  name: 'transferMoney',
  description: 'Transfers money between accounts.',
  inputSchema: z.object({
    toAccountId: z.string().describe('the account id of the transfer destination'),
    amount: z.number().describe('the amount in integer cents (100 = $1.00)'),
  }),
  outputSchema: z.object({
    status: z.string().describe('the outcome of the transfer'),
    message: z.string().optional(),
  })
}, async (input, {context, interrupt, resumed})) {
  // if the user rejected the transaction
  if (resumed?.status === "REJECTED") {
    return {status: 'REJECTED', message: 'The user rejected the transaction.'};
  }
  // trigger an interrupt to confirm if amount > $100
  if (resumed?.status !== "APPROVED" && input.amount > 10000) {
    interrupt({
      message: "Please confirm sending an amount > $100.",
    });
  }
  // complete the transaction if not interrupted
  return doTransfer(input);
}

Dans cet exemple, lors de la première exécution (lorsque resumed n'est pas défini), l'outil vérifie si le montant dépasse 100 $et déclenche une interruption si c'est le cas. Lors de la deuxième exécution, il recherche un état dans les nouvelles métadonnées fournies et effectue le transfert ou renvoie une réponse de refus, selon qu'il est approuvé ou refusé.

Redémarrer les outils après une interruption

Les outils d'interruption vous permettent de contrôler entièrement les éléments suivants:

  1. Lorsque la première requête d'outil doit déclencher une interruption.
  2. Quand et si vous devez reprendre la boucle de génération.
  3. Informations supplémentaires à fournir à l'outil lors de la reprise.

Dans l'exemple présenté dans la section précédente, l'application peut demander à l'utilisateur de confirmer la requête interrompue pour s'assurer que le montant du transfert est correct:

let response = await ai.generate({
  tools: [transferMoney],
  prompt: "Transfer $1000 to account ABC123",
});

while (response.interrupts.length) {
  const confirmations = [];
  // multiple interrupts can be called at once, so we handle them all
  for (const interrupt in response.interrupts) {
    confirmations.push(
      // use the 'restart' method on our tool to provide `resumed` metadata
      transferMoney.restart(
        interrupt,
        // send the tool request input to the user to respond. assume that this
        // returns `{status: "APPROVED"}` or `{status: "REJECTED"}`
        await requestConfirmation(interrupt.toolRequest.input);
      )
    );
  }

  response = await ai.generate({
    tools: [transferMoney],
    messages: response.messages,
    resume: {
      restart: confirmations,
    }
  })
}

// no more interrupts, we can see the final response
console.log(response.text);