レスポンス語彙の設計で小規模 LLM の精度が 15 点変わる: pathfinder での実験記録
MCP サーバー pathfinder のレスポンスに含めるメタデータフィールドを変えるだけで、小規模 LLM のパス解決精度が 52〜67/70 の範囲で大きく揺れた。maybe_exts、alike、exist_similar の3種を試した実験から、条件付き出現・小カーディナリティ・直接 actionable というレスポンス語彙の設計原則を導いた。
結論
MCP サーバーのレスポンスに含めるメタデータフィールドをひとつ変えるだけで、小規模 LLM のベンチマーク精度が 15 点以上変動する。
pathfinder のパス解決レスポンスから maybe_exts(同一 stem を持つ候補の拡張子リスト)を削除したところ、67/70 だったスコアが 52〜63/70 に劣化した。復元すると 65/70 に回復。さらに alike: N(同一 stem ファイル数)を追加すると LLM が無限ループに陥り、exist_similar: bool を常時返却すると揺れ幅が 10 点に拡大した。
最終的に、小規模 LLM 向けの MCP レスポンス語彙には以下の設計原則が有効だとわかった。
- 条件付き出現 — 曖昧なケースだけフィールドを返す。不在が確信を意味する
- 小カーディナリティ — 要素数 2〜3 のリスト。数値 273 は危険
- 直接 actionable — 「試すべき拡張子」のような具体的な指示。抽象的な類似度ではなく
- 最小レスポンス長 — 毎回返すフィールドはコンテキストに蓄積して注意帯域を食う
背景
pathfinder は自作の MCP サーバーで、ColBERT MaxSim ベースのセマンティック再ランキングとレキシカルスコアリングを組み合わせてファイルパス解決を行う。qwen3.5-35b-a3b(Q5_K_M 量子化)をベンチマーク用 LLM として使い、70 テストのパス解決精度を継続的に計測していた。
この作業の出発点は、以前のレキシカル最適化(lexiopt4)で達成した 67/70 というベストスコアに対し、一部変更を revert した後にスコアが 52〜63/70 まで劣化したことだった。
revert 後のスコア劣化の調査
ベンチ結果ファイル 3 回分(lexiopt4-1, 4-2, 4-3)を比較した。revert 前の lexiopt4(67/70)と lexiopt5(61/70)に対し、revert 後は 63, 52, 57 と大きくばらついていた。
主な劣化ポイント:
| カテゴリ | revert 前 | revert 後 |
|---|---|---|
| G: Retry operations | 3/3 | 0/3(Run 2,3 で全滅) |
| L: Test variant/config | 4/4 | 0〜2/4 |
| D: Wrong extensions | 7/7 | 6/7(Test 31 が安定して失敗) |
最初に疑ったのはコードの revert 自体だった。segment_overlap、dir_ext_counts、warmup 改善、tool_retry_with_resolve の tool_name 追加 — これらは revert で失われたはずの機能だった。
revert されていなかった
実際にコードを grep で確認すると、4 つの機能は全て現在のコードに存在していた。revert されていなかった。
ではスコア劣化の原因は何か。git diff で lexiopt4 ベンチ実行時点のコミット(9192d6b)と現在の HEAD(d6c9dc8)を比較した。
maybe_exts の発見
diff で判明した変更はひとつ — maybe_exts フィールドの削除だった。
maybe_exts は、解決結果の best candidate と同じ stem(ファイル名から拡張子を除いたもの)を持つ候補の拡張子一覧を返すフィールドだ。例えば config.go を解決したとき、同じ stem config を持つ config.rs も候補にあれば maybe_exts: ["go", "rs"] を返す。
このフィールドは 4 箇所に存在していた:
PathResolveOut構造体resolve()内の集約ロジックtool_path_resolveのレスポンス- tool description
maybe_exts 復元と効果検証
4 箇所を全て復元し、ビルド・テスト通過を確認してベンチを実行した。
結果: 65/70 — ベースラインの揺れ範囲に回復。
| カテゴリ | opt4 (67) | revert 後 (52-63) | 復元後 (65) |
|---|---|---|---|
| G: Retry | 3/3 | 0-3/3 | 3/3 |
| K: Cross-lang | 4/6 | 4/6 | 5/6 |
| L: Config | 0-4/4 | 0-2/4 | 2/2 |
たった数トークンのメタデータ(maybe_exts: ["rs","go"])が LLM の拡張子判断を助け、10 点以上のスコア差を生んでいた。
レスポンス語彙のさらなる実験
「ヒントとなるテキスト、小さくて効果的な語彙選択が精度を分ける」— この知見から、追加のシグナルを試した。
alike: N(同一 stem ファイル数)の追加
maybe_exts が「何が」似ているかを伝えるのに対し、alike は「いくつ」似ているかを数値で伝える設計だった。
結果は想定外だった。config という stem のファイルが 273 個あり、alike: 273 というレスポンスが返った。LLM はこの大きな数値に反応して過剰な探索モードに入り、list_dir の API 呼び出しに失敗、さらにサマリ作成時にカウント確認の無限ループに陥った。「59 PASS, 1 FAIL」の計算を何十回も繰り返し、Test 61-70 を完全にスキップしてしまった。
exist_similar: bool への切り替え
数値が危険なら bool にする。exist_similar: true/false を常に返すようにした。
3 回のベンチ結果: 66, 60, 56 — 揺れ幅が 10 点に拡大。lexiopt6-1 は K=6/6, L=4/4 で過去最高水準を達成したが、lexiopt6-3 は G=0/3 に崩壊。maybe_exts のみの構成(65/70 で安定)より揺れが増えた。
「false も確定を強くする意味で安定性を上げるかもしれない」という仮説で常時返却にしたが、逆効果だった。
maybe_exts のみに回帰
exist_similar と alike を削除し、maybe_exts のみに戻した。
原理の発見
実験結果から 3 つの原理が浮かび上がった。
コンテキスト汚染はレスポンス長に比例する
毎テストに返るフィールドはコンテキストに蓄積する。candidates リスト(top-K のパス・スコア・why・root)は 1 件あたり 50-80 トークン、top-5 × 70 テストで数千トークンの蓄積になる。exist_similar: false は 1 フィールドだが、70 回の出現が累積して注意帯域を食う。
maybe_exts が安定する理由 — 大半のテストでは出現しない(一意解のため)。出るのは本当に曖昧なケースだけ。コンテキスト汚染がほぼゼロ。
Primacy-Recency カスケード
小規模 LLM はコンテキストの最初と最後に強く引きずられる。中盤のテスト(G: Test 46-48)は「注意の谷」に位置する。常時返却フィールドがコンテキストに蓄積すると、中盤で臨界点を超えて崩壊し、そのまま終盤にもカスケードする。G と L が連動して落ちるパターンがこれを裏付けていた。
過ぎたるは猶及ばざるが如し
情報を増やせば精度が上がるとは限らない。alike: 273 は不安シグナルとして増幅され、exist_similar: false は常時出現のノイズになった。最も効果的だったのは「条件付きで出現し、小さく、直接 actionable」な maybe_exts だった。
タスクベースベンチの拡張
70 テストベンチで安定的に失敗するカテゴリ(F: intent, K: cross-lang, L: config disambiguation)をカバーする 10 タスクを追加し、タスクベーステスト(test_prompt_tasks.md)を 30 → 40 に拡張した。
追加タスク 31-40:
- Go vs Rust client 判別(K カテゴリ対応)
- Hand-written vs generated 判別(K カテゴリ対応)
- Dev vs prod Kubernetes overlay(L カテゴリ対応)
- VPC vs ECS Terraform module(L カテゴリ対応)
- Go config vs Rust config(D カテゴリ対応)
- Deep OAuth callback, Postgres vs PostgREST, vendor 除外, GraphQL schema vs generated, reranker entry point
ベンチ結果: 40/40 全問正解、tool calls 42 回。タスクベースでは LLM が intent を自然に持つため、pathfinder の intent_text 活用が効きやすかった。
filesystem MCP ツールのみで同じタスクを解く baseline 版テスト(test_prompt_tasks_baseline.md)も作成した。
ベンチスコア推移
| 構成 | スコア | 揺れ幅 |
|---|---|---|
| maybe_exts あり (lexiopt4) | 67/70 | baseline |
| maybe_exts なし (revert) | 52-63/70 | 11 点 |
| maybe_exts 復元 (latest) | 65/70 | baseline 範囲 |
| alike: N 追加 | 崩壊(ループ) | — |
| exist_similar: bool 追加 | 56-66/70 | 10 点 |
| maybe_exts のみ (最終) | 65/70 | 安定 |
コード変更のまとめ
maybe_extsの復元(PathResolveOut, resolve(), tool_path_resolve, tool description)alike: usizeの追加と削除exist_similar: boolの追加と削除- タスクベーステストの 30 → 40 拡張
- filesystem baseline 版タスクテストの作成
