46のリポジトリにわたる単一のナレッジグラフの構築
airClosetのCTO、Ryanです。
私は3ヶ月をかけて code-graph を構築しました。これは、複数のサービスにわたる46のリポジトリを統合する単一のナレッジグラフです。
多くの人は、すべてのコードをAIに渡して質問すれば済むと考えています。しかし、これには2つの理由から失敗します。
- コンテキストウィンドウ:46のリポジトリにわたる数年分のコードを、1つのプロンプトに収めることはできません。
- ハルシネーション(幻覚):AIは関係性を推論しようとする際に間違いを犯します。つながりを見落としてしまうのです。
これを解決するために、私は静的解析を用いて「信頼できる唯一の情報源(source of truth)」を構築しました。
課題:境界を越えること
大規模なコードベースは複雑です。1つのAPIが5つの異なるリポジトリから呼び出されているかもしれません。1つのデータベーステーブルが3つの異なるサービスで使用されているかもしれません。
1つのリポジトリだけを見ていると、全体像を見失います。これは危険です。コードを変更した際に、実際の「影響範囲(blast radius)」が見えていないと、システムを壊してしまいます。
私のアプローチでは、tree-sitter を使用してコードを構文木にパースします。しかし、tree-sitter 単体ではリポジトリの境界を越えて見ることはできません。
これを解決するために、境界ノード(boundary nodes)を構築しました。
仕組み:
- tree-sitter を使用して、リポジトリ内の関係性を抽出します。
- TypeScript Compiler API を使用して、型と変数を解決します。
- ツールが見落とす動的なケースを処理するために Gemini を使用します。
AIに推測させるのではなく、事実を与えます。「このAPIはリポジトリXからも呼び出されています」と伝えるのです。これにより、ハルシネーションを防ぎます。
最難関:フレームワークの乱立
本当の戦いは、これらの境界を抽出することでした。フレームワークごとに境界の書き方が異なります。
あるチームは NestJS のデコレータを使用し、別のチームは Express のルートを使用し、また別のチームは生の jQuery を使用しています。それぞれがコード内に異なる構造を作り出します。
これを実現するために、以下のためのカスタムパーサーを構築する必要がありました。
- NestJS と TypeORM
- Express と Fastify
- AngularJS と Redux
- さまざまな path-alias スキーム
私たちは99%の精度を目指さなければなりませんでした。接続率が90%しかなければ、AIは接続の10%を見落とします。本番環境において、その10%にバグが潜んでいるのです。
現在は毎日チェックを行っています。接続率が5%以上低下した場合、アラートが飛びます。これにより、新しいコードパターンによってパーサーが壊れたことを検知できます。
現在の限界
グラフは完璧ではありません。
- 検索が困難です。検索を開始するために、関数名を知っておく必要があることがよくあります。
- ノードの爆発。パスを辿ると、数千もの小さくて役に立たないヘルパー関数が引き込まれてしまうことがあります。
- メンテナンス。スタックに新しいフレームワークが導入されるたびに、新しいパーサーを書かなければなりません。
これはパート1です。パート2では、これらのギャップを埋めるために構築した service-product-graph (SPG) レイヤーについてお話しします。
Optional learning community: https://t.me/GyaanSetuAi
