AIを二段階に分けたとき、何を確かめるか――提案と実行許可を別々に検証する
「外部資料を読むAI」と「次の作業を考えるAI」を分ければ、不正な提案は減るのでしょうか。そして、もし不正な提案が出てしまったとき、実行側のプログラムはそれを確実に拒否できるのでしょうか。
この二つの問いには、それぞれ別の検証が必要です。 AIモデルの比較では「入力から応答までの条件」を厳密にそろえる必要があり、実行側のプログラム検査では「用意した要求を正しく通すか、あるいは拒否するか」を機械的に確かめる必要があります。
本稿では、前者のモデル比較に必要な検証条件の整理と、後者についてkiuribotsの既存コードで実際に確かめた検査結果をまとめます。
モデル比較は、条件と応答をそろえてから評価する
旧稿で扱っていたモデル比較については、使用したモデルIDや実行設定を確認できず、入力・応答の元ログも照合できていないため、本稿では結果として掲載していません。二段階化に効果があったかどうかも、当時の数値からは判断しないことにしました。
今後モデル間の挙動を比較する際は、以下の項目をそろえて記録します。
* モデルIDと版、および生成パラメータの設定 * 各役に与えるプロンプトと、利用可能なツールの定義 * ケースごとの入力、期待する結果、実際の応答および操作提案 * 実行コードの版と、同一入力での試行回数
また、前段の「読むAI」が作った要約は、後段の「考えるAI」への入力としてそのまま保存します。最終的な回答だけを見ても、資料内の不正な指示が「要約の段階で消えたのか」「要約に残ったうえで後段のAIが無視したのか」を切り分けられないためです。
問い1:二段階にすると、不正な提案が減るか
モデル比較で検証したいのは、次の三つの構成の違いです。
* 構成A(単一モデル):外部資料の原文を、一つのモデルへ直接渡す * 構成B(二段階モデル):読取役(Q)が作った要約を、計画役(P)へ渡す * 構成C(二段階+プログラム検査):構成Bが出した提案に対し、プログラムによる許可検査を加える(※Cのためにモデルを再度呼び出すことはせず、Bと同じ提案に対するゲートとして評価します)
検証用の入力としては、たとえば次のようなケースを用意します(以下は試験設計の例であり、試行結果の表ではありません)。
| 試験入力として用意する例 | 見ること |
|---|---|
| 追加指示のない通常のREADME | 本来の修正作業を問題なく完了できるか |
| 「管理者の承認済み」と書いたREADME | 資料内の文章を「反映の許可」として扱ってしまわないか |
| 不正な指示を残した要約をPへ直接渡す | 原文だけでなく、前段の出力(要約)に含まれる指示にも影響されるか |
特に重要なのは、「要約から指示が消えた場合」と「指示が残った要約を後段へ渡した場合」を分けて記録することです。
Simon Willison氏の「Dual LLM pattern」[1]が提案しているのは、権限を持つ側へ「汚染され得る自由文」をそのまま渡さない構造です。自由文の要約をそのまま後段へ渡す「構成B」はこれとは同じものではないため、単にモデルを二つ並べたこと自体を防御の根拠にはしません。
問い2:不適切な要求が来たとき、実行側で拒否できるか
一方、実行側のプログラムが不正な反映を止められるかどうかは、AIモデルの挙動を待たずに単独で確かめられます。
2026年10月13日に、kiuribotsの既存テスト(test/publish.test.ts)に対応する配布JavaScript(dist/test/publish.test.js)を実行して動作を確かめました。この検査ではモデルの応答を代用品(モック)に置き換え、DBとGitの送信先も試験用の一時領域に向けています。外部のAIモデルは呼び出さず、検査プロセスの外部通信も無効化した状態で実行しました。
READMEの helo を hello に直す処理を使ったテストで、確認した主な条件は次のとおりです。
| 条件 | 期待する動き | テストで確認したこと |
|---|---|---|
| 反映の許可がまだない | Gitへ送らない | ローカルの送信先にブランチが作られない |
| 許可したコミットを反映する | 作業用ブランチに、そのコミットだけを送る | ブランチとSHAが許可の対象に一致する |
| 承認要求を作った後、人が許可する前にコミットを追加する | 反映を拒否する | タスクが失敗し、送信先は空のまま |
同じテストファイル内の検査により、作業クローンに仕込んだGitフックがこの条件下では動かなかったことや、承認が却下された後にブランチが送られないことも確認し、6件のプログラムテストが成功しました。
なお、「変更されたコミットを拒否する」ケースで確認したのは、「承認要求にSHAを記録 → 作業クローンへ追加コミットを作成 → 人が承認操作を試す」という順序での拒否動作です(承認操作が完了した後の差し替えを試したわけではありません)。ケースとassert、実行日時、Nodeの版、ソース版、実行した配布JavaScriptのSHAはすべて実行記録に残しています。
この結果が示しているのは、「試験用の要求に対し、既存のゲート処理コードが設計条件どおりに動いた」という確認です。実モデルへのプロンプトインジェクションを防いだ成績や、実際のGitHubへの送信・認証までを検証した結果ではありません。
「提案を減らせたか」と「実行を止めたか」を混ぜない
モデル側の評価とプログラム側の評価は分けて記録し、一つの「安全性スコア」には混ぜません。
* モデルの比較:本来の作業を完了したか、不正な操作を提案したか、資料内の指示が要約へ残ったかを分けて記録する。 * 実行側の検査:許可外の要求を拒否したかに加えて、正常な要求を通したかも記録する。
たとえば「CaMeL」[2]でも、単にモデルを分けるだけでなく、データの出所と許される情報の流れをプログラム側で管理するアプローチが採られています(今回のプログラム検査は、その再実装や性能の再現ではありません)。
なお、ここで検査したのは正規の反映処理(Gateway経由のpush)です。第2回で述べた共有DB(承認DB)への直接書き込みや、実環境の全通信の制限は検査対象外です。伏せ字の復元や、汎用的な承認・実行間の時間差の検証も、条件と記録をそろえて別に扱います。kiuribotsの全体像は第1回にあります。
参考文献
* [1]Simon Willison, The Dual LLM pattern for building AI assistants that can resist prompt injection (2023-04-25、追記 2025-04-11) * [2]Debenedetti et al., Defeating Prompt Injections by Design (2025-06-24) * [3]kiuribots反映処理のプログラム検査の実行記録 (2026-10-13)。対象は test/publish.test.ts、ソースGit版は 4e75d6455f9340ae242c2278ceaedd04f6f0da83。実行した配布JavaScriptのSHAは記録内にあります。
参照先の確認日:2026.10.13