Mac Studio 1台に Dify と LM Studio を置き、日本語PDF 65本の取得、図表のテキスト化、RAGへの取込、300問への回答、LLM-as-judge、集計までをローカルで実行した。
有料のクラウドAPIは使っていない。
この記事で扱うのは、モデルの順位やスコア差の分析ではなく、日本語PDF RAGを作り、固定した設問で性能を測り、再検証できるローカル成果物を残すまでの構成と手順だ。
最後まで実行できたことの確認値として、固定した評価条件で 0.853(256/300)を得た。
評価には、公開されている日本語RAG評価データをもとに文書と正解の版を固定した RAG Eval JA Repro の65PDF・300問を使った。
参照元、派生関係、変更履歴、ライセンスは同ページにまとめている。
本記事のスコアは、この再現用評価基準CSVとローカルJudgeを使った完走確認値であり、他システムとの比較には使わない。
評価パイプラインのコード、Difyテンプレート、プロンプト、r1-r4の公開成果物は saka-cons/rag-eval-ja-pipeline に置いた。
本記事のコマンド例は、この公開リポジトリのルートで実行する前提で書く。
完成形
ハードウェア、推論サーバ、RAGアプリケーションを次の構成にした。
表1: ローカルRAG構築に使用した構成
| 区分 | 構成 |
|---|---|
| ハードウェア | Mac Studio M4 Max 36GB |
| RAG | Dify 1.14.2(Docker・セルフホスト) |
| ローカル推論 | LM Studio(OpenAI互換API) |
| PDF処理 | PyMuPDF 1.27.2.3(MuPDF 1.27.2 / Python 3.13.7) |
| VLM | qwen/qwen3-vl-8b |
| 埋め込み | Qwen/Qwen3-Embedding-8B-GGUF |
| 回答生成・Judge | qwen/qwen3.6-35b-a3b |
処理は6段に分かれる。
図1: 入力固定から取込、回答、Judge、集計、run成果物固定までの6段フロー
- Wayback固定URLからPDF 65本を取得し、SHA-256を検証する。
- PyMuPDFでPDF本文を抽出し、図・表・ページ画像はVLMでテキスト化する。文字化けを検知したページはVLM OCRに切り替える。
- 本文と図表テキストをDifyのナレッジへ投入する。
- 300問をDifyアプリへ送り、回答を保存する。
- 質問、正解、回答をローカルJudgeへ渡してO/Xを判定する。
- スコアを集計し、回答、判定、入力、ローカルキャッシュをrun単位で固定する。
図表を別工程にしたのは、300問のうち table が82問、imageが76問あり、通常のPDFテキスト抽出だけでは評価対象の情報が欠けるためだ。
VLMの有無による比較結果の詳細はZenn第5章で公開している。
36GBに全モデルを常時載せるのではなく、取込時はVLMと埋め込みモデル、回答時は埋め込みモデルと生成モデル、判定時はJudgeというように、工程ごとにLM Studioで使用モデルを切り替える。
1. 入力を固定する
PDFバイナリは再配布しない。
代わりに、Waybackの固定URL、取得スクリプト、SHA-256 manifestを公開し、同じ採用現物を再取得できるようにした。
リポジトリを取得し、Hugging Face dataset から再現用評価基準CSVを置いてから、文書をダウンロードして検証する。
git clone https://github.com/saka-cons/rag-eval-ja-pipeline.git
cd rag-eval-ja-pipeline
curl -L -o rag_evaluation_master.csv \
https://huggingface.co/datasets/SakataConsul/rag-eval-ja-repro/resolve/main/rag_evaluation_master.csv
python3 repro/download_documents.py
(cd documents && shasum -a 256 -c ../repro/documents_checksums.sha256)
確認対象は65本すべてだ。
ここで確認できるのは、この検証で採用したコーパスを同一バイトで再取得できたことまでだ。
設問と正解には、採用したPDFとの現物照合結果を記録した再現用評価基準CSV(rag_evaluation_master.csv)を使う。
元の列は変更せず、question_new、target_answer_new、target_page_no_new に差分を記録した。
文書固定と再現用評価基準CSV作成の経緯はZenn第1章・第2章・第3章にまとめている。
2. DifyとLM Studioを接続する
LM Studioでは「Serve on Local Network」を有効にする。
Mac上のスクリプトからは http://localhost:1234/v1、DifyのDockerコンテナからは http://host.docker.internal:1234/v1 を参照する。
Dify側では次を設定する。
- チャットボットの生成モデルをLM Studioのローカルモデルへ向ける。
-
prompts/generation_system_prompt.mdをシステムプロンプトへ貼る。 - 検索を hybrid、
top_k=10、リランカ offに固定する。 - ナレッジ作成時に埋め込みモデルを指定する。
APIキーはリポジトリルートの .env に置く。
cp .env.example .env
# .env に DIFY_KNOWLEDGE_API_KEY と DIFY_APP_API_KEY を記入
set -a; source .env; set +a
3. PDFをVLMで処理してナレッジへ入れる
LM Studioで qwen/qwen3-vl-8b と埋め込みモデルを利用可能にして、VLMありのナレッジを作る。
./scripts/run_r1_r4_ingest.sh vlm --refresh
スクリプトは次を行う。
- 再現用評価基準CSVの再生成
- PyMuPDFによるPDF本文の抽出
- 図表のVLMテキスト化
-
RAG-Eval-JA-master-vlmの新規作成 - 65文書の投入とindexing完了待ち
- VLMと埋め込みモデルの利用記録の保存
ナレッジを作り直すとIDが変わる。
取込完了後、Dify Studioのチャットボットで「コンテキスト」を RAG-Eval-JA-master-vlm へ差し替え、アプリを公開し直す。
この更新をしないまま回答収集へ進むと、新しいナレッジが検索対象にならない。
VLM結果は、実行したマシン上でPDF単位の vlm_cache/ と画像単位の vlm_imgcache/ に保存する。
同じ入力で再実行するときはキャッシュを再利用できる。
ただし、これらはPDF本文や図表から派生したテキストを含むローカル成果物であり、公開配布物には含めない。
公開するのは、再生成用スクリプト、固定URL、SHA-256 manifest、設定、集計結果に限定する。
4. 300問への回答、Judge、集計を通す
LM Studioで qwen/qwen3.6-35b-a3b をロードし、Difyの生成モデルも同じモデルへ切り替える。
次の1コマンドで、qwen35B-VLMあり(r4)の回答収集、Judge、集計、成果物整理までを実行する。
./scripts/run_r1_r4_chatbot.sh r4 all --fresh
Judgeには質問、再現用評価基準CSVの正解、生成回答を渡す。
主要な事実、数値、結論が一致すればO、欠落、矛盾、数値違い、回答放棄があればXとする。
通常は temperature=0、反復ループなどでO/Xを返せない場合だけ temperature=0.3 で1回再判定する。
生成プロンプトとJudgeプロンプトはZenn第4章で公開している。
--fresh は同じrunの既存成果物を消して最初から実行する指定だ。
途中再開では外し、generate、judge、report、finalize を個別に実行できる。
5. 再検証に必要な成果物をローカルrun単位で残す
実行後のローカル results/r4/ には次が残る。
表2: run単位で保存する成果物
| 成果物 | 用途 |
|---|---|
answers.csv |
300問の生成回答、latency、token usage |
judged.csv |
O/X、判定理由、Judge token usage |
analysis.md |
score、type/domain別集計、モデル・ナレッジ条件 |
report.md |
公開用の集計出力 |
rag_evaluation_master.csv |
そのrunが参照した正解基準のスナップショット |
vlm_cache/ |
PDF単位の抽出結果スナップショット。ローカル保存のみで公開配布しない |
vlm_imgcache/ |
画像単位のVLM応答スナップショット。ローカル保存のみで公開配布しない |
dify/*.yml |
Difyアプリ設定のスナップショット。エクスポートDSL版は 0.6.0
|
スコアだけを記録しても、入力、検索条件、プロンプト、Judgeが変われば同じ実験にはならない。
runごとにローカル成果物をまとめることで、idx単位の差分確認と再集計ができる。
一方で、vlm_cache/ と vlm_imgcache/ はPDF由来の派生テキストを含むため、GitHubやHugging Faceで配布する公開リポジトリには置かない。
6. 完走確認と処理規模
上記構成の qwen35B-VLMあり(r4)は、300問すべてで回答と判定を完了し、answer_errors=0、**0.853(256/300)**だった。
r4の0.853は本記事の手順が最後まで動作したことを示す確認値であり、他システムとの順位や同等性能を主張するものではない。
O/Xは人手で確定した真値ではなく、ローカルQwen Judgeの判定だ。
qwen35B-VLMありでは回答生成とJudgeに同じモデルを使っているため、自己選好や誤りの相関を排除できない。
評価条件と結果の詳しい読み方はZenn第6章で公開している。VLM有無の比較の詳細は第5章で公開している。
処理規模の参考として、gpt20B-VLMあり(r2)では、取込後の回答収集とJudgeに合計3,831,344トークンを記録した。
表3: gpt20B-VLMあり(r2)の回答収集とJudgeで記録したトークン数
| 工程 | 入力トークン(prompt tokens) | 出力トークン(completion tokens) | 合計 |
|---|---|---|---|
| 回答収集(300問) | 2,805,137 | 328,383 | 3,133,520 |
| 判定(300問) | 212,694 | 485,130 | 697,824 |
| 合計 | 3,017,831 | 813,513 | 3,831,344 |
VLM抽出、文書の埋め込み・indexing、検索時のクエリ埋め込みは含まない。
モデルごとにトークナイザが異なるため、他社APIの課金トークンへそのまま換算できる値でもない。
この構成で揃えたかった条件
ローカルで動かすこと自体より、入力から判定までを同じ環境に置き、条件を固定して比較できることに意味がある。
- 入力は固定URLとSHA-256で固定する。
- 正解基準は再現用評価基準CSVとして変更履歴を残す。
- 取込、検索、生成、Judgeを別工程にする。
- 回答と判定をCSVで保存する。
- キャッシュとアプリ設定をローカルrunに同梱する。PDF由来の
vlm_cache/とvlm_imgcache/は公開配布しない。
これで、モデルや前処理を変えたときに、総合スコアだけでなく300問のどこが改善・悪化したかを確認できる。
VLMの有無を比較した結果の詳細はZenn第5章で公開している。生成モデル差と既存評価との接続の詳細は第6章で公開している。
業務文書のローカル/オンプレRAGの構築、検索設計、評価設計に取り組んでいる。
社内ナレッジ活用やオンプレ生成AIの実装にご関心があれば、お気軽にご相談を。
→ サカタコンサル(SAKATA Consulting) https://www.sakata-consul.com/
関連記事: RAG構成とプロンプト → Zenn第4章 / VLM比較 → Zenn第5章 / 結果と評価条件 → Zenn第6章
