使用中断暂停生成

中断是一种特殊的工具,可以暂停 LLM 生成和工具调用循环,以将控制权还给您。准备就绪后,您可以发送 LLM 用于进一步生成的回复,以继续生成。

中断的最常见用途可分为以下几类:

  • 人参与环节:让交互式 AI 的用户能够在 LLM 完成操作之前澄清所需信息或确认 LLM 的操作,从而提供一定程度的安全性和信心。
  • 异步处理:启动只能在非正规渠道中完成的异步任务,例如向人工审核员发送批准通知或启动长时间运行的后台进程。
  • 退出自助任务:在可能迭代一系列工具调用的工作流中,为模型提供标记任务为已完成的方法。

准备工作

此处记录的所有示例都假定您已设置了一个安装了 Genkit 依赖项的项目。如果您想运行此页面上的代码示例,请先完成使用入门指南中的步骤。

在深入探究之前,您还应熟悉以下概念:

中断概览

概括来讲,与 LLM 交互时,中断如下所示:

  1. 调用应用会向 LLM 发出请求。提示包含一系列工具,其中至少包含一个用于中断的工具,LLM 可以使用该工具生成回答。
  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 在收到中断工具调用后会立即返回响应。

响应中断

如果您已向 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 方法添加自定义逻辑,以便围绕何时触发中断以及在中断重启时执行什么操作添加额外的元数据。

定义可重启的工具

每个工具都可以在其实现定义的第二个参数中访问两个特殊的辅助程序:

  • 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 美元,如果超过,则触发中断。在第二次执行时,它会在提供的新元数据中查找状态,并根据批准或拒绝情况执行转移或返回拒绝响应。

在中断后重启工具

借助中断工具,您可以完全控制以下各项:

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