はじめに
私はIT業界未経験からWeb開発を学んできました。そのアウトプットとして、情報処理安全確保支援士試験の過去問学習を支援するWebアプリ「支援士対策室」を個人開発し、公開しました。
自分が試験勉強で感じた不便を題材に、過去問の答案をAIで添削できるアプリとして設計・実装しました。
支援士対策室:https://shienshi.com
ユーザー登録後、無料で利用できます。
このアプリでは、過去問PDFを見ながら答案を入力し、問題文・設問・模範解答などを踏まえたAIの添削を受けられます。分からない設問について、同じ問題の文脈をもとにAIへ質問することもできます。
このアプリの設計で特に重視したのは、長い問題文や図表を含む試験問題の文脈を、どのようなデータとしてLLMへ渡すかという点でした。
この記事では、アプリを作った背景と、次の実装上の工夫を紹介します。
- ネットワーク構成図をPlantUMLの記法でテキスト化する
- 問題文・設問・模範解答・出題趣旨・採点講評を一つの文脈にまとめる
- Structured Outputsで添削結果を安定して扱う
- Redis Lockとレート制限で、無料公開するAI機能を守る
AIの回答は、必ずしも正確とは限りません。模範解答や参考書と照らし合わせながら、学習の参考としてご利用ください。
開発の背景
Web開発を学ぶ中でITの基礎知識を体系的に身につけたいと考え、基本情報技術者試験、応用情報技術者試験、情報処理安全確保支援士試験の学習に取り組みました。
午前問題(現在の科目A)の対策では、各試験を通して過去問道場にお世話になりました。一方、記述式の解答が中心となる支援士試験の午後問題では、同じように過去問演習を進められるアプリを見つけられませんでした。
午後問題(現在の科目B)では、長い問題文や図表から必要な情報を読み取り、限られた文字数で答案をまとめます。模範解答と異なる表現でも正解といえるのかを自分で判断するのは難しく、本番の問題選択や時間配分に慣れるためにも、過去問を一通り解くことが重要だと感じました。
学習中は、分からない用語や模範解答の前提知識を生成AIに質問し、理解を深めながら支援士試験に合格できました。ただし、設問そのものについて質問や添削を求めるには、問題文、設問、模範解答、図表の内容をその都度AIへ共有する必要があるため、過去問演習には活用していませんでした。
そこで、過去問の文脈をあらかじめアプリ側で整理し、「過去問を読む」「答案を書く」「添削を受ける」「分からない点を質問する」という学習の流れを、AIを活用して一つのアプリで実現したいと考えました。これが「支援士対策室」を開発したきっかけです。
作ったもの
「支援士対策室」では、主に次のことができます。
- 過去問PDFを閲覧しながら答案を入力する(モバイル表示では、答案入力のみとしています)
- 問題を解きながら、PDFにハイライトをつける
- 入力した答案について、AIによる添削を受ける
- 分からない問題についてAIへ質問する
- 答案の提出状況、進捗を確認する
PDFの閲覧、答案の入力、添削結果の確認を一つの画面にまとめ、問題を読んでいた文脈を保ったまま学習を続けられるようにしています。
使用技術とシステム構成
| 分類 | 主な技術 | 役割 |
|---|---|---|
| フロントエンド | React、TypeScript、react-pdf | 過去問PDFの表示、答案入力、添削結果の表示 |
| バックエンド | Laravel、OpenAI API | API、プロンプト構築、AI応答の検証・保存 |
| データベース・キャッシュ | MySQL、Redis | 試験・答案データの保存、AI処理の排他制御 |
| インフラ・CI/CD | Docker、Caddy、Xserver VPS、Amazon S3、GitHub Actions | 本番環境の構築、バックアップ、品質確認、デプロイ |
フロントエンドをReact、バックエンドをLaravelで実装し、Dockerコンテナで構成した同一VPS上で動かしています。
工夫した点1:図を含む試験問題の文脈をLLMへ渡す
ネットワーク構成図をPlantUMLの記法でテキスト化する
情報処理安全確保支援士試験の科目Bでは、ネットワーク構成図やシステム構成図が頻繁に登場します。
問題文だけをテキストとしてLLMへ渡すと、図に示された機器同士の接続関係、通信経路、ネットワークの境界といった情報が欠けてしまいます。しかし、これらは設問を解釈するうえで重要な情報です。
OpenAI APIでは、画像やPDFをそのまま入力することもできます。ただし、PDFを入力すると、抽出されたテキストに加えて各ページの画像もモデルへ渡され、画像も入力トークンとして計算されます。
(参考:OpenAI「File inputs」、OpenAI「Images and vision」)
そこで本アプリでは、図の見た目を再現するのではなく、LLMが設問を解釈するために必要な機器や接続関係をPlantUMLの記法でテキスト化し、問題文の一部として保存しています。AIへ渡すデータをテキストに統一して一度のリクエストで送ることで、画像やPDFを参照する処理を追加せずにLaravel側の実装をシンプルにしつつ、API利用料、処理時間を抑えられると考えました。
実際の過去問を例に、図をどのようにテキスト化したのかを紹介します。
出典:令和7年度秋期 情報処理安全確保支援士試験 午後 問2 図1「A社のシステム構成(抜粋)」
例えば、この図の主要な関係をPlantUMLの記法では、次のように表現できます。
@startuml
cloud "インターネット" as Internet
frame "クラウド基盤" {
rectangle "仮想FW" as VirtualFW
rectangle "VPNゲートウェイ" as VPNGateway
rectangle "顧客向け\nサーバ" as CustomerServer
rectangle "従業員向け\nサーバ" as EmployeeServer
rectangle "基幹\nサーバ" as CoreServer
rectangle "台帳\nサーバ" as LedgerServer
VirtualFW -- VPNGateway
VirtualFW -- CustomerServer
VirtualFW -- EmployeeServer
VirtualFW -- CoreServer
VirtualFW -- LedgerServer
}
frame "A社本社" {
rectangle "FW" as FW
rectangle "L3SW" as L3SW
collections "業務PC" as BusinessPC
frame "施設K" {
rectangle "鍵管理PC" as KeyManagementPC
}
FW -- L3SW
L3SW -- BusinessPC
}
Internet -- VirtualFW
Internet -- VPNGateway
Internet -- FW
@enduml
このように図の情報をテキストとして保持することで、問題文としてLLMへ渡せます。
試験回ごとの設問・解答欄をデータで表現する
情報処理安全確保支援士試験には、長文の記述、短い語句の入力、単一選択、複数選択など、さまざまな解答形式があります。解答欄の数や字数制限も、試験回や設問によって異なります。
試験回や設問ごとに専用の入力フォームやCSSを用意すれば、それぞれに合わせて、より入力しやすい画面を作ることもできます。一方で、過去問を追加するたびに個別の実装と調整が必要になり、対応する試験回を増やしにくくなります。
そこで本アプリでは、設問文、解答形式、選択肢、解答欄のラベル、字数制限などを、設問ごとのデータとしてquestionsテーブルにまとめています。React側では共通の親コンポーネントがこのデータを読み込み、解答形式に応じて記述、入力、単一選択、複数選択のコンポーネントを切り替えます。
設問ごとに入力フォームを個別最適化するのではなく、設問データを登録すれば共通の仕組みで表示できるようにすることで、入力のしやすさと、過去問を追加・保守するための時間とのバランスを取りました。
また、設問文と模範解答だけでは、AIが添削するための条件が不足する場合があります。例えば、二つの解答欄に入る語句は順番が逆でもよい場合や、模範解答に複数の説明が示されていても、答案ではそのうち一つを書けばよい場合です。
こうしたAIだけに必要な補足はtext_for_aiとして保存しています。公開用APIのレスポンスには含めず、AI添削時にだけプロンプトへ追加することで、利用者に表示する情報とAIの判断材料を分けています。
工夫した点2:Structured Outputsで添削結果を安定して扱う
AIの回答を自由形式のテキストで受け取ると、設問ごとの評価を画面へ表示したり、データベースへ保存したりする際に扱いづらくなります。そのため、開発当初から添削結果を決まったJSON形式で受け取るようにしていました。
当初はStructured Outputsがまだ提供されていなかったため、Function Callingの引数として添削結果を返してもらう方法を採用していました。実際に外部の関数を実行するのではなく、関数の引数を構造化されたJSONの受け取り口として利用していました。
その後、Structured Outputsが利用できるようになったため、現在はResponses APIのtext.formatにJSON Schemaを指定する方式へ移行しています。
'text' => [
'format' => [
'type' => 'json_schema',
'name' => 'answer_evaluations',
'schema' => $schema,
'strict' => true,
],
],
JSON Schemaでは設問コード・評価・コメントを必須にし、評価を「◯・△・×」のいずれかに制限しています。さらにLaravel側でも各値の形式を確認してから保存することで、画面表示や保存処理への影響を抑えています。
以下は実際のサービス内での添削例です。
問題は先ほどシステム構成図を引用した令和7年度秋期 情報処理安全確保支援士試験 午後 問2の設問2を使用しています。(本文中の下線①は「サイドチャネル攻撃」)
答案の方向性は合っているものの、具体的な観測対象の記述が不足しているとして、評価は「△」になりました。
出典:令和7年度秋期 情報処理安全確保支援士試験 午後 問2
工夫した点3:無料公開するAI機能を守る
このアプリは学習目的で無料で公開しており、OpenAI APIの利用料は開発者である私が負担しています。できるだけ多くの方に使ってもらえる状態を保ちながら、二重実行や過剰なリクエストによる意図しない費用の増加を防ぐ必要があります。
AI添削や質問への回答には、数十秒かかる場合があります。二重送信によって同じ処理が並列実行されると、API利用料が重複するだけでなく、データの不整合にもつながります。
フロントエンドで送信ボタンを無効化するだけでは、別タブやAPIへの直接リクエストまでは防げません。そこで、バックエンド側でもRedis Lockを取得してからAI処理を実行しています。
$lock = $store->lock($key, 180);
if (! $lock->get()) {
throw new AiRequestInProgressException('already_processing');
}
try {
return $callback();
} finally {
$lock->release();
}
ロックキーにはユーザーIDと試験・設問の識別子を含め、同じ単位のAI処理が同時に実行されないようにしています。例外が発生した場合も、finallyでロックを解放します。
また、Redis Lockとは別に、短時間の連続リクエストと1日あたりの利用回数にレート制限を設定しています。
- Redis Lock:同じAI処理が同時に走ることを防ぐ
- レート制限:一定時間内に呼び出せる回数を制限する
役割の異なる二つの仕組みを組み合わせ、意図しない二重実行と過剰利用の両方に対応しています。
公開・運用するために行ったこと
個人開発でも、ユーザー登録を受け付け、外部のAI APIを利用するアプリを公開する以上、機能を作るだけでなく、安全に運用するための仕組みが必要だと考えました。
このアプリでは、APIコストの発生するAI機能には認証を求める一方、認証のために保管するユーザー情報は必要最小限にすることを重視しました。
AI機能の過剰利用を防ぐためのユーザー認証
過去問はユーザー登録やログインなしで閲覧できます。一方、AI添削にはAPIの利用料が発生するため、ユーザー登録とログインを必須にしました。匿名のままAI機能を繰り返し呼び出せる状態を避け、誤操作による連続利用や悪意のある大量アクセスによって、意図しないAPIコストが発生するリスクを抑えるためです。
認証に加えて、前述したRedis Lockやレート制限を組み合わせることで、AI機能の二重実行や過剰利用を抑えています。
認証に必要なユーザー情報を最小限にする
AI機能を保護するために認証は必要ですが、そのために必要以上の個人情報を預かる必要はないと考えました。当初はメールアドレスを使って認証する方式で実装していましたが、最終的には任意のユーザー名とパスワードだけで登録できる形に変更しました。
登録時の心理的なハードルを下げるとともに、アプリが収集・保有する個人情報をできるだけ少なくし、万一情報が漏えいした場合の影響を抑えることを意図しています。
一方、メールアドレスを保持しないため、パスワードを忘れた場合の再設定はできません。その場合、以前の学習履歴にはアクセスできなくなりますが、新しいアカウントで学習を再開することはできます。この不便さとのトレードオフを踏まえ、サービスの用途に対して必要以上の個人情報を持たないことを優先しました。
CI/CDとデプロイ
依存関係の監査、静的解析、テスト、型チェックをGitHub Actionsで実行しています。
mainブランチへのpushを契機にこれらの確認を行い、すべて成功した場合のみDockerイメージをGitHub Container Registryへpushし、VPSへデプロイする構成です。
デプロイ先としてAWSも検討しましたが、継続的な運用コストを考慮してアプリ本体はVPSで運用し、データベースのバックアップ先にはAmazon S3を利用しています。
まとめ
情報処理安全確保支援士試験の学習で感じた不便をきっかけに、過去問の答案をAIが添削するWebアプリを開発しました。
開発を通して、LLMをアプリへ組み込むには、APIを呼び出すだけでなく、判断に必要な文脈をどのようなデータとして渡すかが重要だと学びました。特に、ネットワーク構成図をPlantUMLの記法でテキスト化したことは、このアプリで工夫した点の一つです。
情報処理安全確保支援士試験は、2027年度から新試験制度へ移行する予定です。今後の出題範囲や形式を確認しながら、アプリも対応・改善していきたいと考えています。
実際に使ってみた感想や、実装についてのフィードバックをいただけるとうれしいです。



