LLM本文抽出パイプライン再設計(HTTP化以降)
LLM をフィルタとして使用する場合の覚え書きです.
同じ事をされる方の参考になれば幸いです.
1 LLM本文抽出パイプライン再設計(HTTP化以降)
1.1 背景
llama-cli を直接パイプで叩く方式では,以下の問題が発生しました.
- 対話モードへのフォールバック
- 停止条件不明確による無限生成(">" が延々出力)
- stdin / TTY 判定問題
- chat template 自動適用による挙動不安定
これらにより,CLI直叩き方式は廃止しました.
1.2 llama-server 方式への移行
llama.cpp の server モードへ移行しました.
/path/to/llama.cpp/build-cuda/bin/llama-server \ -m model.gguf \ -c 4096 \ -ngl 32 \ --host 127.0.0.1 \ --port 8080
この方式により,
- 1リクエスト=1生成
- max_tokens 明示制御可能
- stop指定可能
- JSONレスポンス取得可能
となり,CLI由来の不安定要素が解消されました.
1.3 curl + jq による問い合わせ方式
OpenAI互換API(/v1/chat/completions)を利用します.
curl -s \ -H "Content-Type: application/json" \ -d @request.json \ http://127.0.0.1:8080/v1/chat/completions
応答抽出:
jq -r '.choices[0].message.content'
1.4 pandoc廃止
当初構成:
HTML → pandoc → Markdown → LLM
しかし,tableレイアウト中心HTMLでは pandoc が本文を 0バイト出力する問題が発生しました.
確認結果:
DEBUG SIZE: 0
よって pandoc は今回の用途に不適切と判断しました.
1.5 HTML直渡し方式へ変更
現在の構成:
HTML
→ nkf で UTF-8 正規化
→ そのまま LLM へ渡す
→ LLM が本文抽出
CONTENT="$(nkf -w -Lu "$src")"
LLMは構造理解が可能なため,
- ナビゲーション削除
- フッタ除去
- tableレイアウト解釈
が可能です.
1.6 文字コード処理方針
重要確認事項:
- LLMは charset を解釈しない
- metaタグの charset は無意味
- UTF-8文字列としてのみ処理される
よって,
- nkf による UTF-8 正規化は必須
- meta charset は削除可能
という設計に確定しました.
1.7 現在の問題点
動作は確認済みですが,
- ローカルLLMが小規模
- max_tokens制限
- context制限
により,
- 本文途中で切断
- 抽出精度不安定
が発生しています.
これは設計問題ではなく,モデル能力の限界と判断しています.
1.8 現在のパイプライン構造
mirror/siteX
→ nkf(UTF-8統一)
→ HTML直渡し
→ llama-server(HTTP)
→ YAML + Markdown出力
→ normalized/siteX
というレイヤ分離構造になっています.
1.9 設計到達点まとめ
- CLI版は不安定
- server方式が安定
- pandocは不適切
- HTML直渡しが最適
- UTF-8正規化は必須
- meta charsetは無視可能
1.10 現在の状況
現在,このフィルタは,コンソールから使うパイプラインとして稼働しています.
- 標準入力を読み込み,
- コマンド内に記されたプロンプトなどを jq で JSON に整形,
- curl でローカルサーバーに投げる.
- 受け取って出力
という手順です.
llama-cli への直接アクセスでは不十分で,サーバー方式に移行する必要がありました.
2 派生物 readability
また,派生物として,mozilla/readability を使用する,不要行削除フィルタも生まれました.
インストール.
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs npm init -y npm install jsdom @mozilla/readability
スクリプト本体.
#!/usr/bin/env node
// mozilla readability をフィルタとして使う
const { JSDOM } = require("jsdom");
const { Readability } = require("@mozilla/readability");
// stdinを全部読む
let html = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", chunk => {
html += chunk;
});
process.stdin.on("end", () => {
try {
const dom = new JSDOM(html, {
url: "https://example.com"
});
const reader = new Readability(dom.window.document, {
charThreshold: 200,
keepClasses: false
});
const article = reader.parse();
if (!article || !article.textContent) {
console.error("No article detected.");
process.exit(0);
}
// テキスト整形(LLM向け)
const cleaned = article.textContent
.replace(/\r/g, "")
.replace(/\n{3,}/g, "\n\n")
.replace(/[ \t]+/g, " ")
.trim();
console.log(cleaned);
} catch (err) {
console.error("Error:", err.message);
process.exit(1);
}
});