Administra mensajes con Dotprompt

La ingeniería de instrucciones es la forma principal en la que tú, como desarrollador de apps, influyes en el resultado de los modelos de IA generativa. Por ejemplo, cuando usas los LLMs, puedes crear instrucciones que influyen en el tono, el formato, la longitud y otras características de las respuestas de los modelos.

La forma en que escribas estas instrucciones dependerá del modelo que uses. Es posible que una instrucción escrita para un modelo no tenga un buen rendimiento cuando se use con otro. Del mismo modo, los parámetros del modelo que establezcas (temperatura, Top-K, etcétera) también afectarán los resultados de manera diferente según el modelo.

Hacer que los tres factores (el modelo, los parámetros del modelo y la instrucción) funcionen juntos para producir el resultado que deseas rara vez es un proceso trivial y, a menudo, implica iteración y experimentación sustanciales. Genkit proporciona una biblioteca y un formato de archivo llamados Dotprompt, cuyo objetivo es hacer que esta iteración sea más rápida y conveniente.

Dotprompt está diseñado con la premisa de que las instrucciones son código. Define tus instrucciones junto con los modelos y los parámetros de modelo para los que están destinados, de forma independiente del código de tu aplicación. Luego, tú (o quizás alguien que ni siquiera esté involucrado en escribir código de la aplicación) puedes iterar rápidamente en las instrucciones y los parámetros del modelo con la IU de desarrollador de Genkit. Una vez que las instrucciones funcionen como desees, podrás importarlas a tu aplicación y ejecutarlas con Genkit.

Cada una de las definiciones de instrucciones se coloca en un archivo con la extensión .prompt. A continuación, se muestra un ejemplo de cómo se ven estos archivos:

---
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 parte de los tres guiones es el material de referencia de YAML, similar al formato de material de referencia que usan GitHub Markdown y Jekyll. El resto del archivo es la instrucción, que puede usar plantillas de Handlebars de forma opcional. En las siguientes secciones, se analizarán con más detalle cada una de las partes que conforman un archivo .prompt y cómo usarlas.

Antes de comenzar

Antes de leer esta página, debes familiarizarte con el contenido que se explica en la página Cómo generar contenido con modelos de IA.

Si quieres ejecutar los ejemplos de código de esta página, primero completa los pasos de la guía de Cómo comenzar. En todos los ejemplos, se da por sentado que ya instalaste Genkit como dependencia en tu proyecto.

Cómo crear archivos de instrucciones

Aunque Dotprompt proporciona varias formas diferentes de crear y cargar instrucciones, está optimizado para proyectos que organizan sus instrucciones como archivos .prompt dentro de un solo directorio (o subdirectorios de este). En esta sección, se muestra cómo crear y cargar instrucciones con esta configuración recomendada.

Cómo crear un directorio de instrucciones

La biblioteca de Dotprompt espera encontrar tus instrucciones en un directorio en la raíz del proyecto y carga automáticamente las instrucciones que encuentre allí. De forma predeterminada, este directorio se llama prompts. Por ejemplo, si usas el nombre de directorio predeterminado, la estructura de tu proyecto podría verse de la siguiente manera:

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

Si deseas usar un directorio diferente, puedes especificarlo cuando configures Genkit:

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

Crea un archivo de instrucciones

Existen dos maneras de crear un archivo .prompt: con un editor de texto o con la IU para desarrolladores.

Cómo usar un editor de texto

Si quieres crear un archivo de instrucciones con un editor de texto, crea un archivo de texto con la extensión .prompt en el directorio de instrucciones: por ejemplo, prompts/hello.prompt.

A continuación, se muestra un ejemplo mínimo de un archivo de instrucciones:

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

La parte entre guiones es el material de referencia de YAML, similar al formato de material de referencia que usan GitHub Markdown y Jekyll. El resto del archivo es la instrucción, que puede usar plantillas de Handlebars de forma opcional. La sección de material preliminar es opcional, pero la mayoría de los archivos de instrucciones al menos contendrán metadatos que especifiquen un modelo. En el resto de esta página, se muestra cómo ir más allá y usar las funciones de Dotprompt en tus archivos de instrucciones.

Usa la IU para desarrolladores

También puedes crear un archivo de instrucciones con el ejecutor de modelos en la IU para desarrolladores. Comienza con el código de la aplicación que importa la biblioteca de Genkit y la configura para que use el complemento de modelo que te interesa. Por ejemplo:

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

Está bien si el archivo contiene otro código, pero lo anterior es todo lo que se requiere.

Carga la IU para desarrolladores en el mismo proyecto:

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

En la sección Modelos, elige el modelo que deseas usar de la lista de modelos que proporciona el complemento.

Ejecutor de modelos de la IU para desarrolladores de Genkit

Luego, experimenta con la instrucción y la configuración hasta que obtengas los resultados que te satisfagan. Cuando esté todo listo, presiona el botón Exportar y guarda el archivo en el directorio de instrucciones.

Ejecuta instrucciones

Después de crear los archivos de instrucciones, puedes ejecutarlos desde el código de tu aplicación o con las herramientas que proporciona Genkit. Independientemente de cómo quieras ejecutar tus instrucciones, primero comienza con el código de la aplicación que importa la biblioteca de Genkit y los complementos de modelos que te interesan. Por ejemplo:

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

Está bien si el archivo contiene otro código, pero lo anterior es todo lo que se requiere. Si almacenas tus instrucciones en un directorio que no es el predeterminado, asegúrate de specificarlo cuando configures Genkit.

Ejecuta instrucciones desde el código

Para usar una instrucción, primero cárgala con el método prompt('file_name'):

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

Una vez cargado, puedes llamar a la instrucción como una función:

const response = await helloPrompt();

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

Una instrucción que se puede llamar toma dos parámetros opcionales: la entrada a la instrucción (consulta la sección a continuación sobre cómo especificar esquemas de entrada) y un objeto de configuración, similar al del método generate(). Por ejemplo:

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

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

Cualquier parámetro que pases a la llamada de instrucción anulará los mismos parámetros especificados en el archivo de instrucciones.

Consulta Genera contenido con modelos de IA para obtener descripciones de las opciones disponibles.

Usa la IU para desarrolladores

A medida que definas mejor las instrucciones de tu app, podrás ejecutarlas en la IU para desarrolladores de Genkit para iterar rápidamente sobre las instrucciones y las configuraciones de modelos, independientemente del código de tu aplicación.

Carga la IU para desarrolladores desde el directorio de tu proyecto:

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

Ejecutor de instrucciones de la IU para desarrolladores de Genkit

Una vez que hayas cargado los prompts en la IU para desarrolladores, podrás ejecutarlos con valores de entrada diferentes y experimentar cómo los cambios en el texto del prompt o los parámetros de configuración afectan el resultado del modelo. Cuando estés conforme con el resultado, puedes hacer clic en el botón Export prompt para guardar la instrucción modificada en el directorio de tu proyecto.

Configuración del modelo

En el bloque de material preliminar de tus archivos de instrucciones, puedes especificar valores de configuración del modelo para tu instrucción de forma opcional:

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

Estos valores se asignan directamente al parámetro config que acepta la instrucción que se puede llamar:

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

Consulta Genera contenido con modelos de IA para obtener descripciones de las opciones disponibles.

Esquemas de entrada y salida

Puedes especificar los esquemas de entrada y salida de tu instrucción definiéndolos en la sección de material preliminar:

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

Estos esquemas se usan de la misma manera que los que se pasan a una solicitud generate() o a una definición de flujo. Por ejemplo, la instrucción definida anteriormente produce un resultado estructurado:

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

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

Tienes varias opciones para definir esquemas en un archivo .prompt: el formato de definición de esquemas de Dotprompt, Picoschema; el esquema JSON estándar o como referencias a esquemas definidos en el código de tu aplicación. En las siguientes secciones, se describen cada una de estas opciones con más detalle.

Picoschema

Los esquemas del ejemplo anterior se definen en un formato llamado Picoschema. Picoschema es un formato de definición de esquema compacto y optimizado para YAML que facilita la definición de los atributos más importantes de un esquema para el uso de LLM. A continuación, se muestra un ejemplo más extenso de un esquema, que especifica la información que una app podría almacenar sobre un artículo:

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

El esquema anterior es equivalente a la siguiente interfaz de 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 admite los tipos escalares string, integer, number, boolean y any. Los objetos, arrays y enums se denotan con un paréntesis después del nombre del campo.

Los objetos definidos por Picoschema tienen todas las propiedades necesarias, a menos que se indique que son opcionales por ?, y no permiten propiedades adicionales. Cuando una propiedad se marca como opcional, también se hace anulable para proporcionar más legibilidad para que los LLMs devuelvan un valor nulo en lugar de omitir un campo.

En una definición de objeto, se puede usar la clave especial (*) para declarar una definición de campo “comodín”. Esto coincidirá con cualquier propiedad adicional que no proporcione una clave explícita.

Esquema de JSON

Picoschema no es compatible con muchas de las capacidades del esquema JSON completo. Si necesitas esquemas más sólidos, puedes proporcionar un esquema JSON en su lugar:

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

Esquemas de Zod definidos en el código

Además de definir esquemas directamente en el archivo .prompt, puedes hacer referencia a un esquema registrado con defineSchema() por nombre. Si usas TypeScript, este enfoque te permitirá aprovechar las funciones de verificación de tipos estáticos del lenguaje cuando trabajes con instrucciones.

Para registrar un esquema, sigue estos pasos:

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

Dentro de la instrucción, proporciona el nombre del esquema registrado:

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

La biblioteca Dotprompt resolverá automáticamente el nombre al esquema Zod registrado subyacente. Luego, puedes usar el esquema para tipificar de forma estricta el resultado de un 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;

Plantillas de instrucciones

La parte de un archivo .prompt que sigue al material preliminar (si está presente) es la instrucción en sí, que se pasará al modelo. Si bien esta instrucción podría ser una cadena de texto simple, a menudo querrás incorporar la entrada del usuario en la instrucción. Para ello, puedes especificar tu instrucción con el lenguaje de plantillas Handlebars. Las plantillas de instrucciones pueden incluir marcadores de posición que hacen referencia a los valores definidos por el esquema de entrada de tu instrucción.

Ya viste esto en acción en la sección sobre esquemas de entrada y salida:

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

En este ejemplo, la expresión Handlebars, {{theme}}, se resuelve en el valor de la propiedad theme de la entrada cuando ejecutas el mensaje. Para pasar la entrada a la instrucción, llámala como en el siguiente ejemplo:

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

Ten en cuenta que, como el esquema de entrada declaró que la propiedad theme es opcional y proporcionó un valor predeterminado, podrías haber omitido la propiedad, y la instrucción se habría resuelto con el valor predeterminado.

Las plantillas de Handlebars también admiten algunas construcciones lógicas limitadas. Por ejemplo, como alternativa a proporcionar un valor predeterminado, puedes definir la instrucción con el ayudante #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.

En este ejemplo, la instrucción se renderiza como "Inventa un elemento de menú para un restaurante" cuando no se especifica la propiedad theme.

Consulta la documentación de Handlebars para obtener información sobre todos los ayudantes lógicos integrados.

Además de las propiedades definidas por tu esquema de entrada, tus plantillas también pueden hacer referencia a valores definidos automáticamente por Genkit. En las siguientes secciones, se describen estos valores definidos automáticamente y cómo puedes usarlos.

Instrucciones de varios mensajes

De forma predeterminada, Dotprompt construye un solo mensaje con un rol de "usuario". Sin embargo, algunas instrucciones se expresan mejor como una combinación de varios mensajes, como una instrucción del sistema.

El ayudante {{role}} proporciona una forma sencilla de crear instrucciones de varios mensajes:

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

Instrucciones multimodales

Para los modelos que admiten entradas multimodales, como imágenes junto con texto, puedes usar el ayudante {{media}}:

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

{{media url=photoUrl}}

La URL puede ser URI de https: o data: codificado en base64 para el uso de imagen "intercalada". En el código, sería así:

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

Consulta también Entrada multimodal, en la página Modelos, para ver un ejemplo de cómo crear una URL de data:.

Partes

Los parciales son plantillas reutilizables que se pueden incluir en cualquier instrucción. Los parciales pueden ser especialmente útiles para las instrucciones relacionadas que comparten un comportamiento común.

Cuando se carga un directorio de instrucciones, cualquier archivo con un prefijo de guion bajo (_) se considera parcial. Por lo tanto, un archivo _personality.prompt puede contener lo siguiente:

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

Esto se puede incluir en otras instrucciones:

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

Los parciales se insertan con la sintaxis {{>NAME_OF_PARTIAL args...}}. Si no se proporcionan argumentos a la parte, se ejecuta con el mismo contexto que la instrucción superior.

Los parciales aceptan argumentos con nombre como los anteriores o un solo argumento posicional que representa el contexto. Esto puede ser útil para tareas como renderizar miembros de una lista.

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

Cómo definir parciales en el código

También puedes definir parciales en el código con definePartial:

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

Los parciales definidos por código están disponibles en todas las instrucciones.

Cómo definir ayudantes personalizados

Puedes definir ayudantes personalizados para procesar y administrar datos dentro de una instrucción. Los ayudantes se registran de forma global con defineHelper:

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

Una vez que se define un ayudante, puedes usarlo en cualquier instrucción:

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

HELLO, {{shout name}}!!!

Variantes de instrucciones

Como los archivos de instrucciones son solo texto, puedes (y debes) confirmarlos en tu sistema de control de versiones, lo que te permite comparar fácilmente los cambios a lo largo del tiempo. A menudo, las versiones modificadas de instrucciones solo se pueden probar completamente en un entorno de producción en paralelo con versiones existentes. Dotprompt admite esto a través de la función de variantes.

Para crear una variante, crea un archivo [name].[variant].prompt. Por ejemplo, si estabas usando Gemini 1.5 Flash en tu instrucción, pero querías ver si Gemini 1.5 Pro tendría un mejor rendimiento, podrías crear dos archivos:

  • my_prompt.prompt: La instrucción de "modelo de referencia"
  • my_prompt.gemini15pro.prompt: Una variante llamada gemini15pro

Para usar una variante de instrucción, especifica la opción de variante cuando realices la carga:

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

El nombre de la variante se incluye en los metadatos de los seguimientos de generación, por lo que puedes comparar y contrastar el rendimiento real entre las variantes en el seguimiento de Genkit con el inspector de registros.

Cómo definir instrucciones en el código

En todos los ejemplos que se analizaron hasta ahora, se asumió que tus instrucciones se definen en archivos .prompt individuales en un solo directorio (o subdirectorios de este), al que tu app puede acceder durante el tiempo de ejecución. Dotprompt se diseñó en torno a esta configuración y sus autores consideran que es la mejor experiencia para desarrolladores en general.

Sin embargo, si tienes casos de uso que no son compatibles con esta configuración, también puedes definir instrucciones en el código con la función definePrompt():

El primer parámetro de esta función es análogo al bloque de material preliminar de un archivo .prompt. El segundo parámetro puede ser una cadena de plantilla de Handlebars, como en un archivo de instrucciones, o una función que muestra 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?` }],
        },
      ],
    };
  }
);