
Webから情報を取る処理を、LLMに毎回「生HTMLを読ませて頑張らせる」形で組むと、だいたい高くて遅くて壊れやすいです。 しかも、同じサイトを何度も読むユースケース(ニュース監視、ドキュメント追跡、価格改定検知など)だと、同じ失敗を何度も繰り返します。
この手の問題は、気合のスクレイピングテクで解決するというより、「一度うまくいった抽出方法を“道具”として固定化し、次回以降は再利用する」と割り切った方が素直に効きます。 この記事では、Web→Adapter→Tool→Agentという変換パイプラインで、スクレイピングを学習で道具化する設計をまとめます。
元々の着想は、以前の記事で紹介した web2cli (GitHubリポジトリ )です。 あの記事の「Every website is a Unix command」という思想を、エージェント運用(再訪/トークン/ドリフト)に寄せていくと、だいたいこの方向に収束します。
最近、その延長線として self-learning-web-adapter という自己学習型スキル(skill: エージェントに渡す手順とツールのパッケージ)を追加しました。 スキル本体は skills/self-learning-web-adapter に置いてあります。
なぜつらいのか: 生HTMLをそのままLLMに渡すとコストが増える
まず前提を揃えます。ここで言うLLMはLarge Language Model(大規模言語モデル)で、文章だけでなく「外部ツールを呼び出して作業する」エージェント(agent: LLMが道具を使って目的を達成する仕組み)として動く想定です。
生HTMLをLLMに渡して抽出させると、次のコストが積み上がります。
- トークン(token: LLMが読む入力単位)コストが高い
- レイテンシ(latency: 処理待ち時間)が伸びる
- DOM(Document Object Model: HTMLを木構造として扱う概念)がちょっと変わるだけで壊れる
- 失敗時の再試行が増えて、さらに高くなる
個人的には「初回はそれでもいいけど、2回目以降は同じ探索を繰り返したくない」が本音です。
解決の方向性: 探索を1回に閉じ込め、実行を軽くする
考え方はシンプルで、Webを毎回スクレイピングするのをやめて、こう変換します。
| |
このモデルだと、LLMが毎回やることは「生HTMLの読解」ではなく「道具を呼ぶ」になります。 うまくいくと、LLMの入力は数百トークンのJSON(JavaScript Object Notation: 構造化データ形式)に圧縮できます。
参考として、特定のサイト(ブログ/マーケティングページ)を例に「生HTML」と「アダプター出力」を比べると、95%〜99%台の入力トークン削減が出ることがあります。 ここは誇張しない方が良くて、初回学習コストは別にかかりますし、サイトによって効きが違います。 ただ、「再訪が多い」ワークロードでは回収しやすい、という方向性はかなり安定しています。
実測: 再訪でどれくらいトークンが減る?
「本当にそんなに減るの?」という話なので、簡単な実測も入れておきます。
トークン計測には tiktoken
(tokenizer: 文字列をトークンに分割する仕組み)を使い、o200k_base で数えました。
比較は3パターンです。
- 生HTMLをそのままLLMに渡す
- 学習済みアダプターのJSON出力をLLMに渡す
- web2cli風ラッパーのJSON出力をLLMに渡す
学習は各サイト3記事で行い、別の1記事(ホールドアウト)で評価しています。
| Site | HTML tokens | Adapter tokens | web2cli tokens | Reduction vs Adapter | Reduction vs web2cli |
|---|---|---|---|---|---|
blog.python.org | 15,057 | 265 | 351 | 98.24% | 97.67% |
blog.rust-lang.org | 7,656 | 263 | 361 | 96.56% | 95.28% |
vercel.com | 224,735 | 255 | 335 | 99.89% | 99.85% |
平均すると、入力トークンの削減率は次の通りでした。
- 平均削減率(アダプター直出力): 98.23%
- 平均削減率(web2cli風コマンド出力): 97.60%
- 平均削減量(アダプター直出力): 82,221.7 tokens / page
- 中央値削減量(アダプター直出力): 14,792 tokens / page
ポイントは2つです。
- 生HTMLは、サイトによっては「それだけで」数万〜数十万トークンになる
- いったん学習してしまえば、必要な情報だけを数百トークンのJSONに圧縮できる
特に vercel.com のような重いページだと、1ページあたり22万 tokens 以上削れていました。
注意点も書いておきます。
- 今回は3サイトだけの小規模実測です
- 抽出項目は主に
titleauthorpublishedに絞っています - 初回アクセス時は学習コストがあるので、本質的な効果は「再訪」で出ます
雑な見積もりはこの式でできます。
| |
軽い記事中心なら中央値(14,792 tokens / page)で見るのが安全です。 SPA(Single Page Application: 1ページ内で画面遷移するWebアプリ)やマーケティングページが多いなら、平均値(82,221.7 tokens / page)側に寄る可能性があります。
Adapterとは何か: サイト固有差分を閉じ込める契約
Adapter(アダプター)は「このホスト(host: example.comのようなドメイン)ならこう取る」を閉じ込めた設定+ルールです。 ポイントは、アダプターがLLMの思考ではなく、抽出の契約(contract)として残ることです。
例えば、記事ページ(article: ブログ記事など)なら、抽出したいのはこのへんです。
title(タイトル)author(著者)published(公開日時)
抽出戦略(strategy: どの情報源を優先するか)も明示します。
- JSON-LD (JSON for Linking Data: HTML内に埋め込める構造化メタデータ)
- Open Graph protocol
(OG: SNS向けメタタグ仕様)や通常の
metaタグ - CSSセレクタ(CSS selector: HTML要素を指定する記法)
さらに、実運用で重要なのが「壊れたかどうかを機械的に判断する」ことです。 ここでDOM fingerprint(DOMフィンガープリント: DOM構造の指紋)を使います。 学習時にDOMの特徴量を保存し、実行時にそれとズレたら「ドリフト(drift: 構造変化)」として扱い、再学習に回します。
Tool/CLIとは何か: 1行で叩ける“Webの窓口”
アダプターをそのまま置いても良いのですが、エージェントに使わせるなら、CLI(Command Line Interface: コマンドラインから叩ける道具)まで落とすのが楽です。
理想は「URLを渡したらJSONが返る」だけ。
| |
この形にしておくと、エージェント側のプロンプト設計も単純化します。 「このコマンドを呼んで、返ってきたJSONのtitle/publishedだけ使う」みたいな指示ができます。
似た発想として、Webをコマンド化するweb2cliがあります。 あの思想(Every website is a Unix command)を、エージェント運用(再訪/トークン/ドリフト)に寄せると、だいたいこの方向になります。
実例: 自己学習型スキル self-learning-web-adapter を動かす
ここからは「スキル寄り」の話をします。 このスキルは、同じホストを何度も読むときに、サイト差分をアダプターへ閉じ込めて再利用します。
目的は次の通りです。
- Why: 2回目以降は、スクレイピング探索を繰り返したくない
- What: URL→構造化JSON(title/author/published + 健康診断情報)を返す
- Prereq: Python 3.10+、ネットワーク到達
- Verify:
python3 skills/self-learning-web-adapter/scripts/web_adapter_cli.py run <url>がJSONを出す
1) セットアップ
スキルを使うだけなら、リポジトリをクローンしなくても大丈夫です。 スキル追加は、Node.js に付属するnpx(npm package runner: 一時的にCLIを実行する仕組み)で済みます。
| |
依存は最小で、HTMLパース用にBeautiful Soup (HTMLパーサ)を入れます。 (Pythonの依存はnpxでは解決されないので、ここは別で入れます)
| |
注意: 以降のコマンドは、skills/self-learning-web-adapter/ がカレントディレクトリ直下に追加されている想定です。
もし別の場所に入った場合は、パスだけ読み替えてください。
2) 学習サンプルを用意する(同一ホストで3本以上)
スキルのルールはシンプルで、学習(learn)は「同じホストのURLを3本以上」です。 ブログなら、同じ著者・同じカテゴリの3記事くらいを選ぶと外しにくいです。
3) learn → runで固定化する
同一ホストの代表ページを渡すと、アダプターが adapter_registry/<host>.json に保存されます。
| |
学習できたら、別のURL(ホールドアウト: 学習に使っていない検証用ページ)で実行します。
| |
出力には、抽出結果だけでなく signature_known や extraction_health.score のような診断情報が入ります。
このへんが「スキル寄り」にする理由で、壊れた時の扱いまで含めて“道具化”できます。
4) ドリフトチェックと再学習
check は run と同じくJSONを返しますが、「再学習が必要そうか」を見る用途です。
| |
needs_retrain: true が立ったら、再学習(retrain)に回します。
| |
5) web2cli風コマンドにエクスポートする
スキルとしての“手触り”が出るのはここです。 学習済みアダプターを、web2cli風の1コマンドに落とします。
| |
エクスポートされたコマンドは web2cli_commands/ に置かれ、web2cli_commands/index.json がレジストリ(registry: コマンド一覧)になります。
エージェント視点では「サイトがツールになった」状態です。
設計の勘所: うまくいくスキルに寄せるコツ
やってみて効くことが多いのはこのへんです。
- まずJSON-LDを疑う
- 次にOpen Graphと通常metaで拾う
- CSSセレクタは最後の逃げ道にする
- 失敗を「例外」ではなく「健康診断」にする(check)
- いきなり完璧を目指さず、欲しいフィールドを絞る(title/dateくらいから)
逆にアンチパターンはこう。
- 1ページでたまたま動いたCSSセレクタを、根拠なく信じる
- エージェント側のプロンプトに抽出ロジックを埋め込み、毎回実行する
- 壊れた時に「その場しのぎ」で直して、学習物として残さない
まとめ: “Webを読む”を、道具の問題に変える
Web→Adapter→Tool→Agentの発想でやると、スクレイピングは「頑張って読む」から「使い回せる道具を作る」に変わります。 この変換が効くのは、特に「同じサイトを何度も読む」ワークロードです。
次のアクション案です。
- 自分がよく読むドメインを1つ選び、欲しいフィールドを3つに絞る(title/published/urlくらい)
- JSON-LD→meta→CSSの優先順位で、まずは動く抽出器を作る
- DOM fingerprintと
checkを入れて、壊れたら自動で学習を回す設計に寄せる
参考リンク
- self-learning-web-adapter(GitHub)
- self-learning-web-adapter skill directory(skills/self-learning-web-adapter)
- SKILL.md
- Token Savings(実測メモ)
- Adapter Format
- tiktoken(GitHub)
- 当ブログ: web2cli: Every website is a Unix command - ブラウザ自動化の前に「HTTPだけで済む仕事」を片付ける
- web2cli(GitHub)
- JSON-LD
- Schema.org
- The Open Graph protocol
- Beautiful Soup documentation