🗂️

自作AIツールがnode_modulesまで読みに行く問題をgit ls-filesで直した

に公開

AIにコードを読ませる前処理が遅い

自作のAIエージェントにリポジトリを読ませる処理を書いていたら、妙に遅かった。

最初はモデルが重いのかと思った。プロンプトが長すぎるのか、コンテキストの詰め方が悪いのか、と疑った。

でも原因はもっと手前だった。

AIに渡すファイルを集める処理が、node_modules.next まで見に行っていた。

ありがちなコードはこれ:

from pathlib import Path

files = []
for pattern in ["*.md", "*.py", "*.ts", "*.tsx"]:
    files.extend(Path(".").rglob(pattern))

リポジトリ内の Markdown / Python / TypeScript を集めて、AIに渡す。READMEを要約する。設計書を読む。コードをざっくり探索する。

小さいうちはこれで動く。

でも少し運用すると、嫌なことが起きる。

  • node_modules まで見に行く
  • .nextout などの生成物を踏む
  • venv やキャッシュが混ざる
  • 探索だけで数秒待つ
  • AIに渡したいソースより、ノイズの方が多くなる

体感としては「AIが遅い」。でも実際には、AIに渡す前のファイル集めが遅い

ここを git ls-files に変えたら、手元のリポジトリでは最大200倍くらい速くなった。

欲しかったのは「全ファイル」ではなかった

よく考えると、欲しかったのは「ディスク上にある全ファイル」ではなかった。

欲しかったのは、だいたいこれ:

Gitで管理している、人間が読むべきソースファイルだけ

node_modules.nextvenv もいらない。ビルド成果物もログもいらない。

それなら、Pythonでファイルシステムを全部歩くより、Gitに聞いた方が自然だった。

git ls-files --cached --others --exclude-standard '*.md' '*.py' '*.ts' '*.tsx'

これで、Git管理済みのファイルと、まだaddしていない未追跡ファイルを取得できる。しかも .gitignore も尊重される。

先にコード

Pythonから使うなら、こうした。

import subprocess
from pathlib import Path


def source_files(repo: str | Path = ".") -> list[Path]:
    root = Path(subprocess.check_output(
        ["git", "rev-parse", "--show-toplevel"],
        cwd=repo,
        text=True,
    ).strip())

    out = subprocess.check_output(
        [
            "git", "ls-files",
            "--cached", "--others", "--exclude-standard",
            "*.md", "*.py", "*.ts", "*.tsx",
        ],
        cwd=root,
        text=True,
    )

    return [root / line for line in out.splitlines() if line]

使う側はこれだけ。

for path in source_files():
    print(path)

自作AIツール、ドキュメント生成、簡易コードレビュー、独自lintなどで「リポジトリ内のソースを集めたい」なら、まずこれで十分だった。

実測: 生成物があるほど差が出る

手元の3つのリポジトリで、次の2つを5回ずつ測った。

  1. Path.rglob() で探索して、あとから除外ディレクトリを弾く
  2. git ls-files --cached --others --exclude-standard を使う

対象拡張子はこれ:

*.md, *.py, *.ts, *.tsx

結果:

repo rglob median git ls-files median
todo-app-demo 294.0ms 1.3ms 約226倍
zenn-content 21.7ms 1.6ms 約14倍
cocop 9.4ms 1.9ms 約5倍

todo-app-demo は Next.js 系の生成物があるので差が大きく出た。

ただ、ここで言いたいのは「常に200倍速い」ではない。

大事なのは、AIに読ませる対象として不要なファイルを、最初から候補に入れない こと。

探索が速くなるだけでなく、AIに渡す前のノイズも減る。

Path.rglob() は悪くない。ただ用途が違う

Path.rglob() は便利だ。悪者ではない。

ただし、見ている対象が違う。

Path(".").rglob("*.ts")

これは「今ディスク上にある *.ts を探す」処理。

Git管理されているかどうか、.gitignore されているかどうか、人間が読むべきソースかどうかは関係ない。

だから、プロジェクトによってはこういうものが混ざる。

node_modules/
.next/
out/
venv/
__pycache__/
coverage/
dist/

もちろん、自分で除外すればある程度は避けられる。

skip = {".git", "node_modules", ".next", "venv", "__pycache__", "out"}

files = [
    p for p in Path(".").rglob("*.ts")
    if not (set(p.parts) & skip)
]

でも、この除外リストは育ち続ける。

dist を足す。coverage を足す。.turbo を足す。別プロジェクトではまた違うディレクトリを足す。

AIツールの前処理としては、このメンテが地味に面倒だった。

git ls-files は「人間が見たいファイル」に近い

git ls-files はGitが知っているファイルを返す。

git ls-files '*.md' '*.py' '*.ts' '*.tsx'

ただし、これだけだと未追跡ファイルは出ない。

AIエージェントでは「今作ったばかりの下書き」も読みたいことがある。なので、未追跡ファイルも含めるならこうする。

git ls-files --cached --others --exclude-standard '*.md' '*.py' '*.ts' '*.tsx'

それぞれの意味はこう:

  • --cached: Git管理済みのファイル
  • --others: 未追跡ファイル
  • --exclude-standard: .gitignore など標準の除外ルールを使う

この組み合わせが、自作AIツールの入力収集にはかなり使いやすかった。

使い分け

自分の基準はこう。

Path.rglob() が向いている

  • Gitリポジトリではないディレクトリを読む
  • ログや生成物も含めて調べたい
  • ディスク上の実態を確認したい
  • バックアップやファイル整理をしたい

git ls-files が向いている

  • AIに渡すソースファイルを集めたい
  • READMEや設計書だけ拾いたい
  • ドキュメント生成対象を集めたい
  • 独自lintやチェックの対象を決めたい
  • .gitignore されたものを自然に除外したい

自分が欲しかったのは後者だった。

Gitリポジトリ外ではフォールバックする

もちろん git ls-files はGitリポジトリでしか使えない。

ツールとして配るなら、Git外では rglob にフォールバックしておくと扱いやすい。

from pathlib import Path
import subprocess


def source_files_or_rglob(repo: str | Path = ".") -> list[Path]:
    repo = Path(repo)

    try:
        return source_files(repo)
    except subprocess.CalledProcessError:
        patterns = ["*.md", "*.py", "*.ts", "*.tsx"]
        files = []
        for pattern in patterns:
            files.extend(repo.rglob(pattern))
        return files

これで、Gitリポジトリなら速く・ノイズ少なく、Git外なら普通に探索できる。

まず「何を読ませないか」を決める

Path.rglob() を使うな、という話ではない。

ただ、自作AIツールが遅いとき、ついモデルやプロンプトを疑ってしまう。

でも実際には、モデルに渡す前の前処理で node_modules や生成物を踏んでいるだけ、ということがある。

その場合、改善は大げさな最適化ではなく、この1行で済む。

git ls-files --cached --others --exclude-standard '*.md' '*.py' '*.ts' '*.tsx'

AIエージェントの待ち時間を減らしたいなら、まず「何を読ませるか」より先に、「何を読ませないか」を決めるのが効く。

GitHubで編集を提案

Discussion