エージェント型開発を探求する -

言語モデルプロンプトの作成

文字列連結を使用して言語モデルのプロンプトを作成することも可能ですが、機能の構成や、プロンプトが言語モデルのコンテキストウィンドウ内に収まるように維持するのは困難です。これらの制限を克服するために、@vscode/prompt-tsx ライブラリを使用できます。

@vscode/prompt-tsx ライブラリは、以下の機能を提供します。

  • TSXベースのプロンプトレンダリング: TSXコンポーネントを使用してプロンプトを構成し、可読性と保守性を向上させます。
  • 優先度ベースのプルーニング(枝刈り): モデルのコンテキストウィンドウに収まるよう、重要度の低いプロンプト部分を自動的に削除します。
  • 柔軟なトークン管理: flexGrowflexReserveflexBasis などのプロパティを使用して、トークン予算を協調的に利用します。
  • ツール統合: VS Codeの言語モデルツールAPIと統合します。

すべての機能の概要および詳細な使用方法については、完全なREADMEを参照してください。

この記事では、このライブラリを用いたプロンプト設計の実践的な例を説明します。これらの例の完全なコードは、prompt-tsxリポジトリで確認できます。

会話履歴の優先順位を管理する

ユーザーが以前のメッセージに対してフォローアップの質問を行えるようにするため、会話履歴をプロンプトに含めることは重要です。しかし、履歴は時間の経過とともに大きくなる可能性があるため、優先順位が適切に扱われるようにする必要があります。最も理にかなったパターンは、通常、以下の順序で優先順位を付けることです。

  1. 基本的なプロンプトの指示
  2. 現在のユーザーのクエリ
  3. 直近の数ターンのチャット履歴
  4. その他の補助データ
  5. 残りの履歴のうち、収まる限りのもの

このため、履歴をプロンプト内で2つの部分に分割し、最近のやり取りが一般的なコンテキスト情報よりも優先されるようにします。

このライブラリでは、ツリー内の各TSXノードは、概念的にCSSのzIndexに似た優先度を持っており、数値が高いほど優先度が高くなります。

ステップ 1: HistoryMessagesコンポーネントを定義する

履歴メッセージをリスト表示するために、HistoryMessagesコンポーネントを定義します。この例は優れた出発点となりますが、より複雑なデータ型を扱う場合は拡張が必要になるかもしれません。

この例では、子要素に対して自動的に昇順または降順の優先度を割り当てる PrioritizedList ヘルパーコンポーネントを使用しています。

import {
	UserMessage,
	AssistantMessage,
	PromptElement,
	BasePromptElementProps,
	PrioritizedList,
} from '@vscode/prompt-tsx';
import { ChatContext, ChatRequestTurn, ChatResponseTurn, ChatResponseMarkdownPart } from 'vscode';

interface IHistoryMessagesProps extends BasePromptElementProps {
	history: ChatContext['history'];
}

export class HistoryMessages extends PromptElement<IHistoryMessagesProps> {
	render(): PromptPiece {
		const history: (UserMessage | AssistantMessage)[] = [];
		for (const turn of this.props.history) {
			if (turn instanceof ChatRequestTurn) {
				history.push(<UserMessage>{turn.prompt}</UserMessage>);
			} else if (turn instanceof ChatResponseTurn) {
				history.push(
					<AssistantMessage name={turn.participant}>
						{chatResponseToMarkdown(turn)}
					</AssistantMessage>
				);
			}
		}
		return (
			<PrioritizedList priority={0} descending={false}>
				{history}
			</PrioritizedList>
		);
	}
}

ステップ 2: Promptコンポーネントを定義する

次に、基本的な指示、ユーザーのクエリ、および適切な優先度を持つ履歴メッセージを含む MyPrompt コンポーネントを定義します。優先度の値は兄弟要素間でのみ有効です。プロンプト内の他の要素に触れる前に、履歴内の古いメッセージをトリミングする必要がある可能性があるため、2つの <HistoryMessages> 要素に分割する必要があることに注意してください。

import {
	UserMessage,
	PromptElement,
	BasePromptElementProps,
} from '@vscode/prompt-tsx';

interface IMyPromptProps extends BasePromptElementProps {
	history: ChatContext['history'];
	userQuery: string;
}

export class MyPrompt extends PromptElement<IMyPromptProps> {
	render() {
		return (
			<>
				<UserMessage priority={100}>
					Here are your base instructions. They have the highest priority because you want to make
					sure they're always included!
				</UserMessage>
				{/* Older messages in the history have the lowest priority since they're less relevant */}
				<HistoryMessages history={this.props.history.slice(0, -2)} priority={0} />
				{/* The last 2 history messages are preferred over any workspace context you have below */}
				<HistoryMessages history={this.props.history.slice(-2)} priority={80} />
				{/* The user query is right behind the based instructions in priority */}
				<UserMessage priority={90}>{this.props.userQuery}</UserMessage>
				<UserMessage priority={70}>
					With a slightly lower priority, you can include some contextual data about the workspace
					or files here...
				</UserMessage>
			</>
		);
	}
}

これで、ライブラリがプロンプトの他の要素をプルーニングしようとする前に、古い履歴メッセージがすべてプルーニングされるようになります。

ステップ 3: Historyコンポーネントを定義する

利用を少し容易にするために、履歴メッセージをラップし、passPriority 属性を使用してパススルーコンテナとして機能する History コンポーネントを定義します。passPriority を使用すると、その子要素は優先順位付けの目的において、親要素の直接の子であるかのように扱われます。

import { PromptElement, BasePromptElementProps } from '@vscode/prompt-tsx';

interface IHistoryProps extends BasePromptElementProps {
	history: ChatContext['history'];
	newer: number; // last 2 message priority values
	older: number; // previous message priority values
	passPriority: true; // require this prop be set!
}

export class History extends PromptElement<IHistoryProps> {
	render(): PromptPiece {
		return (
			<>
				<HistoryMessages history={this.props.history.slice(0, -2)} priority={this.props.older} />
				<HistoryMessages history={this.props.history.slice(-2)} priority={this.props.newer} />
			</>
		);
	}
}

これで、この単一の要素を使用して、チャット履歴を再利用できるようになります。

<History history={this.props.history} passPriority older={0} newer={80}/>

ファイルコンテンツを収まるように拡張する

この例では、ユーザーが現在参照しているすべてのファイルの内容をプロンプトに含めたいとします。これらのファイルは非常に大きく、すべて含めるとテキストがプルーニングされてしまう可能性があります!この例では、flexGrow プロパティを使用して、トークン予算内に収まるようにファイルの内容を協調的にサイズ調整する方法を示します。

ステップ 1: 基本的な指示とユーザーのクエリを定義する

まず、基本的な指示を含む UserMessage コンポーネントを定義します。

<UserMessage priority={100}>Here are your base instructions.</UserMessage>

次に、UserMessage コンポーネントを使用してユーザーのクエリを含めます。このコンポーネントは、基本的な指示の直後に含まれることを保証するために高い優先度を持っています。

<UserMessage priority={90}>{this.props.userQuery}</UserMessage>

ステップ 2: ファイルの内容を含める

これで、FileContext コンポーネントを使用してファイルの内容を含めることができます。基本的な指示、ユーザーのクエリ、履歴の後にレンダリングされるように、flexGrow 値に 1 を割り当てます。

<FileContext priority={70} flexGrow={1} files={this.props.files} />

flexGrow 値を使用すると、その要素は render()prepare() 呼び出しに渡される PromptSizing オブジェクト内の、未使用のトークン予算を取得します。flex要素の動作については、prompt-tsxのドキュメントで詳しく確認できます。

ステップ 3: 履歴を含める

次に、以前に作成した History コンポーネントを使用して履歴メッセージを含めます。履歴はある程度表示したい一方で、ファイルの内容にプロンプトの大部分を占めてほしいという状況であるため、これは少し工夫が必要です。

したがって、History コンポーネントに flexGrow2 を割り当て、<FileContext /> を含む他のすべての要素の後にレンダリングされるようにします。ただし、flexReserve 値に "/5" を設定し、全予算の5分の1を履歴のために確保するようにします。

<History
	history={this.props.history}
	passPriority
	older={0}
	newer={80}
	flexGrow={2}
	flexReserve="/5"
/>

ステップ 3: プロンプトの全要素を結合する

それでは、すべての要素を MyPrompt コンポーネントに結合します。

import {
	UserMessage,
	PromptElement,
	BasePromptElementProps,
} from '@vscode/prompt-tsx';
import { History } from './history';

interface IFilesToInclude {
	document: TextDocument;
	line: number;
}

interface IMyPromptProps extends BasePromptElementProps {
	history: ChatContext['history'];
	userQuery: string;
	files: IFilesToInclude[];
}

export class MyPrompt extends PromptElement<IMyPromptProps> {
	render() {
		return (
			<>
				<UserMessage priority={100}>Here are your base instructions.</UserMessage>
				<History
					history={this.props.history}
					passPriority
					older={0}
					newer={80}
					flexGrow={2}
					flexReserve="/5"
				/>
				<UserMessage priority={90}>{this.props.userQuery}</UserMessage>
				<FileContext priority={70} flexGrow={1} files={this.props.files} />
			</>
		);
	}
}

ステップ 4: FileContextコンポーネントを定義する

最後に、ユーザーが現在参照しているファイルの内容を含む FileContext コンポーネントを定義します。flexGrow を使用しているため、PromptSizing 内の情報を使用して、各ファイルの「興味深い」行周辺の可能な限り多くの行を取得するロジックを実装できます。

簡潔にするため、getExpandedFiles の実装ロジックは省略されています。詳しくは prompt-tsxリポジトリを確認してください。

import { PromptElement, BasePromptElementProps, PromptSizing, PromptPiece } from '@vscode/prompt-tsx';

class FileContext extends PromptElement<{ files: IFilesToInclude[] } & BasePromptElementProps> {
	async render(_state: void, sizing: PromptSizing): Promise<PromptPiece> {
		const files = await this.getExpandedFiles(sizing);
		return <>{files.map(f => f.toString())}</>;
	}

	private async getExpandedFiles(sizing: PromptSizing) {
		// Implementation details are summarized here.
		// Refer to the repo for the complete implementation.
	}
}

概要

これらの例では、基本的な指示、ユーザーのクエリ、履歴メッセージ、およびさまざまな優先順位を持つファイルの内容を含む MyPrompt コンポーネントを作成しました。また、flexGrow を使用して、トークン予算内に収まるようにファイルコンテンツを協調的にサイズ調整しました。

このパターンに従うことで、プロンプトの最も重要な部分が常に含まれ、重要度の低い部分は必要に応じてプルーニングされ、モデルのコンテキストウィンドウ内に収まるようになります。getExpandedFiles メソッドと FileContextTracker クラスの実装の詳細については、prompt-tsxリポジトリを参照してください。

© . This site is unofficial and not affiliated with Microsoft.