― 16人の担当者に分けて読む ―
jiji-mondai-opus.akuramochi.chatgpt.site
テーマと期間を指定すると、ウェブを調べて日経TEST水準の時事演習8問を作るWebアプリ
プログラムを書かない人が、「中で何が、どの順番で、どう動いているか」を自分の言葉で説明できるようになることを目標にしています。用語は3章の「用語ミニ辞典」で先にすべて説明してから使います。本文中で このように 点線の付いた言葉は、その辞典に載っている言葉です。
4章では、このアプリを16人の担当者が並んだ工場のラインと見立てて、1人1ページで説明します。各ページには流れの図(誰から何が来て、どこへ何が出るか)と、左=やさしい言い方/右=同じことを指す技術用語を並べた表が入っています。右側は、あとで自分で調べるための手がかりです。丸数字が橙色の担当は生成AIを呼び、紺色の担当は呼びません。16人のうち、AIを呼ぶのは5人だけです。その意味は6章で説明します。
1章
「テーマ」と「いつからいつまで」を入れると、アプリがウェブを調べ、その期間のニュースから演習問題を8問つくって画面に出す――それだけのアプリです。目的は暗記のテストではありません。ニュースを仕事の判断につながる形で覚え直すことです。
| 押さえる構造 | 4件。事実ではなく、事実を貫く力学。「命題・メカニズム・なぜ重要か・仕事への接続」の4点セット。 |
|---|---|
| 本文(リード文) | 1つ。700〜900字。中に空欄が2か所(A・B)と下線が2か所(a・b)が埋め込まれる。 |
| 設問 | 8問。4択が7問、80字の論述が1問。番号ごとに出題形式まで決まっている(⑪)。 |
| 復習資料 | 用語集8件と年表6件、それに出典の一覧(最大20件。すべてURL付き)。 |
docs/prompt-v3.md、27,787字)に明記されています。アプリはその文章をまるごとAIへの指示に同封しています(⑩)。2章
紺色の枠=AIを呼ばない担当/橙色の枠=AIを呼ぶ担当。点線の枠は「どこで動いているか」の区切り。
3章
この先の説明は、なるべくこの17語だけで書いています。分からなくなったらこのページに戻ってください。
| サーバー | 頼まれた仕事をこなすために待ち構えているコンピュータ。このアプリのサーバーは自宅のPCではなく、インターネット上の貸倉庫(Cloudflare)にあります。 |
| ブラウザ | スマホやPCでWebを見る画面(Safari、Chromeなど)。お客さん側。このアプリでは、工程を1つずつ進める指揮もここでやっています(②)。 |
| API | プログラム同士の受付窓口。「決まった形で頼むと、決まった形で返ってくる」。人間向けの画面ではなく機械向けの窓口です。AIの会社にもこの窓口があります。 |
| JSON | データの書き方の一種。ラベルの付いた箱にデータを入れた形。例:「theme は 半導体、periodTo は 2026-08-11」。人も機械も読めます。 |
| JSONスキーマ | 「返してほしいJSONの設計図」を機械が読める形で書いたもの。AIに渡すと、その形以外では返せなくなります。このアプリの土台です。 |
| データベース | 表の形でデータを溜めておく箱。ここでは Cloudflare D1(中身はSQLiteという小さなデータベース)の3つの表を指します。 |
| 正本(せいほん) | どれが本物か、迷った時に見るべき原本。このアプリの正本は、依頼票の1行に入っている「途中経過」です。画面が持っているのは、その写しにすぎません。 |
| ジョブ(依頼票) | 「このテーマで作ってください」という注文1件ぶんの伝票。番号・入力・使うモデル・今どこまで進んだか・途中経過が、この1行に全部入っています。 |
| 工程(ステップ) | 依頼を最後まで運ぶために割った作業の1区切り。このアプリは9つに割ってあります。1回の呼び出しで進むのは必ず1工程だけ。 |
| 状態(ステータス) | 依頼票が今どの札を付けているか。待ち・走行中・要再試行・完成の4つ。次に何ができるかは、この札だけで決まります。 |
| 排他制御 | 整理券。同じものを2人が同時にいじると壊れるので、順番に1人ずつ通す仕組み。このアプリは「鍵の台帳に条件付きで書き込めた人だけが通れる」方式です(⑥)。 |
| バリデーション | 送られてきたもの・返ってきたものが条件を満たしているか、機械が点検すること。人の目ではなくプログラムが行います。このアプリの半分はこれです。 |
| 大規模言語モデル (LLM) | 大量の文章から言葉の続き方を学んだ、いわゆる生成AI。ここで使うのは Claude の Sonnet 5 と Opus 5。あとの方が丁寧に考えますが、遅く高くなります。 |
| 道具を使わせる (ツール利用) | AIに「自分で調べていいですよ」と道具を渡すこと。ここではウェブ検索。AIは記憶だけで答えず、検索してから書きます。 |
| トークン | AIが文章を数える単位。だいたい1文字〜数文字で1つ。「最大9,000トークンまで」は、書ける分量と費用の上限を決めることです。 |
| プロンプト (指示書き) | AIへの頼み方の文章。このアプリは作問のルールを27,787字の文書(docs/prompt-v3.md)にまとめ、毎回まるごと同封しています。 |
| サーバーレス | 自分でサーバー機を持たず、頼まれた時だけ他社の設備の上で数百ミリ秒だけ動くやり方。使わない間は何も動いておらず、費用もかかりません。 |
テーマ・期間・使うAIを受け取り、明らかにおかしい入力だけをその場で止めて、サーバーに「作ってください」と1回だけ頼む担当。ボタンを押した後の進行は、すぐ②に引き継ぎます。
あなた ──[テーマ・開始日・終了日・モデル]──▶ ① 受付カウンター(画面の中)
《3文字未満 / 開始日が終了日より後 → ここで赤字。サーバーへは送らない》
└──[POST /api/jobs に JSON で1回]──▶ ③ 入力の門番
└──[202番+依頼票]──▶ ② 進行役へバトンタッチ
| 受け取るもの | テーマ(最大400字)、開始日、終了日、使うモデル |
|---|---|
| 出すもの | /api/jobs への依頼1件(JSON) |
| AI | 使わない |
| 失敗した時 | 画面に赤い帯が出るだけ。サーバーには何も届かない |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 画面を開いた瞬間 | 開始日に「日本時間の30日前」、終了日に「日本時間の今日」を入れておく。なぜ:指定する期間は毎回ほぼ同じで、空欄から考えさせる意味がないため | 初期値(初期状態)。jstDate(-30)。閲覧者の時差を足し引きして日本時間の日付を作っている |
| 文字を打っている間 | 入力欄そのものが400字で打ち止まる。あふれる前に止める | 入力制約(maxLength)。画面側の制限。迂回されても③が同じ条件でもう一度見る |
| 「このテーマで問題を作る」を押した瞬間 | テーマ3文字未満/開始日が終了日より後、の2つを見て、赤字を出して止める。なぜ:往復1回ぶんの待ち時間と費用を無駄にしないため | クライアントサイドバリデーション。速さのための検査であって、守りではない。守りは③の仕事 |
| 検査を通った直後 | 4つの値をJSONに詰めて、1回だけ送る | fetch / POST。content-type: application/json と書いて「これはJSONです」と伝える |
| 202番が返ってきた瞬間 | 「受け付けた、まだ終わっていない」という返事。依頼票だけ握って②に渡す | HTTP 202 Accepted。時間のかかる処理の受付。200(完了)とは意味が違う |
| 処理が走っている間 | モデルの選択欄を丸ごと押せなくする。なぜ:途中でモデルが変わると、工程どうしで辻褄が合わなくなるため | 状態に応じたUIの無効化(<fieldset disabled>)。禁じたい操作は、注意書きではなく押せなくする |
このアプリで一番変わっている担当。9つの工程を1つずつ進めるのは、サーバーではなくあなたのブラウザです。依頼票を握って「次へ進めて」を9回叩き、待ちの間は2.5秒ごとに様子を見に行きます。
② 進行役 ──[POST /api/jobs/{id}/advance]──▶ ⑤ 現場監督 ──▶ 1工程だけ実行
▲ │
└────────────[更新後の状態]──────────────┘
《返事が「走行中」のまま → 2,500ミリ秒待って GET で見に行く》
《完成 or 要再試行 になるまで繰り返す。要再試行なら赤帯+再試行ボタン》
| 受け取るもの | 依頼票(ジョブID)と、いまの状態 |
|---|---|
| 出すもの | advance の呼び出し(最大9回)と、画面の進捗表示 |
| AI | 使わない(呼ぶのは、この先の担当) |
| 失敗した時 | その工程で止まる。それまでの成果は保存済み |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 依頼票を受け取った瞬間 | 「いま走らせている」印を1つ立てる。なぜ:履歴から同じ依頼を2回開いても、進行役が2人にならないようにするため | 再入防止(useRef によるガード)。画面が描き直されても消えない置き場所を使う |
| 1工程ごと | 「次へ進めて」を1回叩く。1回の呼び出しで進むのは必ず1工程だけ | 長時間処理の分割。1回のリクエストを短く保つ。貸倉庫には実行時間の上限があるため |
| 返事が「走行中」だった時 | 2,500ミリ秒待って、今度は読むだけの窓口に様子を見に行く。なぜ:別のタブが同じ工程を走らせている最中だから | ポーリング(一定間隔での問い合わせ)。押しかけずに待つ。常時つなぎっぱなしにするより作りが単純 |
| 画面を開いた直後(毎回) | 履歴を読み、完成も失敗もしていない依頼があれば自動で拾って続きから進める | レジューム(再開)。工程ごとに保存してあるから成り立つ。⑮が土台 |
| 完成した瞬間 | 80ミリ秒おいてから、できあがった問題まで滑らかに移動する | scrollIntoView({behavior:"smooth"})。描画が終わるのを待つためのわずかな遅延 |
| 途中で通信が切れた時 | 最新の状態を取り直して画面に出す。ブラウザを閉じても、保存済みの地点は消えない | 真実の所在(source of truth)。画面の表示は写し。正しい状態は常にサーバーの台帳にある |
サーバー側で最初に動く担当。①と同じ検査をもう一度やり、通ったら依頼票を1枚作って番号を振ります。ここから先、9工程の成果はすべてこの1行にぶら下がります。
① 受付カウンター ──[JSON]──▶ ③ 門番
《テーマ3〜400字 / 日付の形 / 開始日≦終了日 / モデル名は一覧の2つだけ》
├─[違反]──▶ 400番+日本語の理由 ──▶ 画面に赤字
└─[合格]──▶ 依頼票を1枚(番号・待ち・工程は「調査計画」・0工程目)
└──[202番+依頼票]──▶ ② 進行役へ
| 受け取るもの | 画面から送られたJSON |
|---|---|
| 出すもの | 依頼票1件(202番)、または400番+理由 |
| AI | 使わない |
| 失敗した時 | 依頼票は作られない。データベースには1行も残らない |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 依頼が窓口に届いた瞬間 | 画面で検査済みでも、もう一度同じ検査をする。なぜ:画面の検査は迂回できる。窓口を直接叩かれても壊れないため | サーバーサイドバリデーション。外から来たものは信用しない(untrusted input)という原則 |
| テーマを見る時 | 前後の空白を落としてから3〜400字か数える。空白だけの入力は通らない | trim() と長さ検査。「見た目は入力されている」を弾くための正規化 |
| 日付を見る時 | 「2026-08-11」の形をしているかを、文字の並びの型で照合する | 正規表現 /^\d{4}-\d{2}-\d{2}$/。形だけを見ており、暦として実在するかは見ていない(11章) |
| モデル名を見る時 | 一覧に載っている2つ以外は拒む。指定が無ければ Sonnet 5 にする | 許可リスト(allowlist)方式。「駄目なものを挙げる」ではなく「良いものだけ挙げる」。抜け道が生まれにくい |
| 合格した直後 | 重複しない番号を発行し、札は「待ち」、工程は「調査計画」、進んだ工程数は0で1行書く | UUID(crypto.randomUUID())と状態機械の初期状態。ここが9工程の出発点 |
| 返事を返す時 | 200(できた)ではなく202(受け付けた)を返す。中の作業はまだ1つも始まっていない | HTTP 202 Accepted。「これから時間をかけて作る」という約束を、番号で表す |
データの置き場所への出入口を1か所にまとめ、表がまだ無ければその場で作る担当。9工程ぶんの途中経過を預かる棚を、誰も気づかないうちに用意しています。
どの担当も ──[getDb()]──▶ ④ 管理人 ──▶ Cloudflare D1(表3つ+索引6つ)
《小部屋が立ち上がって最初の1回だけ ensureSchema を実行》
《model 列が無い古い倉庫 → PRAGMA で列を数え、その場で1列だけ足す》
└─[道具箱に棚が入っていない]──▶「保存用データベースに接続できませんでした。」
| 受け取るもの | 「棚を使いたい」という呼び出しだけ |
|---|---|
| 出すもの | データベースへの取っ手(この1本しか出入口がない) |
| AI | 使わない |
| 失敗した時 | その工程は失敗。前の工程までの成果は残る |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 棚に触る直前(どの工程でも) | 外から手渡された道具箱の中に棚があるか確かめ、無ければ日本語で断る | バインディング(依存を外から渡す)。接続先をコードに書かない。渡す側を差し替えれば繋ぎ先が変わる |
| 小部屋が立ち上がって最初の1回 | 表と索引を作る指示9つを、まとめて1回で送る | CREATE TABLE IF NOT EXISTS。既にあれば何もしない=何度やっても結果が同じ(冪等) |
| その1回を覚えている間 | 「作業中の約束」を1つだけ持ち回し、同時に何本呼ばれても実行は1回に束ねる | Promiseのメモ化(シングルフライト)。同じ重い仕事の重複起動を防ぐ定石 |
| その1回が失敗した時 | 覚えた約束を捨てる。なぜ:失敗を覚えたままだと、次からずっと「作成済み」と思い込んで表が永久にできないため | 失敗のキャッシュを避ける。エラー時に状態を巻き戻す。地味だが、これが無いと復旧できない |
| 表ができた直後 | 表の列を数えて、model 列が無ければその場で足す(既定は Sonnet 5) | PRAGMA table_info と ALTER TABLE。動いているものを止めずに形を変える=マイグレーション |
| それとは別に | drizzle/ にも同じ形のSQLが2本置いてある。実行時にコードでも作る、という二重構え | スキーマ定義の二重管理。片方だけ直すとずれる。11章に挙げた割り切りの1つ |
「次へ進めて」という電話を受け、その1回で必ず1工程だけ動かす担当。始める前に鍵を取り、終わったら結果を台帳に書き、作業札を次の工程に貼り替えます。
② 進行役 ──[POST /api/jobs/{id}/advance]──▶ ⑤ 現場監督
├─依頼票が無い → 404 / 完成済み → そのまま返す / 要再試行 → 409
├─[⑥に鍵を頼む]──取れない──▶ 409「別の処理が進行中です。」
└─取れた──▶ ⑦ 割り振り係 ──▶ 1工程を実行(ここで秒数を計る)
├─成功──▶ ⑮ 台帳へ記録・次の工程へ ──[更新後の状態]──▶ ②
└─失敗──▶ ⑮ 失敗も記録 ──▶ 422+日本語の理由
| 受け取るもの | 依頼票の番号(住所の一部として届く) |
|---|---|
| 出すもの | 更新後の状態(JSON)。工程ごとの秒数もここに含まれる |
| AI | 使わない(呼ぶのは⑦の先の担当) |
| 失敗した時 | その工程だけ失敗。前の工程の成果は残り、そこから再試行できる |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 電話が鳴った瞬間 | 台帳から今の状態を読む。無ければ404で終わり | HTTP 404。「その依頼票は存在しない」。500(こちらの不具合)と区別する |
| 状態を見た直後 | 完成済みなら何もせずそのまま返す。何度叩かれても壊れない | 冪等(べきとう。何度やっても結果が同じ)。②が二重に呼んでも安全にするための性質 |
| 要再試行だった時 | 409を返し、勝手に再開しない。なぜ:やり直すかどうかは人が決めることだから | HTTP 409 Conflict。自動での再試行(⑬)と、人が押す再試行(⑮)の線引きがここ |
| 実行に入る直前 | ⑥に鍵を頼む。取れなければ409で引き返し、何も動かさない | 排他制御。二重実行の防止。同じ工程でAIを2回呼ぶと、費用も結果も二重になる |
| 実行している間 | 開始時刻を控え、終わったら差を出して所要ミリ秒を記録する。画面の「12.4秒」はこれ | 計測(Date.now() の差分)。どの工程が重いかを、推測ではなく記録で知る |
| 例外が飛んだ時 | 握りつぶさず、日本語の理由を台帳に書いてから422を返す | 例外処理と観測可能性。記録に残すから、後から同じ地点をやり直せる |
同じ工程が同時に2回走らないようにする担当。鍵の渡し方が独特で、「台帳の1行を条件付きで書き換えて、本当に1行変わった人だけが通れる」という方式をとっています。
⑤ 現場監督 ──[鍵をください]──▶ ⑥ 鍵預かり係
UPDATE … SET status='running'
WHERE id=? AND current_step=? AND status IN ('queued','waiting')
├─変わった行が 1 → 鍵を渡す(何回目の挑戦かも数えて渡す)
└─変わった行が 0 → 誰かが先に取った。鍵は渡さない ──▶ 409
《別口:走行中のまま4分(240,000ミリ秒)動かない行 → 要再試行に落として救出》
| 受け取るもの | いまの依頼票 |
|---|---|
| 出すもの | 鍵(何回目の挑戦か・開始時刻)、または「渡せない」 |
| AI | 使わない |
| 失敗した時 | 呼んだ側が409で引き返すだけ。データは壊れない |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 鍵を頼まれた瞬間 | 札が「待ち」か「これから」の時だけ応じる。走行中・完成済みには渡さない | 状態機械のガード条件。次に進める札が何かを、あらかじめ決めてある |
| 渡すと決めた瞬間 | 「今の札と工程がこのままなら、走行中に変えろ」と1文で書き換える。確かめてから書くのではなく、条件付きで書いて結果を見る | 楽観的並行性制御(条件付きUPDATE)。「読む→確かめる→書く」の間に横取りされる隙を無くす |
| 書き換えた直後 | 変わった行数が1かどうかだけを見る。1でなければ負け、として静かに引き下がる | meta.changes の判定。「1行だけ変わった」という事実が、勝者であることの証拠になる |
| 鍵を渡す時 | この工程が過去に何回走ったかを数え、+1した番号を一緒に渡す | 試行回数(attempt)。台帳側の一意制約(依頼・工程・試行回数)と組んで、二重記録も防ぐ |
| 台帳を読むたび(毎回) | 走行中のまま4分動きがない行を見つけたら「通信が中断されました」として要再試行に落とす | タイムアウトによる回収。見張り役を別に常駐させず、読むついでに掃除するという設計 |
| 4分という数字について | コードに直接書かれた固定値。本当に4分を超える工程があると、動いているのに失敗扱いになりうる | マジックナンバー。根拠が式ではなく経験則の数値。11章の割り切りの1つ |
依頼票に書かれた「今の工程名」を見て、誰を呼ぶかを決めるだけの担当。このファイル1枚を読めば、アプリが何を、何の順でやるかが全部分かります。
⑤ ──[依頼票(今の工程名つき)]──▶ ⑦ 割り振り係
1 research_plan → ⑧ 調査計画 6 questions_1 → ⑪ 問1〜4
2 research_1 → ⑨ 情報収集 1/3 7 questions_2 → ⑪ 問5〜8
3 research_2 → ⑨ 情報収集 2/3 8 questions_3 → 数を数えるだけ(AIなし)
4 research_3 → ⑨ 情報収集 3/3 9 finalize → ⑭ 最終検品(AIなし)
5 blueprint → ⑩ 設計図
└──[新しい途中経過と成果物]──▶ ⑤ へ返す(保存するのは⑮)
| 受け取るもの | 依頼票(入力・使うモデル・ここまでの途中経過) |
|---|---|
| 出すもの | 更新した途中経過と、その工程の成果物 |
| AI | 工程による。9工程のうち6工程で呼ぶ(呼ぶ担当は⑧⑨⑩⑪) |
| 失敗した時 | 材料が揃っていなければ、AIを呼ぶ前に止める |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 呼ばれた瞬間 | 途中経過を書き換えずに写しを作る。なぜ:途中で失敗した時に、元の内容を汚さないため | イミュータブル(変更しない)な扱い。{...state} で複製してから足す |
| 情報収集の工程に入る前 | 調査計画が保存されているか確かめる。無ければAIを呼ばずに止める | 事前条件(前提の検査)。費用のかかる呼び出しの前に落とす、が鉄則 |
| 設計図・設問・最終検品に入る前 | 3回ぶんの調査が全部そろっているか数え、そろっていればその場で1つに束ねる | 集約(requireResearch)。束ねた結果は保存せず、必要なたびに作り直している |
| 8工程目(questions_3) | AIを一切呼ばない。設問が2バッチそろっているかを数えるだけの工程 | チェックポイント。人が進捗を確認するための区切りでもある |
| どの工程でも | 成果物を返すだけで、自分では保存しない | 責務の分離。「実行する人」と「保存する人」を分けると、両方が単純になる |
| 知らない工程名が来た時 | 「実行する工程を特定できませんでした。」ではっきり止める | フォールスルーの明示。黙って何もしないより、はっきり失敗させるほうが安全 |
AIを使う最初の担当。いきなり調べ始めず、テーマを重ならない3つの調査単位に割るための計画だけを作らせます。設問は作らせません。
⑦ ──[テーマ・期間]──▶ ⑧ 調査計画係 ──▶ Claude(Sonnet 5 か Opus 5)
《返してほしい形を先に渡す:thesis(見立て)+ batches(label・query・focus)》
《上限 2,000トークン / 考える深さ low / ウェブ検索は渡さない》
├─調査単位ちょうど3件、各focus 2件以上 → 合格 ──▶ ⑮ 台帳へ
└─件数が違う → ⑬ 直し係が理由を添えて1回だけ作り直させる
| 受け取るもの | テーマ、開始日、終了日 |
|---|---|
| 出すもの | 調査計画(見立て1つ+調査単位3つ。各単位に検索方針と確認事項) |
| AI | 使う。上限2,000トークン、考える深さ low、ウェブ検索なし |
| 失敗した時 | 1回直させても駄目なら、この工程で停止。依頼票は残る |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 呼ばれた瞬間 | 「あなたは調査設計者です。設問は作りません」と役割と禁止事項を1文で固定する | システムプロンプト。毎回の指示とは別に、役割を固定する場所がある |
| 頼む時 | 返してほしいJSONの形を、機械が読める仕様として一緒に渡す。書かれていない項目は入れられない | 構造化出力(JSONスキーマ)。additionalProperties:false で余計な項目も禁じる |
| 同じ時 | 考える深さを「low」、書ける量を2,000トークンに絞る。なぜ:計画は短くてよく、長くすると遅く高くなるため | effort と max_tokens。品質・速度・費用を調整する2つのつまみ |
| 返事が来た瞬間 | 中身を見る前に、途中で切れていないかを先に確かめる | stop_reason。end_turn(書き切った)以外は、全部失敗として扱う |
| 中身を見る時 | 調査単位がちょうど3件か、各単位の確認事項が2件以上あるかを数える | 事後条件の検査。形式が合っていても、件数は別に数えないと分からない |
| 数が合わなかった時 | 前回のJSONと「何が駄目だったか」を添えて、もう1回だけ作らせる | 自己修復ループ(1回限り)。詳しくは⑬。無限に直させない設計 |
実際にウェブを検索する唯一の担当。3つの調査単位を、それぞれ別の工程として3回動きます。3回そろったら、出典をURLで名寄せして1本に束ねます。
⑦ ──[調査単位1つ]──▶ ⑨ 情報収集係 ──▶ Claude ──[検索 最大3回]──▶ ウェブ
《初回は検索を強制。まだ続きがある(pause_turn)なら会話を継いで最大2周》
└─[900〜1,500字の証拠+出典(最大8件・2件未満は失敗)]──▶ ⑮ 台帳へ
3回そろった後 ── URLで重複を落とし S01… と採番(最大20件・5件未満は失敗)
| 受け取るもの | 調査単位1件(見出し・検索方針・確認事項)と、テーマ・期間 |
|---|---|
| 出すもの | 日本語の証拠パック(900〜1,500字)と、出典一覧 |
| AI | 使う。上限5,000トークン、考える深さ low、ウェブ検索つき |
| 失敗した時 | その回だけ失敗。他の2回ぶんの成果は残る |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 工程に入った瞬間 | 「政府・公的機関・企業など一次情報を優先」「日付・数値・因果には引用を付ける」「設問は作らない」と条件を固定する | システムプロンプトによる制約。調べ方の方針そのものを、毎回同じ文章で固定している |
| 最初の1回 | 検索を必ず使わせる。なぜ:AIは訓練で覚えた古い記憶だけで、それらしく書いてしまうことがあるため | ツール強制(tool_choice)。「使ってもよい」ではなく「まず使え」と指定できる |
| 検索している間 | 1工程あたり検索は最大3回まで。3工程あるので、1回の生成で最大9回 | max_uses: 3。道具の使用回数の上限。時間と費用の歯止め |
| 返事が「まだ途中」だった時 | 会話を継ぎ足してもう1周だけ回す。2周で終わらなければ「規定回数内に完了しませんでした」 | pause_turn と継続呼び出し。長い道具使用を分割して返す仕組みに、こちらが付き合う |
| 出典を拾う時 | 引用ブロックだけでなく、返事の入れ子の奥まで再帰的に探してURLを集め、URLで重複を落とす。引用文は700字で切る | 再帰探索と重複排除。返り値の形が変わっても拾えるようにした保険でもある |
| 3回そろった後 | 3回ぶんをURLで名寄せし、S01 から番号を振り直す。5件未満なら失敗にする | 名寄せと下限のしきい値。この番号を、後で設問が「根拠はS03」と指す(⑪⑭) |
設問を書く前に、本文・押さえる構造・用語集・年表という「土台」だけを作らせる担当。ここで本文に空欄と下線の目印を埋め込み、その並び順まで機械が検査します。
⑦ ──[調査結果+出典]──▶ ⑩ 設計図係 ──▶ Claude(正本27,787字を同封)
《固定:構造4件・本文1つ(700〜900字)・用語8件・年表6件。上限9,000トークン》
《本文の目印 [[BLANK:A|正答]] [[UNDERLINE:a|文]] … 登場順は必ず A→a→B→b》
├─件数と並び順が一致 → 合格 ──▶ ⑮ 台帳へ
└─ずれ → ⑬ 直し係が1回だけ作り直させる
| 受け取るもの | テーマ・期間・3回ぶんの調査結果・出典一覧 |
|---|---|
| 出すもの | 設計図(題名/情報の流動性の注意書き/構造4/本文1/用語8/年表6) |
| AI | 使う。上限9,000トークン、考える深さ low |
| 失敗した時 | 1回直しても駄目なら停止。調査結果は残るので、ここから再試行できる |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 頼む時 | docs/prompt-v3.md(作問の正本、27,787字)を丸ごと指示に同封する | プロンプトの外部ファイル化。コードを書き換えずに、文章を直すだけで作問方針を変えられる |
| 同じ時 | 正本と食い違う所は「MVP固定仕様を優先」と明記する。なぜ:正本は12〜15問だが、このアプリは8問だから | 指示の優先順位の明示。矛盾する指示を渡す時は、必ずどちらが勝つかを書く |
| 返ってきた瞬間 | 構造4・本文1・用語8・年表6の件数をそのまま数える。1件でも違えば不合格 | 事後条件の検査。「だいたい合っている」を許さない |
| 本文を見る時 | HTMLタグではなく [[BLANK:A|正答]] という独自の目印を使わせる。なぜ:見た目を作るのは⑯の仕事で、AIには中身だけ書かせたいから | 中間表現(マークアップ)。データと見た目の分離。同じデータを画面にも印刷にも使える |
| 目印を数える時 | 空欄はA・Bの2つ、下線はa・bの2つ、しかも本文での登場順が A→a→B→b であることまで見る | 字句の切り出し(正規表現)と順序の検証。「正本の原則5:記号は本文の登場順に」を機械で担保している |
| 目印の書き損じ | 切り出した後の普通の文に [[ が残っていたら、壊れていると判断する | 残骸の検出。壊れた目印は⑯で「そのままの文字列」として画面に出てしまうため |
8問を4問ずつ2回に分けて作らせる担当。どの番号がどの形式かは人があらかじめ決めてあり、AIが書くのは中身だけ。正解は必ず1番目に置かせます。
⑦ ──[設計図+調査結果]──▶ ⑪ 設問の作成係(1回目=問1〜4/2回目=問5〜8)
Q01 空欄補充 Q02 下線(深掘り) Q03 共通キーワード Q04 資料読解[新形式]
Q05 職業への接続[新形式] Q06 空欄補充 Q07 下線(深掘り) Q08 論述80字
《正答は必ず options[0]・correctIndex=0。並べ替えは⑫の仕事》
《誤答ごとの説明は作らせない(空配列で固定)。解説は140〜220字》
└─[4問ぶんのJSON]──▶ ⑫の検査 ──▶ ⑮ 台帳へ
| 受け取るもの | 設計図、調査結果、出典一覧、何回目のバッチか |
|---|---|
| 出すもの | 設問4問(設問文・選択肢4・解説・出題理由・出典ID) |
| AI | 使う。上限9,000トークン、考える深さ low |
| 失敗した時 | そのバッチだけ失敗。もう片方の4問は残る |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 工程に入った瞬間 | 作る4問の番号・種類・形式を文章で列挙して渡し、指定外のIDは出すなと明記する | 出力範囲の固定と分割生成。8問を一度に作らせない。長い出力は途中で崩れやすい |
| 選択肢を作らせる時 | 正解を必ず1番目に置かせる。仕様上、それ以外の位置は選べない | スキーマの enum による制約。正解の位置を決めるのは機械(⑫)の仕事、と役割を切っている |
| 解説を書かせる時 | 140〜220字。冒頭で正答の中身を述べ、誤答には触れさせない | 出力仕様の数値化。正本にある「誤答ごとの説明」を、ここでは明示的に打ち消している |
| 出典を付けさせる時 | 使ってよい出典IDを渡し、それ以外は機械が捨てる。1問あたり最大3件。全部捨てられたら先頭の出典を当てる | 許可リストによる後処理。AIが実在しない番号を作っても、混入しない |
| 下線問題(問2・問7)を作らせる時 | 「下線文の言い換えは禁止。背景・因果・二次的影響・企業判断を問う」と目標を1文足す | 追加制約(depthGoal)。形式だけでなく「難しさの質」を指示で作る。守れたかは⑫が測る |
| 論述(問8)を作らせる時 | 必須キーワード2語と採点観点3件をきっちり要求し、数が違えばその場で不合格 | 事後条件の検査。頼むだけでは守られない、を前提にした二段構え |
このアプリでいちばん意外な担当。正解が①ばかりに偏らないよう並べ替え、正答が下線部の言い換えで済んでいないかを見抜きます。どちらもAIではなく、足し算と文字の重なりだけでやっています。
⑪ の出力(正答は必ず1番目) ──▶ ⑫ 公平さの検査係
《テーマの全文字のコード合計 ÷ 4 の余り = ずらし幅 → 正解位置の並びが決まる》
例)余りが2 → ③④①②③④①… の順に、4択7問へ順番に配る
├─並べ替え:1番目の正答を、決められた位置へ差し込む ──▶ ⑭ へ
└─言い換え判定:下線文と正答を3文字ずつに刻み、72%以上重なれば差し戻す
(同じ道具を逆向きにも使う:正答と解説が重ならなければ、解説が別の話 → 差し戻す)
| 受け取るもの | 正答が1番目に置かれた設問と、テーマ |
|---|---|
| 出すもの | 正解位置を散らした設問(誤答どうしの順番はそのまま) |
| AI | 使わない。計算と文字比較だけ |
| 失敗した時 | 言い換えと判定されたら工程を失敗させ、⑬に作り直させる |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 並べ替える前 | テーマの文字コードを全部足し、4で割った余りを出す。乱数は使わない | 決定的アルゴリズム。同じテーマなら毎回同じ結果=再現できる。試験の再作成や検証がやりやすい |
| 余りが出た直後 | 「余り、余り+1、余り+2…」を4で割った余りで、11問ぶんの位置をあらかじめ作る | 剰余演算による巡回。①②③④が均等に回る。正本の原則4「正解の位置を分散させる」の実装 |
| 設問ごとに | 1番目にある正答を抜き取り、決められた位置に差し込む | 配列操作(splice)。誤答どうしの相対順は保たれる |
| 下線問題ができた直後 | 下線部の文と正答から記号と空白を落とし、3文字ずつの破片に刻んで重なりを測る。72%以上なら「言い換えただけ」 | N-gram(3文字組)による類似度。意味は分からなくても、写しかどうかは測れる |
| 同じ判定を、別の目的でも | 正答が8文字以上あるのに解説と重ならないなら、解説が別の選択肢の話をしているとみなして差し戻す | 整合性検査。1つの道具を逆向きに使い、AIの「解説と正答のずれ」を捕まえている |
| 短い文の時 | 8文字未満は判定しない。なぜ:短い言葉はどうしても重なってしまうため | 誤検知(false positive)を避けるための下限。検査は厳しすぎても使えなくなる |
44行しかない小さな担当ですが、このアプリの粘り強さのほぼ全部がここにあります。形式違反は「理由を添えてもう1回だけ作り直させる」、通信の一時不良は「700ミリ秒待って1回だけ送り直す」。どちらも1回だけです。
【形式が違う時】検査に落ちる ──▶ ⑬ ──[前回のJSON+落ちた理由]──▶ Claude
├─2回目が合格 → 何事もなかったように先へ(画面には出さない)
└─2回目も不合格 → 「自動修正後も形式検証を通過できませんでした: …」
【通信が不調な時】408 / 425 / 429 / 500 / 502 / 503 / 504 / 529 か接続失敗
└──▶ 700ミリ秒待つ ──▶ もう1回だけ送る(それでも駄目なら工程を失敗に)
| 受け取るもの | 不合格になった成果物と、その理由(検査が出したエラー文) |
|---|---|
| 出すもの | 作り直した成果物(1回だけ) |
| AI | 使う。作り直しの時だけ、同じモデルをもう一度呼ぶ |
| 失敗した時 | 2回目も駄目なら、その工程を失敗にして人に返す |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 検査に落ちた瞬間 | エラーの文言を、そのまま「何が駄目だったか」として使い回す | 例外メッセージの再利用。検査の言葉が、そのままAIへの指示の言葉になる。両方を1か所で保てる |
| 作り直させる時 | 前回のJSONを丸ごと同封し、「合格している所は維持し、違反箇所だけ直せ」と頼む。なぜ:白紙から作り直させると、良かった部分まで変わるため | 差分修正の指示。全面再生成との違いは大きい。事実関係の揺れを抑えられる |
| 2回目が合格した時 | ログに1行残すだけで、画面には何も出さない。利用者から見れば「1回で成功した」 | 内部リトライ。成功した回復は、利用者に見せない。ただし記録には残す |
| 2回目も落ちた時 | そこで諦め、工程ごと失敗にする | 再試行回数の上限。無限ループと費用の暴走を防ぐ。人が押す再試行(⑮)に引き継ぐ |
| 通信が失敗した時 | 相手が返した番号を見て、待てば直る種類かを判定する。混雑(429)や一時障害(5xx)だけ送り直す | 一時的エラー(transient error)の切り分け。入力ミス(400番台)は何度送っても直らないので送らない |
| 送り直す前 | 700ミリ秒だけ待つ。回数は1回だけで、待ち時間は増やさない | バックオフ(間を置く)。ただし段階的に延ばす方式(指数バックオフ)は採っていない。11章 |
9工程目。バラバラに作ってきた部品を1つの問題集に組み立て、十数項目の検査を通す担当。ここを通らなければ、保存も表示もされません。
⑦ ──[調査・設計図・設問2バッチ]──▶ ⑭ 組立 ──▶ 問題集1式(ProblemSet)
検査:構造3〜5件/本文1つ/設問8問(4択7+論述1)/選択肢はどれも4つ/
正解位置が⑫の並びと完全一致/職業への接続1問以上/新形式1問以上/
論述はキーワード2語・観点3件/出典IDが実在/用語8〜12件/年表4〜10件/
本文の空欄2・下線2/空欄下線を問う設問は本文を必ず1つ参照
├─全部通る ──▶ ⑮ 台帳へ保存(札を「完成」に)
└─1つでも駄目 ──▶「最終検証に失敗しました: …」(先頭5件だけ見せる)
| 受け取るもの | ここまでの途中経過すべて(調査・設計図・設問2バッチ) |
|---|---|
| 出すもの | 完成した問題集1式(JSON) |
| AI | 使わない |
| 失敗した時 | 完成扱いにしない。途中経過は残るので、この工程だけ再試行できる |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 組み立てる瞬間 | 題名は設計図から、テーマ・期間は利用者の入力から、出典は調査結果から取る。AIの言い値をそのまま使わない項目がある | 正規化(canonicalize)。同じ情報が複数の出所にある時、信頼できる方で上書きする |
| 同じ時 | 難易度「B」、想定読者の文言、基準日=終了日をコード側で固定する | 定数化。毎回同じ値は、AIに書かせない。ぶれる余地を減らす |
| 検査する時 | 落ちた項目で止めず、全部集めてから返す。なぜ:1つ直すたびに次が出る、を避けるため | エラーの収集。1件目で例外を投げる作りとの違い。直す側の手間が桁違いに減る |
| 設問を数える時 | 8問・4択7問・論述1問。加えて「職業への接続」と「新形式」が1問以上あるか | 仕様の数値化。プロンプトで頼み(⑪)、機械で確かめる(⑭)の二段構え |
| 出典を照合する時 | 設問が指す出典IDが、実際の出典一覧に存在するか1つずつ確かめる | 参照整合性。存在しない根拠番号は、読み手からは検証できない。だから機械で弾く |
| 同じ関数の中で | 設問13問の古い仕様を通す分岐が、いまも残っている | デッドコードに近い分岐。いまの経路では13問は生成されない。11章の割り切りの1つ |
工程が終わるたびに、成果・所要時間・失敗理由をまとめて1回で書き込む担当。この担当がいるから、途中で止まっても続きから再開できます。
⑤ ──[成果・所要時間]──▶ ⑮ 台帳係 ──[3つの書き込みを1回のまとまりで]──▶ D1
① job_steps に1行(工程名・何回目・秒数・成果のJSON)
② generation_jobs を更新(次の工程名・進んだ数・途中経過)
※ WHERE に「走行中で、工程がこのまま」を付ける=横取りされていたら書かない
③ 完成時のみ problem_sets に完成品(同じ依頼なら上書き)
失敗時 ── 失敗も1行残して札を「要再試行」に / 同じ工程の失敗3回で打ち止め
| 受け取るもの | 依頼票、成果物、所要ミリ秒、(完成時は)問題集1式 |
|---|---|
| 出すもの | 更新された台帳。呼び出し側へは何も返さない |
| AI | 使わない |
| 失敗した時 | 「工程の保存に失敗しました。」中途半端には書かない |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 工程が成功した瞬間 | 3つの書き込みを1回のまとまりとして送る。なぜ:一部だけ書かれた状態を作らないため | バッチ実行と原子性。すべて成功か、すべて失敗か。「工程は進んだのに成果が無い」を防ぐ |
| 依頼票を更新する時 | 「走行中で、工程がこのままなら」という条件を付けて書く。⑥で取った鍵を、書き戻す時にもう一度確かめている | 楽観ロックの再確認。実行中に時間切れ回収(⑥)が走った場合でも、上書きしない |
| 次の工程名を決める時 | 9工程の一覧を見て次の名前を取り、9つ目まで来ていたら札を「完成」にする | 状態遷移。順番はコードの配列1つで決まる。工程を足すのは、その配列に1行足すこと |
| 進捗を出す時 | 進んだ工程数 ÷ 9 を百分率に丸める。保存はしない | 導出値。計算で出せる値は保存しない。保存すると、ずれた時にどちらが正しいか分からなくなる |
| 失敗した時 | 失敗も1行として残す。何回目の挑戦だったかも記録する | 監査ログ。成功だけ記録しても原因は追えない。画面の「再試行 1」はこの記録から出ている |
| 再試行を頼まれた時 | 同じ工程の失敗が3回に達していたら断り、「入力を見直して新しいジョブを」と案内する | 再試行の上限。履歴は既定12件・最大50件まで。処理中の履歴は削除できない(409) |
完成した問題集のJSONを、決まった形の画面に流し込むだけの担当。AIはHTMLを1文字も書きません。本文の目印を、押せる空欄や下線に変えるのもここです。
⑮ 台帳の完成品(JSON) ──▶ ⑯ 表示係 [[BLANK:A|正答]] → 押すと答えが出るボタン(もう一度押すと隠れる) [[UNDERLINE:a|文]] → (a) の付いた下線 [[REF:a]] → 設問の中の (a) ボタン ──押す──▶ 本文の該当箇所へ移動 「解答・解説を見る」──▶ 正解に○、選んだ誤答に印、解説を開く 年表・用語集・出典は折りたたみ / 解答の状態はメモリだけ(保存しない)
| 受け取るもの | 完成した問題集1式(JSON) |
|---|---|
| 出すもの | 画面。印刷にも対応する |
| AI | 使わない |
| 失敗した時 | 完成品が無ければ、この部分ごと画面に出ない |
| いつ動くか | 誰が・何を・なぜ(やさしく言うと) | 技術のことば(入り口) |
|---|---|---|
| 完成品を受け取った瞬間 | 本文を目印で切り分け、普通の文・空欄・下線・参照の4種類に分ける | パーサ(構文解析器)。同じ関数を、検査(⑩⑭)でも使い回している=ずれが起きない |
| 空欄を描く時 | 記号(A・B)だけを出し、押されたら中の正答に入れ替える | 開閉状態(トグル)。押した記録はメモリだけ。localStorage は使っていない |
| 設問の (a) を押した時 | 折りたたまれた本文を先に開いてから、該当箇所へ滑らかに移動して焦点を当てる | scrollIntoView と requestAnimationFrame。開く前に動かすと位置がずれるため、1フレーム待つ |
| 「解答・解説を見る」を押した時 | 正解に○を付け、選んでいた誤答に印を付け、解説を開く。上部には一括で開閉するボタンもある | 採点はブラウザの中だけで完結。答え合わせのためにサーバーへ行かない |
| 画面の幅が960ピクセル以下になった時 | 2段組みをやめ、本文を折りたたみパネル(高さ44vh)にする。広い時は本文が左に貼り付いて動かない | レスポンシブ(メディアクエリ)。正本プロンプトの指定は860pxで、実装は960px。ずれている(11章) |
| 印刷する時 | 見出し・入力欄・ボタンを消し、本文を開き、設問がページ間で割れないようにする | @media print と break-inside: avoid。PDF書き出しの機能を作らず、印刷用の見た目だけ用意している |
5章
ボタンを押してから問題集が出るまで、実際に何が起きているか。「次へ進めて」の電話が9回かかる、というのがこの表の骨格です。
| 回 | 工程名(画面の表示) | 動く担当 | AI | この回に保存されるもの |
|---|---|---|---|---|
| ― | 依頼票をつくる (POST /api/jobs) | ①→③ | ― | 依頼票1行(番号・テーマ・期間・モデル・札は「待ち」) |
| 1 | 調査計画を作成 | ⑧ | あり | 調査計画(見立て+調査単位3件) |
| 2 | 情報収集 1/3 | ⑨ | あり+検索 | 証拠パック1本+出典(最大8件) |
| 3 | 情報収集 2/3 | ⑨ | あり+検索 | 証拠パック1本+出典 |
| 4 | 情報収集 3/3 | ⑨ | あり+検索 | 証拠パック1本+出典(ここで合計5件未満だと失敗) |
| 5 | 問題の設計図を作成 | ⑩ | あり | 設計図(題名・構造4・本文1・用語8・年表6) |
| 6 | 問題 1〜4を作成 | ⑪→⑫ | あり | 設問4問(正解位置は⑫が配り直した後の形) |
| 7 | 問題 5〜8を作成 | ⑪→⑫ | あり | 設問4問 |
| 8 | 問題形式を確認 | ⑦ | なし | 数えた結果だけ(2バッチ・8問そろっているか) |
| 9 | 検証して保存 | ⑭ | なし | 完成した問題集1式/札が「完成」に変わる |
advance を叩く → ⑥が条件付きの1行書き換えで鍵を取る(取れなければ409で引き返す)。工程の区切りは、そのまま「やり直せる地点」の区切りです。9つに割ったのは進捗を見せるためだけではありません。1回のAI呼び出しが失敗した時に、そこまでの調査結果や設計図を捨てずに済むようにするためです。もし「1回のお願いで全部作る」形なら、8問目で形式違反が見つかった時、調査からやり直しになります。割り方=失敗の巻き戻し幅、という設計判断がここにあります。
6章
「AIアプリ」と一口に言っても、どこまでAIに任せているかは大きく違います。このアプリの線引きは、はっきりしています。
| 担当 | 呼ぶ回数 | 上限トークン | 何を任せているか |
|---|---|---|---|
| ⑧ 調査計画係 | 1回 | 2,000 | テーマを重ならない3つの調査単位に割る(判断) |
| ⑨ 情報収集係 | 3回 | 5,000 ×3 | ウェブを検索し、日本語の証拠と出典にまとめる(調査と要約) |
| ⑩ 設計図係 | 1回 | 9,000 | 本文・構造・用語・年表を書く(文章作成) |
| ⑪ 設問の作成係 | 2回 | 9,000 ×2 | 設問文・選択肢・解説を書く(文章作成) |
| ⑬ 内部の直し係 | 0〜7回 | 同上 | 形式違反があった時だけ、同じ担当をもう1回呼び直す |
ウェブ検索が走るのは最大9回(3工程 × 各3回まで)。使うモデルは claude-sonnet-5(既定)か claude-opus-5 で、利用者が生成のたびに選びます。両方とも同じ工程を通ります——モデルを変えても、工程の数も検査の厳しさも一切変わりません。
| つい「AIがやっている」と思いがちな所 | 実際にやっていること |
|---|---|
| 正解が①②③④に散らばっている | テーマの文字コードの合計を4で割った余りから並びを決めている。乱数ですらない(⑫) |
| 「下線部の言い換えでしかない設問」を弾く | 文字を3つずつに刻んで、重なりが72%以上かを数えている(⑫) |
| 設問と本文の空欄がきちんと対応している | 目印の並び順を正規表現で数えている(⑩) |
| 問題数や用語数が毎回きっちり揃う | コードに数字が書いてあり、合わなければ差し戻す(⑭) |
| 途中で失敗しても続きから再開できる | 工程ごとにデータベースへ保存している(⑮)だけ |
| 画面のレイアウトが毎回同じ | AIはHTMLを書いていない。人が書いた固定の型に流し込んでいる(⑯) |
このアプリは、AIに「文章を書く」と「調べる」だけを任せ、数・順番・位置・整合性は全部プログラムで確かめています。生成AIは、同じ頼み方をしても毎回違うものを返します。だから「毎回そろっていてほしいもの」は、頼むのではなく機械で数える。これはこのアプリに限らず、AIを業務に組み込むときの一般的な作法です。逆にいえば、数えられない品質(面白いか、本当に重要な論点か)は、まだ人が読んで判断するしかありません。
7章
どれも派手ではありませんが、この7つが無いと「たまに壊れるアプリ」になります。
画面の検査は速さのため、サーバーの検査は守りのため。窓口は直接叩けるので、画面を通らない依頼が来ても壊れないようにしてあります。片方だけでは足りません。
「空いているか確かめる → 取る」の間には、必ず隙間があります。その隙間で他の人に取られる。だから確認と取得を1文にまとめ、「1行だけ変わった」ことをもって勝者とします。データベースが1行ずつの書き換えを保証してくれることを利用した、古典的で確実な方法です。
工程の記録・依頼票の更新・完成品の保存を別々に3回送ると、2回目で失敗した時に「工程は進んだのに成果が無い」という矛盾が残ります。1つのまとまりにすれば、全部書かれるか、1つも書かれないかのどちらかになります。
粘れば直るものは粘り、直らないものは早く諦める。上限を決めておかないと、費用と時間が青天井になります。混雑(429)や一時障害(5xx)だけ送り直し、入力ミス(400番台)は送り直さない、という切り分けもここに入っています。
AIに自由に書かせて後から読み取るのではなく、JSONの設計図を渡して、その形でしか返せなくする。書式の崩れを、そもそも起こさない作りです。それでも件数や順番までは保証されないので、6と7で確かめます。
本文の目印を読み解く関数は1つしかなく、設計図の検査・最終検品・画面表示の3か所すべてが同じ関数を呼んでいます。だから「検査は通ったのに表示で崩れる」が起きません。
走行中のまま4分動かない依頼票を、台帳を読むたびに「要再試行」へ落とします。見張り役を常駐させると、それ自体が壊れます。ふだんの動作のついでに掃除するのは、常時動くものを増やさないための工夫です。
7つのうち6つは、AIとは無関係な普通のプログラムの作法です。AIを使うアプリだからといって、特別な守りが要るわけではありません。むしろ「返事が毎回違う」「時間がかかる」「途中で止まる」という3つの厄介さに対して、昔からある手当てをきちんと当てているかが効いてきます。
8章
| 表の名前 | 1行の意味 | 主な中身 |
|---|---|---|
| generation_jobs | 依頼票1件 | 番号・テーマ・開始日・終了日・使うモデル・札・今の工程・進んだ数・途中経過(state_json)・完成品・エラー文・3つの時刻 |
| job_steps | 工程の実行1回 | 依頼番号・工程名・何回目の挑戦か・成功か失敗か・使ったモデル・成果のJSON・所要ミリ秒・エラー文 |
| problem_sets | 完成した問題集1式 | 依頼番号・題名・テーマ・期間・問題集まるごとのJSON(依頼1件につき1行だけ) |
索引は6本。job_steps には「依頼・工程・試行回数」の組で重複を許さない索引があり、同じ試行が二重に記録されないようになっています。
generation_jobs.state_json。調査計画・3回ぶんの調査・設計図・設問2バッチが、この1つの箱に順に足されていきます。②が持っている画面の表示は、その写しにすぎません。generation_jobs.result_json(画面が読む方)と problem_sets.body_json(保管用)。同じ内容が入ります。画面が読むのは前者です。| 相手 | やり取りする中身 |
|---|---|
| Anthropic(AIの会社) | 出る:テーマ、期間、正本プロンプト27,787字、調査結果、前回のJSON(直しの時)。入る:計画・証拠・設計図・設問のJSON |
| ウェブ(検索) | AI側の道具として実行される。アプリが直接どこかのサイトを取りに行くことはない |
| ブラウザ | 画面と、依頼票・完成品のJSON |
APIキーはコードに1文字も書かれていません。動く場所から渡される設定(env.ANTHROPIC_API_KEY)を読み、無ければ手元の環境変数を見る、という順に探します。手元で動かす時は、1つ上の階層の共通 .env を読み込みます。
「途中経過を1つのJSONの箱に入れて、依頼票の1行に持たせる」のが、このアプリの背骨です。工程ごとに専用の表を作らなかったので、工程を足したり中身を変えたりしても、表の設計を変えずに済みます。代わりに中身の形はデータベースからは見えません――何が入っているかを保証しているのは、表の定義ではなくコードの型と検査だけ、という割り切りです。
9章
| 層 | 実体 |
|---|---|
| 公開先 | jiji-mondai-opus.akuramochi.chatgpt.site(OpenAI の Sites 基盤) |
| サインインの壁 | アプリの外側。基盤が「ChatGPTでサインイン」の画面を出している。アプリのコードには認証の処理が1行もない |
| サーバー本体 | Cloudflare Worker 1つ(worker/index.ts)。呼ばれた時だけ動き、返事を返すと消える |
| 画面の枠組み | React 19 + vinext(Next.js風の書き方を、Vite で Worker 用に組み直す道具) |
| データベース | Cloudflare D1(中身はSQLite)。バインディング名は DB |
| 手元で動かす時 | npm run dev → http://localhost:3023。共通の .env からAPIキーを読む |
| 方法 | 住所 | 役目 |
|---|---|---|
| GET | /api/jobs | 履歴の一覧(既定12件・最大50件、新しい順) |
| POST | /api/jobs | 依頼票を作る(202)。テーマ・期間・モデルを渡す |
| GET | /api/jobs/{id} | 1件の状態・工程の記録・完成品を読む |
| DELETE | /api/jobs/{id} | 履歴を消す(処理中は409で断る。3つの表からまとめて消す) |
| POST | /api/jobs/{id}/advance | 1工程だけ進める。②が9回叩く窓口 |
| POST | /api/jobs/{id}/retry | 失敗した工程を「待ち」に戻す(同じ工程で3回失敗していたら断る) |
/api/research と /api/generate の2つは、画面から一度も呼ばれていません。これは旧方式(1回のお願いで13問まとめて作る)の名残で、中身も現在の仕様(8問)と食い違ったままです。使うモデルも claude-opus-5 に固定されています。
ただし窓口としては生きており、外から直接叩けば動きます。サインインの壁の内側なので誰でもというわけではありませんが、「消し忘れた扉」がある状態です。11章に挙げた片付け候補の筆頭です。
守りを担っているのはコードではなく、置き場所です。アプリ自身は「誰が使っているか」を一切知りません。だからこのコードを別の場所へ引っ越すと、同じコードのまま無防備になります。個人用の道具として成立している前提が、置き場所に乗っている――という理解が要ります。
10章
| 仕組み | 何を守っているか |
|---|---|
| 型(TypeScript) | 「問題集にはこういう項目がある」をコードの段階で固定する。lib/types.ts がその定義。書き間違いは、動かす前に見つかる |
| JSONスキーマ | AIの返す形を、こちらから指定して固定する。3か所(計画・設計図・設問)で使用。余計な項目も禁じている |
| 件数と順番の検査 | ⑩⑫⑭。プロンプトで頼み、機械で数える二段構え。スキーマでは守れない「4件ちょうど」「A→a→B→b」を担保する |
| 自動修正と自動再送 | ⑬。落ちた理由を添えて1回だけ作り直させる/700ミリ秒待って1回だけ送り直す |
| 工程ごとの記録 | ⑮。成功も失敗も所要時間つきで残る。どの工程が重いか・どこで落ちやすいかが、推測ではなく記録で分かる |
| ESLint | 書き方の乱れと、よくある間違いを機械的に指摘する(npm run lint) |
tests/ というフォルダは空です。動くテストは scripts/test-auto-repair.mjs の1本だけで、npm run test:repair で実行します。中身は次の3つを確かめるものです。
package.json に test というコマンドはありません(test:repair のみ)。つまり⑬以外の担当には、自動テストが1つも無い状態です。⑫の正解位置の計算や言い換え判定、⑩の目印の並び順検査、⑮のまとめ書き込みは、いずれも入出力がはっきりしていてテストしやすいのに、まだ書かれていません。
品質の重心が「実行時に機械が確かめる」側に大きく寄っています。壊れたものは実行時に必ず止まるので、間違った教材が世に出ることはありません。その代わり、コードを直した時に「前は動いていた所」が壊れていないかを、自動で確かめる手段がありません。個人用の道具としては十分成立しますが、手を入れる頻度が上がると、まずここが痛みます。テストを足すなら、AIを呼ばずに動く⑫(正解位置・言い換え判定)が最も費用対効果の高い出発点です。
11章
欠陥という意味ではなく、「今はこう割り切ってある」という設計上の選択と、その代償を並べます。
/api/research と /api/generate。旧方式(13問・Opus固定)のまま生きています。tests/ は空(10章)。2026-99-99 のような実在しない日付は、③を通り抜けます。db/index.ts と drizzle/)と、13問の旧仕様を通す分岐(⑭)。| 順 | やること | 効き目と手間 |
|---|---|---|
| 1 | 使われていない2つの窓口を消す(lib/anthropic.ts・lib/prompts.ts・13問の分岐も一緒に) | 読む人の混乱と、外から叩ける扉を同時に減らせる。消すだけなので手間は小さい |
| 2 | ⑫と⑩に自動テストを足す(正解位置の計算・言い換え判定・目印の並び順) | AIを呼ばずに動く部分なので、テストが速くて安い。直した時の安心感が一番増える |
| 3 | 進行役をサーバー側へ移す(画面を閉じても最後まで進むようにする) | 「見ていないと終わらない」の解消。ただし作りは一段複雑になる |
| 4 | 再送を段階的に延ばす(700ミリ秒 → 2秒 → 5秒) | 混雑時の成功率が上がる。⑬の数行を直すだけ |
検査を緩めて「とりあえず完成にする」こと。いまは1項目でも合わなければ完成扱いにしません。厳しく見えますが、この教材の値打ちは「出典があり、形式がそろっていること」そのものです。ここを緩めると、AIが書いたそれらしい文章が、検証されないまま「問題集」として残ります。緩めるくらいなら、失敗を人に見せて、やり直させるほうが筋が通っています。
付録
実物を開いて確かめたい時のための対応表です。すべて C:\Users\aky_m\Scripts\current-affairs-mvp\ の下にあります。
| この資料での呼び名 | 実際の名前 | 場所 |
|---|---|---|
| ① 受付カウンター | StudioClient / submit | app/studio-client.tsx |
| ② 進行役 | driveJob / loadHistory / openJob | app/studio-client.tsx |
| ③ 入力の門番と整理券発行 | parseGenerationInput / parseWorkflowModel / createJob | lib/request.ts / app/api/jobs/route.ts |
| ④ 保存庫の管理人 | getDb / ensureSchema / SCHEMA_SQL | db/index.ts(+ db/schema.ts, drizzle/) |
| ⑤ 現場監督 | POST ハンドラ | app/api/jobs/[id]/advance/route.ts |
| ⑥ 鍵預かり係 | acquireStep / recoverStaleJob | lib/job-repository.ts |
| ⑦ 工程の割り振り係 | executeWorkflowStep / WORKFLOW_STEPS | lib/workflow-runner.ts / lib/workflow-shared.ts |
| ⑧ 調査計画係 | createResearchPlan | lib/workflow-ai.ts |
| ⑨ 情報収集係 | researchBatch / mergeResearch / parseResearch | lib/workflow-ai.ts |
| ⑩ 設計図係 | createBlueprint / validateBlueprintMarkup / passageMarks | lib/workflow-ai.ts / lib/passage-markup.ts |
| ⑪ 設問の作成係 | createQuestionBatch / QUESTION_SPECS | lib/workflow-ai.ts |
| ⑫ 公平さの検査係 | answerPattern / positionCorrectOption / isNearParaphrase | lib/problem-schema.ts / lib/question-quality.ts |
| ⑬ 内部の直し係 | validateWithRepairOnce / retryTransientOnce | lib/auto-repair.ts |
| ⑭ 最終検品と組立係 | assembleProblemSet / validateProblemSet | lib/workflow-ai.ts / lib/problem-schema.ts |
| ⑮ 台帳係 | completeStep / failStep / retryJob / listJobs / deleteJob | lib/job-repository.ts |
| ⑯ 表示係 | ProblemSetView / QuestionCard / MarkedText | app/components/problem-set-view.tsx |
| 作問の正本(27,787字) | 時事演習問題 作成プロンプト v3.0 | docs/prompt-v3.md |
| データの形の定義 | 型定義 | lib/types.ts / lib/workflow-shared.ts |
| 貸倉庫の入口 | worker.fetch | worker/index.ts |
| 見た目(配色・レイアウト) | スタイル 1,393行 | app/globals.css |
| 唯一の自動テスト | npm run test:repair | scripts/test-auto-repair.mjs(tests/ は空) |
| 使われていない旧経路 | researchTopic / generateProblemSet | lib/anthropic.ts / app/api/research・generate/route.ts |
| 工程の数 | 9 | AIの呼び出し | 7回 | ウェブ検索 | 最大3回×3工程 |
| 設問 | 8問(4択7・論述1) | 押さえる構造 | 4件 | 用語集 / 年表 | 8件 / 6件 |
| 本文の字数 | 700〜900字 | 解説の字数 | 140〜220字 | 論述 | 80字・語2・観点3 |
| テーマの長さ | 3〜400字 | 出典(1工程) | 最大8件・2件未満で失敗 | 出典(統合後) | 最大20件・5件未満で失敗 |
| 上限トークン | 2,000 / 5,000 / 9,000 | 引用文の長さ | 700字で切る | 1問の出典 | 最大3件 |
| 自動修正 | 1回だけ | 自動再送 | 700ミリ秒後に1回 | 人が押す再試行 | 同じ工程で3回まで |
| 進捗の見に行き方 | 2,500ミリ秒ごと | 時間切れの回収 | 4分 | 履歴 | 既定12件・最大50件 |
| 言い換えの判定 | 3文字組の重なり72%以上 | 判定の下限 | 8文字未満は判定しない | 画面の折り返し | 960px / 680px |
最後に
このアプリの一番の判断は、「1回のお願いで問題集を作る」のをやめて9つに割ったことです。割った理由は進捗を見せるためではなく、失敗したときに巻き戻る幅を小さくするため。8問目で形式違反が出ても、調査からやり直しにはなりません。AIに限らず、時間のかかる処理を人に頼む時・機械に頼む時の、そのまま使える型です。「どこまで戻ってやり直すことになるか」を先に決める、と言い換えられます。
問題数8問、構造4件、正解位置の散らばり、出典番号の実在――毎回そろっていてほしいものは、全部プログラムが数えています。AIは同じ頼み方でも毎回違うものを返すので、「お願いしたから大丈夫」は成り立ちません。逆に、頼んで確かめられるものは頼めばいい。「これは頼むことか、数えることか」で切り分ける――AIを仕事に入れる時の、実務的な判断軸そのものです。
このアプリには認証のコードが1行もありません。サインインの壁は配信基盤が出しており、同じコードを別の場所に置いた瞬間、守りは消えます。APIキーも同じで、コードには書かれておらず、動く場所から渡されます。「このアプリは安全か」という問いは、コードだけを見ても答えが出ない。何がコードの責任で、何が置き場所の責任なのか――外注するとき・引き継ぐときに、まず確かめるべき境目です。
※ この資料は、2026年8月11日時点の current-affairs-mvp のプログラムを実際に読んで作成しました。
※ 「担当者」という言い方はこの資料の説明のための呼び名で、プログラム内にその名前が付いているわけではありません。対応は付録の表のとおりです。
※ 数値(上限、待ち時間、字数、しきい値)はすべて実際のコードから取った値です。プログラムを直せば、これらの数値も変わります。
※ サインインの壁の挙動だけは、コードではなく公開URLの応答から確認したものです。