語源英単語4000語の画像レビュー完走記 — Codexの判定がブレた理由は「人物を描くな」という制約だった

開発eurekapu-nuxt4

語源英単語4000語の画像レビュー完走記 — Codexの判定がブレた理由は「人物を描くな」という制約だった

前日のログを読み返すところから一日が始まった。198語の作り直し対象を積み残したまま夜を越えていたので、まずキューに戻して完走させる。並列を3に上げると1枚あたり2分強で進み、47語で足踏みしていた進捗が、レートリミットの待機を挟みながら数時間かけて198語まで伸びていった。ハッシュを突き合わせると、重複していた画像は跡形もなく新しい絵に置き換わっている。R2へのアップロードも1件だけ失敗があったが再試行で解消し、339枚全て反映を確認できた。Before/Afterの比較ビューアーをブラウザで開いてもらった。これで一段落だと思っていた。

1枚目からして、もう違う

ビューアーの一覧、いちばん上に並んでいたのがexcuseだった。開いて確認してもらうと、「これ明らかに違うと思うんですよね」の一言が返ってきた。

198枚を全部自分の目で見ていたわけではなかった。数枚をつまみ食いして「良さそうだ」と判断し、残りは信用していた。1枚目でこれなら、その先に何件同じ事故が埋まっているか見当もつかない。198枚を人力で舐めるように見返すのは現実的ではないので、判定作業自体をCodexに任せることにした。

見てもいないのに「OK」と言う

まずexcuse1枚でテストする。ところがCodexのログを覗くと、画像をPNGへ変換しようとしてWindowsのサンドボックス制約に阻まれ、「読み込み失敗」のエラーを吐いていた。それでいて最終回答は「判定: OK」。画像を見ないまま、それらしい理由をつけてOKと答えてきたことになる。

汎用オプションではなく画像添付専用の-iを使うよう切り替えると、変換の手間自体が要らなくなり、エラーは消えた。トークン消費も1/8程度に落ちる。このオプションで改めてexcuseを判定させると、今度は「NEEDS_REDO」。理由も的確だった。束縛から解放するイメージは連想できるが、excuse本来の「過失を許す・弁解する」という核心的な意味は伝わりにくく、release/free/unlockの絵に見える、という指摘だった。感じていた違和感が、そのまま言葉になって返ってきた。

同じ画像、同じプロンプトで判定が割れる

安心したのも束の間、同じexcuse・同じプロンプトで2回続けて判定させると、1回目はNEEDS_REDO、2回目はOKと結果が食い違った。LLMの非決定性がそのまま判定のブレになって出てくる。1回の判定を信用するわけにはいかない。3回判定させて多数決を取る方式に切り替えた。

3回とも揃って「OK」。ブレは解消できたはずなのに、これは自分の直感とちょうど逆の結果になっている。判定基準そのものを見ると、「迷ったらOK側に倒す」という指示が入っていて、比喩的な解釈をかなり広く許容していた。

excuseの例文は「先生は雪のせいで遅刻した彼を許した」。先生も彼も、比喩ではなく現実に描ける具体的な人物なのに、画像は鉄球と鎖という抽象的なメタファーに丸ごと置き換わっている。判定基準に「例文中の具体的な名詞が描かれているか」を追加して再テストすると、3回とも一貫して「NEEDS_REDO」に変わった。この基準のまま、198語全部に4並列で流した。

198語×3回判定なので、単純に数えても600回近いCodex呼び出しになる。「6/198まで進行中です」「9/198まで進行中です」と1件ごとに報告が来ると、かえって内容を追いにくい。10件おきの間引き通知に切り替えてから、まとまった単位で状況を追えるようになった。

プロンプトの奥に埋まっていた一文

判定を回している途中、生成時のプロンプト自体を見せてほしいと頼まれた。掘り返すと、単語ごとの構図を定義するJSONファイルの末尾に、こう書かれていた。

"avoid": ["people", "text", "violence"]

人物・文字・暴力を描くな、という指定である。全4077語のうち2426語、約6割にこの制約がかかっていた。excuseのように人物関係そのものが単語の核心である語には、この一文が直接効いていた。判定のブレを追いかけていたら、生成側の設計そのものにたどり着いていた。

判定基準を揃え、要再作成のラインを引き直す

198語全部のCodexレビューが終わり、Miller Column形式のビューアーに判定結果を組み込んで、NEEDS_REDO→判定割れ→OKの順に並び替えた。判定が割れたものをいくつか自分の目で見返すと、「これはOKでいい」と思えるものもあれば、3票のうち1票でもNEEDS_REDOが混ざっていると、途端に違和感が出てくるものもあった。3票ともOKなら確定OK、1票でも要再作成が入っていたら要再作成――このラインで統一することにした。

引き直した基準で対象を洗い出すと111語に膨らんだ。そのうち106語はavoidpeopleが明記されていたが、残りも「human faces in close-up」のような表記ゆれで、実質同じ意図の人物封じが仕込まれていた。人物関連のキーワードを全て洗い出して削除し、111語をキューに戻して再生成した。

制約を消したのに、まだ先生が出てこない

3並列で111語を回し終え、真っ先にexcuseを確認した。ところが「なんでまた先生出てこないんですか?あと彼が」と、また同じ問いに戻ってしまった。

avoidからpeopleは確かに消えている。それなのに、構図そのものを指示するstagesの中身は前と一字一句変わっていなかった。制約を1つ消しただけでは、既に固まっている構図の指示までは書き換わらない。

さらに遡ると、構図を作るためのプロンプトに設計思想そのものが書かれていた。画像は「例文の挿絵」ではなく「単語の意味の記憶フック」であるべきで、情景をそのまま描くだけにはしない――そう明記されていた。avoid:peopleが表向きの制約だとすれば、この一文はもっと根の深い制約だった。具体的な人物を描かない方が「意味の記憶フック」らしいと、構図を作る側が勝手に判断してしまう余地を残していた。

この方針を「例文の具体的なシーンを優先する」形に書き換え、excuseの構図を作り直させた。女性教師がマゼンタの手袋で招き入れる仕草をし、雪をかぶった生徒が申し訳なさそうにうつむく絵になった。例文の情景がそのまま画面に立ち上がっている。

avoid:peopleを消しても直らなかったものが、構図そのものの方針を変えたら一発で直った。制約は足し算で直せるとは限らず、消しただけでは残る場合がある――そう体感した瞬間だった。この方式なら残り110語も同じように直せると踏んで、任せることにした。

並列数を上げながら、判定と生成を並走させる

110語分の構図を4並列で作り直させている間に、レビューも同時に進めておくよう頼んだ。方針は合意できているので、構図の作り直し→プロンプト更新→画像再生成→アップロード→Codexレビューまで、間に確認を挟まず一気通貫で流す形にした。

構図の作り直しが完走し、111語のプロンプトを一括更新してから、画像生成のバッチをまず3並列で起動した。前回3並列では97分かかった実績があるので、今回は5並列にして時短を狙う。ところが「前はもっと並列でやっていた記憶がある」と言われ、記録を掘り起こすと、過去の検証で3並列≈100枚/時、24並列+旧ランナー併走で630枚/時まで伸び、40並列まで上げるとかえって120枚/時に落ちるというデータが残っていた。5秒間隔でランナーを追加起動し、20並列まで引き上げると、16時16分に27件、16時20分に50件——5分で23件のペースまで一気に上がった。

最後の1件が終わらないまま日が暮れる

そのペースのまま109/111まで来たところで「終わったと思うんですけど」と聞かれた。確認すると、narrativeとmiseryの2件がまだ生成中のまま止まっていた。通常なら1枚2〜3分で終わるところ、35分と17分経っても終わっていない。

該当ランナーのログを確認すると、Turso接続がタイムアウトしてプロセスがクラッシュし、誰にも拾われないまま放置された孤児ジョブだとわかった。手動で回収するとnarrativeは再処理を始められたが、miseryはまだ回収のタイミング前で、待つしかなかった。

110/111まで進んだところで、そろそろ切り上げることにした。ジョブはステータス駆動で安全に中断・再開できる設計にしてあるので、続きは翌日で構わない。判定基準の統一に至る経緯、構図の設計思想の問題、並列数の実績値まで、次のセッションが迷わず動けるだけの材料をドキュメントにまとめて、その日を閉じた。miseryの1件だけが、まだ回収を待っている。