背景

ローカル開発環境でLLMを使い,ファイルの読み書き・リスト・検索などのツール呼び出しを行う際,パス関連エラーが頻発する という問題に直面した。

特に以下のシナリオでエラーが起きやすい:

  • LLMが internal/config/internal/config.go と誤記(ENOENT)
  • ファイル名は正しいが,同名ファイルが複数ディレクトリに存在し,目的のものを指定できない
  • 相対パスと絶対パスの混在で,ツールが期待するパス形式と合わない

これらのエラーで開発フローが中断され,エンジニアが再度命令を修正する必要が生じていた。特にローカルLLMの場合,ツール再試行が繰り返し失敗すると,エラーメッセージとリトライの往復でコンテキストが無駄に消費される。これはモデルの優劣の問題ではなく,プロジェクト固有の問題でもない — パス解決という共通課題をツール側で吸収できれば,コンテキストの圧縮に直結するのではないか。この考えのもとpathfinderを開発した。

目的

LLMのパス生成に頼らず,エラーが発生した時点で自動的にパスを解決し,リトライする仕組みを構築 すること。これにより:

  • ユーザーが手動で修正する頻度を削減
  • ツール呼び出しの成功率向上
  • LLMの「正確さ」に依存しない安定運用

アプローチ

1. LLMの役割を限定

LLMに求めるのは「最終的な選択」のみで,パス生成は行わせない。プロセスは以下の流れ:

  Failed Tool Call → Path Resolve Tool →  Ranked Candidates (Top-K) 
                                              ↓
                                    (LLM 選択 or 自動リトライ)
  

LLMが提供する情報は intent_text(「このファイルで何をしたいのか」)だけで十分。

2. 候補生成と再ランキング

  • 候補生成 (高速): 失敗したパス + intent_text から,ファイルシステム上の候補を短時間で抽出(fzf的なfuzzy matching)
  • 再ランキング (高精度): 候補をColBERTベースのmaxsim再ランキングで並べ替え。単なる文字列類似度ではなく,セマンティック理解を含める

例えば internal/config/ というパスと intent_text の意味的な関連性から,正しいファイルを上位に配置できる。

3. 自動リトライと確実な実在確認

候補として返すパスは すべて stat() で確認済みのもののみ。架空のパスが返されることはない。

ツール側(MCP)が自動的に Top-K 候補を順番に試すため,ユーザーは1度エラーを報告すれば,復旧を任せられる。

実装の特徴

Rust + ONNX による高速パス解決

  • 言語: Rust(低レイテンシ,メモリ安全性)
  • 再ランキングモデル: ColBERT(answerai-colbert-small, INT8量子化)
  • 推論エンジン: ONNX Runtime(CPU専用,INT8で平均9ms)
  • スコアリング: レキシカル特徴量12 + 履歴相関2 + ニューラル再ランキング1

MCP(Model Context Protocol)サーバとして実装し,Claude Code / Zed環境で実運用中。

キー構成要素

A. ファイルシステムインデックス

起動時にルートディレクトリ以下の全ファイル・ディレクトリを索引化:

  • 絶対パス,相対パス,ベースネーム,拡張子
  • 親ディレクトリのトークン化(“internal/config” → tokens: [“internal”, “config”])
  • オプション: package.json, Cargo.toml からメタデータ抽出

この索引により,後の候補生成が高速化される。

B. 候補生成フェーズ

失敗したパス + intent_text に基づいて候補を生成:

  • ベースネーム一致: “config.go” を探す場合,すべての config.go をヒット
  • Fuzzy matching: typo(“clinet.go” → “client.go”)を許容
  • Token-based matching: intent_text のキーワード(“internal”, “reranker”)で親ディレクトリを絞込

結果:上位K=200候補程度を短時間で取得。

C. 再ランキング(ColBERT maxsim)

200 候補を,意味的な関連性で再ランキング。

  query = "internal/infra/reranker 配下の client.go を編集中"
candidate = [
  "internal/infra/reranker/client.go",     ← 正解(高スコア)
  "internal/infra/openaihttp/client.go",   ← 関連するがrerankerではない
  "internal/infra/vllm/client.go",         ← 同様
  ...
]
  

ColBERT の token-level max-similarity により,単なる文字列距離では捉えられない「パスとintent_textの意味的な親和性」を学習できた結果として,正解を上位に置ける。

D. リトライロジック

MCP の tool_retry_with_resolve エンドポイント:

  • 元のツール呼び出し(read, list, stat等)と失敗内容を受け取る
  • path_resolve で Top-N 候補を取得
  • 候補を順番に試す(リトライ戦略: best_first or score_desc)
  • 最初に成功したら即座に元のツール結果を返す
  • 全て失敗なら status=“all_failed” を返す

検証と改善

ベンチマーク設計

開発過程で複数のシナリオをテスト:

  1. 単一ファイル名の曖昧性 (“client.go” が 8 個存在)
  2. ディレクトリ親和性 (同一プロジェクト内で,編集中のディレクトリから推測)
  3. typo 許容 (“clinet.go” → “client.go”)
  4. パス前置詞欠落 (“infra/reranker/client.go” → “internal/infra/reranker/client.go”)

例:

  Query: "client.go" (typo なし,単にファイル名のみ)
Candidates: 8 個の client.go がすべてスコア 120.0 で並ぶ
→ 訂正前は,アルファベット順で "llamacpp/client.go" が選ばれていた
→ 履歴情報(「reranker に関する編集中」)を加えると,"reranker/client.go" が上位に来る
  

精度改善

初期段階の問題は「8 つの同点候補の中から,正しい1つを選ぶ」という曖昧性だった。

改善アプローチ:

  • History correlation: 最近編集したディレクトリを context として inject
  • Exact path matching: ベンチマーク内で endswith() ではなく完全一致を要求
  • Query augmentation: intent_text に explicit なディレクトリヒントを含める

結果として,複数の同点候補がある場合でも,History による親和性スコアの微細な差分で正解を選別できるようになった。

技術的な実装詳細

Rust コード構成

  pathfinder/
├── src/
│   ├── bin/resolve_inference.rs      # MCP サーバ本体
│   ├── inference.rs                 # ONNX 推論 (ColBERT)
│   └── main.rs
├── Cargo.toml                       # ort, tokenizers, axum 依存
└── sampling/                        # テスト用 Go/Rust サンプルコード
  

resolve_inference.rs の主要機能

  1. Service struct: ファイルインデックス,埋め込みキャッシュ,Embedderを管理
  2. Index generation: 起動時に全ファイルをスキャンし IndexEntry を生成
  3. Candidate generation: fuzzy match + token-based filter
  4. Reranking: ColBERT maxsim
  5. Existence verification: stat() による実在確認

inference.rs

  pub async fn encode_text_to_token_vectors(
    text: &str,
    embedder: &Embedder,
) -> Result<Vec<Vec<f32>>, String>;

pub fn maxsim(
    query_vectors: &[Vec<f32>],
    candidate_vectors: &[Vec<f32>],
) -> f32;
  

ColBERT の token-level max-similarity を実装。query の各トークンに対して candidate の全トークンとの最大類似度を取り,合計する。

パフォーマンス特性(実測値)

  • インデックス生成: ~1-2秒(3,261ファイル / 628ディレクトリ)
  • パス解決(INT8): 平均 9.07ms(再ランキング含む)
  • 精度: monorepo全体ベンチマーク 87.3%(55ケース中48件正解)。曖昧なベアファイル名のみの部分集合では履歴相関あり 85.0%(20件中17件)
  • リトライ: 平均 1-2 回で成功(Top-5 内での正解率 90%+ )

実運用での考慮点

Cache の設計

埋め込み結果をキャッシュすることで,同じファイルパターンへのクエリを高速化:

  emb_cache: HashMap<String, Vec<Vec<f32>>>
  

ただし,ファイルシステム変更時の cache invalidation を検討する必要がある。MCP の notification メカニズムで,ルート変更時に cache を refresh できれば理想的。

History Correlation の活用

LLM や エディタのセッション情報から「最近編集したファイル/ディレクトリ」を context として resolve に渡すことで,精度が顕著に向上。

設計:

  tool_retry_with_resolve(
  failed_path: "client.go",
  op: "read",
  intent_text: "reranker のクライアント設定を修正中",
  root_hint: "internal/infra/reranker"  # 最近の編集ディレクトリ
)
  

エラーハンドリング

構造化エラーレスポンス:

  {
  "error_kind": "Ambiguous",
  "path": "client.go",
  "candidates": [
    { "path": "/Users/.../internal/infra/reranker/client.go", "score": 95.2 },
    { "path": "/Users/.../internal/infra/vllm/client.go", "score": 94.8 },
    ...
  ],
  "next_question": "reranker と vllm のどちらの client.go を編集中ですか?"
}
  

複数の同点候補がある場合,LLM に再質問することで ambiguity を解決。

実装後の検証と改善

この初期設計を基盤として、7回の改善イテレーションを実施した。詳細はpathfinderの最適化プロセスに記録。

主な改善結果:

  • モデル選定: 3つのColBERTモデルを検証し、すべて同一精度(87.3%)を示すことを確認。answerai-colbert-small INT8を採用(9ms)
  • 履歴相関の導入: 5件のリングバッファで曖昧なベアファイル名の解決率を35% → 85%に改善(+50pp)
  • ファイルシステム監視: notify crateによるリアルタイムインデックス更新
  • 孤児プロセス検出: 親MCPクライアントの消失時に自動終了

残る課題

  • 拡張子間違い(例: matcher.pymatcher.rs)の正解率が57%
  • 曖昧なベアファイル名: 履歴なしでは75%止まり
  • Edit-distanceスコアリングによるtypo補正の強化
  • ツール実行代理の安全性: tool_retry_with_resolve はパス解決後に自動でツールを再実行する。便利だが,解決先のパスが意図しないファイルだった場合に書き込み操作が発生するリスクがあり,安全性の面でさらなる配慮が必要
  • モデルのプラガブル化: 現在はanswerai-colbert-smallに固定しているが,別の再ランキングモデルへ差し替え可能なエントリーポイントを用意し,用途やプロジェクト規模に応じて改良していきたい

CLIヘルプ出力

  [email protected] ~/Development/loftllc-web % pathfinder-mcp -h
pathfinder-mcp — deterministic path resolution MCP server

USAGE
    pathfinder [OPTIONS]

OPTIONS
    --root <PATH>     Add a project root directory to watch and index.
                      May be specified multiple times.  Defaults to $PWD.
    -h, --help        Print this help message and exit.

DESCRIPTION
    An MCP (Model Context Protocol) server that resolves ENOENT / NotFound
    path errors for AI coding agents.  It builds an in-memory path index of
    configured root directories and uses fuzzy matching combined with ColBERT
    MaxSim re-ranking (when an ONNX model is available) to resolve incorrect
    paths to their most likely existing counterparts.

    Communication is via JSON-RPC over stdin/stdout.  Evaluation metrics are
    written to stderr as JSON lines (redirect with 2>metrics.jsonl).

    The server runs a stdin reader thread with periodic orphan detection and
    exits automatically when the parent MCP client process disappears.

ENVIRONMENT VARIABLES
    RESOLVE_MODEL_PRECISION  Model precision: "int8" (default), "fp16", or "fp32".
    RESOLVE_MODEL_DIR        Model directory containing model_*.onnx + tokenizer.json.
    RESOLVE_TOPK             Minimum topk value (default 10).
    INCLUDE_DIRS             Comma-separated directory names to force-include.

MCP TOOLS
    path_resolve             Resolve a failed file path to the best match.
    tool_retry_with_resolve  Resolve and automatically retry the operation.
    roots_list               Return configured root directories.
    reindex_paths            Force a full rebuild of the path index.

MCP CLIENT CONFIGURATION (Claude Code)
    "pathfinder-mcp": {
      "command": "pathfinder",
      "args": ["--root", "${workspaceFolder}"]
    }
  

まとめ

LLM のパス生成という弱点を,ファイルシステム索引 + セマンティック再ランキング + 自動リトライで補強した 結果,エラー復旧を大幅に自動化できた。

これにより,開発者はツール呼び出しに対する「パス指定の正確性」という負担を軽減でき,より高レベルな指示に集中できるようになる。

Rust + ONNX による実装で,レイテンシと精度の両立が実現され,実運用での採用が十分現実的になった。最適化プロセスの詳細はpathfinderの最適化プロセスを参照。