こんにちは!ひさふるです。
最近、こんな記事↓を書いているときに直面した問題があり、面白そうだったので1本の記事にしました。
それは、AIには4択問題が作れないというものです。
AIは4択問題が作れない問題
上記記事では、AIに実装したコードに関する4択問題を生成させる、ということをしていました。
しかし、単に「4択問題を生成して」と指示するだけでは、ほぼ100%と言っていいほど最初の選択肢が正解となってしまったのです。
例) 「Anthropicが開発したAIコーディングツールは?」という問題があったとき
| 番号 | 内容 |
|---|---|
| A | Claude Code ⇐ この位置が必ず正解になる |
| B | Cursor |
| C | Gemini CLI |
| D | Codex |
("AIは4択問題が作れない"は正直釣りタイトルですが...正確には"AIは4択問題の正解をランダムに設定するのが非常に苦手である"というのが実際のところです)
思えば、ずっと昔からある問題だったように感じます。同様の事例に直面したことのある方は多いのではないでしょうか?
AIに4択問題を作らせると、大抵の場合、最初の選択肢が正解になる
なぜAIは最初の選択肢を正解にするのか
(私の推測混じりですが)考えられる要因があります。
昨今、生成AIと言われるものの中で主流である、テキストを入力しテキストを得るタイプのモデルは、正確にはLLM (Large Language Model)と呼ばれます。
LLMは、内部的には入力テキストに対して、続く確率の高い単語(トークン)を少しずつ繋げるように生成する、という挙動をしています。
今回の場合では、問題文に対して正解のテキストの方が必然的に関連性が高くなるので、連続で生成されやすくなるのでは、と考えています。
また、4択問題中に正解は絶対に含めなければなりませんが、不正解の選択肢は何があっても大丈夫です。
よって、1番目に正解を置き続く3つは適当なものを生成したほうが、生成プロセスとしてラクでしょう。
以上のような様々な要因が絡み合って、このような挙動になっていると推測しています。
素人考えなので、専門家の方がいましたらぜひご見解をお願いします🙇
プロンプトだけで対策できるのか?
では、プロンプトで「4択問題で、正解を置く順番はランダムにしてください」と指示した場合はどうでしょうか?
私の実験の場合
最初のご紹介したこの記事内で、Claude Codeがコード実装時に確認の4択問題を出すという機能を作っていました。
そこで、コードに関する4択問題を生成するプロンプト内に以下のようなプロンプトを含めてみました。
【最重要】選択肢は生成後、必ずランダムにシャッフルして提示すること。
問題はA ~ Dの4択で、1回につき3問まとめて生成されます。これを3回試行し、3 x 3 = 計9問を生成させてみました。
結果は8/9の問題でAが正解という、非常に偏った結果になりました。
単純なプロンプトでは、正解の選択肢をランダムにすることは難しい
もうちょっとちゃんとした研究では...
「指示の仕方が悪いんじゃないの?」という方のために。
arxivに以下のような論文がアップロードされていました。
タイトルは「大規模言語モデルはサイコロを振るのが下手である」のような感じでしょうか。
この論文中でも、今回の4択問題のような多肢選択式問題(MCQ)を生成させる実験を行っており、選択肢A~Dが一様に25%の確率で生成されるようにせよ、とした場合でも特定の選択肢に正解が集中するという強いバイアスが見られたそうです。
A(最初の選択肢)だけに正解が集中するということは防げるようですが、モデルによってBやCが正解になる確率が40%以上のモデルもあったようです。
そのため、
A ~ Dが一様に25%の確率で正解の選択肢となるように調整すること
のようなプロンプトには一定の効果が見られるものの、問題を根本から解決するような効果は得られない、ということがわかりました。
研究においても、AIモデルが乱数生成器として使えないことが確かめられている
根本解決を目指すには
プロンプトによる解決が不可能な以上、最も確実な解決方法は外部の乱数生成器に頼る、という方法です。
今回は、Claude Codeでコードを実装後にそれに関する4択問題をユーザーに出題するというOutput Styleに、長さnのランダムに並び替えられた数値配列を生成するというSkillを使わせることで、ランダム性を担保してみたいと思います。
ちなみに、Output StyleやSkillsについてはClaude Codeの機能で、内容は以下の通りです。
- Output Style:Claude Codeの根本的な性格や挙動を決めるための、システムプロンプト。
- Skills:Claude Codeが使うことができる、特定の操作を行うときに使える指示書やツールのようなもの。
今回使っているOutput Styleは以下の記事で紹介していますので、良ければこちらも参照してください!
まず、フォルダ構成はこちら。
.claude
├── output-styles
| └── aiboss.md
└── skills
└── randperm
└── SKILL.md
└── scripts
└── randperm.py
Output Styleファイルはコチラ。
元記事では設定の簡略化を優先して、プロンプトだけでランダムな出力をさせています。
検定フェーズにおける選択肢の生成方法の部分が、この記事独自の差分になります。
---
name: Boss Mode
description: 上司として振る舞い、実装したコードに対しての問題を提示するモード
---
## Boss Mode (上司モード: 実装加速&適応型検定)
このモードは、開発のスピード感を極限まで高めつつ、事後のインタラクティブなコードクイズによって実戦的な理解を深める。
### 1. 実装フェーズ(最短経路)
- **自律的プロトタイピング:** 冗長な確認や前置きは不要。プロフェッショナルとして最適と判断した実装を即座に提供・適用せよ。
- **事後解説主義:** 実装中の解説は最小限に留め、ユーザーの集中を妨げるな。
### 2. 検定フェーズ(コードベース・適応型クイズ)
- **出題のタイミング:** 実装完了後、または `done` 等の合図で実行。
- **形式:** Claude Codeの **「選択型UI(Interactive Selection)」** を使用し、**「2〜4問」** 出題せよ。
- **クイズの設計指針:**
- 内容は固定せず、**「今回変更したコードにおいて、ユーザーが最も理解しておくべき点」** をClaudeが自由に選定すること。
- 実際のコードスニペットや代替案を比較対象として提示し、コードリーディング能力を問う内容にせよ。
- エッジケース、副作用、あるいは設計上のトレードオフなど、その場の文脈で最も価値のある問いを投げよ。
#### 検定フェーズにおける選択肢の生成方法
- 4択(多肢選択)ルール:必ず randperm を使う
- 多肢選択の出題では、選択肢の順番は **必ず `randperm` スキルで決める**。
- 生成順(正解→誤答…)のまま並べるのは禁止。
- 手順(固定)
1. まず「正解1つ+誤答(合計N個)」を作る(この時点で順番は決めない)
2. **`/randperm N`** を実行して順列(例: `[2,0,3,1]`)を得る
3. その順列で選択肢を並べ替える
### 3. 報酬(Senior's Insight)
- **全問正解時:** 「完璧なリーディングです」と称賛し、報酬として、今回の実装に関連する一歩先のトピック(デザインパターン、ベストプラクティス、計算量、スケーラビリティ等)を3〜5行で簡潔に伝授せよ。
- **誤答時:** 簡潔に技術的根拠を示し、理解を修正せよ。
### 4. スタンス
- 実装中は「無言で結果を出す上司」、検定時は「コードの深淵を問うメンター」として振る舞え。
SKILLのファイルはこれです。
randpermという名前で、任意の長さNのランダムに並び替えられた数列を得られます。
今回は4択問題を強く意識した書き方になってしまっていますが、書き方次第で様々な状況下で使えるようになります。
---
name: randperm
description: 4択などの選択肢の並び順をランダム化するための「順列」を作る(位置バイアス対策)
allowed-tools: Bash(python3 .claude/skills/randperm/scripts/randperm.py *)
---
# randperm(順列生成)
## 目的
多肢選択(4択など)で「正解が先頭に固定される」問題を避けるため、選択肢の並び順を必ずランダム化する。
## 使い方
1) 選択肢数 N を渡して順列を作る
`python3 .claude/skills/randperm/scripts/randperm.py <N>`
2) 出力は JSON 配列(例: `[2,0,3,1]`)。これを使って選択肢配列を並び替える。
3) 並び替え後の「正解が何番になったか」を必ず計算して保持する。
- 正解を固定位置(1番目など)だと決めつけない
SKILL内で使用するプログラムはコチラ。
#!/usr/bin/env python3
import argparse
import json
import secrets
def main():
p = argparse.ArgumentParser()
p.add_argument("n", type=int)
args = p.parse_args()
n = args.n
if n <= 0:
print("[]")
return
perm = list(range(n))
secrets.SystemRandom().shuffle(perm) # OS由来の乱数でシャッフル
print(json.dumps(perm)) # 例: [2,0,3,1]
if __name__ == "__main__":
main()
これにより、Claude Codeが何らかの実装を行うたびに検定フェーズとしてユーザーに問題を出題してくれるようになるのですが、このときにrandperm.pyを使って乱数を生成し、ランダムに選択肢を並び替えてくれるようになる、というわけです。
実際に動かしてみると、問題の出題の前にSkill(randperm)と表示されており、スキルが使われていることがわかりますね。
おわりに - 今回から得られる示唆
万能に見える生成AIですが思わぬ落とし穴がありましたね。
ここから得られる示唆としては、単に4択問題の生成が苦手だというほかにも、生成AIはランダム生成が苦手(強力なバイアスがかかる)な可能性が高いということです。
これを理解しておかないと、AIで生成したコンテンツに何らかの偏りが生じる可能性が高いでしょう。
また、(そんなことする人はいないと思いますが)、例えば生成AIにパスワードを生成させるなんて使い方は最悪ですね。簡単に予測できる可能性がありそうです。
解決策としては、先程の論文の言い回しを借りるなら、「AIにサイコロを持たせる=(乱数生成器をツールとして持たせる)」ことが重要です。
AI自身に、出力にバイアスがかかることを理解させ、要所々々で乱数生成器を適切に使用させることが、より自然な確率分布に従う出力を担保するコツとなりそうです。
あなたのエージェントにも、ぜひサイコロを持たせてみてください。
今回も、最後までお読みいただきありがとうございました。
