ストーリー
プロンプトのバージョン管理
プロンプトをコードとして管理
// プロンプトの定義
interface PromptTemplate {
id: string;
version: string;
name: string;
description: string;
systemPrompt: string;
userPromptTemplate: string;
variables: string[]; // テンプレート変数
model: string; // 対象モデル
temperature: number;
maxTokens: number;
createdAt: string;
tags: string[];
}
// ファイルベースのプロンプト管理
// prompts/
// ├── rag-answer/
// │ ├── v1.0.0.yaml
// │ ├── v1.1.0.yaml
// │ └── v2.0.0.yaml
// ├── query-classifier/
// │ ├── v1.0.0.yaml
// │ └── v1.1.0.yaml
// └── metadata-extractor/
// └── v1.0.0.yaml
YAMLベースのプロンプト定義
# prompts/rag-answer/v2.0.0.yaml
id: rag-answer
version: "2.0.0"
name: "RAG回答生成"
description: "検索コンテキストに基づいて質問に回答する"
model: gpt-4o-mini
temperature: 0
maxTokens: 1000
systemPrompt: |
あなたは社内技術ナレッジベースのアシスタントです。
## ルール
1. <context>タグ内の情報のみを根拠にして回答する
2. 情報が不足している場合は「該当する情報が見つかりませんでした」と回答する
3. 回答には必ず [出典N] を付与する
4. 推測や一般知識での補完は行わない
## 出力形式
- Markdown形式
- 簡潔で構造化された回答
userPromptTemplate: |
<context>
{{contexts}}
</context>
質問: {{query}}
variables:
- contexts
- query
tags:
- rag
- production
プロンプトレジストリ
import * as fs from 'fs';
import * as yaml from 'yaml';
import * as path from 'path';
class PromptRegistry {
private prompts: Map<string, Map<string, PromptTemplate>> = new Map();
constructor(private readonly promptsDir: string) {}
async loadAll(): Promise<void> {
const promptDirs = fs.readdirSync(this.promptsDir);
for (const dir of promptDirs) {
const dirPath = path.join(this.promptsDir, dir);
if (!fs.statSync(dirPath).isDirectory()) continue;
const versions = new Map<string, PromptTemplate>();
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.yaml'));
for (const file of files) {
const content = fs.readFileSync(path.join(dirPath, file), 'utf-8');
const template = yaml.parse(content) as PromptTemplate;
versions.set(template.version, template);
}
this.prompts.set(dir, versions);
}
}
get(id: string, version?: string): PromptTemplate {
const versions = this.prompts.get(id);
if (!versions) throw new Error(`Prompt not found: ${id}`);
if (version) {
const template = versions.get(version);
if (!template) throw new Error(`Version not found: ${id}@${version}`);
return template;
}
// 最新バージョンを返す
const latest = [...versions.entries()]
.sort(([a], [b]) => b.localeCompare(a, undefined, { numeric: true }))
[0];
return latest[1];
}
render(id: string, variables: Record<string, string>, version?: string): {
systemPrompt: string;
userPrompt: string;
} {
const template = this.get(id, version);
let userPrompt = template.userPromptTemplate;
for (const [key, value] of Object.entries(variables)) {
userPrompt = userPrompt.replace(new RegExp(`{{${key}}}`, 'g'), value);
}
return {
systemPrompt: template.systemPrompt,
userPrompt,
};
}
}
A/Bテスト
プロンプトのA/Bテスト設計
interface PromptExperiment {
id: string;
name: string;
description: string;
startDate: Date;
endDate: Date;
variants: {
name: string;
promptId: string;
promptVersion: string;
weight: number; // トラフィックの割合 (0-1)
}[];
metrics: string[]; // 計測するメトリクス
}
class PromptExperimentManager {
constructor(
private readonly registry: PromptRegistry,
private readonly metricsStore: MetricsStore,
) {}
async getVariant(
experimentId: string,
userId: string,
): Promise<{ promptId: string; promptVersion: string; variantName: string }> {
const experiment = await this.loadExperiment(experimentId);
// ユーザーIDベースの決定論的な振り分け
const hash = this.hashString(`${experimentId}:${userId}`);
const bucket = (hash % 100) / 100;
let cumulative = 0;
for (const variant of experiment.variants) {
cumulative += variant.weight;
if (bucket < cumulative) {
return {
promptId: variant.promptId,
promptVersion: variant.promptVersion,
variantName: variant.name,
};
}
}
// フォールバック
const last = experiment.variants[experiment.variants.length - 1];
return {
promptId: last.promptId,
promptVersion: last.promptVersion,
variantName: last.name,
};
}
async recordResult(
experimentId: string,
variantName: string,
metrics: Record<string, number>,
): Promise<void> {
await this.metricsStore.record({
experimentId,
variantName,
metrics,
timestamp: new Date(),
});
}
private hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
private async loadExperiment(id: string): Promise<PromptExperiment> {
// データストアから実験設定を読み込む
throw new Error('Not implemented');
}
}
プロンプトの評価とCI/CD
自動評価パイプライン
interface PromptEvalCase {
input: Record<string, string>; // テンプレート変数
expectedOutput?: string; // 期待される出力(完全一致ではなく意味的な比較)
criteria: {
containsKeywords?: string[]; // 含むべきキーワード
excludesKeywords?: string[]; // 含むべきでないキーワード
maxLength?: number;
minLength?: number;
formatCheck?: 'json' | 'markdown';
customCheck?: (output: string) => boolean;
};
}
class PromptEvaluator {
constructor(
private readonly registry: PromptRegistry,
private readonly llm: LLMService,
) {}
async evaluate(
promptId: string,
version: string,
testCases: PromptEvalCase[],
): Promise<{
passRate: number;
results: { case: PromptEvalCase; output: string; passed: boolean; reason?: string }[];
}> {
const results: { case: PromptEvalCase; output: string; passed: boolean; reason?: string }[] = [];
for (const testCase of testCases) {
const { systemPrompt, userPrompt } = this.registry.render(
promptId,
testCase.input,
version,
);
const output = await this.llm.complete(systemPrompt + '\n\n' + userPrompt);
const { passed, reason } = this.checkCriteria(output, testCase.criteria);
results.push({ case: testCase, output, passed, reason });
}
const passRate = results.filter(r => r.passed).length / results.length;
return { passRate, results };
}
private checkCriteria(
output: string,
criteria: PromptEvalCase['criteria'],
): { passed: boolean; reason?: string } {
if (criteria.containsKeywords) {
const missing = criteria.containsKeywords.filter(kw => !output.includes(kw));
if (missing.length > 0) {
return { passed: false, reason: `Missing keywords: ${missing.join(', ')}` };
}
}
if (criteria.excludesKeywords) {
const found = criteria.excludesKeywords.filter(kw => output.includes(kw));
if (found.length > 0) {
return { passed: false, reason: `Found excluded keywords: ${found.join(', ')}` };
}
}
if (criteria.maxLength && output.length > criteria.maxLength) {
return { passed: false, reason: `Output too long: ${output.length} > ${criteria.maxLength}` };
}
if (criteria.formatCheck === 'json') {
try { JSON.parse(output); } catch {
return { passed: false, reason: 'Invalid JSON output' };
}
}
if (criteria.customCheck && !criteria.customCheck(output)) {
return { passed: false, reason: 'Custom check failed' };
}
return { passed: true };
}
}
CI/CDパイプライン
# .github/workflows/prompt-ci.yml
name: Prompt Quality Gate
on:
pull_request:
paths:
- 'prompts/**'
jobs:
prompt-eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Detect changed prompts
id: changes
run: |
CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }} | grep "^prompts/" | cut -d'/' -f2 | sort -u)
echo "prompts=$CHANGED" >> $GITHUB_OUTPUT
- name: Run prompt evaluation
run: |
for prompt in ${{ steps.changes.outputs.prompts }}; do
npm run prompt:eval -- --prompt "$prompt" --threshold 0.90
done
- name: Compare with baseline
run: npm run prompt:compare -- --baseline main
- name: Post results to PR
if: always()
run: npm run prompt:report >> $GITHUB_STEP_SUMMARY
プロンプトの回帰テストセット例
// tests/prompts/rag-answer.test.ts
const ragAnswerTestCases: PromptEvalCase[] = [
{
input: {
contexts: '[出典1] PostgreSQLのデフォルトポートは5432です。',
query: 'PostgreSQLのデフォルトポートは?',
},
criteria: {
containsKeywords: ['5432', '出典1'],
maxLength: 500,
},
},
{
input: {
contexts: '[出典1] Kubernetesのデプロイ手順については記載なし',
query: 'Terraformの使い方を教えて',
},
criteria: {
containsKeywords: ['見つかりませんでした'],
excludesKeywords: ['Terraform'], // コンテキストにない情報で回答してはいけない
},
},
{
input: {
contexts: '[出典1] JWTの有効期限は通常15分-1時間。\n[出典2] リフレッシュトークンは7-30日。',
query: 'トークンの有効期限の設計方針は?',
},
criteria: {
containsKeywords: ['出典'],
minLength: 50,
},
},
];
まとめ
| ポイント | 内容 |
|---|---|
| バージョン管理 | YAMLファイルでプロンプトを定義し、Gitで管理 |
| レジストリ | プロンプトの読み込み、バージョン解決、テンプレートレンダリング |
| A/Bテスト | ユーザーIDベースの振り分けでプロンプト変更の効果を測定 |
| CI/CD | PRでプロンプト変更時に自動評価を実行し、品質ゲートで保護 |
チェックリスト
- プロンプトのバージョン管理パターンを理解した
- プロンプトレジストリの実装を理解した
- A/Bテストの設計パターンを理解した
- CI/CDへの組み込み方を理解した
次のステップへ
プロンプト管理のベストプラクティスを学びました。次のセクションでは、AIシステムの安全性を確保するガードレールとセーフティについて学びます。
プロンプトはコードと同じ。バージョン管理、テスト、レビュー、CI/CDの全てが必要。
推定読了時間: 40分