割り込みを使用して生成を一時停止する

割り込みは、LLM の生成とツール呼び出しのループを一時停止して制御を返すことができる特別な種類のツールです。準備ができたら、LLM が処理して生成を続行するレスポンスを送信して、生成を再開できます。

割り込みの最も一般的な用途は、次のカテゴリに分類されます。

  • 人間がループ内: インタラクティブ AI のユーザーが、必要な情報を明確にしたり、LLM のアクションが完了する前に確認したりできるようにすることで、安全性と信頼性を高めることができます。
  • 非同期処理: アウトバンドでのみ完了できる非同期タスクの開始(人間の審査担当者に承認通知を送信する、長時間実行されるバックグラウンド プロセスを開始するなど)。
  • Autonomous Task からの終了: 一連のツール呼び出しを反復するワークフローで、タスクを完了としてマークする方法がモデルに提供されます。

始める前に

ここで説明する例はすべて、Genkit の依存関係がインストールされたプロジェクトがすでに設定されていることを前提としています。このページのコードサンプルを実行する場合は、まずスタートガイドの手順を完了してください。

詳細を説明する前に、次のコンセプトも理解しておく必要があります。

割り込みの概要

LLM を操作する際の中断の概要は次のとおりです。

  1. 呼び出し元のアプリがリクエストを LLM にプロンプトします。プロンプトには、LLM がレスポンスの生成に使用できる中断用のツールが少なくとも 1 つ含まれています。
  2. LLM は、完全なレスポンスまたは特定の形式のツール呼び出しリクエストを生成します。LLM にとって、割り込み呼び出しは他のツール呼び出しと同じように見えます。
  3. LLM が中断ツールを呼び出すと、Genkit ライブラリは、追加処理のためにレスポンスをすぐにモデルに返すのではなく、生成を一時停止します。
  4. デベロッパーは、割り込み呼び出しが行われているかどうかを確認し、割り込みレスポンスに必要な情報を収集するために必要なタスクを実行します。
  5. デベロッパーは、割り込みレスポンスをモデルに渡すことで生成を再開します。この操作により、ステップ 2 に戻ります。

手動レスポンス割り込みを定義する

最も一般的な種類の中断では、LLM がユーザーに明確化を求めることができます(たとえば、多肢選択式の質問を尋ねる)。

このユースケースでは、Genkit インスタンスの defineInterrupt() メソッドを使用します。

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

割り込みの outputSchema は、ツール関数によって自動的に入力される値ではなく、指定するレスポンス データに対応しています。

割り込みを使用する

他のタイプのツールと同様に、コンテンツの生成時に割り込みは tools 配列に渡されます。通常のツールと割り込みの両方を同じ generate 呼び出しに渡すことができます。

生成

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

プロンプト ファイル

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

次に、コードでプロンプトを次のように実行できます。

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

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

チャット

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 は、割り込みツールの呼び出しを受信するとすぐにレスポンスを返します。

割り込みに対応する

1 つ以上の割り込みを generate 呼び出しに渡した場合は、割り込みを処理できるようにレスポンスで割り込みをチェックする必要があります。

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

割り込みに応答するには、後続の generate 呼び出しで resume オプションを使用し、既存の履歴を渡します。各ツールには、レスポンスの作成に役立つ .respond() メソッドがあります。

再開すると、モデルは生成ループ(ツールの実行を含む)に再び入り、完了するか別の割り込みが発生するまでループを続けます。

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

再開可能な割り込みがあるツール

中断の一般的なパターンとして、LLM が提案するアクションを実際に実行する前に確認する必要があることも挙げられます。たとえば、支払いアプリでは、特定の種類の振替についてユーザーに確認を求める場合があります。

このユースケースでは、標準の defineTool メソッドを使用して、割り込みをトリガーするタイミングと、割り込みが再開されたときに行う処理に関するカスタム ロジックを追加メタデータとともに追加できます。

再起動可能なツールを定義する

すべてのツールは、実装定義の 2 番目の引数にある 2 つの特別なヘルパーにアクセスできます。

  • interrupt: 呼び出されると、このメソッドは特別な種類の例外をスローします。この例外はキャッチされ、生成ループが一時停止します。追加のメタデータをオブジェクトとして指定できます。
  • resumed: 中断された生成からのリクエストが {resume: {restart: ...}} オプション(後述)を使用して再開されると、このヘルパーには再起動時に指定されたメタデータが含まれます。

たとえば、支払いアプリを作成している場合は、一定の金額を超える送金を行う前にユーザーに確認することをおすすめします。

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

この例では、最初の実行(resumed が未定義の場合)に、金額が 100 ドルを超えているかどうかを確認し、超過している場合は割り込みをトリガーします。2 回目の実行では、指定された新しいメタデータでステータスを検索し、承認または拒否に応じて転送を実行するか、拒否レスポンスを返します。

中断後にツールを再起動する

割り込みツールを使用すると、以下を完全に制御できます。

  1. 最初のツール リクエストで割り込みをトリガーする必要がある場合。
  2. 生成ループを再開するタイミングと再開するかどうか。
  3. 再開時にツールに提供する追加情報。

前のセクションの例では、転送金額が正しいことを確認するために、中断されたリクエストの確認をユーザーに求める場合があります。

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