ローカル LLM の tool_call 失敗を減らすために MCP サーバーを自作した: pathfinder の設計と検証
パラメータ 32B 以下のローカル LLM が起こす tool_call のパス解決失敗を、ColBERT ベースのセマンティック検索で決定論的に改善する MCP サーバー pathfinder の設計動機、アーキテクチャ、ベンチマーク結果、CLI 体験をまとめた。
結論
パラメータ 32B 以下のローカル LLM でコーディングエージェントを動かすと、tool_call のパス解決で頻繁に失敗する。ファイルパスの typo、拡張子の取り違え、ディレクトリ階層の省略など、モデルが生成するパスが実在しないケースが多い。これを LLM 側のプロンプト改善で吸収しようとしても限界がある。
そこで、パス解決の失敗を MCP サーバー側で決定論的にリカバリする pathfinder を Rust で自作した。ColBERT(Late Interaction)モデルによるセマンティック再ランキングを組み込み、INT8 量子化で 16MB・CPU 推論のみ・10ms 未満のレイテンシで動作する。GPU の VRAM を一切消費しないため、推論サーバーのリソースを圧迫しない。
約 4,000 ファイルのモノレポで filesystem MCP サーバーと比較した結果、パス解決の精度は僅差だったが、コンテキスト消費量を約 10,000 トークン抑制できた。filesystem MCP は list_directory の祖先チェックでファイル数に比例した O(n) のコンテキスト膨張が起きるのに対し、pathfinder は候補を絞り込んでから返すためコンテキスト効率がよい。
ついでに ModernBERT 推論を Rust で実装したので、開発してきた複数プロジェクトのコードを横断的に俯瞰できる CLI ツールとしても仕上げた。

動機: ローカル LLM の tool_call はなぜ壊れるか
Claude や GPT-4 クラスのモデルでは tool_call のパス解決はほとんど問題にならない。しかし、ローカルで動かす 7B〜32B クラスのモデルでは事情が違う。
典型的な失敗パターン:
| 失敗パターン | 例 |
|---|---|
| ディレクトリ名の typo | src/componets/Button.tsx → components |
| 拡張子の取り違え | config.yaml → 実際は config.yml |
| 階層の省略 | utils/helper.go → 実際は internal/pkg/utils/helper.go |
| 類似ファイル名の混同 | auth/login.ts vs auth/login.test.ts |
これらの失敗が起きるたびに LLM はエラーメッセージを受け取り、リトライを試みる。そのたびにコンテキストが消費され、コンテキストウィンドウの圧迫がさらなる精度低下を招く悪循環になる。
プロンプトで「正確なパスを使え」と指示しても、モデルのパラメータサイズに起因する限界は越えられない。これはモデル側ではなくツール側で吸収すべき問題だと判断した。
アーキテクチャ
pathfinder は Rust 製の MCP サーバーで、stdin/stdout の JSON-RPC 2.0 で LLM クライアント(Claude Code、Zed Editor など)と通信する。
三段階のパス解決
LLM が生成した不正確なパスに対して、以下の三段階で正しいパスを推定する。
- レキシカルスコアリング — ないしょ。1ms 未満
- クエリ履歴の相関 — 直近の解決結果をリングバッファで保持し、作業コンテキストに沿った候補を優先
- ColBERT セマンティック再ランキング — 上位候補に対して ColBERT の MaxSim スコアで再順位付け。約 5-8ms
レキシカルスコアリングだけで高い確信度が得られる場合は、ニューラル再ランキングをスキップする。これにより、大半のクエリは 1ms 未満で返る。
ColBERT モデル
セマンティック再ランキングには ColBERT(Late Interaction)アーキテクチャのモデルを使っている。
| 項目 | 値 |
|---|---|
| モデル | lateon-code-edge(コード特化)/ mxbai-edge-colbert(汎用) |
| パラメータ数 | 約 17M |
| 量子化 | INT8(ONNX Runtime) |
| モデルサイズ | 約 16MB |
| 埋め込み次元 | 128 |
| 推論 | CPU のみ(GPU 不要) |
ONNX Runtime 経由で推論し、HuggingFace Tokenizers でテキストをエンコードする。Python 依存はない。
MCP ツール
pathfinder が公開する主要ツール:
path_resolve— 失敗したパスを受け取り、最も可能性の高い実在パスを返す。intent_text(「Go config loader」のような短い目的記述)を受け取ることで精度が向上するtool_retry_with_resolve— パスを解決した上で、元の操作(read_file、list_dir 等)を自動リトライするcandidate_list— 上位候補をリストで返す。曖昧なケースのブラウジング用roots_list/reindex_paths/server_version— 管理系ツール
tool_retry_with_resolve が実用上の主力で、LLM が read_file で ENOENT を受けた際に、パス解決とリトライを 1 回の tool_call で完結させる。
パス解決の実例
LLM がディレクトリ名を typo した場合:
path_resolve:
check_path: "content/ja/docs/tech/infrastrcture/podman-quadlet-systemd-ubuntu.md"
^^^^^^^^^^^ typo
→ resolved: "content/ja/docs/tech/infrastructure/podman-quadlet-systemd-ubuntu.md"
$ pathfinder --help
pathfinder — semantic path finder & MCP resolution server
USAGE
pathfinder [OPTIONS] Interactive semantic directory finder (default).
pathfinder --mcp [OPTIONS] Start as an MCP server.
FINDER OPTIONS
--include-builds Include build/artifact dirs (target, dist, …).
MCP OPTIONS
--root <PATH> Add a project root directory to watch and index.
May be specified multiple times. Defaults to $PWD.
GENERAL OPTIONS
-h, --help Print this help message and exit.
-V, --version Print version, model, and PCA config to stderr and exit.
MCP TOOLS
1. path_resolve Resolve a failed file path to the best match.
2. tool_retry_with_resolve Resolve + retry the operation in one call.
3. roots_list Return configured root directories.
4. reindex_paths Force a full index rebuild.
ENVIRONMENT VARIABLES
PF_MCP_INFERENCE Inference mode: "general" (default) or "code".
Models (both INT8 quantized):
general → mxbai-edge-colbert (17M, 48-dim)
code → lateon-code-edge (17M, 48-dim)
CLI: vim 操作でプロジェクトを横断的に俯瞰する
MCP サーバーとは別に、同じ ColBERT 推論エンジンを使った対話的な CLI ツールも作った。開発してきた複数プロジェクト(domain 層、infra 層、presentation 層)のコードを横断的に俯瞰する用途を意識している。
操作体系
シェルで pf(コード特化)または pfg(汎用)を起動すると、セマンティック検索のインターフェースが開く。
- vim キーバインド で候補リストをナビゲート
- ディレクトリ階層を ネストの最深部まで掘り下げ、最深部でファイル一覧が表示される
- 右キー(→) で選択ファイルを
lessで開く - 結果を選択すると、そのディレクトリに
cdする
pf # コード特化モード(lateon-code-edge)
pfg # 汎用モード(mxbai-edge-colbert)
ModernBERT の推論を Rust でネイティブ実装しているため、検索のレスポンスは体感的にほぼ即時で返る。

ベンチマーク: filesystem MCP との比較
約 4,000 ファイルのモノレポアプリケーションサンプルで、filesystem MCP サーバーとの比較検証を行った。
パス解決精度
pathfinder 単体の精度テスト(70 テストケース、12 カテゴリ):
| カテゴリ | 内容 | 結果 |
|---|---|---|
| 正しいパス | そのまま返る | 8/8 |
| ディレクトリ typo | componets → components | 10/10 |
| ファイル名 typo | 文字の入れ替え・欠落 | 10/10 |
| 拡張子の取り違え | .yaml → .yml | 7/7 |
| 階層の省略 | 中間ディレクトリが欠落 | 5/5 |
| intent ベースクエリ | 目的記述からの推定 | 4/5 |
| リトライ操作 | resolve + retry の一体動作 | 3/3 |
| 紛らわしいパスペア | 類似名ファイルの区別 | 6/6 |
| 深いネスト | 8 階層以上 | 4/4 |
| 言語横断クエリ | 「Go の設定ファイル」等 | 4/6 |
| テスト/設定ファイル | test, config の区別 | 4/4 |
| 合計 | 67/70 (95.7%) |
コンテキスト消費量
filesystem MCP との精度差は僅差だったが、コンテキスト消費量に約 10,000 トークンの差が出た。
filesystem MCP は list_directory で祖先ディレクトリを順にチェックしていくため、ファイル数に比例してレスポンスのトークン数が増える。4,000 ファイル規模でこの差が出ているので、ファイル数が増えるほど差は拡大する。
pathfinder は候補をスコアリングで絞り込んでから返すため、ファイル数に対するコンテキスト消費が抑えられる。ローカル LLM の限られたコンテキストウィンドウ(8K〜32K)では、この差が実用上の精度に直結する。
リソースフットプリント
| 項目 | 値 |
|---|---|
| バイナリサイズ | シングルバイナリ(Rust) |
| モデルサイズ | 約 16MB(INT8) |
| メモリオーバーヘッド | 50MB 未満 |
| GPU VRAM 消費 | ゼロ |
| 典型的レイテンシ | 10ms 未満(レキシカルのみなら 1ms 未満) |
GPU を一切使わないため、vLLM や llama.cpp が VRAM を最大限使っている状態でも pathfinder の動作に影響しない。これはローカル LLM 環境では重要な特性だ。
注意事項
- レキシカルスコアリングの内部ロジックはないしょ。ctree と合わせて自社のオープンソース LLM パイプライン基盤に組み込んで運用しているため
- 現在のテストスイートは 70 ケースで、言語横断クエリ(intent_text でプログラミング言語を指定するケース)で 2 件の未解決失敗がある
- ベンチマークは約 4,000 ファイルのモノレポで実施。数万ファイル規模での検証は今後の課題
- ColBERT モデルの選択(コード特化 vs 汎用)は
PF_MCP_INFERENCE環境変数で切り替え可能 - MCP プロトコル(2024-11-05)に準拠。Claude Code、Zed Editor 等の MCP クライアントで利用可能
