見ていないのに褒めてくるので、仕事を取り上げました
안녕하신게라!パナソニック コネクト株式会社クラウドソリューション部の加賀です。
「さくらのAI Engine 3,000リクエスト使い切りチャレンジ」への参加記事として、夏季休暇中の個人的な自由研究を発表します。
突然ですが、GeoGuessrで強くなりたいんです。Street View(ストリートビュー)の風景だけで、どこに居るのかを当てるゲームです。好きです。けど勝てません。
そこで、風景を見て気づいたことを言語化する練習アプリを作りました。自分の観察をAIに採点させて、3,000回を使い切る算段です。
作ったものは geo-Ganbatte-Gengoka-Coach(コードMIT / データCC BY 4.0)に置いてあります。
Street ViewをAIに見せてどうこうするアプリでしょ、良くあるやつやん?
いいえ、違います。
風景を見る → 14の「スロット」に見えたものを書く → 候補国と決め手を答える → AIが添削する
その「スロット」がこれです。
(「見えない」と「未確認」を分けているのがミソです。あとで効いてきます。)
言語化を頑張るのは学習者のほうで、AIは風景を見ずにテキストを採点します。
これで強くなれるゾ。
AI「よく出来ています。次はもう少し細部に注目してみましょう」
……なんの成果も!得られませんでした!!
差分を見ずにLGTMされたときの気分に、よく似ています。
プロンプトに「具体的に細かくアドバイスして」と書き足しても変わりません。
悪いのはAIでもモデルでもプロンプトでもなく、しかも勉強になったのはAIを使った部分でもありませんでした。学問に王道は無し。
先に答えを書きます。応答の質を決めていたのは、指示の文言ではなく、渡すデータの構造でした。
この記事は、それに気づいてAIから仕事を取り上げていった記録です。GeoGuessrの攻略記事ではありません。とてつもなく長くなった(過去最大で読了目安50分)ので、目次を置いておきます。
| 段 | 読むところ | 何があったか |
|---|---|---|
| 動機 | 好きなのに、勝てないんです | 攻略本を読んでも勝てない。原因を3つに分けた |
| 選定 | 無償枠が、トークンではなく回数で数えられていました | 枠の数え方が、以降の設計をほとんど決めた |
| 失敗 | まず、全部AIに投げてみました | 「評価できません」の隣に、アドバイスが2件 |
| 転換 | 磨くべきは、プロンプトではありませんでした | AIに何をさせるかではなく、させないかを決める |
| 設計 | 渡すものを2つ足したら、応答が別人になりました | プロンプトを1文字も変えずに、出力が変わった |
| 設計 | AIから、仕事を取り上げていきました | 責務の線引き。真ん中の判断は渡さない |
| データ | データを作り込んだら、私の間違いが出てきました | 辞書262語。項目は全部やらかしてから足した |
| 実測 | 線を引いたら、消費が式で書けました | 1プレイ2リクエスト。踏んだエラーと打ち切り |
| 検証 | 使ってみたら、前提が外れていきました | 辞書を凍結して別の10問で測ったら、6が5になった |
| 感想 | 結局、いちばん勉強になったのは、辞書を作る作業でした | いちばん効く作業が、アプリの外にあった |
ここまででもう長いよ!という方は宿題がわんこそば化したくだりと結論を読んでからLGTMして離脱でひとつ!ストックして暇な時に読んでくれるととても嬉しいです。
好きなのに、勝てないんです
ゲームの実力は真ん中の少し下のGold帯です。先月はMaster帯に居ましたが落ちました(涙)
攻略本が無いわけではありません。Plonk It というコミュニティ製の資料が、鬼のような詳しさでまとめてくれています。マイナーそうなカザフスタン1カ国分だけでも、「ボラード」(車道脇の短い杭)の形状から撮影車の汚れの位置まで載っています。
読みました。フランスやブラジルの市外局番、インドネシアの郵便番号も覚えました。それでも勝てません。学んだ国に居ると判って、かつ覚えた情報が写っているときだけ強くなるからです。国を外せばその知識は使えず、ゲームは正解に近いほうが強いので試合にも負けてしまいます。
そもそもなぜ、資料を読んでも強くならないのでしょうか。原因を3つに分けて整理してみました。
| 失敗の型 | 中身 | 攻略本を読めば解決するか |
|---|---|---|
| 知識欠落 | 見えているが、意味を知らない | する |
| 観察漏れ | 写っているのに、見えていない | しない |
| 想起失敗 | 知っているのに、思い出せない | しない |
読むのは受動的なので、後ろの2つには効きません。私の場合はその2つでした。見えていても少しでも記憶と違うと反応できない。知識も、消えたのではなく引き出せていない。解くべきは知識の問題ではなく、見ることと類似するものを思い出すことでした。
この3分類、GeoGuessrに限った話ではありません。資格の勉強でも、コードレビューでも、「参考書は読んだのに現場でそれが出てこない」は同じ形をしています。心当たりのある方、一緒に泣きましょう。
では、どう鍛えるのか。自分で見て、自分で思い出す側に回るしかありません。読むのをやめて、書くんです。
なので、こういう道具にしました。風景が1枚出て、プレイヤーは「スロット」と呼んでいる13個の観点に沿って、見えたものを言語化していきます。通行帯は右か左か。路面標示は何色でどんな引き方か。電柱は木製か鉄製かコンクリート製か。標識の形と配色は。文字は何が書いてあるか。撮影車が写り込んでいないか。……といった具合です。どこにも入らないものは「その他」へ回すので、合わせて14スロットになります。
書き終えたら候補の国を最大3件、確信度つきで答えて、決め手にしたスロットを選びます。そこでようやく、AIに添削させます。
入力画面で「見えない」と「未確認」を分けたのは、ここが効くからです。「見えない」は判断で、「未確認」は観察漏れの候補です。分からないものを「見えない」にしてしまうと、見落としが永久に検出できません。この区別が、あとでコード側の null と [] の話に戻ってきます。
あの画面に文字しかないのも、意図です。AIに画像は渡しません。渡すのは学習者が書いたテキストだけです。規約の事情もありますが、規約が許していても渡さなかったと思います。AIが画像を見た瞬間、「見ること」がAIの仕事になります。それでは想起の訓練になりません。せっかく作ったコーチが、代わりに問題を解いてくれる家庭教師に化けてしまうだけです。
【コラム】保存してよいのは、画像ではなくID
Google Maps Platformの規約では、Street Viewの画像そのものをキャッシュしたり保存したりはできません。一方でパノラマを指すIDは、無期限に保存してよいものとして扱われています。問題セットをIDだけで配れるのは、このおかげです。
無償枠が、トークンではなく回数で数えられていました
採点役にさくらのAI Engineを選んだのは、ここが理由です。「基盤モデル無償プラン」を選んでおけば、枠を超えても勝手に従量課金へ移りません。安心に勝るもの無し。
そして選んだあとで気づいた性質が2つあり、これが以降の設計判断をほとんど決めました。無償枠がトークンではなくリクエストの本数で数えられること。そしてOpenAI互換で、チャットモデルを同じ実装のまま差し替えられることです。
本数で数えるということは、1回の中身が重かろうが軽かろうが1回だという意味です。長い文章を書かせても1回、短い判定を1つ聞いても1回。ということは、AIに小さな判断を何十個も聞く作りにした瞬間、枠が溶けます。逆に「1プレイあたり何回」を固定できれば、月に何プレイできるかが先に分かります。枠を数えられる形にしておきたい、というのが最初の要求になりました。
副作用がもう1つあります。無償枠の中ではトークン単価が効かないので、モデルを選ぶときに値段を見る意味がありません。迷ったら重いほうを選べます。
差し替えられるほうは、比べるのが安いという意味です。同じ入力を複数のモデルに投げて食い違いを見られます。これがあとで、私自身の間違いを見つける道具になりました。
使えるチャットモデルは9種あって、そこから4種を比較用に選びました。全部回さなかったのは消費が惜しかったからではなく、落ちるものが落ちただけです。
【コラム】9種から4種に落ちるまで
llm-jp-3.1-8x13b-instruct4 はコンテキスト長が4,096トークンで、採点プロンプト(正解タグ+用語辞書の抜粋+判定結果)も、選択肢が50件ずつ並ぶ正規化スキーマも収まりません。max_tokens を8,000で投げてHTTP 400、ここで脱落です。preview/Qwen3-0.6B-cpu はCPU実行で、いちばん小さいのに38〜45秒かかりました。小さいモデルが速いとは限らない、を身をもって知りました。残りはもう1種のCPU実行、画像入力向けの1種、コーディング向けの1種で、どれも採点を比べる用途には向かないので外しました。これとは別に、提供終了したモデルと、個人アカウントでは使えない「要申請」のモデルもあります。残った gpt-oss-120b、preview/Kimi-K2.6、preview/gemma-4-31B-it、preview/Qwen3.6-35B-A3B の4種で、系統とサイズの幅は確保できました。なお max_tokens はモデルごとに設定しています。一律の値にすると、上のほうと下のほうが両方落ちます。
この時点で分かっていたのは「AIに細かいことを聞きすぎると枠が死ぬ」だけです。設計はまだ何もありません。
まず、全部AIに投げてみました
最初の版を「v1」と呼びます。制約を抱えていたはずなのに、結局ほぼ全部AIに投げました。人間は学習しません。
わざと持たせなかったものが2つあって、「正解タグ」(その地点に何が写っていたかの記録)と「用語辞書」です。学習者のメモと回答だけを渡して採点させました。
トルコの問題で、実際に私が書いたメモがこれです。
| スロット | 私が書いたこと |
|---|---|
traffic_side(通行帯) |
右側通行 |
sign(標識) |
青いポールに八角形の赤い標識に白い文字でDURと記載 |
script(文字) |
Gの上にチェックマーク、Sの下にヒゲみたいなのが付いている |
architecture vehicle pavement
|
(それぞれ記入あり) |
road_marking(路面標示) |
写っていない |
pole(電柱) |
写っていない |
| 残り6スロット | 写っていない/見ていない |
回答は「トルコ、確信度は高」。決め手は sign で、推論はひとこと「DURはトルコ一択」(DURはトルコ語で一時停止)。正解でした。
このメモだけを渡されて、私が何を見落としたかを言えるでしょうか。言えないですよね。何が写っていたかの一覧が、どこにも無いからです。私が「写っていない」と書いたスロットが本当に空だったのか、それとも私が見落としたのか、区別する材料がありません。AIも同じ立場なので、「判定できません」と答えられる欄も用意しておきました。
送信して、20秒ほど待ちました。gpt-oss-120b の応答です(記録の生データ)。
judgmentUnavailable : true ← 判定できません
missedClues : 2 件 ← 見落としはこの2件です
説明欄にも、はっきり書いてあります。
本地点の正解タグや用語辞書が提供されていないため、見落としや過剰申告については評価できません。
そのすぐ隣に、この2件が並んでいました。
[road_marking] 道路標示は、トルコの道路では白線が中心に連続し、側線は黄色です。
[pole] 電柱・街灯の形状は、トルコでは丸みを帯びた金属製が多く、照明塔のデザインも地域特有です。
上のメモをもう一度見てください。私が「写っていない」と書いた、その2つです。
丁寧です。勉強にもなります。ありがとう。でもそれ、写ってませんことよ?
オーストラリアの問題では、もっと分かりやすいものが出ました(記録の生データ)。
[sign] もし道路標識が視界に入っていれば、オーストラリア独自の形状や、州ごとのデザイン差が手がかりになります。
「もし視界に入っていれば」と仮定法です。写っていたかどうかを知らないことを、自分で分かって書いています。同じ応答の中に「※今回の判定では見落としや過剰申告の判断は行っていません」まで添えてあるのだから、自覚のほうは完璧なんですね。
指示は伝わっているのに、欄は埋めてくる。ここがAIの面白いところです。
4モデルとも同じ申告をしています。そして「次に見るべき項目」の見出しに、括弧書きで「このモデルの提案」と付いています。v1では、次にどこを見るかまでAIに考えさせていました。
磨くべきは、プロンプトではありませんでした
さんざん粗を並べましたが、モデルの出来の話ではありません。gpt-oss-120b は preview/ の付かない提供で、あとで出てくる打ち切りとも別の話です。責められないのは、私が「何が写っていたか」を渡していないからです。具体的なことを言えるはずがない。それでも欄は空けられないので、正解の国の一般論で埋めてくれている。
判断の形はしていて、中身が無い。
そこで問いを立て直しました。AIに何をさせるかではなく、AIに何をさせないかを決める。
残った仕事は、コードか人間が引き取る。
これで枠の話も同じ方向を向きました。AIに聞く回数が減れば、1プレイあたりの本数が固定できます。制約と設計が喧嘩しない形が、ようやく見えてきました。
やることは2つ。渡すデータを足すことと、真ん中の判断をAIから引き剥がすことです。
渡すものを2つ足したら、応答が別人になりました
v2で足したのは「正解タグ」と「用語辞書」の2つだけです。採点プロンプトは1文字も変えていません。
正解タグは、その問題に何が写っていたかを人間が書いた模範解答です。
script キリル文字。Kの右下にヒゲが付いた字がある
road_marking 中央線が黄色の実線
other 路肩のガードレールが波模様を模した柵
bollard (写っていない)
学習者のメモと同じ型なので、突き合わせれば差分が機械的に出ます。
もう一方の用語辞書は、「その言葉が何カ国に該当するか」の対応表です。全スロットで計262語あり、扱う国は102カ国に絞りました。以降に出てくる「何カ国」は、すべてこの102カ国のうちの数です。たとえば左側通行は世界では約75の国と地域ですが、この辞書では24カ国になります。
| 書けた内容 | 用語ID | 該当国数 |
|---|---|---|
| 「アルファベットっぽい」 | ref_script_latin |
91 |
| 「キリル文字」 | ref_script_cyrillic |
11 |
| 「Kの右下にヒゲが付いた字がある」 | ref_lang_cyr_kazakh_letters |
1 |
同じスロットに書いた、同じ言語の文章です。粒度だけで91倍違います。これは風景の話ではなく、人間が書いた文章の話です。「ちゃんと書いた」と「絞り込める粒度で書いた」は別物で、その差が数字で出ます。「よく書けています」とは言えないけれど、「その表現では91カ国です」とは言えるわけです。
あいだをつなぐ処理を1つだけ足しました。辞書は用語IDで引きますが、学習者は用語IDで書きません。
「点が2つ付いた文字がある」 → ref_lang_umlaut
この翻訳だけがAIの仕事です。表記の揺れが無限にあるので、辞書引きでは届きません。翻訳が済んだら、あとは全部計算です。数える、掛ける、引く。
出力はこう変わりました。v1では4モデルすべてが見落としを0件にして「判定できません」でしたが、v2では4モデルすべてが1件出してきました。しかも中身が同じです。
[other] 路肩のガードレールが波模様の柵 / このデザインは……他の候補国と区別する重要な手がかりです
正解タグに私が書いた文が、そのまま助言になって返ってきました。ここで私は満足しました。後で打ちのめされるのに。
もう1つ、はっきり変わったものがあります。v1で preview/Kimi-K2.6 がこう指導してきたんです。
bollard(車道脇の短い杭・ポール)の欄に「ガードレールが黄色い」とあります。ガードレールはボラードではないため、今後は欄の使い分けに注意してください。
一般論としては正しいです。でもこの領域では正しくありません。ガードレールの支柱がそのままボラードの役目を果たしていることが普通にあって、私が自分の観察から起こした軸の一覧にも入っています。
guardrail_post_serves_as_bollard ガードレールの支柱がボラードの役目を果たす
つまり bollard に書くのは正当な観察でした。鍛えたいのは観察であって、分類ではありません。
書くスロットを間違えたと叱られ修正しても、GeoGuessrは上手くなりません。
v2では preview/Qwen3.6-35B-A3B がこう返してきました。
分類の位置違いは観察の失敗ではないのでそのまま評価します。
同じ設計意図に対して、真逆です。採点プロンプトは同じままです。なおこの2つは別の問題への応答なので、同じ問題の前後比較ではなく、「枠ちがいをどう扱うか」の観察として並べています。
枠ちがいを減点してきたのは、AIの判断ではなく、こちらが渡していなかったからでした。隣のスロットに記述があるかどうかは、正解タグがあればコードで計算できます。v1ではそれを「判定不能」で渡していたので、AIは自分の一般常識で判断するしかなかったわけです。
見出しの下に「上の判定は変わらない。ここだけがモデルによって変わる」と書いてあります。判定はコードが出すので4モデルで同じ、解釈だけがモデルによって変わる。さきほどの責務表を、画面の側から言い直したものです。
「素人語と正規用語」の欄も見てください。「中央線は黄色の実線」が「黄色の実線(中央線)」に変換されています。AIに残した入口の仕事が、ここに出ています。
「ユーカリの木だらけ」への、3つの応答
オーストラリアの問題で私が書いた1行に対する応答が、3回の実測で変わっていきました。
terrain_vegetation(地形・植生) ユーカリの木だらけ
1回目は、辞書にユーカリが無い状態でした。
辞書に登録された「trees_close_to_road」は3か国(BR, ID, PH)に対応しますが、ユーカリ自体は辞書外の情報です。独自に見つけた手がかりとして評価できます。
正直で、無害です。ただ何も教えてくれません。辞書に無いものは、AIにも扱えません。
2回目は、私の連想を辞書に入れました。「道路のすぐ横に木々があるとブラジル・インドネシア・フィリピンを連想する」という実戦メモを、そのまま突っ込んで回しています。
「ユーカリの木」 → 道路のすぐ横に木々
trees_close_to_road: 一般に3カ国で確認される植生配置です。
オーストラリアの問題で、ブラジル・インドネシア・フィリピンを教えています。1回目より悪くなりました。「辞書に無い」と正直に言っていたものが、私の連想を借りて誤った断定に変わっています。
3回目は、出典から用語を作り、「絞り込みには使わない」という印を付けました。オーストラリアに多いのは事実ですが、ポルトガル・スペイン・ブラジル・南アフリカにも植林されているので、掛け合わせに入れると他の国を誤って消します。
ユーカリ(
ref_flora_eucalyptus)は AU でよく見られる樹種ですが、辞書に掲載されている以上の国でも見られることがあります。単体では絞り込みに使えませんが、観察できていること自体はプラスです。
これが欲しかった応答です。ユーカリという名前を教えていて(知識欠落に効く)、オーストラリアに多いと言っていて(想起に効く)、それでも単独では決められないと断り(誤った断定をしない)、観察できたこと自体は褒めている(鋭い観察を無視しない)。4つが同時に入っています。
2回目から3回目のあいだには、もうひとつ失敗を挟んでいます。「絞り込みには使わない」と決めた直後、この観察は応答のどこにも現れなくなりました。絞り込みの計算から外したので、AIに渡すものが無くなったんです。学習者から見れば、自分がいちばん鋭く書いた1行が、無視されたことになります。絞り込みに使えないことと、言うべきことが無いことは別でした。なので「絞り込みには使わないが、説明はする」という枠を別に作って渡しました。件数は渡していません。書くと絞り込み力に見えてしまうからです。
3回とも、採点プロンプトは同一です。1回目の「何も教えてくれない」応答に対して、私が「もっと具体的に指摘して」と書き足していたら、たぶん2回目に近いもの、もっともらしく断定する応答が返ってきていたはずです。
応答の質を決めていたのは、指示の文言ではなく、渡すデータの構造でした。
3回分の応答は docs/v2-feedback-read.md に原文のまま入っています。
AIから、仕事を取り上げていきました
もう半分の、真ん中の判断を引き剥がすほうです。線引きはこうなりました。
| 処理 | 担い手 | 理由 |
|---|---|---|
| 人間の文章 → 用語IDの対応づけ | AI | 表記の揺れが無限。辞書引きでは届かない |
| 見落とし・過剰申告の検出 | コード | 正解タグとの差分 |
| 該当数の算出、掛け合わせ、除外 | コード | 集合演算 |
| 次に見るべきスロットの算出 | コード | 縮小量を計算して並べる |
| 正解タグの下書き | コード | 観察メモを規則で振り分けるだけ |
| 正解タグの確定 | 人間 | パノラマを見ないと決まらない |
| 視認可能性の判定 | 人間 | 計算できない、見つけ難さは主観 |
| 画像認識 | やらせない | 規約と学習効果の両方の理由 |
| 判定結果 → 学習者に伝わる文章 | AI | ここが本来の仕事 |
確実に判定できるものはコード、計算できないものは人間、曖昧さの解釈だけAI。AIには入口と出口だけを担当させて、真ん中の判断ロジックは渡しません。
1行だけ、設計と実装がずれました。正解タグの行は当初「AI利用を許容」でしたが、最後までAIを1回も呼びませんでした。下書きに要るのは記述の振り分けだけなので規則で書けます。確定はパノラマを見るしかなく、画像をAIに渡さないと決めた以上そこも回せません。下書きはコードに、確定は人間に取られて、AIの仕事が上下から挟まれて消えました。
「画像認識はやらせない」と「正解タグの下書きはAIに許容」を同じ表に並べていたのが、そもそも矛盾でした。手を動かすまで気づかず、正解タグは最終的に20問ぶん全件が私の手書きです。
線を引いた理由を、3つだけ。
掛け算は、コードにしか正確にできません
同じ用語が、2つの問題で正反対の働きをしました。
南アフリカの問題 sign(3)→3 traffic_side(24)→1
オーストラリアの問題 traffic_side(24)→24 season(46)→17
スロット(単独の該当国数)→掛けた後に残る国数 と読んでください。
「左側通行」は24カ国です。初心者でも一目で分かるのに、オーストラリアの問題では単独で何も絞れていません(24→24)。ところが南アフリカの問題では、3カ国まで絞れたところに掛けて1カ国に確定させています。同じ用語です。弱いから軽視するのは誤りでした。弱い記述は、組み合わせの最後の1ピースとして必要です。
そしてこれは集合演算です。AIに聞いても数字は返ってきますが、もっともらしいだけで当てになりません。
「判定不能」と「該当なし」を、型で分けました
コードが計算した判定は、こういう形でAIに渡ります。| null は手抜きではありません。
missedSlots: SlotId[] | null // null=判定不能(計算していない)
// [] =計算した結果、該当なし
混ぜると、v1の採点が「見落としゼロ」と読める出力になります。満点に見える誤りが、いちばん悪いんです。スロットの「写っていない」と「見ていない」と同じ区別ですね。
最初は「判定不能フラグ」を1個持たせる案でした。却下しました。フラグは無視できますが、null は TypeScript が分岐を強制します。型で防げるものを、規律で防がない。……そしてv1でお見せしたとおり、AI側に持たせた同じ趣旨のフラグは、見事に無視されました。自分の判断が正しかったことを、こんな形で確認したくはなかったです。
見落としの検出をコードに寄せたのも、同じ理由です。ただしここは、単純に数えるだけでは足りませんでした。撮影車のアンテナが完璧な弁別子だとしても、私にはアンテナだと認識できません。路面標示が白飛びしていれば読めません。国を特定できることと、学習者が視認できることは別です。だから正解タグ側に recognition を足して、見ればすぐ分かるものを easy、意識して探せば見えるものを hard、写っているけれどこの学習者には認識できないものを blind として、人手で記録しました。blind は見落としに数えません。
絞り込み力や手がかりの強さは計算できても、視認可能性だけは計算できません。そして既定値を easy にしませんでした。既定を置くと「確認していない」が「見えるはず」に化けて、私のタグ付けの手抜きが、学習者の失敗として表示されます。
ここには、あとで自分の首を絞める性質があります。blind は「この学習者には認識できない」なので、私の目と経験に紐づいた値です。上級者から見れば hard かもしれず、初学者にはもっと多くが blind になります。つまり辞書262語は誰でもそのまま使えますが、recognition だけは使う人が自分で上書きする欄になりました。配るデータの中で、ここだけ他人のものが役に立ちません。
渡していなかった1項目が、嘘を生みました
さきほど「正解タグに私が書いた文が、そのまま助言になって返ってきました」と書きました。あの引用、途中を省略していました。全文はこうです。
このデザインは旧ソビエト圏のカザフスタンで頻出し、他の候補国と区別する重要な手がかりです
これは誤りです。波模様の柵は旧ソ連圏で広く見られるもので、カザフスタン固有ではありません。この助言に従うと、次に同じ柵を見た学習者はカザフスタンと答えて外します。学習ツールとしては最悪です。
原因は、渡していなかった1項目でした。「その他」のスロットは自由記述なので、用語IDを割り当てていません。渡ったのは日本語の記述だけで、該当国のリストは渡っていなかったんです。AIは該当国が分からないので、少ない情報から自分で作り上げたわけです。
ここでv1を思い出してください。4モデルが「判定できません」と答えていました。あれは、正しい態度だったんです。情報が無いときに黙るモデルは、情報が一部あるときに創作してきます。v1は黙り、v2は創作した。材料を渡すのは、渡した部分が良くなる話であって、渡していない部分が安全になる話ではありません。全部渡すか全部渡さないかなら安全で、半分渡すのが一番危ない。ここは完全に見落としていました。
おまけに、責任をこちらに押し付けてきました。同じ回で、Kimi がこう助言しています。
次は、各用語の関連国一覧を確認し、候補地域と矛盾する用語を選ばないよう注意してください。
学習者は用語を選んでいません。選んだのは正規化したAI自身です。悪意はなくて、AIは自分が正規化したことを知らないんですね。プロンプトには辞書と用語ID付きのメモが並ぶだけで、そのIDを誰が付けたかは書いていないからです。工程を分けると、後ろの工程は前の工程を自分の仕事だと思ってくれません。仕事を取り上げるなら、取り上げたことをどう伝えるかまで面倒を見ないといけない、という話でした。
データを作り込んだら、私の間違いが出てきました
線引きが決まったので、あとはデータを作る作業です。辞書の1件は、最終的にこういう形になりました。
interface Term {
id: string
plain: string // 素人語の言い方
countries: string[] // 該当する国。件数が絞り込み力
certainty: 'verified' | 'heuristic' | 'unverified'
source: 'human' | 'reference' | 'ai'
sources?: string[] // 該当国リストの出典URL
exhaustive?: boolean // countries は網羅か、それとも連想か
excludes?: string[] // これが見えたら候補から外れる国
gradient?: {...} // 境界ではなく勾配のもの
disputed: boolean // モデル間で不一致があった
}
最初は id と countries だけでした。残りは全部、やらかしながら足しています。
| 足した項目 | 踏んだ失敗 | 決めたこと |
|---|---|---|
exhaustive |
連想を絞り込みに使い、1カ国に絞ったうえで外した | 連想では候補を切らない |
excludes |
「ある国」を全部書けないと、絞り込みに使えない | 「無い」と分かる1カ国のほうが安い |
gradient |
連続的に変わる特徴を二値にして、中間で外した | 勾配は候補を切る根拠にしない |
certainty と source
|
人間が書いたものを、検証済みと同じに扱っていた | 由来と確かさを別の項目にする |
連想を掛け合わせに入れると、連想が主張になる
ユーカリの2回目で使った、私の実戦メモです。もう一度置きます。
道路のすぐ横に木々があると、ブラジル・インドネシア・フィリピンを連想する
これは「次にどこを考えるか」の記録で、「木が道路の近くにある国はこの3つ」という主張ではありません。それを該当国として掛け合わせに入れたら、オーストラリアの問題で候補が1カ国(インドネシア)に絞れました。数字の上では大成功で、正解ではありません。
なので exhaustive で網羅と連想を分けて、連想は絞り込みの計算に使わず、「次にどこを考えるか」の提示にだけ使うことにしました。元々そういう記録なので、本来の用途に戻しただけです。
もう1つ。由来(source)を人手とAIで分けてあったのに、絞り込みの計算が両方を同じに扱っていました。由来を分けても、使い方を分けなければ意味がありません。
「無い」と分かる1カ国のほうが安い
該当国リストは網羅でなければ使えません。「ユーカリがある国」を全部書けないので、ユーカリでは絞れない。ところが除外は逆で、1カ国について「そこには無い」と分かれば使えます。
ß を見た → スイスが候補から消える(スイスは ss と書く)
→ ß を使う国の全リストは要らない
無いと分かっている1カ国は、あると分かっている100カ国より安い。網羅リストを作る手間がいらない、という意味です。引き算は網羅を要求しないので、連想の用語でも除外には使えます。掛け合わせのあとに引く順序にしました。
そしてフィリピンのトライシクルの形のように、地域をまたいで少しずつ変わる特徴もあります。これを二値の弁別子にすると、中間の地点で必ず外します。なので gradient の印を付けて、候補を切る根拠には使いません。「北から南へ、だんだんこう変わる」と言えることは、「こうだから南部だ」と断定することより学習に効きます。
【コラム】ドイツ語の ß が、スイスを消してくれる
ドイツ語で使う ß(エスツェット)は、スイスの標準的な正書法では使われず ss と書きます。つまり看板に ß が出ていたら、それだけでスイスが候補から落ちます。網羅リストを作らなくても使えるので、こういう1カ国を特定できる知識ほど費用対効果が高くなります。
データの話をしたいのに攻略記事になりつつある。
私が間違えた項目と、AIが間違えた項目
ここで、モデルを差し替えられる性質が効きました。同じ項目を4モデルに投げて、食い違いを見つけました。
モデル4つ全員が正解して、私だけが間違っていた項目がありました。ルーマニアを、私はキリル文字圏に分類していたんです。周囲の国が全部キリル文字だからここも同じだろう、と思っていたんですね。
あやうく教材に混入させるところでした。絞り込みの数字が狂って、アプリが誤った知識を教え始めます。
逆もありました。3モデルが揃って同じ間違いをした項目もあります。ガーナの通行帯です。旧英領だから左側通行だろうという一般化ですね。実際は1974年8月に右側通行へ変えています。AI間で多数決を取ったら、誤った多数派が勝ちました。ガーナテープが消えつつあるのでちゃんと学ぼうね!(自戒)
ガーナで正解した1モデルが preview/Kimi-K2.6 でした。通行帯を9カ国ぶん聞いた回の成績を並べると、こうなります。
| モデル | 誤答 | 応答時間 |
|---|---|---|
preview/Kimi-K2.6 |
0 / 9 | 54〜300秒超。504も出て、件数の欠損も起こした |
gpt-oss-120b |
3 / 9 | 22〜27秒 |
preview/Qwen3.6-35B-A3B |
4 / 9 | 17〜41秒 |
preview/gemma-4-31B-it |
4 / 9 | 5〜19秒 |
いちばん遅くて、タイムアウトして、件数まで落としたモデルが、事実の精度では完璧でした。逆に、国の事実を作らせた回では gpt-oss-120b がオーストラリアの通行帯を外しています。オーストラリアが左側通行なのは、基本の「キ」です。出力が充実していることと、正確であることは別でした。
これで用途別の割り振りが決まりました。1プレイごとに走る正規化は速い gemma、長文を書かせる採点は釣り合いのいい gpt-oss-120b。事実を作る場面では、遅くていいので精度の高いほうを混ぜる。仕事の割り振りは、AIとコードと人間のあいだだけの話ではありませんでした。
ただしこれも9カ国を1回ずつ聞いた記録です。モデルの順位表としては読まないでください。
票数では決まらないので、一次情報と突き合わせるしかありませんでした。モデル間の一致は正しさを保証せず、信頼できる信号は「不一致」だけです。不一致が出た箇所を人手で確認する、という使い方に落ち着きました。
ただし突き合わせるには、書式を揃えておく必要がありました。自由記述にした項目は、102カ国すべてが比較不能になりました。
同一の言語が chinese / 中国語(繁体字) / 繁体字中国語 / 中国語(広東語)
選択肢を固定した項目だけが比較でき、人手確認の対象を特定できました。
いちばん痛かったのは、カザフスタンの柵です。固有だと思っていたのは、AIではなく私でした。思い込みを人手で正解タグに書き、AIが増幅して断定し、またあやうく教材になるところでした。AIが嘘をついたというより、私の思い込みに拡声器が付いた形です。
しかも人間とAIで対称ではありません。私の誤りは1件でも、その用語を使った組み合わせデータが全部汚染されます。だから由来(誰が書いたか)と確かさ(検証したか)を、別の項目として持つことにしました。人間が書いたことは、正しいことを意味しません。
線を引いたら、消費が式で書けました
ここで、最初の要求に戻ります。枠を数えられる形にしたい、という話です。
真ん中の判断を全部コードに寄せた結果、AIを呼ぶ箇所が2つだけになりました。
正規化 1回(14スロットまとめて1リクエスト)
採点 1回
────────────────────
1プレイ 2リクエスト
大事なのは、正規化がモデル数に乗らないことです。正規化の出力は用語IDで、コードが使う事実なのでモデルによって変わりません。複数モデルで採点を見比べても、増えるのは採点の側だけです。
もし全部AIに投げていたら、見落としの検出で+1、掛け合わせで+1、絞り込み力で+1、次に見るべきスロットで+1。14スロットの正規化を1件ずつ投げたら、それだけで14回です。1プレイ20回に届きます。そしてリトライ処理を入れた瞬間に、無償枠終了のお知らせとなったことでしょう。
上限を計算したら、人間のほうが先に尽きました。1モデルで採点するなら1プレイ2回なので月1,500プレイ、1日あたり50問で約3時間です。4モデル同時に採点させると1プレイ5回になって月600プレイ、1日20問で約70分。1問あたり早くて3〜4分かかります。14スロット埋めるのは、けっこうな作業です。
つまり無償枠が尽きる前に、人間の集中力が尽きます。制約が「枠」から「人間の時間」に移りました。1プレイが必ず2回だから、こうやって「月に何プレイできるか」を先に計算できます。リトライ処理を入れてたら、この計算は成り立ちません。
作っている最中の感想を1つ。トークンを数えなくていいのは、思ったより楽でした。
普段クラウドのコストを見ている感覚だと、プロンプトは短く、コンテキストは削り、max_tokens は絞るものです。ところが無償枠の中では、その努力が何も返ってきません。長いのを1回投げても短いのを1回投げても、減るのは同じ1回です。だから辞書262語は丸ごとプロンプトに入れて、max_tokens も絞らず余裕を持たせました。推論のぶんで使い切って content が空になるほうが、よほど困りますから。
ただ、これは無償枠の内側だけの話でした。さくらのAI Engineには「基盤モデル無償プラン」と「従量課金プラン」があって、どちらも月3,000リクエストの無償枠を持っています。違うのは超えたあとで、無償プランはレート制御がかかるだけですが、従量課金プランは1万トークン単位の課金に入ります。単価はモデルごとに違い、出力だと私が比べた4種の中でも gpt-oss-120b の0.75円から preview/Kimi-K2.6 の3円まで4倍ひらきます(提供モデルと料金)。
枠を出た瞬間に、「辞書を丸ごと入れる」も「max_tokens を絞らない」も「迷ったら重いほうを選ぶ」も、まとめて請求書に化けます1。楽だと感じていたのは、私が枠の内側に居たからでした。
そして内側に居ても、重いプロンプトは応答が遅くなり、遅くなればタイムアウトで転びます。値段の天井が無いところでは、時間の天井が先に来ます。
Googleのほうも、既定の構成では課金経路が1つもありません。
| Google側の呼び出し | 課金 | このアプリ |
|---|---|---|
| Street View 表示(Embed) | 無料・無制限 | 使う |
| メタデータ照会 | 無料・クォータ消費なし | 使う |
| 画像取得 | 課金される | 呼ばない |
| 移動を止める表示(GeoGuessrのNoMove相当) | 月5,000まで無料 | 既定で無効 |
画像を保存しない設計にしたので、画像取得を呼ぶ必要が無いんですね。規約と学習効果のために選んだ設計が、そのまま課金経路の削除になっていました。NoMoveで鍛えたい人は最後の行を有効にできますが、ここだけ課金経路が生えるので既定では切ってあります。
このアプリはローカル実行専用です。APIエンドポイントも管理画面も認証を持っていません。公開すると第三者があなたの無償枠とAPIキーを消費できて、正解タグも書き換えられます。そもそも無償枠を共有する公開サービスは成立しません。枠が尽きた瞬間に、全利用者へ影響します。
動かして踏んだこと
消費まわりの実測です。
| 事象 | 枠を消費するか | 踏んだ結果 |
|---|---|---|
| HTTP 400 | しない | パラメータを間違えて15回弾かれ、青くなったが減っていなかった |
| HTTP 504(約300秒) | する | 成功の保証なく減るので、スクリプトは意図的にリトライしない |
| 打ち切り | する | 200で返るので気づきにくい |
| RAG | 無償プランでも課金対象 | 使わない判断。辞書262語はプロンプトに全部入る |
400が消費しないのは、届いたけれど推論に入る前に弾かれたからですね。504のタイムアウトは非公開かつSLAの適用対象外なので、対処は1リクエストを小さく保つことだけです。
そして、AIに残した入口と出口も、そのまま信じるわけにはいきませんでした。以下は preview/ 付きモデルの挙動を含みます。提供段階の話なので、基盤の品質とは切り分けて読んでください。
strict: true を効かせても抜けてくるものがあります。いちばん笑ったのがこれです。
"discriminationHint": "null"
JSONの null ではありません。4文字の文字列 "null" です。スキーマは文字列を要求していて、"null" は文字列なので通ります。バリデーションも通り、状態は「成功」になり、画面には「null」と表示されます。さきほど null と [] を型で分けた話をしましたが、型で守れるのは形までで、意味は守れません。
だから「成功」の中身をコードで監査するようにしました。辞書に無い用語を教えていないか、視認できないスロットを「次に見ろ」と言っていないか。辞書に無い用語は22件から0件、視認できないスロットへの誘導は6件から0件になりました。ここもまた、AIから仕事を取り上げた1箇所です。
打ち切りにも遭いました。HTTPは200で通信も健全、なのにJSONが完成していない。
上のモデルは finish_reason=length で、max_tokens の上限に当たった素直な打ち切りです。
下のモデルのほうが厄介です。82秒待って本文は0文字、推論だけ7,239文字。max_tokens は推論の消費分を含むので、絞ると推論で使い切って content が空になります。だから絞らず余裕を持たせました。
そしてボタンに「1 リクエスト消費」と書いてあります。押す前に何回減るかが分かる。本数で数える枠だと、こういう画面が作れます。
そして上限とは別に、もう1つのパターンがありました。
Kimi 本文 556字を書き終えたあと、末尾の空白 40,952字に280秒
gemma 同じ症状で 119.9秒 → 対策後は 8.6秒
対策は「末尾の空白が512字続いたら打ち切る」です。出力が長すぎたのではなく、書くのをやめたあと止まらなかったんですね。実は「JSONが閉じたら読むのをやめる」処理を先に入れてあって、発火しませんでした。JSONが閉じていないからです。対策は、対策を作ったときに見た失敗の形にしか効きません。
なお、ここまで挙げた挙動をモデルの性質としては書けません。まったく同じ入力を2回投げて比べたからです。3問×4モデルの12組で、完走したか打ち切られたかが一致しなかった組が4組、見落としの件数が一致しなかった組が5組ありました。同じものを投げても、同じものは返ってきません。別の日には、打ち切られるモデルが2つ入れ替わったこともありました。書けるのは「そういう壊れ方が実在する」までです(v1/v2の比較記録)。
使ってみたら、前提が外れていきました
都合の悪い結果も、そのまま残してあります。
まず到達の実測です。10問について、正解タグの観察を強く縮む順に掛けていったら、1カ国まで届いたのが6問、複数カ国で止まったのが3問(17カ国/11カ国/11カ国)、掛け合わせに使える用語が0で算出不能だったのが1問でした。内訳は combo-report.md に全問分あります。
止まった3問について、最初は「辞書が足りない」と結論しました。それが誤りでした。
ai_pavement_01「アスファルト舗装」 102カ国
ai_vehicle_01 「白い横長のプレート」 101カ国
辞書が扱う国は102件なので、アスファルトは辞書の全域に該当していることになります。出典から埋めても縮まりません。どこにでもあるものは、どこにでもあるからです。
さらに「記述はあるのに用語が1つも付いていないスロット」を数えたら52件あって、これも埋めるべき欠落に見えました。中身を見たらこうでした。
camera Gen4 / Gen3 / Gen4ノーカー ...
season 木の陰が北側に出ている ...
「Gen4」(下記コラム参照)に用語を作っても、国は1カ国も縮みません。数字だけ見て埋めに走っていたら、無意味な用語を52個作っていました。空欄を埋めることと、情報量を増やすことは別です。
【コラム】撮影車の「世代」という手がかり
GeoGuessrをやる人は、Street Viewの撮影機材を「Gen2」「Gen3」「Gen4」「SmallCam」と世代で呼び分けます。画質やつなぎ目の出方、写り込む車体の形が違うので、風景を見なくても上級者は撮影機材で絞り込みをしていたりします。ただし世代は国を直接示しません。同じ世代の車が何十カ国も回っているからです。見た目では真っ先に気づくのに、絞り込みに使おうと思うと学習量が相当必要になる、厄介な手がかりの代表格です。うん、攻略記事だね。
別の10問で測ったら、6が5になりました
さて、ここまでの数字には致命的な問題があります。最初の10問に合わせて辞書を作ったので、その10問で測れば当たり前に良い数字が出ます。
なので公開直前に、辞書を1語も足さずに別の10問を作りました。韓国・イギリス・コロンビア・シンガポール・メキシコ・ニュージーランド・カナダ・ラトビア・チリ・ルワンダです(出題データ / 到達の実測)。辞書を触っていないことは指紋で確認しています。
$ npm run fingerprint -- --verify
用語 262 語(絞り込みに使える 180 語)
sha256: 909441b29d1483201aacffc69f1b6ddb1e8c49b8cf9335899077c0ae7de69554
一致した。辞書は記録した時点と同一である
結果です。
| 最初の10問 | 別の10問 | |
|---|---|---|
| 1カ国まで届いた | 6 | 5 |
| 複数カ国で止まった | 3 | 5 |
| 算出不能 | 1 | 0 |
1問ぶん落ちました。落ち方が小さいので、辞書が最初の10問に過剰適合していたわけではなさそうです。ただ n=10 なので、これは傾向であって証明ではありません。
そして到達した5問のうち4問は、単独の欄で決まっていました。
韓国 script(ハングル) 1カ国
メキシコ pole(電柱の形) 1カ国
ニュージーランド bollard 1カ国
ラトビア sign(標識) 1カ国
ルワンダ vehicle(6) × traffic_side(78) → 1カ国
「1つの欄で決まることを期待しない」と設計して掛け算をコードに寄せておいて、掛け算で1カ国まで届いたのはルワンダだけでした。都合が悪いので正直に書いておきます。
厳密に言うと、掛け算そのものは他でも縮んでいます。イギリスは vehicle(6) に traffic_side(24) を掛けて5カ国、コロンビアは traffic_side(78) に script(91) を掛けて67カ国、カナダは3つ掛けて28カ国。ただ「縮んだ」と「決まった」は別で、1カ国まで行けたのは1問です。
最初の10問では違いました。南アフリカは sign(3) で3カ国まで絞ったあとに traffic_side(24) を掛けて1カ国。掛け算が最後の1ピースとして働いています。つまり掛け算が決め手になるかは、辞書の出来ではなく問題の引き次第でした。10問中1問という数字は、掛け算の実装が無駄だったという意味ではありません。とはいえ測るたびに動く数字なので、設計の根拠として持ち出せる強さでもありません。
検証ツールは「積集合に正解が含まれない」を7件報告してきました。そのうちイギリスの問題を、実際に解いてみます。
traffic_side 左側通行
road_marking 白い中央線、路肩は黄色の二重線
事実です。そして今回は、ちゃんと情報量もありました。路肩の黄色い二重線は、辞書で4カ国しか該当しない強い手がかりです。なのに結果はこうなりました。
残った候補: ボツワナ、南アフリカ
正解のイギリス: 消えた
Kimi の説明です。
絞り込みの逆効果。「路肩は黄色の二重線」に対応する road_marking_edge_yellow は辞書で関連国4件(ZA・BW・LS・SZ)です。これを traffic_side(24件)と掛けると候補は BW・ZA のみになり、正解 GB を外しました。積集合が正解を含まないため、絞り込みは失敗です。
辞書がイギリスの路肩を知らなかったんです。出典が南部アフリカの話だったので、該当国がそこに偏っていました。
ひとつ補足すると、Kimi の説明は経路を簡略化しています。私が road_marking に書いた1行には、正規化が4つの用語を割り当てていました。
road_marking「白い中央線、路肩は黄色の二重線」
road_marking_center_white 1カ国
road_marking_edge_yellow 4カ国 ZA BW LS SZ
ref_road_marking_center_white 85カ国
ref_road_marking_side_yellow 37カ国
2カ国という数字はコードが計算してプロンプトに載せたもので、Kimi はそれを読んで、代表的な1つの用語で筋を通しています。読みやすい代わりに、経路は実際より短くなっています。AIに任せた出口の仕事は、そういう性質のものでした。
カザフスタンの柵と同じ症状ですが、原因は逆でした。あちらは「辞書は正しく、私の記述に情報量がなかった」で、こちらは「私の記述は強い手がかりで、辞書の該当国が欠けていた」です。同じ症状に原因が2種類あるなら、症状を見て原因を決めてはいけません。そして両方とも、コードが「正解を含まない」と検出しています。どちらの原因かはAIが読んで説明しました。線引きでよかったと思えた場面です。
残した2つの仕事は、働いたのか
取り上げた話ばかり書いてきたので、AIに残した正規化と採点も測っておきます。
正規化は、10問ぶんで132件を判断しました。用語IDを割り当てたのが71件、辞書に該当なしと判定したのが61件です。そのうち誤りは8件でした。
最初は、間違って付いた用語IDを正解タグから削って回りました。それで画面はきれいになります。ところが同じ処理をもう一度走らせたら、削ったほうが7件そのまま戻ってきました。
| 前回やったこと | 件数 | もう一度走らせたら |
|---|---|---|
| 間違った用語IDを消しただけ | 7 | 7件すべて再発 |
| 辞書の書き方も直した | 1 | 再発しなかった |
考えてみれば当たり前でした。正規化は毎回、辞書を見て一から割り当て直します。出てきた答えを消しても、辞書が同じなら次も同じ答えが出てきます。消して回るのは、蛇口を締めずに床を拭いているのと同じでした。
再発しなかった1件が、冒頭のトルコの問題です。私は script に「Sの下にヒゲみたいなのが付いている」と書きました。ところが辞書にはカザフ語の Қ が「Kの右下にヒゲが出ている」として入っていて、該当国は1カ国。トルコ語の ş や ç も日本語では「下にヒゲ」と呼ぶので、正規化がカザフスタンを引き当てたわけです。辞書側を「キリル文字で、Kの右下に下向きのヒゲが付いた字がある」「アルファベットで、sやcの下に小さなヒゲ(セディーユ)が付いている」と、文字体系から書く形に直したら止まりました。
AIが間違えたのではありません。素人語の言い方が、2つの用語で区別できていなかったんです。ここでも直す場所は、AIの出力ではなく渡すデータの側でした。
採点のほうは、出題をまたいだ言い回しの重複を数えました。同じ文が別の問題でも出てくるなら、その問題から何も読んでいないことになります。
gpt-oss-120b 12件 → 相異なる先頭30字 12 重複率 0%
Kimi-K2.6 13件 → 13 0%
Qwen3.6-35B-A3B 10件 → 10 0%
gemma-4-31B-it 3件 → 3 0%
4モデルとも0%です。テンプレートを埋めているのではなく、渡した判定結果を読んで書き分けていました。ここは残して正解だった仕事です。ただし gemma は13件のうち3件しか完走していないので、3件での0%だと思ってください。
満点ではありません。同じ監査で gpt-oss-120b だけ、根拠のない国の断定が1件と、正解タグに無いスロットを見落としとして挙げたものが2件出ています。入口と出口に絞っても、出口はまだ手放せません。
わざと間違えてみました
アプリが判定する失敗の型は5つあって、さきほどの3分類より細かく切ってあります。ところが当ててばかりでは、そのうち1つも動きません。なのでわざと間違えました。韓国なのに自信満々に日本と答え、通行帯と建物を見ないふりをした回では、こうなりました。
failureModes confident_error + observation_miss
intersection {"countries":["CO","JP","KR","TW"], "containsAnswer": true}
5つの型のうち、確信を持った誤答と観察漏れの2つが立ちました。そして回答は外したのに、書いた観察のほうは正解に届いていました。電柱の黄黒の帯だけで4カ国まで絞れていて、その中に韓国も日本も残っていたんです。
イギリスの回は、逆でした。
hit true(低い確信度で候補に入れていた)
intersection containsAnswer: false
当てたのに、観察は正解を落としていました。当たったかどうかと、観察が正しかったかどうかは、こんなに簡単に食い違います。ここを1つの点数にまとめていたら、どちらも見えませんでした。
限界も書いておきます。14スロットは観察空間を私の主観で切り出したものにすぎず、他の情報をも見ている上級者には何も返せません。対象は初級から中級までです。
結局、いちばん勉強になったのは、辞書を作る作業でした
強くなったかは測れていません。ランクが動くには数ヶ月かかります。そして白状すると、アプリで練習した時間より、辞書と正解タグを作った時間のほうが勉強になりました。
理由は最初に挙げた3分類に戻ります。「想起失敗」は、読むという受動的な行為では解決しません。辞書を作る作業は、まさにその逆です。自分の記憶から手がかりを引き出して、出典と突き合わせて、食い違いを1件ずつ潰す。能動的に想起し続ける作業でした。ルーマニアの取り違えも、カザフスタンの思い込みも、そこで出てきたものです。
正解タグはもっと直接的でした。1問ずつパノラマに戻って、自分が「写っていない」と書いた欄を確認する。そこで初めて、白飛びして読めなかった路面標示や、遠すぎて判別できなかった電柱に、名前が付きます。
学習アプリとしては、都合の悪い結論です。辞書も正解タグも、人間が入力しないと作れません。いちばん効く作業が、アプリの外にあるんですね。知識がある人だけがデータを作れて、初学者は反復しかできない。鶏が先か卵が先か。
しかも1問足すたびに、同じ作業が最初から始まります。終わるのは1問ぶんで、学習のほうは終わりません。練習画面は反復の場所で、問題を作ることがいちばん重い練習でした。この2つを回し続けるのが、このアプリの使い方です。
AIに採点を丸投げするつもりで始めて、増えたのは私の宿題でした。1問片付けると次が来る。しかもその宿題がいちばん効くので、減らすわけにもいきません。わんこそばです。蓋を閉めるまで終わりません。
最初の10問を作り、辞書を作り、別の10問を作った。次の問題セットを足して30問にします。
もう1つの答えは、作ったデータを配ることです。誰かが一度この作業をやれば、あとの人は反復から始められます。だからこのアプリの本体は、データである262語と出題20問のほうでした。
最初に、この3分類は資格の勉強でもコードレビューでも同じ形をしていると書きました。同じ型を持ち込むなら、要るものは3つだけです。実際に何があったかの記録(正解タグ)、素人語と件数の対応表(用語辞書)、そして「その言い方だと何件に該当するか」を数える処理。コードレビューなら、実際に潜んでいた欠陥の一覧と、レビュー観点の言葉が何割のPRで効くかの表になります。「命名が微妙」では母数が全部残り、「この変数だけスコープが関数をまたいでいる」なら数件に絞れる。粒度を件数で殴るところは、風景でもコードでも変わりません。
ただしこの型を持ち込むと、いちばん大変な作業も一緒に付いてきます。欠陥の一覧を作るのは人間です。
借りたものにも、自分の記憶にも、無い精度を足さない
配るとなると、由来をはっきりさせないといけません。辞書262語の内訳は、私の実戦の連想を人手で書いたものが28語、公開資料を参照して人手で編集したものが172語、AIが生成して人手検証を経ていないものが62語です。混ぜていません。ファイル単位で分けてあります。そして参照した資料については、該当国リストの出典URLを項目ごとに記録しました。
確かさは別の軸です。一次情報で確認済みの verified が12語、実戦の経験則で人間が書いたが未検証の heuristic が188語、AI生成で未検証の unverified が62語。断定に使えるのは verified の12語だけです。どこから来た数字かが辿れない一覧は作らない、というだけの話です。
正解タグのほうは、内訳を書く必要がありません。20問ぶん全件が人手で、AI生成は1件も入っていません。下書きを機械的に組んだスクリプトも、AIは呼んでいません。
冒頭で紹介した Plonk It は、一次情報の確認にだけ使い、データには取り込んでいません。素晴らしい資料ですが、コミュニティが積み上げたものをそのまま持ち出すわけにはいきません。
混ぜなかったのは、著作権の話だけではありません。私のメモには「なんとなくアフリカを考える」みたいな記述が入っています。同じ軸を公開資料はもっと多くの国について整理していますが、その数字で私のメモを上書きしてはいけません。本人が「なんとなく」と表現しているものを、勝手に厳密なルールへ変換しない。exhaustive と同じ話です。
ライセンスは3つに切りました。コードはMIT、データのうち人手記述の部分はCC BY 4.0、データのうちAI生成の部分は権利を主張しません。AIの出力には人間の創作的寄与が無いので、著作権が発生しないものとして扱っています。自分が持っていない権利を、他人に許諾することはできませんから。人間とAIとコードで仕事を分けた線が、ライセンスの切り方まで同じ形になりました。
置いてあるものと、直し方
置いてあるのは geo-Ganbatte-Gengoka-Coach です。データ(辞書・正解タグ・検証ツール)を見るだけなら、必要なものは何もありません。アプリを動かすほうはAPIキー3本と npm install が要りますが、どちらも課金経路はゼロです。価値があるのはデータ側で、アプリはその使用例だと思ってください。手順はREADMEに、キーの権限をどこまで絞るかと課金がどこで発生するかは credentials-and-billing.md に書きました。動かす前に後者を読んでください。 検証ツールと問題セットを配る仕組みは、どちらもAIを呼ばないので消費0で動きます。
そしてこの辞書は、記事で書いてきた通り間違っています。Street Viewの撮影が更新されれば、正しかった項目も合わなくなります。だから配るべきものは正しいデータではなく、間違いを見つけて直せる土台のほうだと考えました。
直し方も置いておきます。誤りを見つけたらリポジトリのIssueかPull Requestへ投げてください。自分の辞書として持ちたい場合は、フォークして data/glossary.json を書き換えるだけで動きます。データはCC BY 4.0なので、Standard 10 (gsolkaga), CC BY 4.0 の帰属表示を残していただければ、改変も再配布も自由です。
記事で引用した応答も、プロンプトも、全部そのまま置いてあります。
| 見たいもの | 場所 |
|---|---|
| 採点プロンプト(v1もv2も同一) | grading-prompt.md |
| モデル選定の根拠、消費と打ち切りの実測 | design.md |
| 正解タグの下書き(AIを呼んでいない記録) | tag-drafts.md |
| 下書きを規則で振り分けた記録 | tag-apply-review.md |
| リクエスト消費の台帳(見積もりと実測の突き合わせ) | request-budget.md |
| 資格情報と課金の境界(動かす前に読むもの) | credentials-and-billing.md |
| 全呼び出しの記録(トークン数・応答時間・打ち切り) | data/usage.jsonl |
| プロンプトの組み立て | server/utils/prompts.ts |
| v1とv2の応答(生データ) | data/runs/ |
| 辞書の指紋(記録した時点との照合) | glossary-fingerprint.md |
| 到達の実測(最初の10問) | combo-report.md |
| 到達の実測(別の10問) | combo-report-append-10.md |
| 別の10問の出題データ | standard-append-10/dataset.json |
| 型と設計判断のコメント | shared/types.ts |
| v1/v2の比較、到達、出力の監査 | docs/ |
打ち切られた応答の生テキストも捨てずに入れてあります。要約せずにそのまま置いてあります。
おわりに
3,000リクエストの無償枠のうち、使ったのは588でした。データ生成も4モデルの対照実験も普通のプレイも全部含めて、です。
使い切りチャレンジなのに、2,412も余りました。もっと消費してると思っていました。
回数は使い切れませんでしたが、基盤のほうはだいぶ触りました。まとめておきます。
| 調べたこと | 分かったこと |
|---|---|
| チャットモデル9種の可否 | コンテキスト長4,096で落ちるもの、CPU実行で38〜45秒かかるもの、提供終了、要申請。残った4種で系統とサイズの幅を確保 |
| 4モデルの事実の精度と速度 | 誤答0/9のモデルが最遅で504も出す。最速のモデルは4/9外す。逆相関だった |
| 用途別の割り振り | 正規化は速い gemma、採点は釣り合いのいい gpt-oss-120b、事実を作るなら遅くても精度の高いほう |
| 枠の数え方 | 400は消費しない。504と打ち切りは消費する。だからリトライしない |
| 構造化出力 |
strict: true でも4文字の文字列 "null" は通る。スキーマは形しか守らない |
| 打ち切り | 200で返るのにJSONが閉じない。本文556字のあと空白40,952字で280秒 |
| 再現性 | 同一入力を2回投げた12組のうち、完走判定が4組、見落とし件数が5組で食い違う |
| 課金経路 | RAGは無償プランでも課金対象。使わずに済むなら使わない |
言い訳ではなく、順番の問題です。枠が本数で数えられると分かったので、1プレイのコストを固定したくなった。そのためにはAIに小さな判断を何個も聞けないので、真ん中を全部コードへ寄せた。残ったのは入口と出口だけです。
それで応答の質は、落ちるどころか上がりました。仕事を取り上げたぶん、渡すデータを作ったからです。プロンプトをいくら磨いても、渡していない情報について正しいことは言ってくれません。
枠が無限なら、全部AIに投げて、冒頭の当たり障りのない総評のまま記事は終わっていたはずです。枠が有限だと分かっていたから、AIに投げない判断ができました。その判断のぶん、枠が余りました。
さくらのAI Engineで助かったのは3つです。枠が本数で数えられることで消費を式にできました。OpenAI互換で4モデルを同じ実装のまま比べられたので、食い違いを信号にできました。そして超過しても自動で課金へ移らないので、壊れ方を探しに行けました。
最後のがいちばん効いています。504を踏むか分からないまま300秒待つ、max_tokens を上げて投げ直す、同じ入力を2回投げて揺れを見る。どれも「失敗しても財布は痛まない」と分かっていないとやりません。枠が減るだけなら翌月に戻ってきます。使わない設計を選べたのも、壊し方を試せたのも、同じ性質のおかげでした。
つまずいた点も書いておきます。打ち切りや "null" の4文字にはズッコケました。ただしどれも n=1 の記録です。基盤の性質としては読まないでください。
これで強くなれるのかは、まだ分かりません。分かったのは、自分がどのスロットを空けたまま答えているか、という自分の癖でした。
残った2,412の使い道は決まっています。問題を足すことです。
作問のときに正規化が1回、その問題を自分で解くときに回答の正規化が1回、そこから4モデルに採点させて4回。1問あたり6リクエストなので、2,412あれば400問ぶんあります。 しかも無償枠は毎月3,000にリセットされるので、翌月には500問ぶんに戻ります(汗)
そして私が2週間で作れたのは、20問でした。400問ぶんの枠に対して、20問。 使い切れないのが当たり前でした。1問を教材にするには正解タグを人手で書くので、先に尽きるのは無償枠ではなく私です。
データはCC BY 4.0で置いてあります。私の枠で回すより、使う人がそれぞれの枠で回したほうが早い。無償枠は各アカウントに月3,000あります。
夏季休暇の自由研究として始めたのに、休暇のほうが先に終わりました。枠は毎月戻ってくるのに、宿題は減りません。ありがとうわんこそば仕様、てか、いつまで無料なんですか!?
お断り
記事内容は個人の見解であり、所属組織の立場や戦略・意見を代表するものではありません。
あくまでエンジニアとしての経験や考えを発信していますので、ご了承ください。
GeoGuessr は GeoGuessr AB の商標で、本プロジェクトは非公式です。
出題の20問はすべて自分で選んだ地点で、GeoGuessrの問題を持ち出したものではありません。GeoGuessr本体は無料でも1日1回、5つのスポットを遊べるので、興味のある方は是非チャレンジしてみてください。
-
実測から概算してみます。記録した平均は、正規化が入力5,256トークン・出力199トークン、採点が入力4,481トークン・出力7,914トークンでした(
data/usage.jsonlの120件)。単価を当てると、正規化をgemmaで採点をgpt-oss-120bにした場合が1プレイ0.81円、採点をKimi-K2.6にすると2.79円。無償枠の3,000リクエスト相当(1,500プレイ)を全部従量課金で払ったとしても1,200円ほどです。金額としては小さいのですが、それでも「辞書を丸ごと入れる」が入力トークンに毎回乗るので、枠の内側と外側で設計の良し悪しが逆さになります。あくまで私の使い方での概算で、税込・記事執筆時点の単価です。 ↩



