SCALE
SCALE Build Hub
機能集
開発パターン TypeScript Library

AIスクリーニング

出典: SCALE Base
実装時間
240〜600分
難度
簡単
価格
¥30,000

依存パッケージ

react

ファイル

lib/recruit-ai-screening.ts

AIスクリーニング

:LiTarget: 用途

応募者・候補者をAIでスクリーニングして優先度判定するロジック。

:LiSparkle: 特徴

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: lib/recruit-ai-screening.ts の中身そのもの。コピペ即可。

import type { Candidate } from './recruit-types'

// AI Screening scoring (rule-based + Claude API when available)
// Returns score 0-100 and reason text

interface ScreeningResult {
  score: number
  reason: string
  breakdown: {
    sales_experience: number    // max 25
    teleapo_experience: number  // max 20
    monthly_hours: number       // max 15
    track_record: number        // max 15
    communication: number       // max 10
    self_drive: number          // max 10
    culture_fit: number         // max 5
  }
}

// Rule-based scoring (always available)
export function ruleBasedScreening(candidate: Partial<Candidate>): ScreeningResult {
  const breakdown = {
    sales_experience: 0,
    teleapo_experience: 0,
    monthly_hours: 0,
    track_record: 0,
    communication: 0,
    self_drive: 0,
    culture_fit: 0,
  }

  // Sales experience (max 25)
  switch (candidate.sales_experience_years) {
    case '5_plus': breakdown.sales_experience = 25; break
    case '3_to_5': breakdown.sales_experience = 22; break
    case '1_to_3': breakdown.sales_experience = 15; break
    case 'less_than_1': breakdown.sales_experience = 5; break
  }

  // Teleapo experience (max 20)
  switch (candidate.teleapo_experience) {
    case 'same_industry': breakdown.teleapo_experience = 20; break
    case 'other_industry': breakdown.teleapo_experience = 12; break
    case 'none': breakdown.teleapo_experience = 0; break
  }

  // Monthly hours (max 15)
  switch (candidate.monthly_hours) {
    case '120_plus': breakdown.monthly_hours = 15; break
    case '80_to_120': breakdown.monthly_hours = 12; break
    case '50_to_80': breakdown.monthly_hours = 8; break
    case 'less_than_50': breakdown.monthly_hours = 0; break
  }

  // Track record — analyze self_pr for numbers (max 15)
  const pr = candidate.self_pr || ''
  const hasNumbers = /\d+%|\d+|\d+|\d+|\d+|\d+アポ/g.test(pr)
  const hasResults = /達成|実績|成果|記録/g.test(pr)
  breakdown.track_record = hasNumbers && hasResults ? 15 : hasNumbers ? 10 : hasResults ? 7 : 3

  // Communication — PR text quality (max 10)
  const prLength = pr.length
  if (prLength >= 100) {
    breakdown.communication = pr.includes('。') && prLength >= 150 ? 10 : 7
  } else if (prLength >= 50) {
    breakdown.communication = 5
  } else {
    breakdown.communication = 2
  }

  // Self-drive — keywords (max 10)
  const driveKeywords = ['自主', '自発', '改善', '工夫', '提案', '学習', '挑戦', 'スクリプト', '独自']
  const driveCount = driveKeywords.filter(k => pr.includes(k)).length
  breakdown.self_drive = Math.min(driveCount * 3 + 1, 10)

  // Culture fit — occupation type bonus (max 5)
  if (candidate.current_occupation === 'freelance') breakdown.culture_fit = 5
  else if (candidate.preferred_compensation === 'commission') breakdown.culture_fit = 4
  else if (candidate.current_occupation === 'employee') breakdown.culture_fit = 3
  else breakdown.culture_fit = 2

  const score = Object.values(breakdown).reduce((sum, v) => sum + v, 0)

  // Generate reason
  const reasons: string[] = []
  if (breakdown.sales_experience >= 20) reasons.push('営業経験豊富')
  else if (breakdown.sales_experience >= 15) reasons.push('営業経験あり')
  else reasons.push('営業経験が浅い')

  if (breakdown.teleapo_experience >= 15) reasons.push('テレアポ経験あり')
  else if (breakdown.teleapo_experience === 0) reasons.push('テレアポ未経験')

  if (breakdown.track_record >= 12) reasons.push('数値実績が具体的')
  if (breakdown.self_drive >= 7) reasons.push('自己成長意欲が高い')
  if (breakdown.monthly_hours >= 12) reasons.push('十分な稼働時間')
  else if (breakdown.monthly_hours <= 5) reasons.push('稼働時間が少なめ')

  let recommendation = ''
  if (score >= 80) recommendation = '即面談候補として推奨。'
  else if (score >= 60) recommendation = '書類通過。追加確認の上、面談調整へ。'
  else if (score >= 40) recommendation = '要レビュー。担当者の判断を仰ぐ。'
  else recommendation = '不合格候補。基準を満たしていない。'

  const reason = `${reasons.join('。')}。${recommendation}`

  return { score, reason, breakdown }
}

// Claude API enhanced screening
export async function aiScreening(candidate: Partial<Candidate>): Promise<ScreeningResult> {
  const ruleResult = ruleBasedScreening(candidate)

  // If Claude API key is not configured, use rule-based only
  const apiKey = typeof window !== 'undefined'
    ? null // Client-side — can't access env directly, would need API route
    : process.env.ANTHROPIC_API_KEY

  if (!apiKey) {
    return ruleResult
  }

  try {
    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': apiKey,
        'anthropic-version': '2023-06-01',
      },
      body: JSON.stringify({
        model: 'claude-sonnet-4-20250514',
        max_tokens: 500,
        messages: [{
          role: 'user',
          content: `あなたはテレアポ人材採用のスクリーニング担当AIです。以下の応募者情報を評価し、JSONで回答してください。

応募者情報:
- 営業経験: ${candidate.sales_experience_years || '不明'}
- テレアポ経験: ${candidate.teleapo_experience || '不明'}
- 月間稼働可能時間: ${candidate.monthly_hours || '不明'}
- 希望報酬: ${candidate.preferred_compensation || '不明'}
- 現在の働き方: ${candidate.current_occupation || '不明'}
- 自己PR: ${candidate.self_pr || 'なし'}

ルールベーススコア: ${ruleResult.score}点/100点

以下のJSON形式で回答:
{"score": 0-100の整数, "reason": "評価理由を2-3文で"}

スコア基準:
- 80+: 即面談候補
- 60-79: 書類通過
- 40-59: 要レビュー
- 39以下: 不合格候補`,
        }],
      }),
    })

    if (response.ok) {
      const data = await response.json()
      const content = data.content[0]?.text || ''
      const match = content.match(/\{[\s\S]*\}/)
      if (match) {
        const parsed = JSON.parse(match[0])
        return {
          score: Math.round((ruleResult.score + parsed.score) / 2), // Blend
          reason: parsed.reason || ruleResult.reason,
          breakdown: ruleResult.breakdown,
        }
      }
    }
  } catch {
    // Fallback to rule-based
  }

  return ruleResult
}

// Get auto-action based on score
export function getAutoAction(score: number): {
  action: 'auto_interview' | 'screening_pass' | 'review' | 'auto_reject'
  label: string
  templateType: string
} {
  if (score >= 80) {
    return { action: 'auto_interview', label: '即面談候補 → 面談調整メール自動送信', templateType: 'interview_invite' }
  }
  if (score >= 60) {
    return { action: 'screening_pass', label: '書類通過 → 追加質問メール送信', templateType: 'screening_pass' }
  }
  if (score >= 40) {
    return { action: 'review', label: '保留 → 担当者レビュー待ち', templateType: '' }
  }
  return { action: 'auto_reject', label: '不合格候補 → お見送りメール送信(承認後)', templateType: 'rejection' }
}

:LiFolder: SCALE Base 内のパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/recruit-ai-screening.ts

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項