EXERCISE 60分

ストーリー

佐藤CTO
原則は頭に入った。ガードレールの重要性も分かった。あとは実践だ
佐藤CTO
本番品質のプロンプトテンプレートを設計し、構造化出力のバリデーション、バージョン管理の仕組み、そしてガードレールの統合まで一気通貫で実装してほしい
あなた
つまり、Step 3で学んだ全てを1つのパイプラインに統合するということですね
佐藤CTO
そういうことだ。本番に投入できるレベルを目指してくれ

演習の概要

4つのミッションに取り組み、本番品質のプロンプトエンジニアリングスキルを磨きます。

ミッションテーマ難易度
Mission 1プロンプトテンプレートの設計中級
Mission 2Zodによる構造化出力バリデーション中級
Mission 3プロンプトバージョニングシステム上級
Mission 4ガードレール統合パイプライン上級

Mission 1: プロンプトテンプレートの設計

要件

以下の3つのユースケースに対応するプロンプトテンプレートを設計してください。

ユースケース:
1. 技術QA: ナレッジベースから検索した情報に基づく質問応答
2. インシデント分析: 過去のインシデントレポートを基にした根本原因分析
3. コードレビュー: コードスニペットに対する改善提案

共通要件:
- システムプロンプトとユーザープロンプトを分離
- デリミターで入力を明確に区切る
- 出典の明示を必須にする
- 日本語で回答

以下を設計してください:

  1. 各ユースケースのシステムプロンプト
  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スキーマでバリデーション
- パースエラー時のリトライロジック
- 型安全なレスポンス型の生成

以下を実装してください:

  1. 回答のZodスキーマ定義
  2. パース + バリデーション + リトライのパイプライン
  3. エラーハンドリング
解答例
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テスト用のトラフィック分割
- 変更時のテスト実行(回帰テスト)

以下を実装してください:

  1. プロンプト定義のYAMLスキーマ
  2. バージョン管理とロールバックのロジック
  3. 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分