Pausar a geração usando interrupções

As interrupções são um tipo especial de ferramenta que pode pausar o ciclo de geração e chamada de ferramenta do LLM para retornar o controle a você. Quando estiver tudo pronto, você poderá retomar a geração enviando respostas que o LLM processa para mais gerações.

Os usos mais comuns para interrupções se enquadram em algumas categorias:

  • Human-in-the-Loop:permitir que o usuário de uma IA interativa esclareça as informações necessárias ou confirme a ação do LLM antes da conclusão, oferecendo uma medida de segurança e confiança.
  • Processamento assíncrono:iniciar uma tarefa assíncrona que só pode ser concluída fora da banda, como o envio de uma notificação de aprovação para um revisor humano ou o início de um processo em segundo plano de longa duração.
  • Sair de uma tarefa autônoma:fornecer ao modelo uma maneira de marcar uma tarefa como concluída em um fluxo de trabalho que pode iterar por uma longa série de chamadas de ferramentas.

Antes de começar

Todos os exemplos documentados aqui pressupõem que você já tenha configurado um projeto com as dependências do Genkit instaladas. Se você quiser executar os exemplos de código desta página, primeiro conclua as etapas no guia Primeiros passos.

Antes de se aprofundar, você também precisa conhecer os seguintes conceitos:

Informações gerais sobre interrupções

De modo geral, a interrupção tem esta aparência ao interagir com um LLM:

  1. O aplicativo de chamada solicita o LLM com uma solicitação. O comando inclui uma lista de ferramentas, incluindo pelo menos uma para uma interrupção que o LLM pode usar para gerar uma resposta.
  2. O LLM gera uma resposta completa ou uma solicitação de chamada de ferramenta em um formato específico. Para o LLM, uma chamada de interrupção se parece com qualquer outra chamada de ferramenta.
  3. Se o LLM chamar uma ferramenta de interrupção, a biblioteca do Genkit pausa automaticamente a geração em vez de transmitir imediatamente as respostas de volta ao modelo para processamento adicional.
  4. O desenvolvedor verifica se uma chamada de interrupção foi feita e executa qualquer tarefa necessária para coletar as informações necessárias para a resposta de interrupção.
  5. O desenvolvedor retoma a geração transmitindo uma resposta de interrupção para o modelo. Essa ação faz com que você volte à Etapa 2.

Definir interrupções de resposta manual

O tipo mais comum de interrupção permite que o LLM solicite esclarecimentos do usuário, por exemplo, fazendo uma pergunta de múltipla escolha.

Para este caso de uso, use o método defineInterrupt() da instância do 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()
});

O outputSchema de uma interrupção corresponde aos dados de resposta que você vai fornecer, em vez de algo que será preenchido automaticamente por uma função de ferramenta.

Usar interrupções

As interrupções são transmitidas para a matriz tools ao gerar conteúdo, assim como outros tipos de ferramentas. É possível transmitir ferramentas normais e interrupções para a mesma chamada generate:

gerar

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' });

Arquivo de comando

---
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.

Em seguida, execute o comando no código da seguinte maneira:

```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');

O Genkit retorna imediatamente uma resposta ao receber uma chamada de ferramenta de interrupção.

Responder a interrupções

Se você transmitiu uma ou mais interrupções para a chamada de geração, verifique a resposta para interrupções para que elas possam ser processadas:

// 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

A resposta a uma interrupção é feita usando a opção resume em uma chamada generate subsequente, transmitindo o histórico atual. Cada ferramenta tem um método .respond() para ajudar a construir a resposta.

Depois de retomado, o modelo entra novamente no loop de geração, incluindo a execução da ferramenta, até que ele seja concluído ou outra interrupção seja acionada:

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);

Ferramentas com interrupções reiniciáveis

Outro padrão comum para interrupções é a necessidade de confirmar uma ação que o LLM sugere antes de realmente executá-la. Por exemplo, um app de pagamentos pode pedir que o usuário confirme determinados tipos de transferências.

Para esse caso de uso, você pode usar o método defineTool padrão para adicionar uma lógica personalizada sobre quando acionar uma interrupção e o que fazer quando uma interrupção for reiniciada com metadados adicionais.

Definir uma ferramenta reiniciável

Todas as ferramentas têm acesso a dois auxiliares especiais no segundo argumento da definição de implementação:

  • interrupt: quando chamado, esse método gera um tipo especial de exceção que é detectada para pausar o ciclo de geração. É possível fornecer metadados adicionais como um objeto.
  • resumed: quando uma solicitação de uma geração interrompida é reiniciada usando a opção {resume: {restart: ...}} (consulte abaixo), esse auxiliar contém os metadados fornecidos ao reiniciar.

Se você estivesse criando um app de pagamentos, por exemplo, talvez fosse necessário confirmar com o usuário antes de fazer uma transferência que exceda um determinado valor:

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);
}

Neste exemplo, na primeira execução (quando resumed está indefinido), a ferramenta verifica se o valor excede US $100 e, se sim, aciona uma interrupção. Na segunda execução, ela procura um status nos novos metadados fornecidos e realiza a transferência ou retorna uma resposta de rejeição, dependendo se ela é aprovada ou rejeitada.

Reiniciar ferramentas após interrupção

As ferramentas de interrupção dão controle total sobre:

  1. Quando uma solicitação inicial de ferramenta precisa acionar uma interrupção.
  2. Quando e se o ciclo de geração será retomado.
  3. Quais informações adicionais fornecer à ferramenta ao retomar.

No exemplo mostrado na seção anterior, o aplicativo pode pedir ao usuário que confirme a solicitação interrompida para garantir que o valor da transferência esteja correto:

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);