AIスクリーニング
:LiTarget: 用途
応募者・候補者をAIでスクリーニングして優先度判定するロジック。
:LiSparkle: 特徴
- AI判定
- スコアリング
- 理由説明
: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: 注意事項
- 依存パッケージを忘れず追加