ストーリー
演習の概要
4つのミッションに取り組み、本番品質のプロンプトエンジニアリングスキルを磨きます。
| ミッション | テーマ | 難易度 |
|---|---|---|
| Mission 1 | プロンプトテンプレートの設計 | 中級 |
| Mission 2 | Zodによる構造化出力バリデーション | 中級 |
| Mission 3 | プロンプトバージョニングシステム | 上級 |
| Mission 4 | ガードレール統合パイプライン | 上級 |
Mission 1: プロンプトテンプレートの設計
要件
以下の3つのユースケースに対応するプロンプトテンプレートを設計してください。
ユースケース:
1. 技術QA: ナレッジベースから検索した情報に基づく質問応答
2. インシデント分析: 過去のインシデントレポートを基にした根本原因分析
3. コードレビュー: コードスニペットに対する改善提案
共通要件:
- システムプロンプトとユーザープロンプトを分離
- デリミターで入力を明確に区切る
- 出典の明示を必須にする
- 日本語で回答
以下を設計してください:
- 各ユースケースのシステムプロンプト
- ユーザープロンプトのテンプレート
- テンプレートの変数定義(TypeScript型)
解答例
1. テンプレート変数の型定義
interface PromptVariables {
common: {
contexts: { id: number; title: string; content: string }[];
query: string;
language: 'ja' | 'en';
maxTokens?: number;
};
techQA: PromptVariables['common'];
incidentAnalysis: PromptVariables['common'] & {
severity: 'P1' | 'P2' | 'P3' | 'P4';
timeRange?: string;
};
codeReview: PromptVariables['common'] & {
code: string;
language: string;
framework?: string;
};
}
2. 技術QAプロンプト
function buildTechQAPrompt(vars: PromptVariables['techQA']): ChatMessage[] {
return [
{
role: 'system',
content: `あなたは社内技術ナレッジベースのアシスタントです。
## 役割
- 提供されたコンテキストに基づいて、技術的な質問に正確に回答する
- 回答は実践的で、すぐに活用できる内容にする
## 制約
- <context>タグ内の情報のみを根拠にする
- コンテキストに情報がない場合は「該当する情報が見つかりませんでした」と回答する
- 推測や一般知識での補完は行わない
- 回答には必ず [出典N] 形式で出典を含める
## 出力形式
- Markdown形式(見出し・箇条書き・コードブロックを活用)
- 簡潔かつ構造化された回答
${vars.maxTokens ? `- ${vars.maxTokens}トークン以内` : ''}`,
},
{
role: 'user',
content: `<context>
${vars.contexts.map(c => `<source id="${c.id}" title="${c.title}">\n${c.content}\n</source>`).join('\n\n')}
</context>
<query>${vars.query}</query>
上記のcontextの情報のみを使って、queryに回答してください。`,
},
];
}
3. インシデント分析プロンプト
function buildIncidentAnalysisPrompt(
vars: PromptVariables['incidentAnalysis'],
): ChatMessage[] {
return [
{
role: 'system',
content: `あなたはSREチームのインシデント分析アシスタントです。
## 役割
- 過去のインシデントレポートを分析し、根本原因と再発防止策を導き出す
- 類似インシデントのパターンを特定する
## 分析フレームワーク
1. タイムライン整理: 何が、いつ、どの順序で発生したか
2. 根本原因分析: 5 Whys の手法で深掘り
3. 影響範囲: どのサービス・ユーザーに影響があったか
4. 再発防止策: 短期(Quick Win)と長期(根本対策)を分離
## 制約
- 提供されたインシデントレポートの事実のみに基づく
- 推測は「推測:」と明示する
- 出典を [出典N] 形式で含める`,
},
{
role: 'user',
content: `<context>
${vars.contexts.map(c => `<source id="${c.id}" title="${c.title}">\n${c.content}\n</source>`).join('\n\n')}
</context>
<query>${vars.query}</query>
<severity>${vars.severity}</severity>
${vars.timeRange ? `<time_range>${vars.timeRange}</time_range>` : ''}
上記のインシデントレポートを分析し、queryに回答してください。`,
},
];
}
4. コードレビュープロンプト
function buildCodeReviewPrompt(
vars: PromptVariables['codeReview'],
): ChatMessage[] {
return [
{
role: 'system',
content: `あなたはシニアソフトウェアエンジニアとして、コードレビューを行います。
## レビュー観点
1. **バグリスク**: 潜在的なバグやエッジケース
2. **パフォーマンス**: N+1問題、不要な再計算、メモリリーク
3. **セキュリティ**: インジェクション、認証・認可の漏れ
4. **可読性**: 命名規則、関数の責務分離
5. **テスタビリティ**: テストしやすい構造か
## 出力形式
各指摘を以下の形式で報告:
- 🔴 Critical: 即座に修正が必要
- 🟡 Warning: 修正を推奨
- 🔵 Info: 改善提案
## 制約
- 具体的な修正コードを提示する
- 参考となるベストプラクティスがcontextにあれば [出典N] で引用する`,
},
{
role: 'user',
content: `<context>
${vars.contexts.map(c => `<source id="${c.id}" title="${c.title}">\n${c.content}\n</source>`).join('\n\n')}
</context>
<code language="${vars.language}"${vars.framework ? ` framework="${vars.framework}"` : ''}>
${vars.code}
</code>
<query>${vars.query}</query>
上記のコードをレビューし、queryに回答してください。`,
},
];
}
Mission 2: Zodによる構造化出力バリデーション
要件
LLMからの出力をZodスキーマでバリデーションし、型安全に扱う仕組みを実装してください。
要件:
- 技術QAの回答を構造化データとして取得
- Zodスキーマでバリデーション
- パースエラー時のリトライロジック
- 型安全なレスポンス型の生成
以下を実装してください:
- 回答のZodスキーマ定義
- パース + バリデーション + リトライのパイプライン
- エラーハンドリング
解答例
import { z } from 'zod';
// 1. Zodスキーマ定義
const TechQAResponseSchema = z.object({
answer: z.string().min(1).describe('質問に対する回答'),
confidence: z.enum(['high', 'medium', 'low']).describe('回答の確信度'),
sources: z.array(z.object({
id: z.number().describe('出典番号'),
relevance: z.enum(['high', 'medium', 'low']).describe('関連度'),
})).min(1).describe('使用した出典'),
relatedTopics: z.array(z.string()).optional().describe('関連トピック'),
caveats: z.string().optional().describe('注意事項や制限'),
});
type TechQAResponse = z.infer<typeof TechQAResponseSchema>;
// 2. 構造化出力パイプライン
class StructuredOutputPipeline<T extends z.ZodType> {
constructor(
private readonly llm: LLMService,
private readonly schema: T,
private readonly maxRetries: number = 2,
) {}
async execute(
messages: ChatMessage[],
): Promise<{
data: z.infer<T>;
raw: string;
retries: number;
}> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const raw = await this.llm.complete(messages, {
responseFormat: { type: 'json_object' },
temperature: 0,
});
// JSONパース
const parsed = JSON.parse(raw);
// Zodバリデーション
const validated = this.schema.parse(parsed);
return { data: validated, raw, retries: attempt };
} catch (error) {
lastError = error as Error;
if (attempt < this.maxRetries) {
// リトライ時はエラー情報をフィードバック
messages = [
...messages,
{
role: 'assistant',
content: (error as Error).message.includes('JSON')
? '(前回の出力はJSONパースに失敗しました)'
: `(前回の出力はバリデーションエラー: ${(error as z.ZodError).issues?.map(i => i.message).join(', ')})`,
},
{
role: 'user',
content: '正しいJSON形式で再度回答してください。全てのフィールドを含めてください。',
},
];
}
}
}
throw new Error(
`構造化出力の取得に${this.maxRetries + 1}回失敗: ${lastError?.message}`,
);
}
}
// 3. 使用例
async function techQAWithStructuredOutput(
query: string,
contexts: RetrievedContext[],
): Promise<TechQAResponse> {
const pipeline = new StructuredOutputPipeline(llm, TechQAResponseSchema);
const messages = buildTechQAPrompt({
query,
contexts: contexts.map((c, i) => ({
id: i + 1,
title: c.chunk.metadata.documentTitle,
content: c.chunk.content,
})),
language: 'ja',
});
// 出力形式の指示を追加
messages[messages.length - 1].content += `
必ず以下のJSON形式で回答してください:
${JSON.stringify(
{
answer: '回答テキスト',
confidence: 'high|medium|low',
sources: [{ id: 1, relevance: 'high' }],
relatedTopics: ['トピック1'],
caveats: '注意事項(あれば)',
},
null,
2,
)}`;
const result = await pipeline.execute(messages);
console.log(`リトライ回数: ${result.retries}`);
return result.data;
}
Mission 3: プロンプトバージョニングシステム
要件
プロンプトをコードと同様にバージョン管理し、A/Bテストやロールバックができるシステムを設計・実装してください。
要件:
- プロンプトをファイルベースで管理(YAML形式)
- セマンティックバージョニング(Major.Minor.Patch)
- ロールバック機能
- A/Bテスト用のトラフィック分割
- 変更時のテスト実行(回帰テスト)
以下を実装してください:
- プロンプト定義のYAMLスキーマ
- バージョン管理とロールバックのロジック
- A/Bテスト対応のルーティング
解答例
1. プロンプト定義(YAML)
# prompts/tech-qa/v2.1.0.yaml
metadata:
id: tech-qa
version: "2.1.0"
description: "技術QA用プロンプト - CoT追加版"
author: "team-ai"
createdAt: "2025-01-15"
changelog: "Chain of Thoughtによる推論ステップを追加"
system: |
あなたは社内技術ナレッジベースのアシスタントです。
## 役割
- 技術的な質問に正確に回答する
## 制約
- コンテキスト内の情報のみを使用
- 出典を [出典N] 形式で明示
## 推論プロセス
1. 質問の意図を分析
2. 関連するコンテキストを特定
3. 回答を構成
4. 出典を付与
user: |
<context>
{{contexts}}
</context>
<query>{{query}}</query>
上記のcontextの情報のみを使い、推論プロセスに従って回答してください。
config:
model: "gpt-4o-mini"
temperature: 0
maxTokens: 1000
responseFormat: "json"
tests:
- input:
query: "JWTの有効期限の推奨値は?"
contexts: ["JWTの有効期限は15分を推奨。リフレッシュトークンは7日間。"]
expected:
containsAny: ["15分", "リフレッシュトークン"]
confidence: "high"
- input:
query: "存在しないトピックについて"
contexts: ["PostgreSQLのインデックス戦略について"]
expected:
containsAny: ["見つかりませんでした", "情報がありません"]
2. バージョン管理ロジック
import { parse } from 'yaml';
import { readFileSync, readdirSync } from 'fs';
interface PromptVersion {
metadata: {
id: string;
version: string;
description: string;
author: string;
createdAt: string;
changelog: string;
};
system: string;
user: string;
config: {
model: string;
temperature: number;
maxTokens: number;
responseFormat?: string;
};
tests: Array<{
input: Record<string, unknown>;
expected: Record<string, unknown>;
}>;
}
class PromptVersionManager {
private versions: Map<string, PromptVersion[]> = new Map();
constructor(private readonly promptsDir: string) {
this.loadAllVersions();
}
private loadAllVersions(): void {
const promptDirs = readdirSync(this.promptsDir, { withFileTypes: true })
.filter(d => d.isDirectory());
for (const dir of promptDirs) {
const files = readdirSync(`${this.promptsDir}/${dir.name}`)
.filter(f => f.endsWith('.yaml'))
.sort();
const versions = files.map(f => {
const content = readFileSync(
`${this.promptsDir}/${dir.name}/${f}`, 'utf-8',
);
return parse(content) as PromptVersion;
});
this.versions.set(dir.name, versions);
}
}
getLatest(promptId: string): PromptVersion {
const versions = this.versions.get(promptId);
if (!versions || versions.length === 0) {
throw new Error(`Prompt not found: ${promptId}`);
}
return versions[versions.length - 1];
}
getVersion(promptId: string, version: string): PromptVersion {
const versions = this.versions.get(promptId);
const found = versions?.find(v => v.metadata.version === version);
if (!found) {
throw new Error(`Version not found: ${promptId}@${version}`);
}
return found;
}
rollback(promptId: string): PromptVersion {
const versions = this.versions.get(promptId);
if (!versions || versions.length < 2) {
throw new Error(`Cannot rollback: ${promptId} has < 2 versions`);
}
return versions[versions.length - 2];
}
}
3. A/Bテスト対応ルーティング
interface ABTestConfig {
experimentId: string;
control: { promptId: string; version: string; weight: number };
treatment: { promptId: string; version: string; weight: number };
startDate: Date;
endDate: Date;
}
class PromptRouter {
constructor(
private readonly versionManager: PromptVersionManager,
private readonly abTests: ABTestConfig[],
) {}
resolve(promptId: string, userId: string): {
prompt: PromptVersion;
variant: 'control' | 'treatment' | 'default';
experimentId?: string;
} {
const activeTest = this.abTests.find(
t => t.control.promptId === promptId
&& new Date() >= t.startDate
&& new Date() <= t.endDate,
);
if (!activeTest) {
return {
prompt: this.versionManager.getLatest(promptId),
variant: 'default',
};
}
// ユーザーIDベースの決定的な振り分け
const hash = this.hashUserId(userId);
const threshold = activeTest.control.weight
/ (activeTest.control.weight + activeTest.treatment.weight);
const variant = hash < threshold ? 'control' : 'treatment';
const config = variant === 'control'
? activeTest.control
: activeTest.treatment;
return {
prompt: this.versionManager.getVersion(config.promptId, config.version),
variant,
experimentId: activeTest.experimentId,
};
}
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = ((hash << 5) - hash) + userId.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) / 2147483647;
}
}
Mission 4: ガードレール統合パイプライン
要件
入力ガードレール、プロンプト実行、出力ガードレールを統合した完全なパイプラインを実装してください。
要件:
- 入力バリデーション(プロンプトインジェクション検出、PII検出、長さ制限)
- 構造化出力の取得とバリデーション
- 出力フィルタリング(機密情報漏洩、ハルシネーションの簡易チェック)
- 全ステップの監査ログ
- メトリクス収集(レイテンシ、ガードレール発火率)
解答例
interface PipelineMetrics {
totalLatencyMs: number;
inputGuardLatencyMs: number;
retrievalLatencyMs: number;
generationLatencyMs: number;
outputGuardLatencyMs: number;
inputGuardTriggered: boolean;
outputGuardTriggered: boolean;
retryCount: number;
cacheHit: boolean;
}
class GuardedPromptPipeline {
constructor(
private readonly inputGuard: InputGuardrail,
private readonly outputGuard: OutputGuardrail,
private readonly promptRouter: PromptRouter,
private readonly retriever: RAGRetriever,
private readonly llm: LLMService,
private readonly cache: SemanticCache,
private readonly logger: AuditLogger,
private readonly metricsCollector: MetricsCollector,
) {}
async execute(
userId: string,
query: string,
): Promise<{
response: TechQAResponse;
metrics: PipelineMetrics;
guardrailFlags: string[];
}> {
const startTime = Date.now();
const metrics: Partial<PipelineMetrics> = {};
const guardrailFlags: string[] = [];
// Step 1: 入力ガードレール
const inputStart = Date.now();
const inputResult = await this.inputGuard.validate(query);
metrics.inputGuardLatencyMs = Date.now() - inputStart;
metrics.inputGuardTriggered = !inputResult.isValid;
if (!inputResult.isValid) {
guardrailFlags.push(...inputResult.risks.map(r => r.type));
await this.logger.logBlocked(userId, query, inputResult.risks);
return {
response: {
answer: 'この質問にはお答えできません。質問内容を確認してください。',
confidence: 'low',
sources: [],
},
metrics: { ...metrics, totalLatencyMs: Date.now() - startTime } as PipelineMetrics,
guardrailFlags,
};
}
// Step 2: キャッシュチェック
const cached = await this.cache.get(inputResult.sanitizedInput);
if (cached) {
metrics.cacheHit = true;
return {
response: cached.data as TechQAResponse,
metrics: { ...metrics, totalLatencyMs: Date.now() - startTime } as PipelineMetrics,
guardrailFlags,
};
}
metrics.cacheHit = false;
// Step 3: プロンプト解決(A/Bテスト対応)
const { prompt, variant, experimentId } = this.promptRouter.resolve(
'tech-qa', userId,
);
// Step 4: 検索
const retrievalStart = Date.now();
const contexts = await this.retriever.search(inputResult.sanitizedInput);
metrics.retrievalLatencyMs = Date.now() - retrievalStart;
// Step 5: LLM生成(構造化出力)
const genStart = Date.now();
const pipeline = new StructuredOutputPipeline(this.llm, TechQAResponseSchema);
const messages = this.buildMessages(prompt, inputResult.sanitizedInput, contexts);
const result = await pipeline.execute(messages);
metrics.generationLatencyMs = Date.now() - genStart;
metrics.retryCount = result.retries;
// Step 6: 出力ガードレール
const outputStart = Date.now();
const outputResult = await this.outputGuard.validate(result.data.answer, {
originalQuery: query,
retrievedContexts: contexts.map(c => c.chunk.content),
});
metrics.outputGuardLatencyMs = Date.now() - outputStart;
metrics.outputGuardTriggered = !outputResult.isAcceptable;
if (!outputResult.isAcceptable) {
guardrailFlags.push(...outputResult.issues.map(i => i.type));
result.data.answer = outputResult.filteredOutput;
}
// Step 7: メトリクス記録
metrics.totalLatencyMs = Date.now() - startTime;
await this.metricsCollector.record({
...metrics as PipelineMetrics,
variant,
experimentId,
});
// Step 8: キャッシュ保存
await this.cache.set(inputResult.sanitizedInput, result.data);
// Step 9: 監査ログ
await this.logger.logRequest({
userId,
query: inputResult.sanitizedInput,
response: result.data,
variant,
experimentId,
guardrailFlags,
metrics: metrics as PipelineMetrics,
});
return {
response: result.data,
metrics: metrics as PipelineMetrics,
guardrailFlags,
};
}
private buildMessages(
prompt: PromptVersion,
query: string,
contexts: RetrievedContext[],
): ChatMessage[] {
const contextStr = contexts
.map((c, i) => `<source id="${i + 1}" title="${c.chunk.metadata.documentTitle}">\n${c.chunk.content}\n</source>`)
.join('\n\n');
return [
{ role: 'system', content: prompt.system },
{
role: 'user',
content: prompt.user
.replace('{{contexts}}', contextStr)
.replace('{{query}}', query),
},
];
}
}
達成度チェック
| ミッション | 完了 |
|---|---|
| Mission 1: プロンプトテンプレート設計 | [ ] |
| Mission 2: Zod構造化出力バリデーション | [ ] |
| Mission 3: プロンプトバージョニング | [ ] |
| Mission 4: ガードレール統合パイプライン | [ ] |
まとめ
| ポイント | 内容 |
|---|---|
| テンプレート設計 | ユースケース別にシステム/ユーザープロンプトを分離し、デリミターで構造化 |
| 構造化出力 | Zodスキーマでバリデーション + リトライで信頼性を確保 |
| バージョン管理 | YAML定義 + セマンティックバージョニング + A/Bテスト |
| ガードレール統合 | 入力→検索→生成→出力の各段階で多層防御 |
チェックリスト
- ユースケースに応じたプロンプトテンプレートを設計できた
- Zodで構造化出力のバリデーションパイプラインを実装できた
- プロンプトのバージョン管理とA/Bテストの仕組みを構築できた
- ガードレールを統合した本番品質のパイプラインを実装できた
次のステップへ
プロンプトエンジニアリングの演習を完了しました。次はStep 3のチェックポイントクイズに挑戦しましょう。
推定所要時間: 60分