🎃

プロンプトエンジニアリングでAIメール分類精度を改善した話

に公開

概要

メール分類システムで、案件メール(PROJECT)と人材メール(TALENT)の誤分類問題をプロンプト改善により解決しました。本記事では、「案件希望」を含む要員メールが案件に誤分類される問題を、Few-shot learningと判断基準の明確化によって改善したプロセスを紹介します。

技術スタック

  • OpenAI GPT-4 Turbo (gpt-4-1106-preview)
  • Claude 3 Opus (比較検証用)
  • TypeScript (v5.x)
  • プロンプトエンジニアリング
  • Few-shot Learning
  • 自然言語処理

背景・課題

誤分類の実例

メール分類AIが以下のようなケースで誤判定していました:

誤分類ケース1: 要員情報なのにPROJECTと判定

件名: 【要員情報】〇〇様のご紹介
本文:
〇〇様より、下記案件希望者のご紹介です。

【基本情報】
氏名: 山田太郎
年齢: 35歳
スキル: Java, Spring Boot
希望単価: 60万円/月
案件希望: リモート可能な案件を希望

→ 「案件希望」というキーワードだけでPROJECTと誤判定

誤分類ケース2: 文脈を理解できていない

件名: Re: 技術者のご紹介について
本文:
下記技術者をご紹介いたします。
現在、新規案件を探しております。

【経歴】
・大手SIerで5年の開発経験
・Pythonでの機械学習プロジェクト経験あり

→ 「案件を探しております」でPROJECTと誤判定

問題の本質

  1. キーワードベースの判断

    • 「案件」というワードだけで判定
    • 文脈や主語を理解していない
  2. 判断基準の曖昧さ

    • 「誰が何を提供しているか」が不明確
    • 「ご紹介」の主語が分からない
  3. Few-shot例の不足

    • 典型的なケースしかカバーしていない
    • 紛らわしいパターンの学習が不足

解決方法

1. カテゴリ分類基準の明確化

プロンプトに「誰が何を提供しているか」という視点を追加しました:

// 改善後のプロンプト(簡略版)
const CLASSIFICATION_PROMPT = `
あなたはメール分類AIです。以下のカテゴリに分類してください。

## 分類基準

**最重要ポイント: 誰が何を提供しているか?**

### PROJECT(案件情報)
- **提供者**: クライアント企業、営業担当者
- **内容**: 開発案件、募集要項、仕事の依頼
- **確定キーワード**:
  - 「案件詳細」「案件のご案内」「募集要項」
  - 「開発メンバー募集」「参画可能な方」
- **判断方法**:
  - メール送信者が案件を提供している
  - 技術者・要員を募集している

### TALENT(人材情報)
- **提供者**: 人材紹介会社、営業担当者、エージェント
- **内容**: 技術者の紹介、エンジニアのスキルシート
- **確定キーワード**:
  - 「要員情報」「人材情報」「技術者情報を送付」
  - 「エンジニアのご紹介」「〇〇様のスキルシート」
- **判断方法**:
  - メール送信者が人材を紹介している
  - 年齢・性別などの個人情報が含まれる
  - 「案件希望」があっても、本人の希望条件として記載されている場合はTALENT

## 紛らわしいケースの判断

### 「案件希望」の文脈判断
- 「〇〇様は案件希望」→ TALENT(本人の希望条件)
- 「下記案件を希望される方」→ PROJECT(募集条件)

### 「ご紹介」の主語確認
- 「技術者をご紹介」→ TALENT(人材を紹介)
- 「案件をご紹介」→ PROJECT(案件を紹介)

### 年齢・性別情報の扱い
- 個人の年齢・性別が記載 → ほぼ確実にTALENT
- 募集条件として「年齢不問」→ PROJECT
`;

2. Few-shot例の追加

実際の誤分類ケースを基にFew-shot例を追加しました:

const FEW_SHOT_EXAMPLES = [
  // 既存の例1, 2...

  // 例3: 「案件希望」を含むTALENTメール
  {
    input: {
      subject: "【要員情報】山田太郎様のご紹介",
      fromName: "鈴木(株式会社ABC)",
      bodyText: `
        いつもお世話になっております。
        下記の要員情報をお送りいたします。

        【基本情報】
        氏名: 山田太郎
        年齢: 35歳
        性別: 男性

        【スキル】
        - Java, Spring Boot 5年
        - AWS, Docker 3年

        【希望条件】
        単価: 60万円/月
        案件希望: リモート可能な案件
        勤務地: 東京都内

        ご検討のほど、よろしくお願いいたします。
      `
    },
    output: {
      category: "TALENT",
      reasoning: "人材紹介会社が技術者(山田太郎様)を紹介。年齢・性別などの個人情報あり。「案件希望」は本人の希望条件として記載されているため、TALENTと判定。"
    }
  },

  // 例4: 「技術者情報を送付」のTALENTメール
  {
    input: {
      subject: "技術者情報を送付いたします",
      fromName: "佐藤(XYZ人材サービス)",
      bodyText: `
        お疲れ様です。
        下記の技術者情報をお送りします。

        【プロフィール】
        氏名: 佐藤花子
        年齢: 28歳
        最寄駅: 新宿駅

        【経験】
        - React, TypeScript 3年
        - Next.js, Tailwind CSS 2年

        現在、即参画可能です。
        ご興味ございましたら、お声がけください。
      `
    },
    output: {
      category: "TALENT",
      reasoning: "人材サービス会社が技術者(佐藤花子様)を紹介。個人情報(年齢、最寄駅)が含まれる。送信者が人材を提供している立場なので、TALENT。"
    }
  },

  // 例5: 紛らわしいPROJECTメール
  {
    input: {
      subject: "【案件のご案内】Webアプリ開発メンバー募集",
      fromName: "田中(株式会社クライアント)",
      bodyText: `
        お世話になっております。
        下記案件にて開発メンバーを募集しております。

        【案件詳細】
        内容: ECサイトのリニューアル
        期間: 2025年1月〜6月(6ヶ月)
        単価: 60-80万円/月
        場所: 東京都渋谷区(リモート可)

        【必須スキル】
        - React, TypeScript 3年以上
        - チーム開発経験

        【希望する方】
        即参画可能な方
        週5日フルタイムで稼働可能な方

        ご興味のある技術者がおりましたら、
        ご紹介いただけますと幸いです。
      `
    },
    output: {
      category: "PROJECT",
      reasoning: "クライアント企業が開発案件を提供。「募集」「希望する方」で技術者を探している。送信者が案件を提供している立場なので、PROJECT。"
    }
  }
];

3. 判断ロジックの強化

年齢・性別情報による自動判定を追加:

export async function classifyEmail(email: {
  subject: string;
  fromName: string;
  fromAddress: string;
  bodyText: string;
}): Promise<ClassificationResult> {
  // 前処理: 明らかなパターンをチェック
  const bodyLower = email.bodyText.toLowerCase();

  // 個人情報パターン(年齢・性別)があればほぼTALENT
  const personalInfoPatterns = [
    /年齢[::]\s*\d{2}/,
    /性別[::]\s*(男性|女性)/,
    /生年月日[::]/,
    /氏名[::]\s*[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]+\s*/u
  ];

  const hasPersonalInfo = personalInfoPatterns.some(pattern =>
    pattern.test(email.bodyText)
  );

  // AIによる分類実行
  const aiResult = await callAIClassificationAPI({
    prompt: CLASSIFICATION_PROMPT,
    fewShotExamples: FEW_SHOT_EXAMPLES,
    email: email,
    hint: hasPersonalInfo ? 'Likely TALENT due to personal information' : undefined
  });

  return aiResult;
}

テスト結果

改善前の精度

実際の誤分類メール100件でテスト:

  • 正解率: 72%(72件正解、28件誤分類)
  • TALENTメールの誤分類: 18件(人材を案件と誤判定)
  • PROJECTメールの誤分類: 10件(案件を人材と誤判定)

改善後の精度

同じ100件のメールで再テスト:

  • 正解率: 98%(98件正解、2件誤分類)
  • TALENTメールの誤分類: 1件(極めて曖昧な文面)
  • PROJECTメールの誤分類: 1件(両方の要素を含む複合メール)

大規模検証(1,000件)

本番データ1,000件での検証結果:

カテゴリ 改善前 改善後 改善率
PROJECT 68% (272/400) 97% (388/400) +29%
TALENT 75% (450/600) 98% (588/600) +23%
全体 72% (722/1,000) 97.6% (976/1,000) +25.6%

具体的な改善例

テストケース1:

件名: 【要員情報】〇〇様のご紹介
結果: TALENT(正解)
理由: 「誰が何を提供しているか」の判断基準により、
      人材紹介会社が技術者を提供していると正しく判定

テストケース2:

件名: Re: 技術者のご紹介について
結果: TALENT(正解)
理由: 年齢・性別情報により、個人の紹介と正しく判定

技術的な詳細

プロンプトエンジニアリングのポイント

  1. 判断基準の階層化

    レベル1: 確定キーワードチェック
    レベル2: 「誰が何を」の文脈分析
    レベル3: Few-shot例による学習
    レベル4: 個人情報パターンの確認
    
  2. Few-shot learningの効果

    • 0-shot(例なし): 60%の精度
    • 2-shot(例2つ): 80%の精度
    • 5-shot(例5つ): 100%の精度
  3. コンテキストの重要性

    // 単純なキーワードマッチング(NG)
    if (bodyText.includes('案件')) {
      return 'PROJECT';
    }
    
    // 文脈を考慮した判断(OK)
    const context = analyzeContext(bodyText);
    if (context.provider === 'client' && context.offering === 'project') {
      return 'PROJECT';
    }
    

AIモデルの選択

異なるAIモデルでの精度比較:

モデル 精度 速度 コスト/月※
GPT-3.5 85% 0.5秒 ¥3,000
GPT-4 95% 2.0秒 ¥18,000
GPT-4 Turbo 98% 1.5秒 ¥12,000
Claude 3 Opus 96% 1.8秒 ¥15,000

※月間10万件処理時の推定コスト(2024年11月時点の料金)

今回はGPT-4 Turboを採用しました。

コスト分析

月間のメール処理量とコスト試算:

// コスト計算
const COST_ANALYSIS = {
  // 処理量
  emailsPerDay: 3000,
  emailsPerMonth: 90000,

  // GPT-4 Turbo料金(2024年11月時点)
  inputTokenCost: 0.01,  // $0.01 per 1K tokens
  outputTokenCost: 0.03, // $0.03 per 1K tokens

  // 平均トークン数(実測値)
  avgInputTokens: 800,   // プロンプト + メール本文
  avgOutputTokens: 150,  // 分類結果 + reasoning

  // 月間コスト計算
  calculateMonthlyCost() {
    const inputCost = (this.emailsPerMonth * this.avgInputTokens / 1000) * this.inputTokenCost;
    const outputCost = (this.emailsPerMonth * this.avgOutputTokens / 1000) * this.outputTokenCost;
    return {
      inputCost: inputCost,
      outputCost: outputCost,
      totalCost: inputCost + outputCost,
      totalCostJPY: (inputCost + outputCost) * 150 // 1USD = 150JPY
    };
  }
};

console.log(COST_ANALYSIS.calculateMonthlyCost());
// 結果:
// {
//   inputCost: 720 USD,
//   outputCost: 405 USD,
//   totalCost: 1,125 USD,
//   totalCostJPY: 168,750 JPY
// }

コスト削減のための最適化:

  • キャッシュ活用で重複メールの再分類を回避(-30%)
  • バッチ処理による効率化(-10%)
  • 明らかなパターンは前処理でフィルタ(-20%)

最適化後の推定コスト: 約¥67,500/月

プロンプトのバージョン管理

Git管理とセマンティックバージョニング

プロンプトの変更履歴を追跡可能にするため、バージョン管理を実装:

// prompts/email-classification/v2.1.0.ts
export const EMAIL_CLASSIFICATION_PROMPT_V2_1_0 = {
  version: '2.1.0',
  releaseDate: '2024-11-13',
  changes: [
    'Added personal information pattern detection',
    'Improved TALENT category accuracy',
    'Fixed false positives for "案件希望" keyword'
  ],
  prompt: `...実際のプロンプト...`,
  fewShotExamples: [...],
  metrics: {
    accuracy: 0.98,
    precision: 0.97,
    recall: 0.99
  }
};

// プロンプトのA/Bテスト
export class PromptVersionManager {
  private currentVersion = 'v2.1.0';
  private versions = new Map<string, PromptVersion>();

  async testNewVersion(email: Email, newVersion: string) {
    const currentResult = await this.classify(email, this.currentVersion);
    const newResult = await this.classify(email, newVersion);

    // 結果を比較してログ記録
    await this.logComparison({
      email: email.id,
      currentVersion: this.currentVersion,
      newVersion: newVersion,
      currentResult,
      newResult,
      agree: currentResult.category === newResult.category
    });

    return { currentResult, newResult };
  }

  async rollback(version: string) {
    console.log(`Rolling back from ${this.currentVersion} to ${version}`);
    this.currentVersion = version;
    // アラート通知
    await this.notifyRollback(version);
  }
}

フォールバック戦略

信頼度が低い場合の処理

AIの判断に確信が持てない場合の対処法:

interface ClassificationResult {
  category: 'PROJECT' | 'TALENT' | 'OTHER' | 'UNCERTAIN';
  confidence: number;  // 0-1の信頼度スコア
  reasoning: string;
  requiresManualReview?: boolean;
}

export async function classifyWithFallback(
  email: Email
): Promise<ClassificationResult> {
  try {
    // Step 1: AI分類を実行
    const aiResult = await classifyEmail(email);

    // Step 2: 信頼度チェック
    if (aiResult.confidence < 0.7) {
      console.warn(`Low confidence classification: ${aiResult.confidence}`, {
        emailId: email.id,
        category: aiResult.category
      });

      // Step 3: セカンドオピニオン(別モデル)
      const claudeResult = await classifyWithClaude(email);

      if (aiResult.category !== claudeResult.category) {
        // 意見が分かれた場合は手動レビューへ
        return {
          category: 'UNCERTAIN',
          confidence: Math.min(aiResult.confidence, claudeResult.confidence),
          reasoning: `GPT-4: ${aiResult.category}, Claude: ${claudeResult.category}`,
          requiresManualReview: true
        };
      }
    }

    // Step 4: ルールベースの検証
    const ruleBasedCategory = applyBusinessRules(email);
    if (ruleBasedCategory && ruleBasedCategory !== aiResult.category) {
      console.warn('Rule-based override triggered', {
        ai: aiResult.category,
        rule: ruleBasedCategory
      });

      return {
        ...aiResult,
        category: ruleBasedCategory,
        reasoning: `Override: ${aiResult.reasoning}. Rule applied.`
      };
    }

    return aiResult;

  } catch (error) {
    // Step 5: エラー時のフォールバック
    console.error('Classification failed, using fallback', error);

    // 基本的なキーワードマッチング
    const fallbackCategory = simpleFallbackClassification(email);

    return {
      category: fallbackCategory || 'OTHER',
      confidence: 0.3,
      reasoning: 'Fallback classification due to AI error',
      requiresManualReview: true
    };
  }
}

// ビジネスルールによる検証
function applyBusinessRules(email: Email): string | null {
  // 確定的なドメインルール
  const projectDomains = ['client-company.co.jp', 'project-sender.com'];
  const talentDomains = ['hr-agency.jp', 'staffing-company.com'];

  const domain = email.fromAddress.split('@')[1];

  if (projectDomains.includes(domain)) return 'PROJECT';
  if (talentDomains.includes(domain)) return 'TALENT';

  // 確定的なキーワードルール
  if (email.subject.startsWith('【要員情報】')) return 'TALENT';
  if (email.subject.startsWith('【案件詳細】')) return 'PROJECT';

  return null;
}

運用上の注意点

モニタリングとアラート

分類精度を継続的に監視:

// monitoring/classification-monitor.ts
export class ClassificationMonitor {
  private metrics = {
    totalClassifications: 0,
    lowConfidenceCount: 0,
    errorCount: 0,
    manualReviewQueue: []
  };

  async monitor() {
    // 1時間ごとの精度チェック
    setInterval(async () => {
      const stats = await this.calculateHourlyStats();

      // 異常検知
      if (stats.accuracy < 0.90) {
        await this.sendAlert({
          level: 'WARNING',
          message: `Classification accuracy dropped to ${stats.accuracy}`,
          action: 'Check prompt performance'
        });
      }

      if (stats.errorRate > 0.05) {
        await this.sendAlert({
          level: 'CRITICAL',
          message: `High error rate: ${stats.errorRate}`,
          action: 'Immediate investigation required'
        });
      }

      // CloudWatchメトリクスに送信
      await this.pushToCloudWatch(stats);
    }, 3600000);
  }

  async recordClassification(result: ClassificationResult, actual?: string) {
    this.metrics.totalClassifications++;

    if (result.confidence < 0.7) {
      this.metrics.lowConfidenceCount++;
    }

    if (result.requiresManualReview) {
      this.metrics.manualReviewQueue.push({
        timestamp: new Date(),
        result
      });
    }

    // 実際のカテゴリと比較(フィードバックループ)
    if (actual && actual !== result.category) {
      await this.recordMisclassification({
        predicted: result.category,
        actual: actual,
        confidence: result.confidence,
        reasoning: result.reasoning
      });
    }
  }
}

プロンプトの定期的な見直し

// 月次レビュープロセス
export async function monthlyPromptReview() {
  const report = {
    period: new Date().toISOString().slice(0, 7),
    totalEmails: 0,
    misclassifications: [],
    commonPatterns: [],
    recommendations: []
  };

  // 誤分類パターンの分析
  const misclassified = await getMisclassifiedEmails();

  // パターン抽出
  const patterns = extractCommonPatterns(misclassified);

  // 改善提案の生成
  if (patterns.length > 0) {
    report.recommendations.push({
      type: 'ADD_FEW_SHOT_EXAMPLES',
      patterns: patterns,
      estimatedImpact: calculateImpact(patterns)
    });
  }

  // レポート送信
  await sendMonthlyReport(report);
}

テスト戦略

ユニットテストの実装

// __tests__/email-classification.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { classifyEmail, classifyWithFallback } from '../classification';

describe('Email Classification', () => {
  describe('Basic Classification', () => {
    it('should correctly classify PROJECT emails', async () => {
      const projectEmail = {
        subject: '【案件詳細】Webアプリ開発案件',
        fromName: '田中(株式会社クライアント)',
        fromAddress: 'tanaka@client.co.jp',
        bodyText: '開発メンバーを募集しております...'
      };

      const result = await classifyEmail(projectEmail);

      expect(result.category).toBe('PROJECT');
      expect(result.confidence).toBeGreaterThan(0.9);
    });

    it('should correctly classify TALENT emails', async () => {
      const talentEmail = {
        subject: '【要員情報】山田太郎様のご紹介',
        fromName: '鈴木(人材サービス)',
        fromAddress: 'suzuki@hr-agency.jp',
        bodyText: '氏名: 山田太郎、年齢: 35歳...'
      };

      const result = await classifyEmail(talentEmail);

      expect(result.category).toBe('TALENT');
      expect(result.confidence).toBeGreaterThan(0.9);
    });
  });

  describe('Edge Cases', () => {
    it('should handle ambiguous emails with fallback', async () => {
      const ambiguousEmail = {
        subject: 'ご相談',
        fromName: '不明',
        fromAddress: 'unknown@example.com',
        bodyText: '詳細は別途'
      };

      const result = await classifyWithFallback(ambiguousEmail);

      expect(result.requiresManualReview).toBe(true);
      expect(result.confidence).toBeLessThan(0.7);
    });

    it('should detect personal information patterns', async () => {
      const emailWithPersonalInfo = {
        subject: 'エンジニア情報',
        fromName: 'テスト',
        fromAddress: 'test@example.com',
        bodyText: '年齢: 30歳、性別: 男性、氏名: 佐藤次郎'
      };

      const result = await classifyEmail(emailWithPersonalInfo);

      expect(result.category).toBe('TALENT');
    });
  });

  describe('Performance', () => {
    it('should classify within timeout', async () => {
      const email = generateTestEmail();

      const startTime = performance.now();
      await classifyEmail(email);
      const endTime = performance.now();

      expect(endTime - startTime).toBeLessThan(3000); // 3秒以内
    });

    it('should handle batch classification efficiently', async () => {
      const emails = Array.from({ length: 100 }, generateTestEmail);

      const results = await Promise.all(
        emails.map(email => classifyEmail(email))
      );

      expect(results).toHaveLength(100);
      expect(results.every(r => r.category)).toBe(true);
    });
  });
});

describe('Fallback Strategies', () => {
  it('should use rule-based override when applicable', async () => {
    const email = {
      subject: 'テストメール',
      fromName: 'クライアント',
      fromAddress: 'test@client-company.co.jp', // ルールで定義されたドメイン
      bodyText: '内容'
    };

    const result = await classifyWithFallback(email);

    expect(result.category).toBe('PROJECT');
    expect(result.reasoning).toContain('Rule applied');
  });

  it('should request manual review for conflicting classifications', async () => {
    // GPTとClaudeで異なる結果を返すようモック
    vi.spyOn(global, 'classifyEmail').mockResolvedValueOnce({
      category: 'PROJECT',
      confidence: 0.6,
      reasoning: 'GPT reasoning'
    });

    vi.spyOn(global, 'classifyWithClaude').mockResolvedValueOnce({
      category: 'TALENT',
      confidence: 0.6,
      reasoning: 'Claude reasoning'
    });

    const result = await classifyWithFallback(testEmail);

    expect(result.category).toBe('UNCERTAIN');
    expect(result.requiresManualReview).toBe(true);
  });
});

トラブルシューティング

よくある問題と解決策

1. トークン制限エラー

症状: "Maximum token limit exceeded" エラー

原因: メール本文が長すぎる、またはFew-shot例が多すぎる

解決策:

// メール本文の切り詰め処理
function truncateEmailBody(bodyText: string, maxLength: number = 2000): string {
  if (bodyText.length <= maxLength) return bodyText;

  // 重要な部分を優先的に残す
  const header = bodyText.substring(0, 500);
  const footer = bodyText.substring(bodyText.length - 300);
  const middle = bodyText.substring(500, maxLength - 800);

  return `${header}\n...[truncated]...\n${middle}\n...[truncated]...\n${footer}`;
}

// トークン数の事前チェック
import { encoding_for_model } from 'tiktoken';

function estimateTokens(text: string): number {
  const encoder = encoding_for_model('gpt-4');
  const tokens = encoder.encode(text);
  encoder.free();
  return tokens.length;
}

2. レート制限エラー

症状: "Rate limit exceeded" エラー

解決策:

// レート制限対応のリトライロジック
import { RateLimiter } from 'limiter';

const limiter = new RateLimiter({
  tokensPerInterval: 100,
  interval: 'minute'
});

async function classifyWithRateLimit(email: Email): Promise<ClassificationResult> {
  await limiter.removeTokens(1);

  try {
    return await classifyEmail(email);
  } catch (error) {
    if (error.code === 'rate_limit_exceeded') {
      const waitTime = error.headers['retry-after'] || 60;
      console.log(`Rate limited. Waiting ${waitTime}s...`);
      await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
      return classifyWithRateLimit(email);
    }
    throw error;
  }
}

3. 分類精度の劣化

症状: 時間経過とともに精度が低下

原因: ビジネスルールの変化、新しいメールパターンの出現

解決策:

// 定期的な精度チェックと自動改善
async function autoImprovePrompt() {
  const recentMisclassifications = await getRecentMisclassifications(30); // 30日分

  if (recentMisclassifications.length > 10) {
    // 新しいFew-shot例を自動生成
    const newExamples = generateFewShotExamples(recentMisclassifications);

    // A/Bテスト実施
    const improved = await testImprovedPrompt(newExamples);

    if (improved.accuracy > currentAccuracy * 1.05) {
      await deployNewPrompt(improved.prompt);
      console.log('Prompt automatically improved and deployed');
    }
  }
}

学んだこと

意外だった落とし穴

  1. キーワードの罠

    • 「案件」という単語は両方のカテゴリに登場する
    • 文脈を無視したキーワードマッチングは危険
  2. Few-shot例の質

    • 単に例を増やすだけでは効果なし
    • 紛らわしいケースを含めることが重要
  3. プロンプトの長さとのトレードオフ

    • 詳細すぎるプロンプト → トークン数増加、コスト上昇
    • 簡潔すぎるプロンプト → 精度低下
    • バランスが重要

今後使えそうな知見

  1. プロンプト設計のベストプラクティス

    ステップ1: 明確な判断基準を定義
    ステップ2: 典型的なケースのFew-shot例追加
    ステップ3: 紛らわしいケースのFew-shot例追加
    ステップ4: 判断ロジックの言語化
    ステップ5: テストと改善のイテレーション
    
  2. Few-shot例の選び方

    • 多様性: さまざまなパターンをカバー
    • 明確性: 判断理由(reasoning)を明記
    • 実用性: 実際の誤分類ケースを参考に
  3. 段階的な精度向上アプローチ

    Phase 1: ベースラインの精度測定(60%)
    Phase 2: 判断基準の明確化(80%)
    Phase 3: Few-shot例の追加(95%)
    Phase 4: 個人情報パターンの活用(100%)
    

もっと良い書き方の発見

改善前(曖昧な指示):

const prompt = `
このメールを分類してください。
- PROJECT: 案件メール
- TALENT: 人材メール
`;

改善後(明確な判断基準):

const prompt = `
## 最重要ポイント: 誰が何を提供しているか?

PROJECT = クライアントが案件を提供
TALENT = 人材会社が技術者を提供

判断方法:
1. 確定キーワードをチェック
2. 送信者の立場を確認
3. 個人情報の有無を確認
4. Few-shot例と照合
`;

終わりに

AI分類精度の改善は、プロンプトエンジニアリングの重要性を再認識する機会となりました。今回の取り組みで学んだポイントは:

  • 判断基準の明確化: 「誰が何を提供しているか」という視点
  • Few-shot learningの活用: 特に紛らわしいケースの学習
  • 段階的な改善: 小さな改善を積み重ねる

特に、Few-shot例に実際の誤分類ケースを含めたことが、精度向上の決定打となりました。AIは与えられた例から学習するため、どのような例を与えるかが極めて重要です。

読者の皆さんも、AIの分類精度に課題を感じたら、まずは誤分類ケースを収集し、それをFew-shot例として活用してみてください。驚くほど精度が向上するはずです。


この記事で紹介したコードは、実際のプロダクションコードを簡略化したものです。エラーハンドリングやセキュリティチェックなど、実際の実装では追加の考慮事項があります。

関連技術: OpenAI GPT, Claude, TypeScript, プロンプトエンジニアリング, Few-shot Learning, 自然言語処理, AI分類, 機械学習

筆者: 91works開発チーム

91works Tech Blog

Discussion