世界を動かす技術を、日本語で。

味に対して単体テストはできない

概要

  • In the Long Run は、Stravaの走行距離を使い、世界の有名ルートを仮想走破できるランナー向けアプリ
  • ルート上の進捗を インタラクティブマップ で可視化し、長期的なモチベーションを提供
  • GeoNames などの外部データを活用し、観光名所や史跡を自動抽出するパイプラインを構築
  • AI(LLM) は補助的役割に留まり、主に「主観的な評価」付与に活用
  • ルートごとのカスタマイズ性やデータバイアスの課題に直面しつつも、実用的なPOI(興味ポイント)機能を実現

In the Long Run:仮想ランニング体験の拡張

  • Stravaの 累積距離 をもとに、世界各地の有名ルートを仮想で走破できるアプリ
  • インタラクティブな地図上で自身の進捗を可視化し、長期間の目標設定・モチベーション維持を実現
  • 一時的なスランプや不調があっても、長期的な成果を実感できる設計
  • 地図上でルート周辺の 観光名所や史跡 を表示し、ユーザーの興味や知的好奇心を喚起
  • ルートに詳しくない地域でもスケーラブルに名所データを抽出するためのデータパイプライン構築

データセットとツール選定

  • GeoNames データベースを基盤に、世界中の地名・施設・カテゴリ情報を活用
  • Pythonを主要言語とし、 Apache Parquet 形式で中間データを保存
  • DuckDB をクエリレイヤーとして採用、SQLによる効率的なデータ抽出を実現
  • 新技術の導入は一度に1~2個までに抑え、学習コストとプロジェクト進行のバランスを重視
  • AIコーディングエージェント(Claude)を活用しつつも、主要技術は自ら理解・管理

パイプライン設計と実装

  • Claudeとともに プロジェクト計画 を作成、各ステップごとに仕様を定義・反復
  • GeoNamesデータの不要行(行政区画など)を除外、興味深い施設コードのみ抽出
  • 人口や標高でフィルタリングし、初期段階でデータ量を大幅削減
  • Wikipediaリンクの有無や多言語記事数を「知名度シグナル」として利用
  • ルートごとにGeoJSONからバウンディングボックスを作成し、近傍のPOIのみ抽出

興味ポイント(POI)の選定とバイアス

  • ルート近傍(50km以内)のPOIを抽出し、「ルート上のどこで表示するか」を距離属性で管理
  • 英語Wikipedia依存による「英語圏バイアス」が顕著に現れる事例
  • ルートごとのPOI数:Iceland ring road(1,321km)で511件、Cape Town~Magadan(23,257km)で10,000件、Route 66(3,787km)で14,181件
  • 都市部ではPOIが人口分布図のようになりやすく、田舎では自然地形や史跡が中心

AI(LLM)の役割と課題

  • Wikipedia要約やWikidata多言語数をもとに、 LLM(Anthropic Haiku) でPOIの「主観的評価」を自動生成
  • LLM要約文は読みやすいが、事実誤認や「幻覚」が多発(例:地名の混同、人口や標高の誤り)
  • Wikipedia要約文に回帰し、LLMは「評価」付与のみに限定
  • LLMの評価により、単なる知名度だけでなく「面白さ」や「重要度」のバランスを調整
  • LLMは「基盤」ではなく「補助ツール」として最適化

パイプラインの検証とデバッグ

  • Leaflet ベースの可視化ツールでPOI配置を地図上で確認
  • SQLYacやDuckDBでParquetファイルの内容をスポットチェックし、誤検出の有無を検証
  • 生成物(POIリスト)はJSONでバージョン管理、ルートごとにパラメータ調整が必要であることが判明

ルートごとの最適化と今後の展望

  • 各ルートで文化的・地理的特性が大きく異なるため、 パラメータの個別調整 が不可欠
  • 人口集中地域ではPOIが都市部に偏りやすく、自然景観や史跡とのバランス調整が課題
  • LLMの主観スコアと客観データ(知名度・カテゴリ)の重み付け最適化
  • 今後はローカルモデルやコスト効率の高いAIへの切り替えも検討
  • 「味」や「面白さ」の自動評価は依然として難しく、AIはあくまで 人間の判断を補助 する立ち位置

まとめ

  • In the Long Runは、 データ処理・AI・地理情報 を組み合わせて仮想ランニング体験を拡張
  • AIは万能ではなく、 伝統的な手法と組み合わせることで最大の効果 を発揮
  • ルートごとの多様性やデータバイアスに対応し、ユーザーにとって魅力的なPOI体験を提供
  • 今後もパイプラインや評価手法の改善を継続し、より豊かな仮想旅行体験を目指す

Hackerたちの意見

ランナーが「Xはマラソンで、スプリントじゃない」と言うと、時間をかけて積み重ねる努力やエネルギーの最適な使い方を暗示していて、思わず微笑んじゃう。私もよく使う表現だけど、マラソンはスプリントよりも長いのはもちろん、どちらもちゃんと走れば、最後には一滴の余力も残らないくらい brutal な努力だよね。努力の長さや瞬発力は変わるけど、たぶん「レースじゃなくてマラソンの準備」って言った方が正確かも。でも、表現力はほとんど失われちゃうけどね(笑)。いいプロジェクトだね!

「努力の長さや瞬発力は変わるけど」って、それがそのフレーズの意味だよね?消耗品X(エネルギーやお金など)を明日がないかのように使い切っちゃダメだよ。今はあるけど、まだたくさんのそれをクリアしなきゃいけないから、100%を注ぎ込むよりも75%でこのタスクを終わらせた方がいいよ。

テイストについて何を意味するかを書き留めていないと、ユニットテストはできないよね。外部化できるなら、できるけど。この考え方を続けると、AIに優しい答えは簡単だよ。私たちが知っていることを全部外部化すれば、Claudeが私の望むことを実装できるから。ただ、私は自分を完全に外部化できないんだ。システムのデバッグには、システムを動かすよりも多くのリソースが必要だし。もし私が知っていることを全部書き留めて機械に渡せるなら、そうするけど、それは不可能なんだ。人は本やハッシュマップじゃないから。何かを作りたいなら、道具を使う必要がある。道具に自分を使わせるんじゃなくてね。[編集: これについて何かできることがあるか考えてる。話したいならメールしてね -- tr at tern dot sh]

コードとして書き留められないのがポイントなんだ。私はコーディングのテイストには慣れているけど、せいぜい説明できるのは、結果として得られるコードがコードベースの他の何かと微妙に違いすぎて、別のバグを隠しているとか、コードが示すことに従っていないってことかな。良い点は、これがユニットテストできないとしても、必要なことを人に伝えるためのドキュメントやコードコメントを書けること。でも、この記事で説明されているようなテイストには、定義すらないんだ。結局のところ、「不透明な重みを信頼する」って論理になっちゃった。

「ホットかノット」スタイルのペアテストでテイストを効果的に外部化できるかもしれないね。十分な比較があれば、MLが人間のテイストを模倣できるようになると思う。私たちがあまり意識していない特徴に引っかかるから。

面白いのは、これが僕がクロードのために作ったチケットシステムで「ゲート」を実装した方法なんだ。ビーズは検証なしでチケットを閉じちゃうからね。僕には「人間の検証」レベルのチケットがあって、それは次の利用可能なものが出るまで機能するんだ。僕がモデルに閉じるように指示するまでね。だから、その精神で言うと、外部検証を実装すれば、味のユニットテストもできるよ。ユニットテストは実行されて、人間の入力を待ってから合格か不合格かを判断するんだけど、これは普通じゃないかもしれない。でも、すでにQAが手動テストをやってるからね。

味について何を意味するかを書き留めていなければ、味のユニットテストはできないよ。外部化できれば、テストできるけどね。ちょっと疑問だな。例えば、プログラムがXSSや他のインジェクション脆弱性がないことをどう書き留めるかは分かるけど、その特性のユニットテストはどうするの?

ランダム化試験だね。半分の人はAIを自由に使うことを誓い、もう半分は絶対に使わないと誓う。数ヶ月後にアンケートやAIを使わないテストで比較するって感じ。逆にして、使わない人が数ヶ月使って、その後使う人が使わないってのもありかも。損失や利益が安定してるか見てみたい。

同意だね。重要なことは全部外部化しちゃおう。特定のパターンや慣習に従いたいなら、それを定義しよう。例えば、アクティブレコードとリポジトリパターンの違いを明確にして、ADRとして残しておく!何が欲しいかわからない?Claudeが出すものを見て、好みを育てていこう。それを未来のセッションで従う慣習としてマークして、でも一つの慣習に固執しよう!LLMを、タスクのACを満たすことだけを気にするジュニア開発者として扱おう。全体の視点を見ずに、与えられたパターンがグローバルに適用されるかどうかも考えないし、他のパターンがあるかどうかも気にしないから。

テイストに関してはユニットテストは絶対にできないよ。Big QueryからPostgresへの移行をOpusを使ってやった時の体験があるんだけど、元のコードとの整合性を保証するためにユニットテストを作ったんだ。でもOpusはsqlglotの上に独自のクエリビルダー(例えばdef _where(very_complicated_params))を作ろうとした。元のコードはシンプルで読みやすいのに、指示を何度も繰り返しても、近づくのに苦労した。結局、"昔ながらのやり方"で、コードの一部をClaudeにコピーして、各部分に明示的な指示を出すことになった。要件は外部化していたはずなのに、それだけでは不十分だった。さらにユニットテストを進めるには、ASTを使って出力を評価する必要があったけど、そのメトリクスをエンコードすることすらできなかった。

働く上での大きな問題は、外部化すると(スキルを書いたりして)それが雇用主の著作権を持つ仕事になるってことなんだ。技術的には、私が仕事でやってる他のいくつかのこと、例えば .emacs や .bashrc ファイル、ワークステーションの ~/bin にある小さなスクリプトなんかもそうなんだけど、雇用主がこれを主張することはほとんどない。よっぽど無関係な理由で意地悪な場合を除いてね。エージェントスキルファイル、特に「その通りに動く」ように見えるもの(白鯨!)は全然違って、未来の雇用主で使おうとすると追いかけられるのが見えるよ。

自分がセンスだと思うことを外部化するには、一般的なステートメントを書き下ろすことができるけど、そのステートメントには境界条件や例外も指定しないといけない。ただ、例外には例外があって、ルールを適用するタイミングと例外を使うタイミングは文脈による判断が必要なんだ。だから、明確に表現できない残りの部分はセンスや判断と呼ばれる。

Hacker Newsで議論の続きを見る