夜の湾岸が見たくて堪らなかった。Gemini 2.5 Flash(GoogleのAIモデル)に観光相談を投げ、地図と会話を同じ画面に並べた。座標と魅力度を数値で揃えたら旅の段取りがはっきりした。
作ったものの骨格
横浜と京都の夜景スポットを質問ひとつで返す観光地図アプリを組み直した。ブラウザ内でプロンプト生成→PHP(サーバーサイドのプログラミング言語)プロキシ→Gemini→Leaflet.js(地図表示ライブラリ)描画を直列化し、temperature 0.2 と maxOutputTokens 4096、最大5件の出力で番号付きマーカーを描く。迷った。これだ。構造化プロンプトには必須フィールドとトレイリングカンマ禁止を明記した。
設計の手筋と最初の失敗
最初はJSON(データ形式)が途切れて JSON.parse が50件中11回落下した。想定が甘かった。パート連結→コードブロック抽出→cleanupJSON→フォールバック抽出を差し込み、欠けても緯度経度を拾えるようにした。
// midori300/gemini-maps.js(抜粋)
cleanupJSON(jsonText) {
jsonText = jsonText.replace(/,(\s*[\]}])/g, '$1');
const openBrackets = (jsonText.match(/\[/g) || []).length;
const closeBrackets = (jsonText.match(/\]/g) || []).length;
if (openBrackets > closeBrackets) {
jsonText += ']'.repeat(openBrackets - closeBrackets);
}
const openBraces = (jsonText.match(/\{/g) || []).length;
const closeBraces = (jsonText.match(/\}/g) || []).length;
if (openBraces > closeBraces) {
jsonText += '}'.repeat(openBraces - closeBraces);
}
return jsonText;
}
修復後はパースエラーが0回になり、平均応答時間は2.4秒→2.7秒で収まった。
JSON修復の検証ログ
観光レスポンスはトレイリングカンマや未閉じ括弧が頻発する。3種類のレスポンスを試せるツールで修復前後を比較し、cleanupJSON でも直らないケースは extractPlacesFromBrokenJSON で座標だけ残す。
座標ガードとビジュアルの整え方
北海道の稚内が緯度46度、与那国島が経度122度なのでこの枠外は載せない。意外でした。海上座標が返ったときは緯度24〜46度・経度122〜154度のガードで除外し、Leafletでは ['#3498db', '#e74c3c', '#f39c12', '#2ecc71', '#9b59b6'] の番号付きdivアイコンで視線を誘導する。
ポップアップを感情的に磨く
ポップアップはカード型にまとめ、カテゴリ、ハイライト、滞在時間、アクセス、人気度、CTAを一枚で読めるようにした。評価4.6 なら星四つ+ハーフスターを描き、100文字以内の説明とGoogle Maps+Web検索CTA、全画面モーダルへ繋げた。
試したことの記録
観光記事から固有名詞を抜くデモでは、助詞終わりの語を弾き、ヒット数と代表的スポット数を指標にした。ヒット率40%を切るとフォールバックが増えると分かり、プロンプト調整に反映した。課題だった。改善した。
内部ロジックは midori295: Gemini API(外部サービスとやり取りする仕組み)でシンプルなチャットを作る と midori299: タロット占いエンジン の知見を再利用している。
使ってみて
デモはLeaflet.jsベースでPCでもモバイルでも滑らかに動く。中央の「開始」を押すと、ユーザー入力→プロンプト生成→PHP→Gemini→JSON修復→Leaflet.js→モーダルの順にハイライトされる。
大きな画面で試したい場合は、旧サイトのデモを見る から全画面表示できる。
今回のまとめ
- Gemini 2.5 Flash(GoogleのAIモデル)への構造化プロンプトを再設計し、構造化JSON(データ形式)の破損率を50件中11→0件に抑えた
- cleanupJSONと正規表現フォールバックで括弧欠けやコードブロック混入を吸収した
- 緯度24〜46度・経度122〜154度のガードで海上座標を除外し、番号付きマーカーで視線を制御した
- ポップアップをカード型に整理し、CTA(行動喚起)と指標を100文字以内に収めて全画面モーダルに繋げた
さらに知識を深めるなら
同じような仕組みを試している方の参考になれば嬉しいです。最後まで読んでくださり、ありがとうございました。