AI採用・履歴書マッチングOSS比較:doccano vs OpenResume vs Claude APIで求人マッチングをセルフホスト
オープンソースラボ編集部 ・ 2026年6月13日
AI採用・履歴書マッチングOSS比較:OpenCV vs doccano vs Manatal OSS で求人マッチングをセルフホスト
採用管理SaaSはGreenHouse月$599〜・Lever月$3,000〜と高額です。doccano(データラベリング)・OpenResume(AIレジュメパーサー)・セルフホスト型ATS(Applicant Tracking System)と、オープンソースのNLP・LLMを組み合わせることで、履歴書の自動解析・求人とのマッチングスコア計算・面接スケジューリングをコストゼロで構築できます。
AI採用ツールをOSSで構築する理由
- コスト削減: 採用管理SaaSの月$600〜3,000を自社構築でほぼゼロに
- データプライバシー: 応募者の個人情報・履歴書を外部SaaSに送らない(個人情報保護法・GDPR対応)
- カスタムマッチング: 自社の採用基準・必要スキルに合わせたスコアリングロジック
- LLM活用: Claude・GPT-4等でJDと履歴書の意味的マッチング(単純キーワード一致を超えた)
- 継続学習: 採用実績データから精度を改善するファインチューニングパイプライン
OSSスタックの構成
doccano(データラベリング)
NERエンティティ抽出・テキスト分類のアノテーションツールです。GitHubスター9k+。履歴書・JDからスキル・職歴・学歴を自動抽出するNERモデルのトレーニングデータ作成に使います。アノテーターが履歴書テキストに対して「Python」→SKILL、「東京大学」→EDUCATION、「2020年〜2024年」→PERIOD等のラベルをつけ、NERモデルの学習データを作成できます。
# doccanoをDockerで起動
docker run -d --name doccano --restart always -p 8000:8000 doccano/doccano:latest
# 初期設定
docker exec -it doccano python manage.py create_admin --noinput --username admin --password admin@password
# API経由でラベリングデータをエクスポート(学習用)
curl -H "Authorization: Token your-api-token" "http://localhost:8000/v1/projects/1/export?format=JSONL" -o training_data.jsonl
# doccanoでラベル付けした履歴書データでNERモデルをトレーニング
# pip install spacy transformers datasets
import json
from datasets import Dataset
from transformers import AutoTokenizer, AutoModelForTokenClassification, TrainingArguments, Trainer
import torch
# doccanoのエクスポートデータを読み込む
def load_doccano_ner(jsonl_path: str):
examples = []
with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
examples.append({
'text': data['text'],
'entities': data['label'] # [[start, end, label], ...]
})
return examples
# BIO形式に変換(NERトレーニングデータ)
def convert_to_bio(text: str, entities: list, tokenizer) -> dict:
tokens = tokenizer(text, return_offsets_mapping=True, truncation=True, max_length=512)
offsets = tokens['offset_mapping']
LABEL_MAP = {
'SKILL': 1, 'EXPERIENCE': 3, 'EDUCATION': 5, 'PERIOD': 7, 'COMPANY': 9
}
labels = [0] * len(offsets) # 0 = O(Other)
for start, end, label in entities:
for i, (tok_start, tok_end) in enumerate(offsets):
if tok_start == start:
labels[i] = LABEL_MAP.get(label, 0) # B-LABEL
elif tok_start > start and tok_end <= end:
labels[i] = LABEL_MAP.get(label, 0) + 1 # I-LABEL
return {'input_ids': tokens['input_ids'], 'attention_mask': tokens['attention_mask'], 'labels': labels}
# モデルをロードして学習
MODEL_NAME = "cl-tohoku/bert-base-japanese-v3" # 日本語BERT
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForTokenClassification.from_pretrained(MODEL_NAME, num_labels=11)
examples = load_doccano_ner('training_data.jsonl')
dataset = Dataset.from_list([convert_to_bio(e['text'], e['entities'], tokenizer) for e in examples])
training_args = TrainingArguments(
output_dir="./resume-ner-model",
num_train_epochs=10,
per_device_train_batch_size=16,
save_strategy="epoch",
evaluation_strategy="epoch",
)
trainer = Trainer(model=model, args=training_args, train_dataset=dataset)
trainer.train()
trainer.save_model("./resume-ner-model/final")
LLMベースの求人マッチングシステム
Claude APIを使って履歴書とJD(Job Description)の意味的マッチングスコアを算出するシステムを構築できます。
# Claude APIを使った履歴書×JDマッチングスコアリング
# pip install anthropic pdfplumber
import anthropic
import pdfplumber
import json
from dataclasses import dataclass
client = anthropic.Anthropic()
@dataclass
class MatchScore:
total: float # 総合スコア(0〜100)
skills: float # スキルマッチ(0〜100)
experience: float # 経験年数マッチ(0〜100)
culture: float # カルチャーフィット(0〜100)
summary: str # マッチング理由の要約
gaps: list # 不足しているスキル・経験
def extract_resume_text(pdf_path: str) -> str:
'''PDFから履歴書テキストを抽出'''
with pdfplumber.open(pdf_path) as pdf:
return '
'.join(page.extract_text() or '' for page in pdf.pages)
def match_resume_to_jd(resume_text: str, jd_text: str) -> MatchScore:
'''履歴書とJDのマッチングスコアをClaude APIで算出'''
prompt = f'''
以下の求人票と候補者の履歴書を分析して、マッチングスコアをJSON形式で返してください。
## 求人票
{jd_text}
## 候補者の履歴書
{resume_text}
## 出力形式(JSONのみ)
{{
"total": 0〜100の総合スコア,
"skills": 0〜100のスキルマッチスコア,
"experience": 0〜100の経験年数マッチスコア,
"culture": 0〜100のカルチャーフィットスコア,
"summary": "マッチング理由の一言要約(日本語、50字以内)",
"gaps": ["不足スキル1", "不足経験1", ...],
"highlights": ["強みポイント1", "強みポイント2", ...]
}}
評価基準:
- スキルは必須・歓迎要件とのマッチ度で判断
- 経験年数は求められる年数と実績を比較
- カルチャーフィットは職務経歴書の文章スタイル・志向性から推測
'''
response = client.messages.create(
model='claude-sonnet-4-6',
max_tokens=1024,
messages=[{'role': 'user', 'content': prompt}]
)
result = json.loads(response.content[0].text)
return MatchScore(**result)
# 複数の候補者を一括スコアリングしてランキング
def rank_candidates(jd_text: str, resume_paths: list) -> list:
results = []
for path in resume_paths:
resume_text = extract_resume_text(path)
score = match_resume_to_jd(resume_text, jd_text)
results.append({'path': path, 'score': score})
return sorted(results, key=lambda x: x['score'].total, reverse=True)
オープンソースATSとの統合
// Next.jsベースのシンプルなATS(Applicant Tracking System)
// Supabaseで応募者・求人・マッチングスコアを管理
// lib/ats.ts
import { createClient } from '@supabase/supabase-js';
import Anthropic from '@anthropic-ai/sdk';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!);
const claude = new Anthropic();
// 応募者登録 + AIスコアリング
export async function addApplicant(
jobId: string,
name: string,
email: string,
resumeText: string
) {
// 求人票を取得
const { data: job } = await supabase.from('jobs').select('description').eq('id', jobId).single();
// AIマッチングスコアを計算
const response = await claude.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 512,
messages: [{
role: 'user',
content: `求人: ${job.description}
候補者: ${resumeText}
マッチスコア(0-100)と理由をJSON{"score":N,"reason":"..."}で返してください`,
}],
});
const { score, reason } = JSON.parse(response.content[0].text);
// Supabaseに保存
const { data } = await supabase.from('applicants').insert({
job_id: jobId,
name,
email,
resume_text: resumeText,
ai_score: score,
ai_reason: reason,
status: 'new',
created_at: new Date().toISOString(),
}).select().single();
return data;
}
// 応募者を面接ステージに進める
export async function advanceStage(applicantId: string, newStatus: string, notes: string) {
return supabase.from('applicants').update({
status: newStatus, // new → screening → interview → offer → hired/rejected
notes,
updated_at: new Date().toISOString(),
}).eq('id', applicantId);
}
機能比較表
| ツール | ライセンス | 用途 | 特徴 |
|---|---|---|---|
| doccano | MIT | NERデータラベリング | Webアノテーションツール |
| OpenResume | MIT | 履歴書パーサーUI | Next.js製・PDF解析 |
| HireBeat OSS | MIT | ATS(応募管理) | Kanbanボード |
| Claude API | 商用 | マッチングスコア | 最高精度・柔軟 |
AI採用ツールと組み合わせるHRオートメーションはHRカテゴリ/categories/hrを参照してください。LLMを使った意味的テキスト分析のパターンはLLMツールカテゴリ/categories/llm-toolsにまとめています。
FAQ
Q. 履歴書のAI解析は個人情報保護法・GDPRの観点で問題ありませんか?
A. セルフホスト型AIを使う場合は外部への個人情報送信がないため、外部SaaSより安全ですが、いくつかの注意事項があります。個人情報保護法(日本): ①応募者から「AIによる自動評価を行う旨」の同意を取得する②採用選考でAIスコアのみを根拠に不合格とせず、人間のレビューを必ず挟む③履歴書データの保管期間・廃棄ポリシーを定める。GDPR(EU): GDPR第22条により「自動化された決定」のみで採用・不採用を決めることは原則禁止。AIスコアは「参考情報」として位置づけ、最終判断は人間が行う。Claude API等の商用LLMを使う場合: Anthropicのデータ処理契約(DPA)を締結し、プロンプトに含む個人情報のサーバー外処理について同意を確認する。
Q. 日本語の履歴書(JIS形式・市販の履歴書PDF)をOCRで解析する精度はどのくらいですか?
A. 解析精度は書類の種類によって大きく異なります。高精度(85%+): デジタル入力の履歴書(テキストとして保存されたPDF)・LinkedInからエクスポートしたPDF・Word/Googleドキュメントで作成した職務経歴書。中精度(60〜75%): スキャンしたJIS規格の手書き履歴書(OCRmyPDFで前処理が必要)。精度改善のアプローチ: ①pdfplumber(テキストPDF)+ OCRmyPDF(画像PDF)を自動判定して適用②固有名詞・スキル名はdoccanoでNERモデルを学習させると精度が大幅に向上③GPT-4 Vision(画像理解)でスキャン画像をそのままClaude/GPT-4に送る方法も有効。日本の新卒採用ではマイナビ・リクナビのデータ連携API(有償)が最も正確な構造化データを取得できます。
Q. 採用担当者なしで書類選考をAI全自動化するリスクは何ですか?
A. AI全自動化は推奨しません。主なリスク: ①バイアスの増幅: 過去の採用データに偏りがある場合(例: 特定の大学出身者ばかり採用していた)、AIがそのバイアスを学習して再現する②優秀な候補者の見落とし: 非線形なキャリア(転職多数・ブランクあり)をAIが低評価する③法的リスク: 差別的なフィルタリングは採用差別に該当する可能性④候補者体験の悪化: AI不合格を受けた候補者からのレビュー(GlassDoor等)が企業イメージを傷つける。推奨構成: AIは「スコアリングと優先度付け」に特化し、採用担当者が全応募者の書類を確認した上でAIスコアを参考にする。AIスコア80点以上を優先面接に呼ぶ等、AIをフィルターでなく補助ツールとして使う。
Q. OpenResume(GitHubスター18k+)はどんなツールですか?
A. OpenResumeはNext.js製の履歴書作成・解析ツールです。2つの機能を提供します: ①履歴書ビルダー: Webブラウザ上でリアルタイムプレビューしながら履歴書をPDF出力できる②履歴書パーサー: アップロードしたPDFから氏名・職歴・スキル・学歴等を自動抽出してJSONで出力する。パーサーのコードをそのまま自社ATSに統合できます(MIT License)。
// OpenResumeのパーサーをAPIとして使う
import { extractResumeFromPdf } from 'resume-parser-and-generator'; // OpenResumeベース
export async function POST(req: Request) {
const formData = await req.formData();
const pdfFile = formData.get('resume') as File;
const pdfBuffer = Buffer.from(await pdfFile.arrayBuffer());
const resume = await extractResumeFromPdf(pdfBuffer);
// { name, email, phone, workExperiences, educations, skills, ... }
return Response.json(resume);
}
まとめ
| ユースケース | 推奨ツール |
|---|---|
| 履歴書PDF解析 | OpenResume + pdfplumber |
| NERモデル構築 | doccano + BERT |
| 意味的マッチング | Claude API |
| 応募者管理 | Supabase + Next.js ATS |