本記事のコードは、個人で運営しているクイズサイトで使っているものを一般化したものです。
しきい値は多肢選択式の作問ガイドで使われる目安で、どの試験にも当てはまる基準ではありません。
はじめに
新卒エンジニア向けのクイズサイトを作るにあたって、約 720 問の 4 択・○× 問題を生成 AI(Claude)の支援を受けて作りました。
別の AI にレビューさせ、SQL は実際に実行して正答を確かめ……と、正しさは何重にも確認していました。ところが公開後に機械的に調べてみると、正しさとは別の欠陥が大量に見つかりました。
- エンジニア英語の問題では、94% の問題で正答が一番長い選択肢だった(偶然なら 25% 前後)
- ある分野の ○× 問題は、8 問中 7 問の答えが × だった
どちらも、受験者が「一番長いのを選ぶ」「迷ったら × を選ぶ」だけで点が取れてしまう状態です。この記事では、
- 回答データが無くても分かる偏りを、テストで検出して再発を防ぐ方法
- 回答データが溜まったあとに、問題の質を測る項目分析(識別力・誤答の機能)
の 2 つを、コードつきで紹介します。
なぜ AI が作ると正答が長くなるのか
推測ですが、AI は「正答は正しいことを説明しなければならない」ので条件や理由を書き足し、誤答は「誤りだと分かればよい」ので短く済ませがちです。実際の問題(直す前)はこうでした。
Q. Git の `hook`(pre-commit hook など)とは?
1. コミットなど特定のタイミングで自動的に実行される処理 ← 正答
2. 釣り針や洋服掛け
3. 曲のさびの耳に残るフレーズ
4. コミットを禁止する設定
日常の意味を誤答に入れる方針自体は良いのですが、長さだけで答えが分かります。直したあとはこうなりました。
1. コミット時など決まったタイミングで自動で動く処理 ← 正答
2. 物を引っ掛ける釣り針や、洋服を掛けるフック
3. 曲の中で何度も繰り返される、聴く人の耳に残るフレーズ
4. ブランチへのコミットを一律に禁止する保護設定
誤答を「実際にやりがちな間違い」のまま具体的に書き足すか、正答の冗長な修飾を削ります。正答の意味は変えないのがポイントです。
1. 回答データなしで検出するテスト
問題は JSON で持っていて、node --test で全問を検査しています。偏りの検査は次の 3 つです。
正答だけが目立って長い問題
// 4 択: 正答が 2 番目に長い選択肢の 1.3 倍以上、かつ 6 字以上長ければ失敗
const plainLen = (s: string) => s.replaceAll('`', '').length;
test('正答だけが目立って長い選択肢になっていない', () => {
const bad: string[] = [];
for (const q of ALL) {
if (q.kind !== 'choice') continue;
const lens = q.choices.map(plainLen);
const other = Math.max(...lens.filter((_, i) => i !== q.answer));
if (lens[q.answer] >= other * 1.3 && lens[q.answer] - other >= 6) {
bad.push(`${q.id}(正答 ${lens[q.answer]} 字 / 他の最長 ${other} 字)`);
}
}
assert.deepEqual(bad, []);
});
「1.3 倍かつ 6 字以上」の 2 条件にしているのは、短い選択肢(ls -a と ls -l など)で数字だけの比が大きく出るのを避けるためです。
ジャンル全体で「正答が最長」の割合
1 問ずつは基準内でも、ジャンル全体で正答がいつも少しだけ長い、という偏りもあります。こちらは割合で見ます。
test('ジャンルごとに「正答が最も長い」問題は 60% まで', () => {
const by = new Map<string, { n: number; longest: number }>();
for (const q of ALL) {
if (q.kind !== 'choice') continue;
const lens = q.choices.map(plainLen);
const s = by.get(q.genre) ?? { n: 0, longest: 0 };
s.n++;
if (lens[q.answer] > Math.max(...lens.filter((_, i) => i !== q.answer))) s.longest++;
by.set(q.genre, s);
}
const bad = [...by].filter(([, s]) => s.n >= 10 && s.longest / s.n > 0.6);
assert.deepEqual(bad, []);
});
○× の偏り
test('○× の ○ の割合が 25〜75%(6 問以上あるジャンル)', () => {
// ジャンルごとに ○ の数を数えて、範囲外なら失敗
});
○× を直すときは、answer だけを反転してはいけません。「〜はできる。→ ×」を「〜はできない。→ ○」のように、文を正しい内容に書き換えてから答えを ○ にします。論点(何を学ばせたいか)はそのまま残ります。
結果
| ジャンル | 正答が最長(前 → 後) |
|---|---|
| エンジニア英語 | 94% → 19% |
| 指摘・レビュー | 79% → 35% |
○× は、答えが × に偏っていた分野を 1/8 → 3/8 に直しました。以後はテストが落ちるので、新しく問題を足しても同じ偏りは入りません。
2. 回答データが溜まったら:項目分析
長さや○×の偏りは「見た目」の欠陥です。問題が実力を正しく測れているかは、回答データが無いと分かりません。そこで、テストの古典的な手法である項目分析を使います。
サイトは採点をブラウザで行い、1 回ごとに使い捨ての run_id を付けて、解いた問題・正誤・選んだ選択肢を匿名で集計しています。run_id を「受験者 1 人」とみなし、その回のほかの問題の正答率を実力の目安にします。
識別力(点双列相関)
正解した人ほど実力が高ければ、その問題は実力を測れています。
/** 2 値の変数 flag と連続値 score の点双列相関 */
export function pointBiserial(flags, scores) {
const ones = scores.filter((_, i) => flags[i]);
const zeros = scores.filter((_, i) => !flags[i]);
const s = sd(scores);
if (!ones.length || !zeros.length || s === 0) return null;
const p = ones.length / scores.length;
return ((mean(ones) - mean(zeros)) / s) * Math.sqrt(p * (1 - p));
}
実力の目安には、その問題自身を除いた正答率を使います(含めると相関が水増しされます)。
const rest = (run.correct - (ok ? 1 : 0)) / (run.answered - 1);
- 0.2 未満:実力と関係なく当たり外れが出ている(当てずっぽうで当たる、読み違いで決まる)
- 負:実力のある人ほど間違える。正答が誤っている疑いが強い
実力のある人ほど選ぶ誤答
いちばん効く指標がこれです。誤答を選んだ人の平均の実力が、正解した人の平均以上なら、その誤答に上位者が流れている=問題文があいまいか、その誤答も条件次第で正しい可能性があります。
for (const d of distractors) {
if (d.n >= 5 && d.mean >= correctMean) {
flags.push(`選択肢 ${d.choice} を実力のある人が選ぶ: あいまい・別解の疑い`);
}
}
最初は「誤答を選んだかどうか」と実力の相関の符号で判定しようとしましたが、ダミーデータで試すと検出できませんでした。上位者が流れる誤答にも下位の人が混ざり、相関が打ち消されるためです。平均の比較に変えたところ、仕込んだ問題だけを検出できました。
選ばれない誤答
選ばれる割合が 5% 未満の誤答は、選択肢として機能していません。差し替え候補です。
判定は週 1 回、自動で Issue に
GitHub Actions で毎週集計し、見直し候補を Issue にまとめています。各問題に 30 件以上の回答が溜まったものだけを判定します。
おまけ:経験年数で逆転する問題
結果画面で、任意でエンジニア歴を答えてもらっています。ベテランほど間違える問題は、特定の現場の作法を正解にしてしまっている疑いがあります。逆に、経験年数で正答率に差がつく問題は、経験の差=現場の勘所を測れている問題です。
まとめ
- AI に作らせた 4 択は、正答だけ丁寧に長く書かれがち。正しさのレビューとは別に、機械的な検査が要る
- 回答データが無くても、選択肢の長さと ○× の割合はテストで検出・防止できる
- 回答データが溜まったら、**識別力と「上位者が選ぶ誤答」**で、問題の質そのものを測れる
使っているサイトはこちらです。新卒研修の確認テストにも使えるように作っています。
- エンジニア瞬発力テスト: https://engineer.keydrop.net/
- 作った経緯の記事: https://www.keydrop.net/posts/engineer-keydrop-quiz-development/
参考
- Haladyna, Downing & Rodriguez (2002), "A review of multiple-choice item-writing guidelines for classroom assessment", Applied Measurement in Education 15(3)
- 多肢選択式の作問ガイド(識別力の目安 0.2〜0.3 以上、誤答は 5% 以上選ばれる): KPU Teaching and Learning Commons