Gérer les requêtes avec Dotprompt

L'ingénierie des requêtes est le principal moyen pour vous, en tant que développeur d'applications, d'influencer la sortie des modèles d'IA générative. Par exemple, lorsque vous utilisez des LLM, vous pouvez créer des requêtes qui influencent le ton, le format, la longueur et d'autres caractéristiques des réponses des modèles.

La manière dont vous rédigez ces invites dépend du modèle que vous utilisez. Une invite écrite pour un modèle peut ne pas fonctionner correctement avec un autre modèle. De même, les paramètres du modèle que vous définissez (température, top-k, etc.) affectent également la sortie différemment selon le modèle.

Faire en sorte que ces trois facteurs (le modèle, les paramètres du modèle et la requête) fonctionnent ensemble pour produire le résultat souhaité est rarement un processus simple et implique souvent des itérations et des expérimentations importantes. Genkit fournit une bibliothèque et un format de fichier appelés Dotprompt, qui visent à rendre cette itération plus rapide et plus pratique.

Dotprompt est conçu sur la base du principe selon lequel les requêtes sont du code. Vous définissez vos requêtes ainsi que les modèles et les paramètres de modèle auxquels elles sont destinées séparément du code de votre application. Vous pouvez ensuite (ou peut-être quelqu'un qui n'est même pas impliqué dans l'écriture du code de l'application) itérer rapidement sur les invites et les paramètres du modèle à l'aide de l'interface utilisateur du développeur Genkit. Une fois que vos requêtes fonctionnent comme vous le souhaitez, vous pouvez les importer dans votre application et les exécuter à l'aide de Genkit.

Vos définitions d'invites sont chacune placées dans un fichier portant l'extension .prompt. Voici un exemple de ces fichiers:

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

La partie entre les trois tirets est la partie avant YAML, semblable au format de partie avant utilisé par GitHub Markdown et Jekyll. Le reste du fichier est l'invite, qui peut éventuellement utiliser des modèles Handlebars. Les sections suivantes détaillent chacune des parties d'un fichier .prompt et expliquent comment les utiliser.

Avant de commencer

Avant de lire cette page, vous devez connaître le contenu de la page Générer du contenu avec des modèles d'IA.

Si vous souhaitez exécuter les exemples de code de cette page, suivez d'abord les étapes du guide Premiers pas. Tous les exemples supposent que vous avez déjà installé Genkit en tant que dépendance dans votre projet.

Créer des fichiers d'invite

Bien que Dotprompt propose plusieurs méthodes différentes pour créer et charger des requêtes, il est optimisé pour les projets qui organisent leurs requêtes en tant que fichiers .prompt dans un seul répertoire (ou sous-répertoires). Cette section vous explique comment créer et charger des requêtes à l'aide de cette configuration recommandée.

Créer un répertoire d'invites

La bibliothèque Dotprompt s'attend à trouver vos requêtes dans un répertoire au niveau de la racine de votre projet et charge automatiquement toutes les requêtes qu'elle y trouve. Par défaut, ce répertoire est nommé prompts. Par exemple, en utilisant le nom de répertoire par défaut, la structure de votre projet peut se présenter comme suit:

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

Si vous souhaitez utiliser un autre répertoire, vous pouvez le spécifier lorsque vous configurez Genkit:

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

Créer un fichier d'invite

Il existe deux façons de créer un fichier .prompt: à l'aide d'un éditeur de texte ou avec l'UI du développeur.

Utiliser un éditeur de texte

Si vous souhaitez créer un fichier d'invite à l'aide d'un éditeur de texte, créez un fichier texte avec l'extension .prompt dans votre répertoire d'invites: par exemple, prompts/hello.prompt.

Voici un exemple minimal de fichier d'invite:

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

La partie entre les tirets correspond à la partie avant YAML, semblable au format de la partie avant utilisé par GitHub Markdown et Jekyll. Le reste du fichier correspond à l'invite, qui peut éventuellement utiliser des modèles Handlebars. La section "Préface" est facultative, mais la plupart des fichiers d'invite contiennent au moins des métadonnées spécifiant un modèle. Le reste de cette page vous explique comment aller plus loin et utiliser les fonctionnalités de Dotprompt dans vos fichiers de requête.

Utiliser l'UI du développeur

Vous pouvez également créer un fichier d'invite à l'aide du moteur d'exécution de modèle dans l'interface utilisateur du développeur. Commencez par le code d'application qui importe la bibliothèque Genkit et la configure pour utiliser le plug-in de modèle qui vous intéresse. Exemple :

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=...
    }),
  ],
});

Le fichier peut contenir d'autres codes, mais le code ci-dessus est tout ce qui est nécessaire.

Chargez l'UI du développeur dans le même projet:

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

Dans la section "Modèles", choisissez le modèle que vous souhaitez utiliser dans la liste des modèles fournis par le plug-in.

Exécuteur de modèle de l'UI du développeur Genkit

Ensuite, testez la requête et la configuration jusqu'à obtenir des résultats qui vous conviennent. Lorsque vous êtes prêt, appuyez sur le bouton "Exporter" et enregistrez le fichier dans votre répertoire d'invites.

Exécuter des requêtes

Une fois les fichiers d'invite créés, vous pouvez les exécuter à partir du code de votre application ou à l'aide des outils fournis par Genkit. Quelle que soit la manière dont vous souhaitez exécuter vos requêtes, commencez par le code d'application qui importe la bibliothèque Genkit et les plug-ins de modèle qui vous intéressent. Exemple :

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=...
    }),
  ],
});

Le fichier peut contenir d'autres codes, mais le code ci-dessus est tout ce qui est nécessaire. Si vous stockez vos requêtes dans un répertoire autre que celui par défaut, veillez à le spécifier lorsque vous configurez Genkit.

Exécuter des requêtes à partir du code

Pour utiliser une requête, commencez par la charger à l'aide de la méthode prompt('file_name'):

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

Une fois chargée, vous pouvez appeler l'invite comme une fonction:

const response = await helloPrompt();

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

Une invite appelable accepte deux paramètres facultatifs: l'entrée de l'invite (voir la section ci-dessous sur la spécification des schémas d'entrée) et un objet de configuration, semblable à celui de la méthode generate(). Exemple :

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

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

Tous les paramètres que vous transmettez à l'appel de requête remplacent les mêmes paramètres spécifiés dans le fichier de requête.

Pour obtenir une description des options disponibles, consultez Générer du contenu avec des modèles d'IA.

Utiliser l'UI du développeur

Lorsque vous affinez les requêtes de votre application, vous pouvez les exécuter dans l'interface utilisateur du développeur Genkit pour itérer rapidement sur les requêtes et les configurations de modèle, indépendamment du code de votre application.

Chargez l'UI pour les développeurs à partir du répertoire de votre projet:

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

Exécuteur de requêtes de l'UI du développeur Genkit

Une fois que vous avez chargé des requêtes dans l'interface utilisateur du développeur, vous pouvez les exécuter avec différentes valeurs d'entrée et tester l'impact des modifications apportées au libellé de la requête ou aux paramètres de configuration sur la sortie du modèle. Lorsque vous êtes satisfait du résultat, vous pouvez cliquer sur le bouton Exporter l'invite pour enregistrer l'invite modifiée dans le répertoire de votre projet.

Configuration de modèle

Dans le bloc "avant-propos" de vos fichiers d'invite, vous pouvez éventuellement spécifier des valeurs de configuration du modèle pour votre invite:

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

Ces valeurs correspondent directement au paramètre config accepté par l'invite appelable:

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

Pour obtenir une description des options disponibles, consultez Générer du contenu avec des modèles d'IA.

Schémas d'entrée et de sortie

Vous pouvez spécifier des schémas d'entrée et de sortie pour votre requête en les définissant dans la section "Préface" :

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

Ces schémas sont utilisés de la même manière que ceux transmis à une requête generate() ou à une définition de flux. Par exemple, l'invite définie ci-dessus produit une sortie structurée:

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

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

Vous disposez de plusieurs options pour définir des schémas dans un fichier .prompt: le format de définition de schéma de Dotprompt, Picoschema ; le schéma JSON standard ; ou en tant que références aux schémas définis dans le code de votre application. Les sections suivantes décrivent chacune de ces options plus en détail.

Picoschema

Les schémas de l'exemple ci-dessus sont définis dans un format appelé Picoschema. Picoschema est un format de définition de schéma compact et optimisé pour YAML qui permet de définir facilement les attributs les plus importants d'un schéma pour l'utilisation de LLM. Voici un exemple plus long d'un schéma, qui spécifie les informations qu'une application peut stocker sur un article:

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

Le schéma ci-dessus équivaut à l'interface TypeScript suivante:

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 est compatible avec les types scalaires string, integer, number, boolean et any. Les objets, les tableaux et les énumérations sont indiqués entre parenthèses après le nom du champ.

Les objets définis par Picoschema ont toutes les propriétés obligatoires, sauf si elles sont indiquées comme facultatives par ?, et n'autorisent pas de propriétés supplémentaires. Lorsqu'une propriété est marquée comme facultative, elle est également rendue nullable pour permettre aux LLM de renvoyer "null" au lieu d'omettre un champ.

Dans une définition d'objet, la clé spéciale (*) peut être utilisée pour déclarer une définition de champ "générique". Cela correspond à toutes les propriétés supplémentaires qui ne sont pas fournies par une clé explicite.

Schéma JSON

Picoschema n'est pas compatible avec de nombreuses fonctionnalités du schéma JSON complet. Si vous avez besoin de schémas plus robustes, vous pouvez fournir un schéma JSON à la place:

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

Schémas Zod définis dans le code

En plus de définir directement des schémas dans le fichier .prompt, vous pouvez référencer un schéma enregistré avec defineSchema() par nom. Si vous utilisez TypeScript, cette approche vous permettra de profiter des fonctionnalités de vérification de type statique du langage lorsque vous travaillez avec des requêtes.

Pour enregistrer un schéma:

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

Dans votre requête, indiquez le nom du schéma enregistré:

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

La bibliothèque Dotprompt résout automatiquement le nom en fonction du schéma Zod enregistré sous-jacent. Vous pouvez ensuite utiliser le schéma pour typifier fortement la sortie d'une requête 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;

Modèles de requêtes

La partie d'un fichier .prompt qui suit la partie avant-propos (le cas échéant) est l'invite elle-même, qui sera transmise au modèle. Bien que cette invite puisse être une simple chaîne de texte, vous devrez très souvent intégrer l'entrée utilisateur à l'invite. Pour ce faire, vous pouvez spécifier votre requête à l'aide du langage de création de modèles Handlebars. Les modèles d'invite peuvent inclure des espaces réservés qui font référence aux valeurs définies par le schéma d'entrée de votre invite.

Vous l'avez déjà vu en action dans la section sur les schémas d'entrée et de sortie:

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

Dans cet exemple, l'expression Handlebars, {{theme}}, renvoie la valeur de la propriété theme de l'entrée lorsque vous exécutez l'invite. Pour transmettre une entrée à l'invite, appelez-la comme dans l'exemple suivant:

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

Notez que, comme le schéma d'entrée a déclaré la propriété theme comme facultative et fourni une valeur par défaut, vous auriez pu omettre la propriété, et l'invite aurait été résolue à l'aide de la valeur par défaut.

Les modèles Handlebars sont également compatibles avec certaines constructions logiques limitées. Par exemple, au lieu de fournir une valeur par défaut, vous pouvez définir l'invite à l'aide de l'aide #if de Handlebars:

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

Dans cet exemple, l'invite s'affiche comme "Inventez un plat pour un restaurant" lorsque la propriété theme n'est pas spécifiée.

Pour en savoir plus sur toutes les aides logiques intégrées, consultez la documentation Handlebars.

En plus des propriétés définies par votre schéma d'entrée, vos modèles peuvent également faire référence à des valeurs définies automatiquement par Genkit. Les sections suivantes décrivent ces valeurs définies automatiquement et comment les utiliser.

Requêtes multimessages

Par défaut, Dotprompt crée un seul message avec un rôle "user". Toutefois, certaines invites sont mieux exprimées sous la forme d'une combinaison de plusieurs messages, comme une invite système.

L'assistant {{role}} permet de créer facilement des invites multimessages:

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

Requêtes multimodales

Pour les modèles compatibles avec les entrées multimodales, telles que les images avec du texte, vous pouvez utiliser l'assistant {{media}}:

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

{{media url=photoUrl}}

L'URL peut être un URI https: ou data: encodé en base64 pour une utilisation d'image "inline". En code, cela se présente comme suit:

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

Consultez également Entrée multimodale sur la page "Modèles" pour voir un exemple de création d'une URL data:.

Partiels

Les partiels sont des modèles réutilisables qui peuvent être inclus dans n'importe quelle invite. Les partielles peuvent être particulièrement utiles pour les requêtes associées qui partagent un comportement commun.

Lors du chargement d'un répertoire d'invite, tout fichier précédé d'un trait de soulignement (_) est considéré comme partiel. Un fichier _personality.prompt peut donc contenir les éléments suivants:

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

Vous pouvez ensuite l'inclure dans d'autres requêtes:

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

Les partiels sont insérés à l'aide de la syntaxe {{>NAME_OF_PARTIAL args...}}. Si aucun argument n'est fourni à la partie, elle s'exécute avec le même contexte que l'invite parente.

Les partiels acceptent à la fois des arguments nommés comme ci-dessus ou un seul argument positionnel représentant le contexte. Cela peut être utile pour des tâches telles que l'affichage des membres d'une liste.

_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}}

Définir des partiels dans le code

Vous pouvez également définir des partiels dans le code à l'aide de definePartial:

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

Les fragments définis par code sont disponibles dans toutes les requêtes.

Définir des assistants personnalisés

Vous pouvez définir des assistants personnalisés pour traiter et gérer les données dans une invite. Les assistants sont enregistrés globalement à l'aide de defineHelper:

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

Une fois qu'une aide est définie, vous pouvez l'utiliser dans n'importe quelle invite:

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

HELLO, {{shout name}}!!!

Variantes d'invites

Étant donné que les fichiers d'invite ne sont que du texte, vous pouvez (et devez) les enregistrer dans votre système de contrôle des versions. Vous pourrez ainsi comparer facilement les modifications au fil du temps. Souvent, les versions modifiées des requêtes ne peuvent être entièrement testées que dans un environnement de production côte à côte avec les versions existantes. Dotprompt est compatible avec cette fonctionnalité via sa fonctionnalité de variantes.

Pour créer une variante, créez un fichier [name].[variant].prompt. Par exemple, si vous utilisiez Gemini 1.5 Flash dans votre requête, mais que vous vouliez voir si Gemini 1.5 Pro était plus performant, vous pouvez créer deux fichiers:

  • my_prompt.prompt : invite "baseline" (ligne de base)
  • my_prompt.gemini15pro.prompt: variante nommée gemini15pro

Pour utiliser une variante d'invite, spécifiez l'option de variante lors du chargement:

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

Le nom de la variante est inclus dans les métadonnées des traces de génération. Vous pouvez ainsi comparer les performances réelles entre les variantes dans l'inspecteur de trace Genkit.

Définir des requêtes dans le code

Tous les exemples abordés jusqu'à présent supposent que vos requêtes sont définies dans des fichiers .prompt individuels dans un seul répertoire (ou sous-répertoires) accessible à votre application au moment de l'exécution. Dotprompt est conçu autour de cette configuration, et ses auteurs considèrent qu'il s'agit de la meilleure expérience de développement globale.

Toutefois, si vous avez des cas d'utilisation qui ne sont pas bien pris en charge par cette configuration, vous pouvez également définir des requêtes dans le code à l'aide de la fonction definePrompt():

Le premier paramètre de cette fonction est analogue au bloc de préface d'un fichier .prompt. Le deuxième paramètre peut être une chaîne de modèle Handlebars, comme dans un fichier d'invite, ou une fonction qui renvoie un 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?` }],
        },
      ],
    };
  }
);