はじめに
...応用情報技術者試験の午後試験の過去問演習ってめんどくさくないですか?!
これはひょっとして僕だけが抱えている悩みなのでしょうか...いやそんなはずがない。(反語)
僕の嘆きをまずはちょっと聞いていただきたい。ブラウザバックするのはその後でも遅くないはずです!
IPAの午後試験の過去問演習のここがめんどくさい!!その1!!!
解説がない
基本的に勉強って問題を解く→解説を見る→へぇってなる
の繰り返しだと思っているんですよ。まぁもちろんただひたすらに教科書みたいなのを読む勉強もありますが、こと資格試験の過去問演習ではこのループの繰り返しであることに異論のある方はあんまりいないんじゃないかと思います。
このループの大事な「解説を見る」をできないのが、IPAの午後試験の過去問演習だと思ってます。IPA公式が出している過去問PDFでは全く解説なんか載っていませんし、我らが過去問道場さんでもまだ手が回っていない様子です!
じゃあ解説付き過去問演習はできないのか?!と思ったそこのあなた!手段がないわけではないんです。あるにはあるんです。そう本で勉強すればいいんですね!
すみません読んではいないのでわからないですが、どうやら表紙には解説付きとあるので多分解説付きなのでしょう。
今世の中には本は二種類ありますね。俺か俺以外かではなく、紙か電子です。じゃあ例えば紙を買ったとしましょう!
IPAの午後試験の過去問演習のここがめんどくさい!!その2!!!
本が重い
我々は忙しいですね?大体の人は週5日毎日8時間労働をしているわけです。まぁブラックな人はもっと働いているかもですが...。そんな状況でなんとか勉強の時間を捻出しようと思ったら通勤の電車の中が最も効率よく勉強できる時間なわけです。ですがそんな満員電車の中で分厚い過去問演習の本を持っていけますか?はいみなさんご唱和ください!せーの
無理!!!
ありがとうございます。そんなの物理的に無理なわけです。じゃあ電子なら良いじゃないかとそう思われる方もいらっしゃるでしょう。
IPAの午後試験の過去問演習のここがめんどくさい!!その3!!!
書き込み問題が解けない!!!
書き込み問題ってこういうのを想定しています。線を付け足して完成させなさいみたいなやつですね。
読者の皆さんがどの電子書籍リーダーをお使いになっているかわからないのでなんとも言えないんですが、少なくとも私が使っている電子書籍リーダーは書き込みができません。書き込み問題も解けないですが、他の普通の問題もまぁメモ帳に解くとかしないと過去問演習ができないんですよね。
IPAの午後試験の過去問演習のここが 最高に めんどくさい!!その4!!!
スマホ一台で完結して過去問演習ができない!!!
何かと管を巻いてギャーギャー行ってきましたが、結局はこれに尽きるんです。僕はスマホ一台で応用情報技術者試験の午後問題の過去問演習をしたいんです!
でもそれを可能にしてくれるものはどこにもない...
ながす) じゃあ作るか!
となって作り上げたのがこの過去問記述ラボです!
長い「はじめに」になりましたが、この記事では
- どういうサービスか
- 技術構成はどうしたか
- どこが実装のこだわり・苦労したこと・工夫したこと
あたりを書いていこうと思っています。単なるサービス紹介ではなくて、何か読者の皆さんの身になればいいなと思って書いていますので是非読んでいってください!なんだこいつ?意味わかんねぇなとか文章がくどいなぁとか思ってもブラウザバックしないでください。フックとしてちょっとテンション高めで書いただけでこれから真面目になる予定なので。(あくまで予定)
過去問記述ラボの概要
さっくりとサービス概要を説明させてください。個人的には結構いいサービスができたと思っているので自慢させてください。まぁ他の誰が使わなくとも僕だけは使い続けるだろうなと思ってくらいには気に入っていますし、便利だと思っています。
過去問記述ラボの目的
まずサービスの目的としては長々と先ほど語りましたが、スマホ一台で応用情報技術者試験の午後問題の過去問演習をできるようにすることこれが至上目的です!
それ以外の達成したい目的としてはさらに効率的に過去問演習ができるようにするというのもありこの二点がこのサービスの達成すべき目的になります。
過去問記述ラボの機能
目的を達成するため、過去問記述ラボにつけたメイン機能は2つです!そのほかにも多くのこだわりはあるのですが、あまりに長すぎる記事だときっと読む気をなくすと思うので、今回はこの2つに絞って紹介していきます。
- 書き込み問題への対応
- AI採点の導入
書き込み問題への対応
書き込み問題があった場合は、画像をタップすると拡大されてスマホの画面を指でなぞるとなんとびっくり、赤い線を書き込むことができるんです!
HTMLのcanvasを使って実現しています!全然知識がなかったので、
ながす) うわぁ...JSでなんかゴリゴリ書かないと線を描くとかできなさそうだなぁ...
とか思ってたのですが、HTML様様です!結構昔からあるらしく僕が無知なだけでしたが、便利な機能がHTMLに実装されていて非常に助かりますね!
長いので畳みますが、こんなコードで実装してみました。こうやってみるとリファクタリングできそうな箇所がいくつかあって恥ずかしいですが、あとで直すと心に決めて恥を晒そうと思います。
canvasのコード
"use client";
import { Button } from "@heroui/button";
import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
interface Coordinate {
x: number;
y: number;
}
interface DrawingPath {
points: Coordinate[];
color: string;
size: number;
}
type CanvasProps = {
originalImageUrl: string;
imageUrl: string;
aspectRatio: number;
onSave: (dataUrl: string) => void;
};
const Canvas: React.FC<CanvasProps> = ({
originalImageUrl,
imageUrl,
aspectRatio,
onSave,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const imageRef = useRef<HTMLImageElement | null>(null);
const [isDrawing, setIsDrawing] = useState<boolean>(false);
const [, setDrawingPaths] = useState<DrawingPath[]>([]);
const currentColor = "#ff0000"; // ブラシ色
const brushSize = 2; // ブラシサイズ
// キャンバスの初期化
const setupCanvas = useCallback(
(srcUrl?: string): void => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
// 横幅基準でサイズ決定(例: 最大600pxに)
const displayWidth = Math.min(window.innerWidth, 600);
const displayHeight = displayWidth / aspectRatio;
// ピクセル密度を考慮したcanvasサイズ
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
// 見た目サイズはCSSで
canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;
// スケーリング(リセットしてからスケール)
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = (): void => {
ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
imageRef.current = img;
};
img.src = srcUrl || imageUrl;
},
[imageUrl, aspectRatio],
);
useEffect(() => {
setupCanvas();
}, [setupCanvas]);
// キャンバス上のイベント座標を取得
const getCoordinates = (
e: React.PointerEvent<HTMLCanvasElement>,
): Coordinate => {
const canvas = canvasRef.current;
if (!canvas) return { x: 0, y: 0 };
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
};
// 描画開始
const startDrawing = (e: React.PointerEvent<HTMLCanvasElement>): void => {
e.preventDefault();
const coords = getCoordinates(e);
setIsDrawing(true);
setDrawingPaths((prev) => [
...prev,
{ points: [coords], color: currentColor, size: brushSize },
]);
};
// 描画中
const draw = (e: React.PointerEvent<HTMLCanvasElement>): void => {
if (!isDrawing) return;
e.preventDefault();
const coords = getCoordinates(e);
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!ctx) return;
setDrawingPaths((prev) => {
const paths = [...prev];
const currentPath = paths[paths.length - 1];
currentPath.points.push(coords);
// 線を描画
ctx.globalCompositeOperation = "source-over";
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.strokeStyle = currentPath.color;
ctx.lineWidth = currentPath.size;
const pts = currentPath.points;
if (pts.length > 1) {
const from = pts[pts.length - 2];
const to = pts[pts.length - 1];
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
}
return paths;
});
};
// 描画終了
const stopDrawing = (): void => {
setIsDrawing(false);
};
// 描画をクリアして元画像を再描画
const clearDrawing = (): void => {
setupCanvas(originalImageUrl); // 元画像を指定して初期化
setDrawingPaths([]);
};
// キャンバスを PNG にしてコールバック
const saveImage = (): void => {
const canvas = canvasRef.current;
if (!canvas) return;
const dataUrl = canvas.toDataURL("image/png");
onSave(dataUrl);
};
return (
<div className="flex flex-col gap-4">
<div className="flex justify-end gap-2 p-2">
<Button onPress={clearDrawing} variant="bordered">
消去
</Button>
<Button onPress={saveImage} className="bg-black text-white">
保存
</Button>
</div>
<canvas
ref={canvasRef}
onPointerDown={startDrawing}
onPointerMove={draw}
onPointerUp={stopDrawing}
onPointerLeave={stopDrawing}
className="rounded cursor-crosshair touch-none"
style={{
touchAction: "none",
maxWidth: "100%",
maxHeight: "70vh",
}}
/>
</div>
);
};
export default Canvas;
AI採点の導入
例えば過去問記述ラボで応用情報技術者試験の過去問演習をしていたとしてこんな解答をしたとしましょう。
「ハッシュ値だから」だとちょっと足りないのはわかる。しかしながら、模範解答と比べてどの要素が足りないかはちょっとわからない。採点基準もわからないし。かといって半分得点するのはやり過ぎかなぁ...。など色々考えるわけですが結局最適な採点はよくわからず丸つけがしづらいという問題を慢性的に抱えていました。少なくとも僕は。
そんな時にAI採点をポチッとすれば!!!
こんなふうに推定配点のうちの何点分得点していて、かつあなたの解答にはどういう要素が足りないか?まで教えてくれるのです!
今回の文だと不可逆性の要素がどうやら足りないみたいですね!じゃあ例えば
「ハッシュ値を元に戻すのは困難だから」のような解答をした時にどうなるのかみてみましょう。個人的には満点をつけても良い解答かなとは思います。模範解答とは違う解答ですが、これを満点にしていいか自分で採点するなら少し迷うラインだと思います。
試してみましょう。
素晴らしい!きちんと満点が取れていますね!このように記述問題の自分でやると曖昧になりがちな採点をきちんとキーワードが入っているか、模範解答と言っていることの本質はあっているかなどの観点からAIが柔軟に自動で丸つけをしてくれます!
そしてその解答に即した解説を自動で作ってくれるのでより効率的に学習ができること間違いなし!
なぜ間違えたのか。どうすれば得点が取れるようになるのか。 この辺りをきちんと簡単に教えてくれるサービスってあんまりなかったように思います。だから過去問記述ラボは便利なんです。
ただAIを無料で使えるわけではないので、AI採点機能は有料にしました。↓のキャンペーンなどで安く使えるようにしているので、気軽に使ってもらえればと思います。
過去問記述ラボの技術構成
すみません。長らくお待たせしました!サービス概要なんて興味ないですよね!qiitaの記事なんですから必要なのは技術的に参考になることを書けよという読者の皆さんからの声が聞こえてきます。
ここでは過去問記述ラボの技術構成について話していこうと思います!技術構成としては以下です。
- Next.js(App Router)
- Supabase
- Vercel
わお!モダン!なんて幻聴が聞こえてきました。モダンな割には手垢がつきまくった構成ですが...それぞれ一つずつなぜ選定したかと使ってみた感想とか工夫あたりを語ろうと思います。
Next.js(App Router)
まず過去問記述ラボはフロントエンドとバックエンドを一つのサーバーで設計しました。Vercelに全部載せてしまいました。
その設計にした理由としては
- 本サービスの価値が、問題を効率的に解けるところとAI採点にあると思ったので、サーバーの担う価値が少なく、UI/UXの部分が価値の多くを占めていると思ったこと
- サーバーの用途がAI採点のあれこれと認証まわりくらいしかなかったこと。(問題等はほぼ不変なのでDBではなくサーバーにおいています)
というような理由です。別でサーバーを建てるのほどのサイズ感のバックエンドにはならないだろうという見通しで一つのサーバー具体的にはVercelのサーバー一つにしています。またバックエンドが肥大化してきたら...その時考えようと思っていますが、基本的にはそんなに機能を追加するつもりはないので大丈夫だろうとタカを括っています。
方針としては
- 認証まわりやAI採点などはServer Actionsを用いて、OG画像などはRoute Handlers(/api)を使ったりしています
- またプロジェクト全体をフルTypeScriptで統一して、型の共有でフロント/バックエンド間の齟齬を減らしています
得られたメリットとしては型の一元化がめちゃくちゃ便利。フロントとバックで型が共有とれているので不整合が絶対にあり得ないというのは結構嬉しかったですね。
また運用が単純化されて、リポジトリも一つ、デプロイ先も一つなので、コストと認知負荷を下げる事に一役買っています。...がたまに今これはサーバーを書いているのか?フロントか?と一瞬脳みそがバグることはあるのがご愛嬌。
デメリットやリスクとしては、柔軟性が低そうだなという感想でした。サーバーだけスケールしたいとか、フロントだけ〇〇にしたいというような要件に対応するのが難しそうです。またこれは私の知識不足もあると思うのですが、なんでもServer Actionsでやってしまうきらいがあって、Server Actionsの本質がPOSTなのにGETでも使ってしまうみたいな乱用が勃発していて、本当は良くないんだろうなぁと思いつつ便利なので使ってしまうことがあります。個人開発ならギリ許されそうですが、実務だと怒られそう...
まああとはフロントとバックエンドが同棲しているので、落ちると全部落ちるのが問題っちゃ問題ですね。
総じて実務やきちんと運用しよういう考えだったら、この構成は結構バツかなと思います。信頼性とか柔軟性が低い構成な気がしています。ただ個人開発のちっちゃいプロジェクトを一人で回すと考えたら、ミニマムでコストも低く、認知負荷も低く相当いいんじゃないかなと思います。
Supabase
RDBを無料で触れるからというのが選定理由です。
なんか流行ってたから触ってみたかった!というのも偽らざる本音ですが、なれてるRDBを安く使いたかったというのも嘘じゃありません。クラウドのRDBを使おうと思うと、AWSのRDSやGCP,Azureとかになると思うんですが...高い...。なんか結構お値段するんですよねこいつら。安く済ませようと思うとNoSQLとかのDBになってそれはそれで結構癖があって使いづらい...。
まぁもちろんNoSQLを使いこなせるようになればいいだけの話ではあるんですが、無料で使えるRDBがあるということで、使ってみたい!&RDB使える!助かる!が選定理由ですかね。
使ってみた感想としてはめっちゃいい!ただのRDBとして使うのであればかなりいいモノだと思います。無料だし、無料だし。
ただ使い方が結構難しいなぁという感想です。Supabaseは一応SaaSというよりはBaaSといって、Backend As A Serviceということでフロントを書いたらバックエンドはうちがやりまっせ!みたいなサービスなわけです。
本来的にはフロント側で叩くことを想定されているサービスなわけですね。あれ?そしたらDBのデータ盗みたい放題じゃね?と賢明な読者の皆さんなら気づくわけですが、それをRLSという仕組みでうまーいことやってるんですね。
それを知らないvibe coderたちがセキュリティ周りで結構危ないのでは?みたいなことが一時期話題になりましたが、さもありなん。閑話休題。
フロントから叩くのはちょっと抵抗あるなぁ...ただバックエンドを介さずに直接Supabaseを叩くほうがパフォーマンス的にはもちろんいいわけです。(めちゃくちゃ変わるかは実際に測る必要がありますが)よっぽど重要な情報じゃなければ、フロントから叩いてもいい気がするけどなぁ...うーんでもちょっとのミスでDBの中身見放題だから綱渡りだなぁ...
という感じで個人的には結構評価が微妙なラインです。もちろん普通のRDBとして使うのであれば結構いいサービスだと思います。手軽だし、無料だし。が...フロントから直接叩くのはどうなん?という問題に対しては...うーんミスらない自信があるなら...いいんじゃね?くらいの気持ちでいます。
判断は保留!というのが私の気持ちです。もちろん普通のRDBとして使い続けるつもりではいます。
あとsupabase Authのメールの送信数の少なさはどうにかならないかなぁ...と思っています。一時間あたり最大2通(?!)しか送れません。(2025/08/02現在)個人開発とはいえ、本番に耐えうるような量じゃないので、別でメールサービスを持ってきて連携しなきゃいけないのが面倒ですね。
Vercel
ここは特に大きな選定理由はありません。Next.js使ってるし、無料だし。と簡単に選定したような気がしています。
デフォルトでCDが備わってるのは嬉しいですね。案外この辺のCI/CD周りって面倒だったりするわけで、後回しになりがちなところなんですが、最初からついていれば面倒なこともなくするっとできるのは結構嬉しい機能なんじゃないかなぁと思っています。
ただvercelちゃんにもちょっとこれはなぁ...と思うところがあります。それはログ機能の貧弱さです
まあ貧弱は言い過ぎかもしれません。ランタイムエラーをなんかslackかなんかに通知するような仕組みが今のところvercelにはないんですね。エラー監視のためには外部ツールと連携したりする必要があるわけで、うーんこれくらいvercelが持っといてもいい機能なんじゃないかなぁ...と思いつつ、開発リソースの問題とかで先送りになってたりするところなのかなぁと想像しています。
まぁそれかもう餅は餅屋でログ監視は俺らの仕事じゃないと割り切っているのかもしれません。
実装のこだわり・苦労したこと・工夫したこと
ここからは打って変わって実装した上での、こだわり等々を語っていこうと思います。この時点で結構長いなぁ...という印象を書いていて思ったのですが頑張ってください。後もう一踏ん張りです。
スマホで見やすくするためのこだわり
スマホの画面って小さいんですよね!!! あんまり普段は意識したことがなかったのですが、レスポンシブデザインを考えたりするとPCサイズではこうすれば見えやすいなぁと思った配置がサイズ的にできないこととかがあります。
あっすみません言い忘れましたが、当方Next.jsはおろかReact触るのも2回目くらいのフロントペーペーなので変なことを言ってもお許しください。なのでスマホ画面に収まり、かつ見やすくするというのは結構大変なことなのだなぁと思いました。
悩みに悩んだ挙句の答えがこれになります!!ポイントを解説しましょう!...と言ってもこだわりは一つです。
問題文を読みながら該当部分を読みたい!!!
今回の場合だったら問題文に書いてある下線①の部分を一緒に見比べたいなぁとずっと思っていました。IPAが出している過去問PDFでも過去問道場さんでも書籍でも毎回一緒に見比べるのは辛い位置に置いてあるんです。
見比べるためには紙をペラペラ、スクロールして戻して、とかとかしなきゃいけなかったんですね。その悩みを解消しました!ブラボー!!!もうこれだけで個人的には価値があると思っています。
もちろんこの状態で後ろの本文はスクロールできますし、ボトムシートを閉じることもできます。スマホだとこのように配置しています。画面が小さく、縦長なのでうまく画面の中で配置をしようと思うとこれがベストかなと思いました。
逆にPCでは潤沢なキャンバスがあるので、こんなふうに贅沢に使って常時見比べられるようにしてあります。(ちょっと時計の配置が変なので直します。)
あまりPCとスマホでレイアウトを変えるのは良くないことなのかもしれませんが、UXの名の下で犠牲になってもらいました。この部分だけはコードがPC用とスマホ用で別れることになっています。保守性とかも犠牲になっていますが、このUXにこだわりを持ちました。
一番このサービスのキモとなる部分だったので妥協なしで実装したかったのです。
AI採点のこだわり
どんな情報を送っているのか
基本的には解答と模範解答だけ...を送っているわけではなくて、 毎回問題文全文を送っています。 できるだけAIが問題文全文を読み込みやすいようにmermaid(図をテキスト形式で書ける)で記述できる部分はmermaidで記述しています。↓こんな感じで。ただこれは一長一短でAIには読み込んでもらいやすくなったのですが、テストの本文の画像からmermaidに起こす過程でどうしても取りこぼしてしまう情報があったりして、果たしてそれでいいのか...?とちょっと悩んでいますが、今はAI採点、解説の精度を高めるためにこうしていますが、そのうち対策します。
また毎回問題文を送るのは非効率的なので、問題文はOpenAI上にキャッシュしてもらうとかRAGとかを使えばもっと安くなりそうとか色々効率化の手法も考えております。ですが今は愚直に全文毎回送っています。
また毎回決まったJSONを返してもらうためにこんなふうにレスポンスのフォーマットを決めてOpenAIのAPIに渡し、決まったレスポンスで返してもらうことができます。このレスポンスフォーマットが崩れたことは今のところないので、信頼性は高いかなぁと思っています。Anthropic APIは稀に壊れるイメージが...使い方悪しなのかもですが。
const responseFormat = {
name: "scoringList",
schema: {
type: "object",
properties: {
scores: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "string" },
score: { type: "number" },
explanation: { type: "string" },
},
required: ["id", "score", "explanation"],
additionalProperties: false,
},
},
},
required: ["scores"],
additionalProperties: false,
},
};
if (!process.env.OPENAI_API_KEY) {
throw new OpenAIError("OPENAI_API_KEYが設定されていません", 500);
}
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
],
response_format: { type: "json_schema", json_schema: responseFormat },
}),
});
//省略
過去問PDFからマークダウン化の自動化
お安く便利に実行したい
見やすくするために、過去問記述ラボではPDFを直接表示するのではなく、PDFをマークダウンにして見やすく整形してから表示するようにしています。
過去問PDF→マークダウン
の工程はAIにもちろんやらせています。claudeにPDFをプロンプトとともに渡して、ちまちま1問ずつ手作業でやっていました。OpenAIやGeminiでもやって見たのですが、このPDFの形式では無理と言われました。そんな中、claudeちゃんだけはちゃんとやってくれました!神!
ただそんなぽちぽちやるのは面倒です。面倒を効率化するのがエンジニアの性ということでAnthoropic APIに並列で投げてやることにしました。ただPDFってトークン数がめっちゃ取られるんですね...確か最初の愚直にPDFを渡す形式だと、試験一回分の文字起こしに2$くらいかかっていた気がします。
15年分くらい文字起こししなきゃならんので、1年に2回あるわけで60$くらいかかる計算です。ヒエーそんなのやってられないですね。そこで対策を講じてなんとか試験一回につき、0.1$まで抑えました。
何をしたかというとバッチとキャッシュを使いました。詳しくはまた別記事にでも起こそうかなと思っているのでそちらを後ほど参考にしてください。
AIのブレをいかに対策するか
これがなかなか面白いことなんですが、おんなじプロンプトで指示してもちょっと認識違いの文字起こしをすることもあるんですよね。全部意図通りに文字起こししてくれることももちろんあるんですが、ブレがあtたりします。そしてそのブレにも法則性があったりするんです。
{
"id": "ap_2020_autumn_pm_q2_group_1",
"text": "設問1",
"questions": [
{
"id": "ap_2020_autumn_pm_q2_group_1_question_1",
"text": "本文中の下線①の状態のままでは危惧される、W社の事業に関する機会損失リスクを、25字以内で述べよ。",
"question_info": [
{
"id": "ap_2020_autumn_pm_q2_group_2_question_1_info_1",
"key": "q1",
"label": "",
"type": "text",
"limitLength": 25,
"format": [
"upTo"
],
"modelAnswer": [
"新事業の創出につながる機会が失われる。"
],
"point": 3
}
]
}
]
}
↑これが設問の正しい文字起こしJSONなんですが、このJSONをたまに↓こういう感じで返すんです
{
"id": "ap_2020_autumn_pm_q2_group_1",
// ↓このテキストに全部突っ込んで
"text": "設問1 本文中の下線①の状態のままでは危惧される、W社の事業に関する機会損失リスクを、25字以内で述べよ。",
"questions": [
{
"id": "ap_2020_autumn_pm_q2_group_1_question_1",
//↓ここを空っぽにするのです
"text": "",
"question_info": [
{
"id": "ap_2020_autumn_pm_q2_group_2_question_1_info_1",
"key": "q1",
"label": "",
"type": "text",
"limitLength": 25,
"format": [
"upTo"
],
"modelAnswer": [
"新事業の創出につながる機会が失われる。"
],
"point": 3
}
]
}
]
}
questions
の中のtextを空っぽにして一番上の階層のtextに問題文を詰めるんですよね...これをプロンプトで強制しても、一向にいうことを聞かないんです。これは問題の一つに過ぎません。他には配点を20点満点になるように配点してね。
と言ってもいうことを聞かないですし、他にも色々いうことを聞いてくれないことがあります。
なので僕はAIちゃんが毎回言うことを聞いてくれると思うのを諦めるべきだと思いました。 じゃなくてAIの出力を信用するのではなく、AIの出力を信用するために人間側で強めに縛ってあげる必要があると思っています。例えば今回はプロンプトでなんとかするのを諦めて、特製のリンターのようなもので縛ることにしました。コード生成の文脈ならばTDDをすれば、振る舞いで縛ることができるのでAIとかなり相性はいいのだと思っています。ただそのテストケースだけは人間が考える必要ありだと思います。
これがそのAIを縛っているスクリプト群です!一部関係ないものもありますが、大体フォーマットを合わせるためのスクリプトです。これによってAIちゃんが適当な仕事を返してきても、このスクリプトたちがその成果を担保してくれるわけです。AIちゃんが2回以上したミスを自動修正したり、自動修正ができないなら検出するスクリプトを書いています。
こういうふうにAIの仕事を担保する仕組みを人間様が定義してあげる必要があるのかなと思っていて、AIとの付き合い方だと思っています。
今後やりたいこと
とりあえず以下は必ず機能として実装しようと思っています。
- 図表番号のリンク機能
- 図1みたいにリンクにして、それを押すと図1の元にスクロールするようにしたい
- 演習履歴・成績分析・復習支援
- 演習履歴を閲覧できるようにして、そこから成績の分析やどこを復習するべきかをレコメンドするようにしたい
- レコメンド機能
- 過去問記述ラボを開いた時に前回解いた問題から自動で次解くべき問題をレコメンドしてくれるようにしたい
- IPA高度試験の問題を全部カバーしたい
このあたりはマストで実装しようと思っています。また過去問記述ラボを使われるサービスにしたいなぁと思っているので、全力でSEOやってみようかなと思っています。その結果としたことをまた記事にまとめようかなと思います。
最後に
僕の欲しかったサービスができました!まだまだどんどんアップデートを重ねて、IPAの記述問題を演習するならあのサイトだよね。と言われるくらいまで認知度を高めていきたいなと思っています。具体的にはMAU15000人を目標にしています!(IPA高度試験受験者数の10%程度)
そのために欲しい機能等々フィードバックがあれば、ぜひ遠慮なくお申し付けいただければなぁと思います。
また学びとしてAIを信用するため、ガチガチに縛る必要がありそうだなと思いました。AIを一つの関数とみなして、テストをガチガチにかいて振る舞いを限定するイメージを僕は持っていますが、少なくともAIに好き放題やらせるVibe Codingとかのやり方とかはもっとAIの発展を待たないと堅牢なシステムは作れなさそうだなと言う印象を持っています。
それでは良い個人開発ライフを!