使用 Dotprompt 管理提示

提示工程是应用开发者影响生成式 AI 模型输出的主要方式。例如,在使用 LLM 时,您可以编写提示,以影响模型回答的语气、格式、长度和其他特征。

您编写这些问题的方式取决于您使用的模型;为一种模型编写的问题在用于另一种模型时可能效果不佳。同样,您设置的模型参数(温度、top-k 等)也会对输出产生不同的影响,具体取决于模型。

让这三个因素(模型、模型参数和问题)协同工作以生成所需输出,这通常并非易事,而且通常需要进行大量迭代和实验。Genkit 提供了一个名为 Dotprompt 的库和文件格式,旨在加快和简化此迭代过程。

Dotprompt 的设计基于提示即代码这一前提。您可以将问题以及适用于这些问题的模型和模型参数与应用代码分开定义。然后,您(或者甚至不参与编写应用代码的人员)可以使用 Genkit 开发者界面快速迭代提示和模型参数。提示按预期运行后,您可以将其导入应用并使用 Genkit 运行它们。

您的提示定义各自位于一个扩展名为 .prompt 的文件中。以下示例展示了这些文件的样子:

---
model: googleai/gemini-1.5-flash
config:
  temperature: 0.9
input:
  schema:
    location: string
    style?: string
    name?: string
  default:
    location: a restaurant
---

You are the world's most welcoming AI assistant and are currently working at {{location}}.

Greet a guest{{#if name}} named {{name}}{{/if}}{{#if style}} in the style of {{style}}{{/if}}.

三条短划线中的部分是 YAML 前言,类似于 GitHub Markdown 和 Jekyll 使用的前言格式;文件的其余部分是提示,可以选择使用 Handlebars 模板。以下部分将详细介绍构成 .prompt 文件的各个部分以及如何使用它们。

准备工作

在阅读本页内容之前,您应先熟悉使用 AI 模型生成内容页面上介绍的内容。

如果您想运行本页中的代码示例,请先完成开始使用指南中的步骤。所有示例都假设您已在项目中将 Genkit 安装为依赖项。

创建提示文件

虽然 Dotprompt 提供了多种不同的方式来创建和加载提示,但它针对将提示整理为单个目录(或其子目录)中的 .prompt 文件的项目进行了优化。本部分介绍了如何使用此推荐设置创建和加载问题。

创建提示目录

Dotprompt 库会在项目根目录下的某个目录中查找您的提示,并自动加载在该目录中找到的所有提示。默认情况下,此目录名为 prompts。例如,使用默认目录名称时,您的项目结构可能如下所示:

your-project/
├── lib/
├── node_modules/
├── prompts/
│   └── hello.prompt
├── src/
├── package-lock.json
├── package.json
└── tsconfig.json

如果您想使用其他目录,可以在配置 Genkit 时指定该目录:

const ai = genkit({
  promptDir: './llm_prompts',
  // (Other settings...)
});

创建提示文件

您可以通过以下两种方式创建 .prompt 文件:使用文本编辑器或使用开发者界面。

使用文本编辑器

如果您想使用文本编辑器创建提示文件,请在“prompts”目录中创建一个扩展名为 .prompt 的文本文件,例如 prompts/hello.prompt

以下是一个提示文件的最小示例:

---
model: vertexai/gemini-1.5-flash
---
You are the world's most welcoming AI assistant. Greet the user and offer your assistance.

带短划线的部分是 YAML 前言,类似于 GitHub Markdown 和 Jekyll 使用的前言格式;文件的其余部分是提示,可以选择使用 Handlebars 模板。前言部分是可选的,但大多数问题文件至少会包含指定模型的元数据。本页面的其余部分将介绍如何更进一步,在提示文件中使用 Dotprompt 的功能。

使用开发者界面

您还可以使用开发者界面中的模型运行程序创建问题文件。首先,编写应用代码,用于导入 Genkit 库并将其配置为使用您感兴趣的模型插件。例如:

import { genkit } from 'genkit';

// Import the model plugins you want to use.
import { googleAI } from '@genkit-ai/googleai';

const ai = genkit({
  // Initialize and configure the model plugins.
  plugins: [
    googleAI({
      apiKey: 'your-api-key', // Or (preferred): export GOOGLE_GENAI_API_KEY=...
    }),
  ],
});

文件包含其他代码也没有关系,但上述代码是必需的。

在同一项目中加载开发者界面:

genkit start -- tsx --watch src/your-code.ts

在“模型”部分,从插件提供的模型列表中选择要使用的模型。

Genkit 开发者界面模型运行程序

然后,对问题和配置进行实验,直到获得满意的结果。准备就绪后,按“导出”按钮,然后将文件保存到“问题”目录中。

运行提示

创建提示文件后,您可以通过应用代码或使用 Genkit 提供的工具运行这些文件。无论您想以何种方式运行提示,请先从导入 Genkit 库和您感兴趣的模型插件的应用代码入手。例如:

import { genkit } from 'genkit';

// Import the model plugins you want to use.
import { googleAI } from '@genkit-ai/googleai';

const ai = genkit({
  // Initialize and configure the model plugins.
  plugins: [
    googleAI({
      apiKey: 'your-api-key', // Or (preferred): export GOOGLE_GENAI_API_KEY=...
    }),
  ],
});

文件包含其他代码也没有关系,但上述代码是必需的。如果您将问题存储在默认目录以外的目录中,请务必在配置 Genkit 时指定该目录。

通过代码运行提示

如需使用提示,请先使用 prompt('file_name') 方法加载提示:

const helloPrompt = ai.prompt('hello');

加载后,您可以像调用函数一样调用提示:

const response = await helloPrompt();

// Alternatively, use destructuring assignments to get only the properties
// you're interested in:
const { text } = await helloPrompt();

可调用的提示接受两个可选参数:提示的输入(请参阅下文中的指定输入架构部分),以及一个配置对象(与 generate() 方法的配置对象类似)。例如:

const response2 = await helloPrompt(
  // Prompt input:
  { name: 'Ted' },

  // Generation options:
  {
    config: {
      temperature: 0.4,
    },
  }
);

您传递给提示调用的任何参数都将替换提示文件中指定的相同参数。

如需了解可用选项的说明,请参阅使用 AI 模型生成内容

使用开发者界面

在优化应用的提示时,您可以在 Genkit 开发者界面中运行这些提示,以便独立于应用代码快速迭代提示和模型配置。

从项目目录加载开发者界面:

genkit start -- tsx --watch src/your-code.ts

Genkit 开发者界面提示运行程序

将问题加载到开发者界面后,您可以使用不同的输入值运行这些问题,并通过实验来了解更改问题措辞或配置参数对模型输出的影响。当您对结果满意后,可以点击导出提示按钮,将修改后的提示保存回项目目录。

模型配置

在问题文件的“前言”块中,您可以视需要为问题指定模型配置值:

---
model: googleai/gemini-1.5-flash
config:
  temperature: 1.4
  topK: 50
  topP: 0.4
  maxOutputTokens: 400
  stopSequences:
    -   "<end>"
    -   "<fin>"
---

这些值直接映射到可调用提示接受的 config 参数:

const response3 = await helloPrompt(
  {},
  {
    config: {
      temperature: 1.4,
      topK: 50,
      topP: 0.4,
      maxOutputTokens: 400,
      stopSequences: ['<end>', '<fin>'],
    },
  }
);

如需了解可用选项的说明,请参阅使用 AI 模型生成内容

输入和输出架构

您可以在前言部分定义提示的输入和输出架构,以便为其指定架构:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    theme?: string
  default:
    theme: "pirate"
output:
  schema:
    dishname: string
    description: string
    calories: integer
    allergens(array): string
---
Invent a menu item for a {{theme}} themed restaurant.

这些架构的使用方式与传递给 generate() 请求或流程定义的架构非常相似。例如,上面定义的提示会生成结构化输出:

const menuPrompt = ai.prompt('menu');
const { data } = await menuPrompt({ theme: 'medieval' });

const dishName = data['dishname'];
const description = data['description'];

您可以通过多种方式在 .prompt 文件中定义架构:Dotprompt 自己的架构定义格式 Picoschema;标准 JSON 架构;或作为对应用代码中定义的架构的引用。以下各部分详细介绍了这些选项。

Picoschema

上述示例中的架构采用名为 Picoschema 的格式进行定义。Picoschema 是一种经过 YAML 优化的紧凑架构定义格式,可让您轻松定义 LLM 使用架构的最重要属性。下面是一个更详细的架构示例,其中指定了应用可能会存储的有关文章的信息:

schema:
  title: string # string, number, and boolean types are defined like this
  subtitle?: string # optional fields are marked with a `?`
  draft?: boolean, true when in draft state
  status?(enum, approval status): [PENDING, APPROVED]
  date: string, the date of publication e.g. '2024-04-09' # descriptions follow a comma
  tags(array, relevant tags for article): string # arrays are denoted via parentheses
  authors(array):
    name: string
    email?: string
  metadata?(object): # objects are also denoted via parentheses
    updatedAt?: string, ISO timestamp of last update
    approvedBy?: integer, id of approver
  extra?: any, arbitrary extra data
  (*): string, wildcard field

上述架构等效于以下 TypeScript 接口:

interface Article {
  title: string;
  subtitle?: string | null;
  /** true when in draft state */
  draft?: boolean | null;
  /** approval status */
  status?: 'PENDING' | 'APPROVED' | null;
  /** the date of publication e.g. '2024-04-09' */
  date: string;
  /** relevant tags for article */
  tags: string[];
  authors: {
    name: string;
    email?: string | null;
  }[];
  metadata?: {
    /** ISO timestamp of last update */
    updatedAt?: string | null;
    /** id of approver */
    approvedBy?: number | null;
  } | null;
  /** arbitrary extra data */
  extra?: any;
  /** wildcard field */

}

Picoschema 支持标量类型 stringintegernumberbooleanany。对象、数组和枚举在字段名称后用英文括号表示。

由 Picoschema 定义的对象具有所有必需的属性(除非由 ? 表示为可选属性),并且不允许添加额外的属性。当某个属性被标记为可选属性时,它也会设为可为 null,以便 LLM 更宽松地返回 null,而不是省略某个字段。

在对象定义中,特殊键 (*) 可用于声明“通配符”字段定义。这将匹配显式键未提供的任何其他属性。

JSON 架构

Picoschema 不支持完整 JSON 架构的许多功能。如果您需要更强大的架构,可以改为提供 JSON 架构:

output:
  schema:
    type: object
    properties:
      field1:
        type: number
        minimum: 20

在代码中定义的 Zod 架构

除了直接在 .prompt 文件中定义架构之外,您还可以按名称引用已向 defineSchema() 注册的架构。如果您使用的是 TypeScript,那么在处理提示时,这种方法可让您利用该语言的静态类型检查功能。

如需注册架构,请执行以下操作:

import { z } from 'genkit';

const MenuItemSchema = ai.defineSchema(
  'MenuItemSchema',
  z.object({
    dishname: z.string(),
    description: z.string(),
    calories: z.coerce.number(),
    allergens: z.array(z.string()),
  })
);

在问题中,提供已注册架构的名称:

---
model: googleai/gemini-1.5-flash-latest
output:
  schema: MenuItemSchema
---

Dotprompt 库会自动将名称解析为底层已注册的 Zod 架构。然后,您可以使用架构对 Dotprompt 的输出进行强类型化:

const menuPrompt = ai.prompt<
  z.ZodTypeAny, // Input schema
  typeof MenuItemSchema, // Output schema
  z.ZodTypeAny // Custom options schema
>('menu');
const { data } = await menuPrompt({ theme: 'medieval' });

// Now data is strongly typed as MenuItemSchema:
const dishName = data?.dishname;
const description = data?.description;

提示模板

.prompt 文件中紧随前言(如果有)之后的部分是问题本身,该部分将传递给模型。虽然此提示可以是简单的文本字符串,但您通常会希望将用户输入纳入到提示中。为此,您可以使用 Handlebars 模板语言指定提示。问题模板可以包含引用问题输入架构定义的值的占位符。

您已经在输入和输出架构部分看到了这种情况:

---
model: googleai/gemini-1.5-flash
config:
  temperature: 1.4
  topK: 50
  topP: 0.4
  maxOutputTokens: 400
  stopSequences:
    -   "<end>"
    -   "<fin>"
---

在此示例中,当您运行提示时,Handlebars 表达式 {{theme}} 会解析为输入的 theme 属性的值。如需将输入传递给提示,请调用提示,如以下示例所示:

const menuPrompt = ai.prompt('menu');
const { data } = await menuPrompt({ theme: 'medieval' });

请注意,由于输入架构声明 theme 属性为可选属性并提供了默认值,因此您可以省略该属性,系统会使用默认值解析提示。

Handlebars 模板还支持一些有限的逻辑结构。例如,除了提供默认值之外,您还可以使用 Handlebars 的 #if 帮助程序来定义提示:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    theme?: string
---
Invent a menu item for a {{#if theme}}{{theme}} themed{{/if}} restaurant.

在此示例中,如果未指定 theme 属性,提示将呈现为“为餐厅发明菜单项”。

如需了解所有内置逻辑帮助程序,请参阅 Handlebars 文档

除了输入架构定义的属性之外,模板还可以引用 Genkit 自动定义的值。以下几节将介绍这些自动定义的值以及如何使用它们。

多消息提示

默认情况下,Dotprompt 会构造包含“用户”角色单条消息。不过,某些提示最好以多条消息的组合表示,例如系统提示。

{{role}} 辅助函数提供了一种构造多消息提示的简单方法:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    userQuestion: string
---
{{role "system"}}
You are a helpful AI assistant that really loves to talk about food. Try to work
food items into all of your conversations.
{{role "user"}}
{{userQuestion}}

多模态提示

对于支持多模态输入(例如图片和文本)的模型,您可以使用 {{media}} 辅助函数:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    photoUrl: string
---
Describe this image in a detailed paragraph:

{{media url=photoUrl}}

网址可以是 https: 或用于“内嵌”图片且采用 base64 编码的 data: URI。在代码中,应如下所示:

const multimodalPrompt = ai.prompt('multimodal');
const { text } = await multimodalPrompt({
  photoUrl: 'https://example.com/photo.jpg',
});

另请参阅“模型”页面上的多模态输入,查看构建 data: 网址的示例。

部分

部分是可重复使用的模板,可添加到任何问题中。对于具有共同行为的相关提示,部分特别有用。

加载问题目录时,前缀为下划线 (_) 的任何文件都被视为部分问题。因此,文件 _personality.prompt 可能包含:

You should speak like a {{#if style}}{{style}}{{else}}helpful assistant.{{/else}}.

然后,您可以在其他提示中添加此值:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    name: string
    style?: string
---
{{ role "system" }}
{{>personality style=style}}

{{ role "user" }}
Give the user a friendly greeting.

User's Name: {{name}}

使用 {{>NAME_OF_PARTIAL args...}} 语法插入部分。如果未向部分提供任何参数,则该部分将使用与父级提示相同的上下文进行执行。

部分接受上述命名参数,也接受表示上下文的单个位置参数。这对于渲染列表成员等任务很有帮助。

_destination.prompt

- {{name}} ({{country}})

chooseDestination.prompt

---
model: googleai/gemini-1.5-flash-latest
input:
  schema:
    destinations(array):
      name: string
      country: string
---
Help the user decide between these vacation destinations:

{{#each destinations}}
{{>destination this}}
{{/each}}

在代码中定义部分

您还可以使用 definePartial 在代码中定义部分:

ai.definePartial(
  'personality',
  'Talk like a {{#if style}}{{style}}{{else}}helpful assistant{{/if}}.'
);

代码定义的部分可在所有提示中使用。

定义自定义帮助程序

您可以定义自定义帮助程序,以便在问题中处理和管理数据。辅助程序使用 defineHelper 进行全局注册:

ai.defineHelper('shout', (text: string) => text.toUpperCase());

定义帮助程序后,您可以在任何提示中使用它:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    name: string
---

HELLO, {{shout name}}!!!

提示变体

由于提示文件只是文本,因此您可以(并且应该)将提示文件提交到版本控制系统,以便您轻松比较一段时间内的更改。通常,调整后的提示版本只能在生产环境中与现有版本并排进行全面测试。Dotprompt 通过其变体功能支持此功能。

如需创建变体,请创建 [name].[variant].prompt 文件。例如,如果您在提示中使用了 Gemini 1.5 Flash,但想了解 Gemini 1.5 Pro 的表现是否更好,则可以创建两个文件:

  • my_prompt.prompt:“基准”提示
  • my_prompt.gemini15pro.prompt:名为 gemini15pro 的变体

如需使用提示变体,请在加载时指定相应变体选项:

const myPrompt = ai.prompt('my_prompt', { variant: 'gemini15pro' });

变体的名称包含在生成跟踪记录的元数据中,因此您可以在 Genkit 跟踪记录检查器中比较和对比变体之间的实际效果。

在代码中定义提示

到目前为止,我们讨论的所有示例都假定您的提示是在单个目录(或其子目录)中的各个 .prompt 文件中定义的,并且应用可以在运行时访问这些提示。Dotprompt 就是围绕这种设置设计的,其作者认为它是总体上最佳的开发者体验。

不过,如果此设置不支持您的某些用例,您还可以使用 definePrompt() 函数在代码中定义提示:

此函数的第一个参数类似于 .prompt 文件的前言块;第二个参数可以是 Handlebars 模板字符串(如提示文件中所示),也可以是返回 GenerateRequest 的函数:

const myPrompt = ai.definePrompt(
  {
    name: 'myPrompt',
    model: 'googleai/gemini-1.5-flash',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  'Hello, {{name}}. How are you today?'
);
const myPrompt = ai.definePrompt(
  {
    name: 'myPrompt',
    model: 'googleai/gemini-1.5-flash',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  async (input): Promise<GenerateRequest> => {
    return {
      messages: [
        {
          role: 'user',
          content: [{ text: `Hello, ${input.name}. How are you today?` }],
        },
      ],
    };
  }
);