Dotprompt로 프롬프트 관리

프롬프트 엔지니어링은 앱 개발자가 생성형 AI 모델의 출력에 영향을 주는 기본적인 방법입니다. 예를 들어 LLM을 사용할 때는 모델 응답의 어조, 형식, 길이 및 기타 특성에 영향을 주는 프롬프트를 만들 수 있습니다.

프롬프트를 작성하는 방법은 사용하는 모델에 따라 다릅니다. 한 모델에 작성된 프롬프트가 다른 모델과 함께 사용될 때는 성능이 좋지 않을 수 있습니다. 마찬가지로 설정하는 모델 매개변수 (강도, 최상위 k 등)도 모델에 따라 출력에 다르게 영향을 미칩니다.

모델, 모델 매개변수, 프롬프트라는 세 가지 요소를 모두 함께 사용하여 원하는 출력을 생성하는 것은 간단한 절차가 아닐 수 있으며 상당한 반복과 실험이 필요할 수 있습니다. Genkit은 이러한 반복을 더 빠르고 편리하게 하는 것을 목표로 하는 Dotprompt라는 라이브러리와 파일 형식을 제공합니다.

Dotprompt는 프롬프트가 코드라는 전제 하에 설계되었습니다. 프롬프트는 애플리케이션 코드와 별도로 모델 및 모델 매개변수와 함께 정의됩니다. 그런 다음 개발자 (또는 애플리케이션 코드 작성과 관련이 없는 사람)가 Genkit 개발자 UI를 사용하여 프롬프트와 모델 매개변수를 빠르게 반복할 수 있습니다. 프롬프트가 원하는 방식으로 작동하면 애플리케이션으로 가져와 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}}.

대시 3개로 표시된 부분은 GitHub 마크다운 및 Jekyll에서 사용하는 전면 자료 형식과 유사한 YAML 전면 자료입니다. 파일의 나머지 부분은 프롬프트이며 원하는 경우 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 파일을 만드는 방법에는 두 가지가 있습니다. 텍스트 편집기를 사용하거나 개발자 UI를 사용하는 것입니다.

텍스트 편집기 사용하기

텍스트 편집기를 사용하여 프롬프트 파일을 만들려면 프롬프트 디렉터리에 .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.

대시로 표시된 부분은 GitHub 마크다운 및 Jekyll에서 사용하는 전면 자료 형식과 유사한 YAML 전면 자료입니다. 파일의 나머지 부분은 프롬프트이며 원하는 경우 Handlebars 템플릿을 사용할 수 있습니다. 앞부분 섹션은 선택사항이지만 대부분의 프롬프트 파일에는 최소한 모델을 지정하는 메타데이터가 포함됩니다. 이 페이지의 나머지 부분에서는 이보다 더 나아가 프롬프트 파일에서 Dotprompt의 기능을 사용하는 방법을 보여줍니다.

개발자 UI 사용

개발자 UI의 모델 러너를 사용하여 프롬프트 파일을 만들 수도 있습니다. 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=...
    }),
  ],
});

파일에 다른 코드가 포함되어 있어도 괜찮지만 위의 코드만 있으면 됩니다.

동일한 프로젝트에서 개발자 UI를 로드합니다.

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

'모델' 섹션에서 플러그인이 제공하는 모델 목록에서 사용할 모델을 선택합니다.

Genkit 개발자 UI 모델 실행기

그런 다음 만족스러운 결과를 얻을 때까지 프롬프트와 구성을 실험합니다. 준비가 되면 내보내기 버튼을 누르고 파일을 프롬프트 디렉터리에 저장합니다.

프롬프트 실행

프롬프트 파일을 만든 후 애플리케이션 코드에서 실행하거나 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 모델로 콘텐츠 생성을 참고하세요.

개발자 UI 사용

앱의 프롬프트를 수정할 때 Genkit 개발자 UI에서 프롬프트를 실행하여 애플리케이션 코드와는 별개로 프롬프트 및 모델 구성을 빠르게 반복할 수 있습니다.

프로젝트 디렉터리에서 개발자 UI를 로드합니다.

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

Genkit 개발자 UI 프롬프트 실행기

프롬프트를 개발자 UI에 로드한 후에는 다양한 입력 값으로 프롬프트를 실행하고 프롬프트 문구 또는 구성 매개변수 변경사항이 모델 출력에 어떤 영향을 미치는지 실험할 수 있습니다. 결과가 만족스러우면 프롬프트 내보내기 버튼을 클릭하여 수정된 프롬프트를 프로젝트 디렉터리에 다시 저장할 수 있습니다.

모델 구성

프롬프트 파일의 앞부분 블록에서 원하는 경우 프롬프트의 모델 구성 값을 지정할 수 있습니다.

---
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는 LLM 사용을 위해 스키마의 가장 중요한 속성을 쉽게 정의할 수 있는 간결한 YAML 최적화 스키마 정의 형식입니다. 다음은 앱이 기사에 관해 저장할 수 있는 정보를 지정하는 스키마의 더 긴 예입니다.

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는 스칼라 유형 string, integer, number, boolean, any를 지원합니다. 객체, 배열, enum은 필드 이름 뒤에 괄호로 표시됩니다.

Picoschema로 정의된 객체에는 ?에 의해 선택사항으로 표시되지 않는 한 필수 속성이 모두 포함되며 추가 속성은 허용되지 않습니다. 속성이 선택사항으로 표시되면 LLM이 필드를 생략하는 대신 null을 반환하도록 편의를 제공하기 위해 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>"
---

이 예에서 핸들바 표현식 {{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는 'user' 역할을 가진 단일 메시지를 구성합니다. 하지만 일부 프롬프트는 시스템 프롬프트와 같이 여러 메시지를 조합하여 가장 잘 표현됩니다.

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

URL은 '인라인' 이미지 사용을 위한 https: 또는 base64로 인코딩된 data: URI일 수 있습니다. 코드에서 다음과 같습니다.

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

data: URL 구성의 예는 모델 페이지의 멀티모달 입력을 참고하세요.

부분

부분은 모든 프롬프트 내에 포함할 수 있는 재사용 가능한 템플릿입니다. 부분은 공통 동작을 공유하는 관련 프롬프트에 특히 유용합니다.

프롬프트 디렉터리를 로드할 때 접두사로 밑줄 (_)이 있는 파일은 부분으로 간주됩니다. 따라서 _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' });

변형 이름은 생성 trace의 메타데이터에 포함되므로 Genkit trace 검사기에서 변형 간의 실제 성능을 비교하고 대조할 수 있습니다.

코드에서 프롬프트 정의

지금까지 논의한 모든 예에서는 프롬프트가 단일 디렉터리 (또는 하위 디렉터리)의 개별 .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?` }],
        },
      ],
    };
  }
);