145
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vibe Coding のトークン消費量の40-60%を占めることもある、Markdownファイルの読み込みトークン消費量を最大98%以上圧縮する markdown-query スキル

145
Last updated at Posted at 2026-05-14

背景:Markdown を「丸ごと読ませる」ことに、そろそろ限界を感じていた

最近、コーディングエージェント(GitHub Copilot、Claude Code、Gemini CLI など)に「このリポジトリの仕様に従って実装して」とお願いする機会が増えました。

便利です。便利なのですが、現場で使い込んでいると、ある違和感がだんだん無視できなくなってきます。

仕様書・設計書・ナレッジが Markdown でリポジトリに積まれているとき、エージェントはそれらを 丸ごと read_file で読み込もうとする ことが多いのです。数万トークンが一瞬で溶けていく。関係ない章まで読まれる。回答に noise が混ざる。

正直、「全文投入で力業」のアプローチは、そろそろ筋が良くないのではと思い始めました。

そこで、ローカル完結で Markdown を横断検索し、該当するチャンクだけをエージェントに返す 仕組み(markdown-query という Skill)を実際に作って運用してみています。本記事はその設計と、運用してみてわかったことの整理です。

このリポジトリーは、私が個人で管理しているもので、私の所属している企業や団体とは一切関係がありません。


注意点 / 前提

  • 本記事は 私個人の検証と見解 です。所属組織の見解ではありません。
  • 紹介する数値(トークン削減率など)は 特定の環境・特定のリポジトリ・特定のクエリ集合 での測定結果です。Markdown の量や構造、クエリの語彙、トークナイザ、マシン性能で結果は大きく変わります。ご自身のリポジトリで必ず再測定してください
  • ローカル完結(外部 API を呼ばない)構成を前提にしています。クラウド埋め込みベースの RAG とは設計思想がそもそも違います。
  • プロダクション利用や一般化を保証するものではありません。あくまで「実務で使ってみた一例」として読んでいただければと思います。

ここで一度、整理してみます

「Context Window を節約する」と一口に言っても、論点を切り分けないと話が噛み合いません。私の中では次のように整理しています。

  • 何が問題か:エージェントが Markdown を全文読み込むと、Context Window が短時間で枯渇し、コスト・速度・回答品質のすべてが劣化する。
  • 何が問題ではないか:Markdown の編集・生成や、ソースコードの検索は、本記事のスコープではありません。そこは別の Skill / ツールの役目です。
  • どこを狙うのか:「読み取り専用で、関連する見出しチャンクだけを抽出して渡す」という、地味だが効く一手に絞ります。

万能ではありません。とはいえ、絞った分だけ確実に効く範囲を作る、というのが今回のスタンスです。


どんな Skill なのか

markdown-query の実体は、mdq という Python 製の CLI です。.mdq 配下に SQLite で索引を持ち、外部 API は一切呼びません。エージェントからは python -m mdq ... のサブプロセスとして起動します。

できることは絞ってあります。

  • BM25 検索:自然言語クエリで関連度順に上位 N 件
  • grep 検索:完全一致したいキーワード用
  • タグ / パス絞り込み:frontmatter の tags や glob で範囲限定
  • 見出し階層の俯瞰mdq list でファイル横断の見出し一覧
  • 本文取得mdq get --chunk-id <ID> で必要な箇所だけ完全な本文

ポイントは、エージェントに返すのは 見出し単位の小さな snippet だけ という割り切りです。全文ではなく、引用元 (path:lines) を添えたチャンクを返す。これだけで Context の使い方が大きく変わります。

アーキテクチャ(簡略図)

索引 DB は (言語, Chunking Strategy) の組み合わせごとに別ファイル として作成されます。複数 Strategy を並行運用しても DB が壊れないよう、物理的に分離してあるのが地味ながら効いている設計です。


なぜチャンク分割を複数戦略にしたのか

最初は素朴な「見出しごとに 1 chunk」だけで始めました。実際に運用してみると、これだけでは扱いきれないクエリがあることに気づきます。

  • 概念を聞きたいクエリ(「〜とは」「概要」)→ 見出しチャンクでは粒度が荒い
  • 物語的な質問(「なぜ」「どのように」)→ 段落単位で意味境界を見たい
  • コード片の検索 → 見出し構造を無視したい
  • ID 風の文字列や固有名詞 → 完全一致で十分

そこで Strategy を増やしました。

戦略 境界 任意依存
heading(既定) Markdown 見出しごと なし
heading_recursive 見出し chunk が大きい場合に段落で再分割 なし
fixed_window 固定窓スライド なし
semantic_paragraph 文 embedding 類似度で意味境界決定 fastembed 等
pageindex 見出しベースのツリー索引+ノードサマリ なし
auto(search 既定) クエリ内容から自動選択

実務では auto をそのまま使うことが多いです。クエリを query_router.py が 7 ルールで分類し、適切な Strategy の DB を選びます。該当 DB が無ければ fallback chain(pageindex → semantic_paragraph → heading_recursive → heading → fixed_window)に沿って切り替わります。

なぜ自動選択にしたか

正直、エージェントに毎回 --strategy を指定させるのは現実的ではありません。「クエリの性質を見て、それなりに妥当な戦略を選ぶ」くらいの自動化がないと、実務では使われなくなります。完璧なルーティングは目指していませんが、まずはここまでで十分だと感じています。


使い方(最小手順)

事前にインデックスを作る必要があります。これは外せません。

mdq index

その後、検索。

mdq search --q "クエリ" --top-k 5 --max-tokens 800

出力は JSONL(1 行 = 1 ヒット)。エージェントから呼ぶ場合は、

このリポジトリ配下の Markdown から "context window" を含む見出しを探して。

のように依頼すれば、Skill が起動してチャンクだけが返ってきます。

ファイル変更を逐次反映したい場合は mdq watch(要 watchdog)も用意していますが、必須ではありません。


ベンチマーク:本当に Context は減るのか

「効きそう」で終わらせず、実測しています。同梱の benchmark.py で、次の 3 シナリオを比較します。

  • baseline_full:全 Markdown 本文を投入
  • mdq_bm25:BM25 検索のヒットのみ
  • mdq_grep:grep 検索のヒットのみ

実行例(本リポジトリの sample 配下、4 ファイル)

測定環境(例)tiktoken/cl100k_base / Python 3.12 / Windows 11 / top_k=5, max_tokens=800, repeat=3
baseline_full:4 files / 83,230 chars / 68,440 tokens

シナリオ avg tokens savings vs baseline latency mean (ms)
mdq_bm25 1,794.6 97.38% 13.35
mdq_grep 700.6 98.98% 1.45

全文投入比で 約 1〜3% までプロンプトトークンが圧縮されました。

…と、ここだけ取り出すと派手な数字ですが、実務では話が変わることが多いです。

数値の読み方に関する注意

  • 削減率が高い = 良い、ではない。絞り込みすぎて期待箇所が抜けたら本末転倒です。「期待パスがヒットに含まれた割合(coverage_proxy)」と必ずセットで見てください。
  • mdq_grep は表記揺れに弱い生成AI パーソナライズ のようなクエリで 0 hit になることがあります(このとき savings % = 100 は「ヒットなし」を意味します)。
  • latency は絶対値で比較しない。同一マシン・同一コミット内での A/B 比較に使う指標です。
  • このベンチマークは LLM API を呼ばない。あくまで「Context 投入量と検索速度の代理指標」です。回答品質の評価は別途必要です。

運用で見えてきた、地味だが大事な工夫

ここから先は、READMEには大きく書かなかったけれど、実運用で「これがないと採用されない」と痛感した部分です。

1. Windows の文字コードで死なせない

cli.py が Windows cp932 環境で UnicodeEncodeError を吐くと exit 1 になり、エージェントは「壊れている」と判断してこのツールを 二度と呼ばなくなります。これは本当に致命的でした。

対策はシンプルで、main() 冒頭で sys.stdout.reconfigure(encoding='utf-8', errors='replace') を呼ぶか、PYTHONIOENCODING=utf-8 を環境変数で立てておく。地味ですが、ここを落とすと他の工夫が全部無に帰します。

2. 上位ルールに優先順位を明記する

リポジトリ最上位の .github/copilot-instructions.md / CLAUDE.md / AGENTS.md に、

Markdown ファイル群の検索は、まず markdown-query Skill を試す。0 ヒットや目的不一致の場合に限り grep_search / read_file にフォールバックする。

と書いておかないと、エージェントは結局 grep_search を選びます。Skill を作るだけでは呼ばれません。呼ばせるための上位指示が要る、というのは運用してみないと実感しにくいポイントです。

3. 初回索引の自動化

索引が無い状態で mdq search を呼ぶと 0 件返却になり、エージェントは「使えないツール」と判断して諦めます。CI、pre-commit、devcontainer 起動スクリプト、あるいは onboarding ステップで mdq stats → 0 件なら自動 index、のような仕掛けを入れておくと採用率が変わります。

4. 利用ログで「実際に呼ばれているか」を確かめる

mdq の全 CLI 呼び出しは .mdq/usage.jsonl に append-only で記録されます。generate_usage_report.py を週次で回し、search の呼び出し回数や Context 削減率を眺めていると、「思ったより呼ばれていない」「auto で何が選ばれているか偏っている」など、Skill 設計のフィードバックが得られます。

なお .mdq/usage.jsonl には検索クエリがそのまま記録されます。機微情報の取り扱いはご注意ください。


ここまでの整理

一度、箇条書きでまとめておきます。

良い点

  • ローカル完結で外部 API 不要。コスト・プライバシー面で安心
  • 全文投入比で 1〜3% 程度までトークンを圧縮できる(あくまで一例)
  • BM25 / grep / セマンティック / pageindex を用途で使い分けられる
  • 索引 DB が (lang, strategy) ごとに分離されているので壊れにくい

注意点

  • 索引が前提。初回 mdq index を忘れると 0 件返却で詰む
  • grep モードは表記揺れに弱い。日本語ではとくに
  • Windows の文字コード問題を踏むと、エージェントが Skill を見限る
  • 上位の指示文書に優先順位を書かないと、結局呼ばれない

限界

  • Markdown の編集・生成はスコープ外
  • ソースコード検索もスコープ外(本来 grep_search 等の役割)
  • セマンティック検索の精度はモデル依存。万能ではない
  • クラウド規模のベクトル検索が必要な要件には向かない

まとめ:「全文を読ませない」という設計判断

エージェントに何でも丸投げできる時代になりつつあるからこそ、何を読ませないか を設計する側で決めるのは、わりと重要な判断だと感じています。

markdown-query は派手な仕組みではありません。BM25 と SQLite と、いくつかの Chunking Strategy。ローカルで完結する、地味な Skill です。

それでも、

  • Context Window を節約したい
  • 引用元 (path:lines) を明示したい
  • 外部 API に依存したくない
  • まずは手元のリポジトリで試したい

という条件が揃うときには、十分実用に耐えると思います。

逆に、将来クラウド埋め込みベースの RAG が標準化されてきたら、本 Skill は素直に退役させる、というスタンスです。極論で「これさえあれば良い」と言うつもりはありません。

まずは自分のリポジトリで mdq indexbenchmark.py を一度回してみて、自分の文脈での削減率と coverage を確認するところから始めてみてください。数字を見てから、続けるか退役させるかを決めれば良いと思います。

最後まで読んでいただきありがとうございました。

145
144
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
145
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?