プログラミング
好みをユニットテストすることはできない
You can't unit test for taste (dev.karltryggvason.com)
要約
筆者は、世界中の有名なルートを仮想的に走行するランナー向けアプリ「In the Long Run」を開発しています。このアプリに興味深い地点(POI)を追加する際、GeonamesやWikipediaなどのデータソースを活用しましたが、AI(LLM)の使用においては、ハルシネーションの問題や「好み」のバイアスに直面しました。最終的には、AIは補助的な役割にとどまり、人間が設定したフィルタリングやデータ処理の重要性を強調しています。
全文翻訳
私は、ランナーが世界中の有名なルートをバーチャルで走る「In the Long Run」というアプリを開発しています。このアプリはStravaの走行距離を集計し、国や大陸を横断するルートに対する合計距離を進捗としてプロットします。目的は長期的なインスピレーションとモチベーションを提供することです。人生はマラソンであり、短距離走ではありません。調子の悪い月やシーズンがあっても、バーチャルな世界横断の進捗を続けることができます。アプリはインタラクティブな地図上で進捗を表示し、ユーザーが自分で探索できるようにします。しかし、私は長い間、地図に興味深い観光スポットや歴史的建造物を追加したいと思っていました。慣れ親しんだルートであれば自分でリストを作成できますが、慣れていない国を横断するルートではスケールしません。そこで、ポイントオブインタレスト(POI)のデータソースを見つけて、それに基づいてパイプラインを構築することにしました。その過程で、私は好みとバイアスに苦しみ、ハルシネーションを起こすLLMと戦いました。当初はAIが主要な機能になると思っていましたが、結局は他のシグナルやデータ処理の主要な要素と並んで補助的な役割にとどまりました。
データセットとツールセット
GeoNamesは、場所、カテゴリ、リンクを含む広範なデータソースであり、明白な出発点でした。完全なデータセットはダウンロード可能で、クリエイティブ・コモンズ・ライセンスの下で提供されています。そこで、友人のClaudeと共に、生データから「In the Long Run」のユーザーに関連するPOIを提供するパイプラインを構築し始めました。私たちはプログラミング言語としてPythonを使用し(目の前のタスクに適した豊富なライブラリサポートがありました)、処理されたデータをApache Parquetファイルとしてローカルに保存し、DuckDBをクエリレイヤーとして使用しました。ParquetとDuckDBの両方を使用するのは初めてでしたが、どちらも使い勝手が良く、Claudeがそれらの機能を段階的に紹介してくれました(DuckDBの作業のほとんどは、私が非常に慣れ親しんでいるSQLでした)。一般的に、プロジェクトに1つか2つの新しいツールやテクノロジーを追加するのが最良の学習方法だと私は感じています。スタック全体が初めてだと学習曲線が急すぎて、プロジェクト全体を諦めてしまうかもしれません。AIコーディングエージェントはこの計算をある程度変えますが、それでも、使用されているテクノロジーのほとんどを把握していれば、エージェントをより良く誘導し、盲目的に従うのではなく、情報に基づいた意思決定ができると私は考えています。イリノイ州スプリングフィールド近郊のルート66を走るランナー向けPOI機能のスクリーンショット。
実装を開始する前に、私はClaudeと共にプロジェクト計画を立て、パイプラインの様々なステップと機能作業の概要を説明しました。作業を進めるにつれて、以前の作業から学ぶにつれて反復可能な各ステップの仕様/計画を構築しました。これは、マイルストーンごとに新しいエージェントセッションを開始できることも意味しました。以前のマイルストーンの結果を次のステップのための短いコンテキストと指示に凝縮することで、より速く、より良い応答が得られます(大きなコンテキストはエージェント作業の品質を急速に低下させると私は感じています)。
注目度と注目すべきバイアス
まず、必要なすべてのファイルをGeonamesからダウンロードして解凍し、データファイルはほとんどがバージョン管理には大きすぎるためgitignoresを設定しました。処理の最初のステップは、ダウンロードしたファイルを関連する列で結合し、私たちの目的には役立たない行をフィルタリングすることでした。例えば、行政区分(国、州、地域など)は除外しました。また、公園、史跡、城、記念碑、山など、最も興味深いと思われる特定の機能コードを選択しました。最後に、人口が多い場所には人口フィルタを、山には標高フィルタを追加しました。これによりいくつかの誤検出が生じたと思いますが、大まかな最初のドラフトが必要でした。やや直感的ではありませんが、alternateNames.txtのGeonamesデータセットにはWikipediaリンクが含まれています(isolanguage=linkでalternate_nameが%en.wikipedia.org%のような場合、このユースケースはスキーマに後付けされたように感じますが、非常に役立つデータです)。これを注目度/関連性シグナルとして使用し、Wikipediaの要約もクリエイティブ・コモンズ・ライセンスの下で提供されているため、そこから説明文を作成することもできました。このパイプラインステップの基本的な健全性チェックを構築し、注目すべきランドマークをスキップしていないことを確認するのに役立ちました。これにより、フィルタリングの一部を調整することができました。例えば、最初のドラフトでは、オーストラリアの田舎の地域ストーンヘンジは取り込まれましたが、先史時代の巨石構造物(より有名な同名のもの)は取り込まれませんでした。英語で作業する場合、関連する代替名/言語を取り込み、関連するWikipedia URLを相互参照として使用することも確認する必要があります(GeoNamesは正規名を現地語で保存します)。このステップの最終結果は、世界のPOIについて約72万5千行のParquetファイルでした。私たちが最初に使用した元のセットの1300万行から大幅に削減されました。
人が住む場所はGeonamesデータセットの大部分を占めています。しかし、POIには道中のすべての町、村、集落を表示させたいわけではありません。第2ステップでは、最初のステップからのすべての候補を、私たちが持っている各ルートと照合しました。まず、ルートのGeoJSONファイルを取得し、バウンディングボックスを作成して、ルートにごく近い地点のみに素早くフィルタリングします。次に、ルート座標を反復処理して、バウンディングボックス内のどの地点がルート自体の指定された距離(デフォルトでは50km)内にあるかを確認します。地理的計算と「ルート沿いの距離」属性を計算するためにShapelyとPyprojを使用し、ランナーにいつPOIを表示するかを決定できるようにしました。このステップからの出力は、ルートのさらなる絞り込みに使用されるルート固有のParquetファイルです。アイスランド環状道路ルート(1,321km)では511個のPOIが得られ、アプリで最長のルートであるケープタウンからマガダン(23,257km)では1万個のPOIが得られた一方、ルート66(3,787km)では14,181個のPOIが得られました。これは、私たちの英語圏Wikipediaシグナルが「英語話者がどこに住み、Wikiを編集しているか」というバイアスにすぎないという初期の兆候でした。
LLMは嘘をつくが、好みは持っている
第3ステップでは、Wikipedia情報でデータを充実させ、LLMを使用して各POIの評価を生成しました。当初はLLMが生成したPOIの要約も使用するつもりでしたが、これは小さな利点しかなく、かなりの課題であることが判明しました。まず、特定のルートについて持っている各地点のWikipedia要約を取得します。同じようにWikidataについても、各Wikipedia URLについて、その主題に関する記事がいくつの言語のWikipediaに存在するかを調べます。これは別の良い注目度シグナルです。多くの言語でページが存在する場合、それは英語のWikipediaにのみエントリがあるものよりも重要である可能性が高いです。ウィキデータはグローバルにキャッシュできます。これにより、後のルートで同じ地点が使用される場合に再フェッチが節約されます。ウィキデータはLLMを活用したステップへの入力でもあります。構造化データを返すためのツールを作成しました。AnthropicのHaikuモデルは速度と価格で選ばれ(驚くことではないが、その「兄弟」であるOpusによってClaude Codeを通じて推奨されました)、さらに価格を節約するためにバッチ処理で呼び出しを行いました(入力および出力トークンが50%割引)。このようにプログラム的にLLMを呼び出すのは初めてでしたが、APIは理解できたものの、その出力は完全に一貫していませんでした。例えば、Anthropic Markup Language(antml)の奇妙なバリアントがツール呼び出し結果の文字列に漏れ出し、クリーンアップが必要になることがありました。バッチ処理されたツール呼び出しは完了までに数時間かかり、より大きなルートのコストは約10ドルでした。ここでは、ローカルまたはより安価なモデルを試して、トレードオフがどうなるか見てみたいと思います。ここで、いくつかのハルシネーションも捕捉しました。最初の試みでは、LLMのエンリッチメントに多くのデータが「根拠」として与えられず、プロンプトに制限も適用されませんでした。これにより、Haikuはイリノイ州ディケーターのセントラルパークを、より有名なマンハッタンの同名の公園として分類し、その重要性が大幅に向上してしまいました。2回目のパスでは、場所や行政メタデータ(国、都市など)をLLMへの入力に追加し、システムプロンプトでより慎重に根拠を与えました。Eve