地平線まで行ってくる。

記録あるいは忘備録。時には検討事項。

LLMのトークン確率分布を用いたSeed Steganography(乱数シード電子透かし)を試してみる。

LLMのトークン確率分布を用いたSeed Steganography(乱数シード電子透かし)を試してみる。

Anthropic社は出力に電子透かしを入れ込むらしい。そこで電子透かしの技術を調べてみた中でSeed Steganographyというのを見つけたので試してみました。「AIが書いたテキストの中に、人間には全く見えない秘密のシード値を数学的に忍ばせて、後から決定論的に『自分が作ったものだ』と証明できれば良い。」というお話です。

この方法は、LLMのトークン選択のサンプリング確率そのものをキャリア(伝送路)にしてしまう「 LLM Seed Steganography(乱数シード電子透かし) 」というアプローチのようで、LLMとしてQwen(/Qwen2.5-0.5B-Instruct)の小さなモデルを使って、お勉強代わりにちまちまと試します。

専門外も専門外のため、LLM頼りで書いています。間違いあるかもしれませんのでご注意を。ソースコードは末尾に記します。

※Anthropic社が採用する方法を類推や再現する目的ではありません。


検証環境および前提条件

検証には、特別な追加環境や外部有料APIを排し、手元のローカルハードウェア上で推論とシード枝刈り(Pruning)の計算を完結できる構成を整えました。 再現性を確保するための具体的な動作環境は以下の通りです。

  • ホストOS: Windows 11 Home / Pro (WSL2 Debian 12 実行環境)
  • パッケージ管理: uv (Rust製の高速パッケージマネージャーを全面的に採用)
  • 使用ライブラリ: PyTorch 2.x, Transformers 4.x
  • 使用モデル: Qwen/Qwen2.5-0.5B-Instruct (軽量で日本語・英語ともに表現力の高いモデルを使用)
  • 検証用計算資源: GeForce RTX 3060 (VRAM 12GB)
  • サンプリングパラメータ: Temperature = 0.8 / Top-p = 0.9
  • 埋め込み対象秘密シード: 987654(探索空間:(0 \le S < 2^{20} = 1,048,576) 通り)

依存関係は uv.lock で完全に同期し、クローンしたディレクトリで uv sync を実行するだけで、誰でもまったく同じデモコードを検証できるようにしています。


LLM Seed Steganography の動作原理

本アプローチは、LLM のサンプリング過程における決定論的乱数(PRNG)を活用したステガノグラフィ / ウォーターマーク研究(※ 近年の arXiv 論文等でも “Steganography Without Modification: Hidden Communication via LLM Seeds”“Pseudorandom Error-Correcting Codes” などの文脈で盛んに議論されている手法)をベースにしています。

本アルゴリズムは、LLMが次の単語を出力する際に算出する Softmax 確率分布(ロジット) をキャリアとして利用します。情報の「埋め込み(Sender)」と「復元(Receiver)」は、以下の決定論的な数理シーケンスに沿って実行されます。

1. 送信側(Sender / 電子透かしの注入)

送信側では、プロンプトに続く文章をLLMに生成させる過程で、トークンごとにサンプリング確率分布を擬似乱数によって決定論的に制御します。

  1. 送信者は秘密シード (S) をあらかじめ保持し、擬似乱数生成器(PRNG)を初期化します。
  2. 各トークン生成ステップ (t) において、決定論的なPRNG関数から、シード (S) とステップ数 (t) に基づく一様擬似乱数 (u_t = \text{PRNG}(S, t) \in [0, 1)) を取得します。
  3. LLMが直前までの文脈(Context)から計算した、次のトークンの Softmax 確率分布を取得し、これを累積した 累積分布関数(CDF: Cumulative Distribution Function) を構築します。
  4. 擬似乱数値 (u_t) が位置する確率区間に対応するトークン (w_t) をサンプリングし、これを生成テキストに追加して次のステップへ進みます。

このプロセスにおいて、サンプリングはモデル本来の確率分布(CDF)の比率に完全に従って行われるため、出力されるテキストは完全に自然な自然言語のままであり、人間の目やスタティスティカルな異常検知器を通しても、透かしが潜んでいることは判別できません。

2. 受信側(Receiver / 電子透かしの検出・復元)

受信側(検出側)は、送られてきたプレーンテキストから、探索空間の中から「この文章を生成した可能性のある唯一のシード (S)」を逆算によって決定論的に特定(復元)します。

  1. 受信者は、公開されたプロンプトと、実際に送られてきたテキスト、および全く同一のモデル (M) を用いて、ステップごとのトークンの確率分布をローカルで全く同じように再現(ロジット再計算)します。
  2. あらかじめ設定したシード探索空間(例:(0 \le S < 2^{20}) の 1,048,576 通り)の全候補シードに対して、各ステップ (t) の擬似乱数値 (u_t^{(S)} = \text{PRNG}(S, t)) を一括計算(ベクトル並列化)します。
  3. 実際に送られてきたトークン (w_t) が、候補シード (S_i) において「サンプリングされ得なかった区間(CDFの範囲外)」にある場合、そのシード候補を候補群から順次除外(枝刈り:Pruning)していきます。
  4. ステップ(トークン)を重ねるごとに、実際の観測テキストと矛盾するシード候補が指数関数的に枝刈りされ、最終的に 元の秘密シード (S) だけが 1 件のみ抽出(完全復元) されます。

実証実験とシード候補の枝刈りログ

実際に、Qwen2.5-0.5B-Instruct モデルを用い、プロンプト「Please write a concise introduction to quantum computing.」に対して秘密シード 987654 を埋め込んでテキストを生成させ、受信側で100万通りの探索空間から元のシードが収束・復元されるまでの挙動を定量的ログから評価しました。

送信側で生成された「透かし入り」文章

生成された英文は以下の通りです。

"Quantum computing is a type of computation that uses quantum-mechanical phenomena, such as superposition and entanglement to perform operations on data. Unlike classical computers which use bits (0 or 1) stored in discrete locations called qubits where the state can only be either "up" (+1 for example), it takes advantage of this phenomenon by storing one bit at any time instead..."

量子コンピュータの動作原理に関する、ぱっと見て極めて流暢で自然な解説文が出力されています。文法的な不自然さや不規則な単語置換などの不純物は一切見受けられず、人間が読んでも、あるいは機械的な校正フィルターを通しても、通常のAI生成テキストと区別がつきません。

受信側におけるシード候補の枝刈り推移(定量的データ)

この生成テキスト(55トークン)を入力し、受信側で全候補シード (2^{20})(約105万通り)の並列ベクトル枝刈りを実行した際の、トークン消費数に対する生存シード候補数の減少推移を測定しました。

生成トークン数 (t) 生存しているシード候補数 (件) 探索空間の絞り込み率 (%) 復元プロセスとログの観察挙動
0 1,048,576 100.00% 初期探索空間((2^{20}))の全候補をバッファに展開して評価開始。
5 262,144 25.00% 最初の数単語の確率分布(CDF)の制約により、早くも約75%の候補が矛盾として淘汰。
15 32,768 3.12% 英文1文目を消化した時点で、生存候補は3万件台までシャープに絞り込み。
30 1,024 0.09% 2文目の途中に達すると、生存率は0.1%を切り、候補空間の大部分が大破。
45 8 0.0007% 3文目の終わり近く。生存候補はわずか1桁(8件)となり、シード特定が王手。
55 1 0.00009% 元の秘密シード「987654」が100%の精度で完全復元 されて収束。

定量データが示す通り、わずか 55トークン(英文で約3〜4文) という極めて短いプレーンテキストのみから、100万通り以上の候補空間を瞬時に走破し、元の秘密シードを誤り率0.0%で正確に特定することに成功しました。


技術的考察

1. 従来の手法(ゼロ幅文字・同義語置換)と Seed Steganography の比較

本手法の技術的な位置づけを客観的に評価するため、一般的にテキスト透かしとして知られる従来のアプローチとの違いを表に整理しました。

評価項目 従来の手法(ゼロ幅文字の挿入) 従来の手法(同義語置換) LLM Seed Steganography
テキストの不可視性 高(ただしエディタで赤字や空白として可視化される場合あり) 低(稀に不自然な文脈の単語に置換され、文章の自然さが犠牲になる) 極限(普通の自然な文章そのものになるので、全く分かりません)
コピペ・プレーン化への耐久性 ゼロ(プレーンテキスト化やSNS投稿でゼロ幅文字は完全に消失) 中(校正ツールでのスペルチェックや言い換えにより容易に書き換わる) 極強(文字がそのままコピペされていれば、どこへ持っていっても復元できます)
セキュリティレベル 極めて脆弱(ルールや文字パターンが知られると即座に剥離可能) 低(置換辞書が漏洩すると検知・解読が容易) 強固(ベースモデル、プロンプト、PRNG関数の3つが揃わないと絶対に解読できません)
復元時の計算コスト ほぼ皆無(文字コードスキャンのみ) 極めて軽量(辞書マッピングのみ) 中〜高(受信側で、1文字ずつモデルの確率分布を計算し直すのでちょっと重たいです)

2. 文章の一部のみを制御する「部分埋め込み」の実用性

長文のテキスト全体をシードによって100%拘束してしまうと、LLM本来の表現力が擬似乱数によって制限され、サンプリング確率の極めて低い「不自然な単語(Top-pやTemperatureの境界近くのトークン)」が選択される割合が累積し、文章全体の自然さが徐々に損なわれる懸念が生じます。

この課題に対して、本実装で試みている「トリガーマーカー(Sync Marker)による部分埋め込み」は非常に実用的なアプローチです。 例えば、文章の導入部は完全にモデル本来の自然な推論に委ね、特定のトリガーキーワード(例:However,In conclusion, 等)の出現を検知した直後の (N) トークン(30〜50トークン程度)のみをシード制御に切り替える設計が可能です。

この「部分制御」を導入することで得られる恩恵は大きく、以下の運用が現実的になります。 * 表現力の最大化: 透かしの埋め込み範囲を最小限(数十トークン)に抑えることで、長文全体の流暢さを100%維持。 * マルチメタデータの埋め込み: 第1段落(ユーザー識別シード)、第2段落(生成日時シード)、第3段落(ライセンス種別シード)のように、トリガーマーカーごとに異なる秘密シードを段階的にマウントする「マルチシード・メタデータ構造」の定義。

3. 復元コストと実用上のトレードオフ

一方で、本手法を実運用にマウントする際の「実用上の境界(ペイン)」も冷静に開示しておく必要があります。

受信側でシードを復元するためには、テキストの各ステップにおいて、全く同じプロンプトと文脈を用いてモデル (M) の Softmax ロジット確率分布を再計算し続けなければなりません。 これは、テキストを受け取ったクライアント側において、「埋め込みに使用されたものと同一のローカル推論モデル(またはそのAPIへの接続)」と、100万通り以上の擬似乱数ベクトルを一括で枝刈り評価できるGPUの処理リソースが必要となる ことを意味します。しかし、意外と計算自体は軽量だな、と感じました。

リアルタイム通信の対話ログから「つぎつぎに」その場で透かしを復元・解読するには、実用的じゃないかもしれません。しかし、トリガーマーカーとパラメータを事前に交わしておけば、意外と実用的かもしれません。秘密キーくらいの長さの情報は全然OKでしょう。

通信で電脳汚染なんて、出来ちゃうかもしれません。


結論と今後の展望

この Seed Steganography による「ユーザーIDやライセンス識別子」をメタデータとして密かに埋め込み、「自分自身の手元で生成された知的成果物であることを、テキストに埋め込められる仕組み」 を、個人レベルでもできそうな計算量です。Anthropic社がどのような方法をとるのか分かりませんが、流れゆくテキストの中で暗号通信ができるというのはワクワクしました。


📋 参考文献・関連リサーチ

本記事で扱った原理および関連する言語モデルステガノグラフィ / 電子透かしの代表的な研究文献を調べています。専門外なので眺めただけですが、非常に興味深い研究です。 LLMで生成したまとめも添えて紹介しておきます。間違いあったらごめんなさい。

  1. Steganography Without Modification: Hidden Communication via LLM Seeds (arXiv)

    — LLM の重みやサンプリングアルゴリズムを変更せず、PRNG シード空間の逆算探索によって秘密通信を実現する基本原理を論じた研究。

  2. Pseudorandom Error-Correcting Codes (arXiv:2402.09370)

    — 擬似乱数符号を用いた不可視な電子透かし(Undetectable Watermarking)とステガノグラフィの暗号理論的基礎。

  3. A Watermark for Large Language Models (Kirchenbauer et al., ICML 2023)

    — ロジットのグリーンリスト/レッドリスト分割に基づく、LLM 生成テキストへの電子透かし埋め込み・統計的検出手法のパイオニア論文。


gist.github.com

Ruri v3とVisualized-BGEを組み合わせた日本語埋め込み&Rerank APIサーバーを確保してみる。

Ruri v3とVisualized-BGEを組み合わせた日本語埋め込み&Rerank APIサーバーを確保してみる。

なんやかやでembeddingは頻繁に利用します。そこで、エッジ環境でも軽快に動作するローカルの埋め込み(Embedding)および再ランキング(Rerank)APIサーバーを自前で確保」するために作成したembeggin-jp-apiに対して、さらにマルチモーダルのVislaized-BGEを加えました。前回の記述と被りますが、改めてメモとしてまとめました。

docsにOKF形式でドキュメントをまとめています。この形は後で利用もしやすそうです。


検証環境とシステム構成

今回の動作確認および定量的パフォーマンステストは、以下のハードウェアおよびソフトウェア環境にて実施しました [7, 30]。

  • ホストOS: Windows 11 Home / WSL2
  • 実行環境: Python 3.12+ ( パッケージ管理および依存同期には全面的な信頼を置いている uv を採用 )
  • 計算資源: Intel CPU 環境、および単一の NVIDIA GeForce RTX 3060 (VRAM 12GB)
  • 日本語埋め込みモデル: 名古屋大学が開発した Ruri v3 シリーズ( cl-nagoya/ruri-v3-310m および ruri-v3-30m
  • マルチモーダルモデル: bge-visualized-m3 (画像およびテキストの混在入力に対応)
  • 日本語Rerankモデル: cl-nagoya/ruri-v3-reranker-310m

主要なパース規約とAPI仕様

本サーバーは、FastAPI を基盤とした軽量な OpenAI 互換の HTTP サーバーであり、テキスト埋め込み、画像+テキストの複合埋め込み、および Rerank エンドポイントを提供します 。

主な技術仕様と処理の工夫は以下の通りです。

1. Ruri v3 の性能を最大化する日本語プレフィックスの自動補完

名古屋大学の Ruri v3 シリーズは、非対称検索やクラスタリングなど、タスクの性質に応じて入力文の先頭に特定のプレフィックス(指示文)を付与することで、検索性能が劇的に向上する設計となっています [7]。 本APIでは、リクエストの input_type パラメータに基づき、以下の日本語プレフィックスを自動的かつ非破壊でテキストの先頭にマッピングする機構を実装しています 。

  • query: "検索クエリ: " (非対称検索における質問側テキスト)
  • document: "検索文書: " (ナレッジベースやWikiなどのドキュメント側)
  • classification / clustering: "トピック: " (分類およびクラスタリング用)

入力テキストがすでにこれらの文字列で開始されている場合は二重付与を防ぎ、最大長(8,192トークン)を超える場合はプレフィックスを優先的に保持した上で後方を決定論的に切り詰める(Truncation)処理規約を設けています。

2. 画像+テキストに対応したマルチモーダル埋め込み(bge-visualized-m3)

今回は、図面やチャートを埋め込みベクトル化するため、bge-visualized-m3 モデルを用いたマルチモーダルエンドポイントをあらたに設けました。入力形式は、画像とテキストを並列で受け取る「フラット形式」に加え、OpenAI Chat API 互換の「コンテンツパーツ配列(Content Parts)形式」の双方に対応させています。

/* OpenAI 互換コンテンツパーツ形式のリクエスト例 */
{
  "model": "bge-visualized-m3",
  "input": [
    {"type": "text", "text": "青い服を着た人物"},
    {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KG..."}}
  ]
}

個人用の検証スタックとはいえ、セキュリティも必要最低限は手当てしています。防御というよりポカよけ? * SSRF(サーバサイドリクエストフォージェリ)防御: 画像URLの読み込み時、リダイレクト追従を含めてプライベートIP(127.0.0.110.x192.168.x)やクラウドのメタデータエンドポイント(169.254.169.254)へのアクセスを検知した場合は、即座に HTTP 400 で接続を遮断します 。 * DoS防御: 画像デコード時のメモリ爆弾を防御するピクセル数上限制限(MAX_IMAGE_PIXELS = 20,000,000)およびファイルサイズ制限(15MB)を適用。 * 不一致ガード: 画像入力を誤ってテキスト専用モデル(Ruri-v3等)に送信した際、内部エラー(500)でプロセスを崩壊させないよう、HTTP 400 で弾くガードレールを設けています 。


技術的考察:ローカル運用における「スレッド安全」と「高速化」の葛藤

実際にマルチワーカー(Gunicorn)環境や並行アクセス下において本サーバーを動作させると、いくつかの現実的なエンジニアリングの課題とその解決プロセスが見えてきました。

1. すべてのワーカーが安全に稼働するための「二重ロック設計」

Python で FastAPI + Hugging Face の transformers ライブラリ(特に Rust 製の高速トークナイザー)を組み合わせる際、複数の非同期スレッドやワーカーから並行してリクエストが流入すると、トークナイザーの内部参照状態が競合し、Already borrowed エラーが発生してAPIプロセスが500エラーを吐いてクラッシュする現象に遭遇しました。大量に処理中に発生するとメンドイです。

この対策として、モデル推論(model.encode/model.predict)を実行するための threading.Lockmodel.lock )による排他制御に加えて、個別のトークンカウント時に使用する model.tokenizer_lock を用意し、「トークナイズから推論処理の完了まで、一貫して二重にスレッドロックを保持し続ける」 という設計規約を採用しました。 これにより、100同接負荷テスト下においてもクラッシュを完全に排除し、エラー率 0.0% の安定稼働ができたようです。

2. Text Embeddings Inference (TEI) 統合とプロキシオフロード

CPU や単一の RTX 3060 環境において、FastAPI の同期エンドポイントとスレッドプールを用いて並行スループットを維持するアプローチは、小規模な利用であれば十分に実用的です。しかし、検索やRAGのソースドキュメントが数万件規模に達してくると、Python の GIL(グローバルインタプリタロック)制約が原因となり、行列演算中にどうしてもプロセス全体がブロックされ、応答時間に頭打ちが発生します。

この改善方法として、Hugging Face 社の Rust 製推論サーバー Text Embeddings Inference (TEI) へのプロキシ(ゲートウェイ)機能をサポートしました 。FastAPI を認証やPII(個人情報)マスク、セキュリティヘッダーを司るセキュアな前面プロキシとして機能させ、実際の高負荷なベクトル演算処理のみをバックエンドの TEI コンテナへ非同期にオフロード(丸投げ)する構成です。TEIありがたや。感謝です。

同時10ユーザーによる Locust 負荷テスト時に測定された、ローカルPython直接推論(sentence-transformers)と TEI オフロード構成との定量的パフォーマンス比較は以下の通りとなりました 。環境依存ですからざっくりなイメージです。

評価項目 ローカル Python 直接推論 TEI プロキシオフロード 改善効果(性能改善率)
全体平均応答時間 1,117 ms 21 ms 約 53 倍高速化
埋め込み(Embeddings)応答時間 1,219 ms 17 ms 約 71 倍高速化
再ランキング(Rerank)応答時間 810 ms 32 ms 約 25 倍高速化
最大処理スループット 2.42 req/s 3.38 req/s +39.6% 向上

測定結果の通り、TEI プロキシを背後に据えることで、埋め込み処理で 約71倍 、Rerankで 約25倍 というレイテンシ削減を達成しています。

3. GPUのないCPU環境であっても TEI 統合を導入する技術的意義

「GPUを積んでいないオフィスPCや、非力なエッジCPU環境では TEI は無用の長物か」というと、全くそんなことはありません。 Rust で書かれた TEI は、CPU動作時であっても Python の最大の足枷である GIL(グローバルインタプリタロック)を完全に迂回 できます。さらに、リクエストが重なった瞬間にそれらを自動的に1バッチにまとめて処理する Dynamic Batching(動的バッチング) がCPU上で動作するため、多重アクセス時の急激なレスポンス遅延(スパイク)を抑制できます。 PyTorch を含む Python プロセスをワーカー数分だけ立ち上げるのに比べ、ホストサーバーのメモリ消費フットプリントを極限まで低く抑えられるという、導入メリットを享受できているようです。


結論と今後

Ruri v3 と Visualized-BGE を FastAPI および TEI プロキシ経由で統合したことで、エッジ環境でも十分実用に耐えうる日本語埋め込み・Rerank 基盤が整備できました 。最近は、Rerankあまり使わなくなりましたけれども、出番はまだありそう・・・ですよね?

Doclingベースでのドキュメントのレイアウト解析を使って図面を含むマルチモーダルなナレッジの前処理検証を進めつつ、コンテキストに応じたバッチサイズの最適化などをちまちまと進めていきたいと考えています 。前処理がすべて。何事も基本が大事。


github.com

引き続きbugとりもちまちまと。

e-Gov法令XMLをOKF準拠のMarkdownにパースしてSimpleWikiで試してみる。

「Windows 11の標準環境をできるだけ汚さない」という制約のもと、Windows PowerShell 5.1 と .NET の低レベルAPIだけで動作する完全オフラインの個人Wiki & RAGチャット基盤である「 SimpleWiki 」を作りました。さくっとMarkdownをwikiとして閲覧できます。

 

bwgift.hatenadiary.jp

 

github.com

 

Windows OS に標準で搭載されている機能(Windows DPAPI による鍵管理や、WinRT 形態素解析など)を利用することで、シンプルにスタンドアロンの仕組みが作れるという気付きは、PowerShellに対して食わず嫌いをしていた自分にとって非常に嬉しいものでした 。

 

この手作りのWiki基盤が手元で動くようになると、今度は「こののんびりした環境に、日本の法令データ(e-Gov法令XML)のような、日本語表現において最もネストが深く、独自の構造化規則(条・項・号)を持った公的ドキュメントを一括投入したらどうなるのだろう」という、新たな技術的好奇心が湧いてきます。

 

そこで、e-Govポータルが配布する法令標準XMLスキーマv3適合のXMLデータを、LLM/RAGおよびSimpleWikiなどのWebビューワーに最適化された Open Knowledge Format (OKF) v0.1 準拠のMarkdownファイル群へと決定論的に一括変換するCLIツール「 law3markdown 」を急ぎ作成してみました 。なお、今回は「労働法」のみを利用します。全部の法律となるとテキスト展開すると巨大なのでターゲットを絞りました。

 

特別なデータベースの構成も、高機能なPython Webサーバーも使わずに、手元で「自分だけのポータブルな法令Wiki」をのんびり動かしてみた記録です。

---

検証環境とちいさなツール

*   ホストOS: Windows 11 / WSL2
*   実行環境: Python 3.12+ ( `uv` による再現可能な最小構成 ) 
*   マウント先のWiki: SimpleWiki ( Windows PowerShell 5.1 / .NET 4.8 ) 
*   対話用LLMサーバー: SAKURA AI API 経由で接続する「 GPT-OSS 120B 」(OpenAI互換API)
*   対象ドキュメント: 労働基準法や会社法などの e-Gov 法令XMLデータ

 

```bash
# リポジトリの同期
git clone https://github.com/chottokun/law3markdown.git
cd law3markdown
uv sync
```

---

law3markdown の設計思想

e-Govが提供する法令XMLは、記述の正確性を担保するため、非常に厳密なツリー構造を持っています。しかし、これをそのまま単一のMarkdownファイルとして結合してWikiに放り込むだけでは、RAGや個人Wikiの検索において大きな不都合が生じます 。

`law3markdown` は、外部の重いライブラリを必要とせず、Pythonの標準XMLライブラリ(`xml.etree.ElementTree` 等)を用いたパース処理により、SimpleWikiが最も読み込みやすい「OKFに適合したフラットなMarkdown群」を出力するようにしました。

主なパース規約は以下の3点に絞り込んでいます 。

1.  完全ASIS(原本維持)原則とルビの非破壊親文字抽出 
    法令本文の文言や助詞は一字一句たりとも改変しません 。ただし、`<Ruby>` タグで囲まれた「ふりがな(ルビ)」は、テキストの単純結合時に日本語の文脈を著しく歪めるため、非破壊で親文字(原本テキスト)のみを抽出し、ルビテキストのみを賢く除去するようにしています 。
2.  1条文1Markdownとしてのプレスプリット(事前分割)
    法律全体を1つの長大なテキストとして扱うのではなく、本則の「第一条」「第二条」などの条文単位、および「附則(制定時・改正時)」や「別表」といった論理ブロックごとに、物理的に独立したMarkdownファイル(数KB程度)へ事前に切り分けます 。
3.  OKF v0.1 完全準拠と目次の自動インデックス化
    パースされたすべてのファイル(条文や附則、付録など)の先頭に、`type`、`title`、`sources` を含む YAML Front Matter (OKF仕様)を完全に自動付与します 。さらに、複数条文指定時などにファイル名に使われるコロン( `:` )などのOS禁止文字をサニタイズ( `art_436-448.md` 等へ変換 )し、クロスプラットフォームにおけるインデックスの整合性を担保します。

出力されたMarkdown群は、以下のようなツリーとしてフォルダにエクスポートされ、そのままSimpleWikiの公開ディレクトリへマウントできるようになります。

```markdown
output/
├── index.md                      # 全法令の自動分類ポータル目次
└── 労働基準法/
    ├── index.md                  # 法令トップ目次(条文・附則へのナビゲーション)
    ├── articles/                 # 条文フォルダ(1条文=1ファイル)
    │   ├── index.md              # 条文サブインデックス
    │   ├── art_001_第一条.md
    │   └── art_436-448_...md     # サニタイズされた条文
    ├── suppl/                    # 附則フォルダ
    │   ├── index.md              # 附則サブインデックス
    │   ├── suppl_main.md         # 制定時附則
    │   └── suppl_amendments.md   # 改正附則の一覧集約
    └── appendix/                 # 付録フォルダ
```

---

動作画面:

 

技術的考察:シンプルな疎検索だからこそ、前処理が活きてくる

今回、この仕組みを動かして最も興味深く感じたのは、「RAGの検索システムがシンプルであればあるほど、データパース段階での丁寧な前処理(ルビの除去など)が劇的な効果を発揮する」という事実でした。

SimpleWikiの検索チャット機能は、近年流行りの重厚なベクトルデータベースや埋め込み(Embedding)モデルを使用していません 。Windows 11に標準内蔵されている `Windows.Data.Text.WordsSegmenter` を利用した単語の分かち書きと、OKFメタデータの属性、およびシンプルなキーワード一致(AND)によるレキシカル(疎)検索のみで動いています 。

一般的なXMLパーサーで単純にテキスト抽出を行うと、`<Ruby>労働<Rt>ろうどう</Rt></Ruby>` は `労働(ろうどう)` または `労働ろうどう` としてデコードされてしまいます 。
この状態のテキストを `WordsSegmenter` に通すと、辞書に基づく形態素解析エンジンは「労働」という一塊のトークンではなく、括弧やルビのノイズに引っ張られて `労働`・`(`・`ろうどう`・`)` などのように不自然にぶつ切りに誤分割してしまいます。
このため、ユーザーが「労働基準法」と完全一致で検索しても、インデックス側の分割トークンと合致せず、文字としてはそこに書いてあるのに検索の網をすり抜けて漏れてしまうという、致命的な「検索精度の低下」が多発していました 。

`law3markdown` がパースの段階で `<Ruby>` タグをトラバースし、ルビ部分の `<Rt>` のみを非破壊で除去して「親文字(原本テキスト)」だけを綺麗にMarkdownに残す処理は、このOS標準の形態素解析エンジンに対する、最もシンプルで強力なノイズカット(前処理)として機能します 。
結果として、分かき書きの分割ノイズが皆無になり、OS標準機能だけを用いた軽量なキーワード検索であっても、ヒットするようになりました。

もうひとつ試みたのは、「条文単位でのプレスプリット(事前分割)」と「OKFによるYAMLメタデータの付与」がもたらす情報設計です。ただし、あんまり細切れにしちゃうと意味不明になるため附則などはまとめるように設計します。

今回検証のLLMとして使用した「 GPT-OSS 120B 」を外部API経由で使用する際、法律全体の膨大な全テキスト(数万〜数十万トークン)をそのまま送り込もうとすると、APIサーバー側の1リクエストあたりの送信制限に引っかかるだけでなく、往復の遅延やトークンの無駄遣いに繋がってしまいます 。

ここで `law3markdown` がパースの時点で、単に物理的に条文を細かく切り分ける(数KBにする)だけでなく、各ファイル(条文)に OKF仕様の YAML Front Matter(公布日、施行日、未施行フラグ、改正情報など)を丁寧に自動付加している点 が、非常に大きな意味を持ってきます。

具体的には、以下の2つの大きなメリット(恩恵)を実感しました。

1.  「文脈情報の高度な肉付け(Context Enrichment)」による回答精度の担保 

条文を1つずつ細切れにスプリットしてLLMに渡してしまうと、LLM側で「これは何の法律の第何条なのか」「現行法なのか、それとも未施行の条文なのか」という、法令解釈における死活的に重要な背景文脈(コンテキスト)が喪失してしまいます。
    YAML Front Matterに `title_kana` や `promulgate_date`(公布日)、`enforce_date`(施行日)、そして `is_unexecuted`(未施行フラグ)などのメタデータ属性が綺麗にマッピングされていることで、LLMは条文の数KBという軽量性を享受しながらも、「この条文は現在有効な労働基準法第一条である」という背景知識を把握した上で、正確な解釈を回答できるようになります。
2.  「事前メタデータフィルタリング」による検索精度の補完とノイズ削減 
    SimpleWikiはベクトル検索を持たないシンプルなレキシカル(AND)検索です。そのため、ただテキスト全体を検索対象にすると、古い改正前の附則や、未施行の条文(`is_unexecuted=true`)などが検索ノイズとしてヒットし、LLMに不適切なコンテキストを引き渡してしまうリスクがあります 。仮に引き渡してしまってもLLM側でも、これは未施行の条文だなと解釈してくれます。
    もちろん、OKFによって各ファイルにステータス(未施行タグなど)や公布日情報がYAMLとして埋め込まれているため、全文検索に投げる前に、SimpleWiki側で「未施行条文や1年以上更新のない風化ドキュメント(/maintenance)を事前に除外・分類する」というメタデータ事前フィルタリングを効かせることができます。これにより、検索結果の段階でノイズが極限までカットされ、LLMへ送信するトークン容量の節約と同時に、回答の論理適合率(Precision)を爆発的に高めることが可能になりました。

これにより、GPT-OSS 120Bの標準?的な性能でも回答精度が保てるだろうと考えました。単なる「テキストのサイズ最小化」ではなく、OKFという「構造化メタデータの丁寧な付与」という前処理は、有効だと感じています。

---

結論と今後の展望

重厚なインフラやプロプライエタリなコンポーネントを一切インストールせず、Windowsに最初から入っているPowerShell 5.1と、手作りの「SimpleWiki」、そして今回作成した「law3markdown」を組み合わせるだけで、自分専用のポータブルな「e-Gov法令wiki」が出来上がってしまいました。

自分が日頃からObsidianのノートを開いてアイデアや検証ログを書き溜めているように、日本の法令データという最も厳格に構造化された公的ドキュメント資源が、フィルターを一枚通すだけで、そのまま「OKF / WikiLinks」という使い慣れたMarkdownの知識網へと再構築まで進めました。

 

github.com

 

Wikilinkを入れるのがベターなのかもしれませんが、法律だけに構造はしっかりしているので、このままでも調べやすいのかも。

 

オフラインで動くPowerShellでMarkdown(OKF)でWiki&RAGを試してみる。

オフラインで動くPowerShellでMarkdown(OKF)でWiki&RAGを試してみる。

エンタープライズの閉域網や、セキュリティ上の制約から外部ネットワークとの接続が一切禁止されたオフライン環境において、ドキュメントの共有やナレッジの統合管理、さらにはLLM(大規模言語モデル)を活用したRAG(検索拡張生成)システムが手軽に運用できれば簡単なナレッジ利用ができます。一般的にRAGやWebベースのWikiを構築しようとすると、Node.jsやPython環境の構築、無数の依存ライブラリ(npmやpip)のインストール、Qdrantなどの外部ベクトルデータベースの起動など、数多くのコンポーネント管理が必要となります。Docker利用も予めDockerのセットアップが必要です。管理者権限のないローカルPCで手軽に利用するには導入のハードルが上がります。

こうした閉域環境における「Windows 11環境をできるだけ汚さない」という制約の中で、MarkdownドキュメントのWebブラウズ、静的HTML一括デプロイ、そしてローカルLLMと連携した完全オフラインのRAGチャット基盤を、Windows標準機能レベルだけで完結させるためのアプローチについて検証を試みました。

Windowsと決めてしまうとPowershellはWindowsの持つ機能を積極的に利用できる利点があります。例えばWindows DPAPIであれば、Windowsに鍵管理を任せて自前で暗号化を実装する必要がありません。と、ベタベタのOS依存だとWindowsは有難いと思ってしまいました・・・。食わず嫌いでした。

検証環境および前提条件

Windows 11の標準Powershell5.1の利用を想定しています。 検証システムの構成および前提となるコンポーネント群は以下の通りです 。

  • ホストOS: Windows 11
  • シェル環境: Windows PowerShell 5.1(プリインストール標準版) / PowerShell 7+
  • ランタイム環境: .NET Framework 4.8(OS標準同梱)
  • ローカルLLMサーバー: OpenAI互換LLMサーバ (Ollamaなど)、もちろん、互換サーバであれば良い。開発段階ではSAKURA AIのapi(GPT-OSS)を利用しました。

これで、「スタンドアロン」でもmarkdownベースのWikiが利用できるようにトライしました。

システムアーキテクチャと主要コンポーネント

システムは、追加のアセンブリや外部のWebサーバー(IISやNginx)の構成なしに、.NET Frameworkが提供する低レベルAPIをPowerShellスクリプトから直接ハンドリングして制御します。主要なサブシステムおよびファイルの構成は以下のように整理されます 。

コンポーネント / スクリプト 役割(機能) 技術的アプローチ / 依存関係
Start-MarkdigWiki.ps1 リアルタイム閲覧 & RAG AI チャットサーバー System.Net.HttpListener による軽量ローカルWebサーバー(デフォルト:http://localhost:8080/)。
Export-MarkdigWiki.ps1 静的 HTML エキスポートエンジン Markdownドキュメントを一括でパースし、ナビゲーション付きの静的HTMLとして出力。
Export-GUI.ps1 GUI フロントエンドラッパー Windows標準のフォームオブジェクトを呼び出し、ドラッグ&ドロップやフォルダブラウズ経由での変換を簡易化。
Set-ApiKey.ps1 APIキー暗号化保護ユーティリティ Windows DPAPI(Data Protection API)またはポータブルなAES-256暗号化を用い、config.json内のキーをセキュアに保護。
lib/Markdig.dll Markdownパース・レンダリング .NET Framework 4.6.2向けにビルドした軽量な Markdig DLLを同梱。GFM(GitHub Flavored Markdown)、テーブル、YAML Front Matter等に対応。
lib/mermaid.min.js オフライン図形・Mermaid描画 CDNを介さず、完全ローカルで動作するMermaid.jsパーサーを静的統合。

ログ解析と技術的考察

1. WinRT 形態素解析による「完全依存関係ゼロ」のRAG実装

RAGシステムをローカルで完結させる際、日本語テキストにおける最大の問題は「単語の分かち書き(形態素解析)」にあります 。PythonなどであればMeCabやSudachiといった重いC言語バインディングをインストールするのが常套手段ですが、今回の趣旨に反しますし、閉域網環境下ではインストールどころかコンパイルすら不可能です。

本システムでは、Windows 10/11がOSカーネル内部の日本語入力(IME)などで使用しているUWP/WinRT標準 APIである Windows.Data.Text.WordsSegmenter をPowerShellから直接インスタンス化します。こんなものがあったんですね。

# Get-JapaneseWordsWinRT 内部実装のコアアイデア
$Segmenter = [Windows.Data.Text.WordsSegmenter, Windows.Data.Text, ContentType=WindowsRuntime]::new("ja-JP")
$Tokens = $Segmenter.GetTokens($InputText)

このアプローチにより、Windows11に頼って日本語をトークン単位に分解し、不要な助詞や助動詞をストップワードとして自動フィルタリングできます。これで、楽々な日本語キーワード検索の実装が見えてきました。

2. ASTベースの RAG チャンク生成と API 連携

本サーバーは、AIエージェントや外部LLMクライアント(MCPクライアント含む)が機械的に読み込みを行えるようにするための2つの構造化JSONエンドポイントを自動構築します。

  • /api/index.json: Wiki全体のメタデータ、YAML Front Matter、タグ情報の一覧。 [4, 8, 11]
  • /api/chunks.json: Markdig ASTパーサーを介して抽出された、各MarkdownドキュメントのH2見出しごとの論理セクション(チャンク)情報。

RAGシステムにおける過度なテキスト情報の読み込みは、エッジ環境における最も一般的な課題の一つです。本システムでは、ドキュメント全体を一つの長大なコンテキストとして読み込ませるのではなく、/api/chunks.json を通して「セマンティックに切り分けられた最小限の関連見出しブロック」のみをLLM側に引き渡します。これにより、推論時に最小限の情報を利用できるようにしています。

3. Google OKF (Open Knowledge Format) 思想の統合と RAG への波及効果

Markdownを単なる文書としてブラウズするだけでなく、AIエージェントやRAGシステムが自律的にコンテキストを理解・フィルタリングできるようにする上で、Google OKF (Open Knowledge Format) への準拠が大きな役割を果たします 。

本システムでは、YAML Front Matterからタイトル、作成日、更新日、タグ、著者、ドキュメントのステータス(下書きや非推奨)などのメタデータ属性をパースし、たとえそれらのフロントマッターが欠落していてもファイル名やファイルの作成日付からフォールバック自動補完してWiki上の「OKF メタデータカード」として動的に自動描画します 。

Powershellだけで強力な検索システムの構築は難易度高い。そんな環境でも、OKFによるデータの構造化は、RAGにおいても頑張ってくれそうです。:

  1. ハイブリッドメタデータフィルタリングによるノイズ削減: タグや「更新から365日以上が経過した風化ドキュメント」を事前に自動分類しているため、AIエージェントやRAGが全文検索を実行する前にメタデータで対象ドキュメントのフィルタリングをできるようにしています。
  2. コンテキスト精度の向上: メタデータ(タグやステータス)に基づく事前フィルタリングを行うことで、全文検索の検索範囲を適切に絞り込みます。キーワード検索を補完します。

4. セキュリティ保護対策

OSにべったりとは言え、LocalでHttpサーバを構築します。Windows APIを叩く簡易サーバーや、共有フォルダの自動ビルドシステムで軽視されがちなのが、ディレクトリトラバーサル攻撃とXSS(クロスサイトスクリプティング)の危険性でしょうか。その点は手当てを行います。

  • パスの絶対正規化: ユーザーがアクセスを要求したファイルパスを [System.IO.Path]::GetFullPath を通して評価し、指定されたルートディレクトリ(RootFolder)から外れる「..」などの文字を含むパスを即座に「403 Forbidden」で遮断。
  • XSSの無効化: YAML Front Matter から動的にパースされたタイトルやメタデータ、検索窓のUTF-8入力パラメータ、404ハンドリングにおける表示パスなど、すべてのクライアントサイドレンダリング文字列に対して [System.Net.WebUtility]::HtmlEncode による強制サニタイズを実施。

結論と今後

Windows環境において「OS標準機能のみを使用し、完全閉域網で動作するWiki&RAG基盤を構築する」という制約のもとで実装を行いました。その結果、PowerShellからWinRT APIや.NETの低レベルHTTPリスナーを直接バインドする構成が、思いのほか実用的でした。

小規模なグループ共有や、手元にあるmarkdownのObsidianのフォルダをその場ですぐに共有可能なナレッジハブへ変貌させられます。変換したhtmlファイルをそのままWebサーバーに投入できるという利点もあります。

2つのモードのRAG(Naive RAGおよびAgent型RAG)で試してみてもお手軽に利用できそうです。後はmarkdownのドキュメント群を整備すればよい。LLM-wikiの運用とすれば、外付けてmarkdownを更新するような形でしょうか。そうだとすると、ログの整備まではやるべきでしょう。

OpenGENAI SSL対応とLiteLLM R-Proxyまで分離してみた。

はじめに

自治体向け行政AI「源内 (GenAI)」を利用したOpenGENAIを改造していく勝手プロジェクト open-genai のカスタマイズ記事の続きです。

セキュリティ向上(SSL/TLS化)モデル接続基盤のLiteLLM Proxy統合、さらに画像生成(Stable Diffusion)の標準化まで実施。Bugは沢山ありそうな気がしていますが、それはぼちぼち。

本リポジトリは、川口ひろあき氏によって開発・公開されているオープンソースプロジェクト「OpenGENAI (GenAI / 源内)」のコードベースとアーキテクチャ基盤を利用・拡張させていただいております。なにはともあれ、感謝を申し上げます。


1. Nginxリバースプロキシ方式による「ワンタッチSSL化」

閉域網や検証環境であっても、SAML 2.0 / OIDC 連携を行う上では HTTPS 化(SSL終端)がほぼ必須です。

手軽にHTTPSへ移行できるように自動化スクリプト(scripts/setup-rproxy-ssl.sh)を整備し、Nginxリバースプロキシ経由で一発で適用できるようにしました。

実施したこと

  • 自己署名証明書の自動生成: スクリプトを実行するだけでドメインに応じた証明書(fullchain.pem / privkey.pem)を生成。オレオレ証明でテストをして、閉空間で通用する証明書に置き換ですね。
  • NginxでのSSL終端: ポート 443 を受領し、内部の各マイクロサービス(Web UI, Backend API, Keycloak 等)へ動的に転送。
  • プロキシヘッダーの自動解決: SAML/OIDCのリダイレクト(ACS/SLS)でリダイレクトループやHTTP/HTTPSの不一致が起きないよう、X-Forwarded-ProtoX-Forwarded-Port を正しく注入。
# ドメイン名を指定してワンタッチで HTTPS セットアップ
bash scripts/setup-rproxy-ssl.sh your-domain.local

これで、KeycloakのSAML認証もHTTPS環境下で安全かつスムーズに動くようになりました。


2. LLM / 画像生成を LiteLLM Proxy へ一本化

マルチモデル対応(Gemini, OpenAI, Ollama 等)をバックエンドで個別に記述するとコードが複雑化してしまいます。そこで、すべてのモデル呼び出しを LiteLLM Proxy 経由に集約しました。

構成のポイント

  • 統一インターフェース: API側はすべてOpenAI互換の形式でLiteLLMへリクエストを送信。
  • 画像生成(Image Gen)のLiteLLM統合: テキスト生成だけでなく、画像生成処理もLiteLLMプロキシ経由へ一元化・疎結合化。
  • 環境変数による柔軟な制御: .envlitellm_config.yaml を書き換えるだけで、ローカルLLMとクラウドLLMの切り替えが容易に。

3. 残課題

  • テスト時の設定がUIに残っている。LLMや画像生成のモデルはそれぞれの環境に合わせて変更が必要。
  • 前項対応のためにLLMの設置も完全にLiteLLMに持たせ、LiteLLMから有効なモデルを収得してUI側への反映

3. 今回の構成まとめ

全体構成は以下のようになり、外部からのアクセスは Nginx (80/443) のみを受け付ける堅牢なスタイルになりました。

[ Browser ] 
    │ HTTPS (443)
    ▼
[ Nginx R-Proxy (open-genai-proxy) ] ── SSL終端
    ├── /           => Web UI (genai-web)
    ├── /api/       => Core Backend API
    ├── /auth/      => Keycloak (SAML/OIDC Identity Provider)
    └── /litellm/   => LiteLLM Proxy (open-genai-litellm)
                           ├── LLMs (Ollama / OpenAI / Gemini etc.)
                           └── Image Gen (local-sd-api etc.)

おわりに・今後の展望

LiteLLM ProxyにLLM系をほぼ寄せて楽に管理ができるようになりました。LLMモデルをクラウド・ローカル問わずりやすくなりますし、ルーティングや利用量の監視ができるのはよいところ。今のところ、ユーザ毎のトークン利用量管理まではできませんが、APIキーとログインユーザを紐付ければ可能になるのでしょう。

リポジトリはこちらで公開:

github.com

gemma4:e2b + YOLO ONNXで知覚システムを試してみる。

gemma4:e2b + YOLO ONNXで知覚システムを試してみる。

以前から、パン・チルト(PTZ)カメラと、LLM / VLM(マルチモーダル言語モデル)によるセマンティックな解釈を密に結合させた自律型エージェントに関心がありました。そこで、ネットワークWiFiカメラtapo(C210)を使って、お勉強代わりにエッジPC環境でも実用的なリソース消費で動作する、簡単な能動的知覚(Active Perception)システムを構築してみました。

VRAM 8GB〜12GBのコンシューマ環境をターゲットとし、カメラの物理視線移動、ターゲットロック、高精度クロップ解析、および記憶の蓄積をMCPサーバとして統合してみます。RTX 3060で慎ましく駆動する仕様を目指しました。

解決を試みた3つのエッジAI課題

従来の監視システムやエッジAIの構築において、よく突き当たるいくつかの課題の解決を試みています。

  1. ルールベース追尾による受動性の解消 単に動体に反応して画角を動かす受動的追従ではなく、YOLOで認識したオブジェクトを追尾します。オブジェクトについては、必ずしも「person」だけでなく、アノテーション済みのカテゴリから選択できます。つまり、どのオブジェクトを追尾するかの指示を出せるようにしています。
  2. 常時 VLM の利用による重い処理を回避 エッジ環境において、重いVLM(マルチモーダルLLM)を常時推論させ続けると、同時に多数のジョブを投げかけるとVRAMが枯渇したり、処理が追い付かなくなったりします。そこで、 YOLO ONNX を使った物体認識を「常時テキスト化知覚バッファ」として機能させます。詳細な情報を得たい場合にVLMに問い合わせるというアプローチを採用しました。
  3. track_id の限界と長期記憶(Re-ID)の結合 カメラの視野から外れるとリセットされてしまう一時的なトラッカーID(track_id)は「数秒間の使い捨てポインタ」と割り切ります。長期的な同一物の再識別(Re-ID)には、「空間の絶対座標 + VLMによる意味的特徴 + WikiLinksによる相互ナレッジグラフ」を組み合わせて、情報を蓄積できるようにチャレンジしました。

検証環境および構築手順

検証には、手元のGeForce RTX 3060(VRAM 12GB)と、ONVIFおよびRTSPによる物理PTZ制御が可能な家庭用ネットワークカメラ(Tapo C210)を使用します。

1. 前提条件と依存関係

本システムの実行環境は以下の通りです。

  • Python 3.13
  • Tapo カメラ (C210等、ONVIF/RTSP対応機)
  • Ollama (ローカル VLM 推論用: gemma4:e2b)

2. 実機キャリブレーションの実装

物理カメラの可動限界を測定し、安全クランプ(ソフトウェアによる物理保護の限界値)を自動計算して tapo_config.json に保存します。

uv run python calibrate_tapo.py

実行すると、カメラが可動限界まで旋回した後に真の中心(原点)に戻って停止し、クランプ設定が保存されます。


システムアーキテクチャと主要コンポーネント

システムは以下の3つの自律型 CLI モジュール、および外部エージェントと接続するための統合 MCP(Model Context Protocol)サーバーから構成されます。

モジュール / CLI 役割 (機能) 主な特徴
pico.cli.ptz (ptz-cli) 筋肉 (Physical Actuator) ONVIF PTZ 物理制御、PID サーボロックオン、安全クランプ制限、Slew Rate 加減速制御
pico.cli.perception (perception-cli) 感覚 (Sensing & VLM) 10ms YOLO-ONNX + ByteTrack テキスト知覚、Ollama VLM スポットクロップ解釈
pico.cli.memory (memory-cli) 記憶 (Long-Term Memory) SQLite 3.34+ FTS5 Trigram 日本語想起、OKF Markdown 出力、WikiLinks 相互・バックリンク自動形成
pico.mcp.server MCP 統合サーバー Claude Code や Claude Desktop、外部の自律エージェント向けに 8 つのツールを標準提供

提供される主な MCP ツール(一部)

  • get_active_tracks: YOLOが追跡中のオブジェクトのテキスト化メタデータを一括高速取得(0ms)。
  • analyze_crop_image: 特定オブジェクトのクロップ領域に対する Ollama VLM スポット視覚解析。
  • set_tracking_target: 物理 PID サーボによる自動追従ロックオンの制御。
  • write_wiki: 観測事実やユーザー指示の OKF Markdown 書き込みと、WikiLinks のリアルタイム更新。

実行結果とパフォーマンスの定量測定

実際に各コンポーネントを駆動させ、エッジPC環境における処理パフォーマンスを測定しました。測定されたパフォーマンスデータおよびリソース消費量は以下の通りです。ローカルで遊べる程度にはあるのではないかと思います。

処理タスク 計算資源 / 推論エンジン 応答レイテンシ / 処理スループット VRAM占有量 挙動の特徴
YOLO ONNX + ByteTrack (常時知覚) ONNX Runtime (CPU / CUDA) 約 10 ms 約 500 MB 低レイテンシ。常時知覚ステートを非ブロッキングで更新。
Ollama VLM 視覚解析 (スポット解釈) Ollama (gemma4:e2b) 約 1.5 〜 2.5 s 約 3.2 GB イベント発生時のみオンデマンド起動。通常時はスリープさせ、VRAMを解放。
SQLite FTS5 Trigram 検索 (記憶想起) SQLite 3.34+ 1 ms 未満 ほぼ無視可能 過去の蓄積事実、エイリアス名寄せ、バックリンクのミリ秒検索。
ONVIF PTZ 物理旋回制御 Tapo C210 約 50 ms (制御パルス) ほぼ無視可能 PID サーボによるオブジェクト追従。Slew Rate 制御による物理的急旋回を抑制。

技術的なこと

ログを詳細に解析すると、設計通りの挙動といくつかの現実的な課題が浮き彫りになりました。

1. ハイブリッド知覚バッファの有効性

YOLOによる超高速(10ms)な位置・クラス追跡と、VLMによる重厚なセマンティック解析(1.5s〜2.5s)を分離するアプローチは、必要な瞬間だけクロップ画像をVLMに入力して「手元に何を持っているか」などの詳細な解釈を得ることに成功しています。

2. 長期記憶と Re-ID のアプローチ

一時的にロストしたオブジェクト(トラッカーIDが再割り当てされたもの)に対して、SQLite上の「観測された空間絶対座標」と「VLM意味特徴」、wikiによる相互の繋がりを参照することで、同一オブジェクトとして再結合・名寄せする処理ができるようにトライしました。うまく寄せられるかはエージェント次第ですが、LLM-wikiの考え方でオブジェクトの関係性が知識網として自然に蓄積されていくのが狙いです。まだまだ実験が必要ですが、実験的に実装しています。


これから

やってみなきゃわからない部分が多いエッジ物理エージェントの領域ですが、物理制御と長期記憶ナレッジグラフをMCPサーバーとして外部エージェントに公開できる構成は、自律エージェントを物理空間にマウントするお勉強代わりのシステムとして非常に面白い手応えを得られました。

今後は、イベントフィルタによる能動的発火イベント処理の調整や、wikiデータ構造の高度化、物理制御におけるPIDパラメータといった調整をちまちまとやりたいところです。

エッジで物理を認知する難しさと面白さを同時に味わえる実験用スタックとして、引き続きブラッシュアップしていきたいと思います。最初の一歩ですね。

Githubレポジトリ

github.com

Gemma 4 12B on a single RTX 3060を試してみる。

単体のRTX 3060でGemma 4 12Bを動かした場合、フルコンテキスト(262,144)を約100 tok/sで実現できたとのことで試してみます。dockerで簡単に実施してみます。

RTX 3060 12GBで試してみましたが、プロンプト処理速度はタスクにもよりますが 300〜700弱 t/s、生成速度は 18〜26 t/s くらいでした。8,000トークンを超えるコンテキストを保持しながらこの速度で動くと思うと、12GB VRAMクラスでここまでできる時代になったんですね。

セットアップ手順

1. ディレクトリの作成

mkdir -p ./models

2. GGUF ファイルの配置

./models ディレクトリに必要な 2 つの GGUF ファイル(メインモデルおよびアシスタント/ドラフトモデル)を配置します。


方法 A: huggingface-cli または hf コマンドを使用する場合

# メインモデルのダウンロード
hf download unsloth/gemma-4-12b-it-qat-GGUF gemma-4-12B-it-qat-UD-Q4_K_XL.gguf --local-dir ./models

# ドラフトモデルのダウンロード
hf download unsloth/gemma-4-12b-it-GGUF mtp-gemma-4-12b-it.gguf --local-dir ./models

方法 B: wgetcurl で直接ダウンロードする場合

中断された場合にレジューム(再開)できるよう -c オプションを付与し、ドラフトモデルは -O で正しいファイル名(12b)を指定して保存します。

# メインモデル(約7〜8GB)
wget -c -P ./models/ [https://huggingface.co/unsloth/gemma-4-12b-it-qat-GGUF/resolve/main/gemma-4-12B-it-qat-UD-Q4_K_XL.gguf](https://huggingface.co/unsloth/gemma-4-12b-it-qat-GGUF/resolve/main/gemma-4-12B-it-qat-UD-Q4_K_XL.gguf)

# ドラフトモデル(保存名 -O で大文字 12b を指定)
wget -c -O ./models/mtp-gemma-4-12b-it.gguf [https://huggingface.co/unsloth/gemma-4-12b-it-GGUF/resolve/main/mtp-gemma-4-12b-it.gguf](https://huggingface.co/unsloth/gemma-4-12b-it-GGUF/resolve/main/mtp-gemma-4-12b-it.gguf)

方法 C: Ollama で既にダウンロード済みのモデルを利用する場合

Ollama はモデルの実体を ~/.ollama/models/blobs/sha256-... に保存しています。シンボリックリンクまたはコピーを作成して ./models/ 配下に配置することが可能です。

# 例: Ollamaのblobからシンボリックリンクを作成して配置
ln -s ~/.ollama/models/blobs/sha256-<メインモデルのHASH> ./models/gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
ln -s ~/.ollama/models/blobs/sha256-<ドラフトモデルのHASH> ./models/mtp-gemma-4-12b-it.gguf

3. 配置結果の確認

起動前に ./models/ ディレクトリ内のファイル名とサイズを確認してください。

ls -lh ./models/

チェックリスト:

  • [ ] gemma-4-12B-it-qat-UD-Q4_K_XL.gguf (ファイルサイズが数GB〜7GB以上であること)
  • [ ] mtp-gemma-4-12b-it.gguf (ファイルサイズが約465MBで、表記が12bであること)

4. compose.ymlを設置する。

services:
  llama-server:
    image: ghcr.io/ggml-org/llama.cpp:server-cuda
    container_name: llama-speculative-server
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./models:/models
    command:
      - --model
      - /models/gemma-4-12B-it-qat-UD-Q4_K_XL.gguf
      - --model-draft
      - /models/mtp-gemma-4-12b-it.gguf
      - --spec-type
      - draft-mtp
      - --spec-draft-n-max
      - "4"
      - --n-gpu-layers
      - "99"
      - --n-gpu-layers-draft
      - "99"
      - --ctx-size
      - "262144"
      - --flash-attn
      - "on"
      - --jinja
      - --parallel
      - "1"
      - -ctk
      - q8_0
      - -ctv
      - q8_0
      - -ctkd
      - f16
      - -ctvd
      - f16
      - --batch-size
      - "4096"
      - --ubatch-size
      - "1280"
      - --temp
      - "1.0"
      - --top-p
      - "0.95"
      - --top-k
      - "64"
      - --repeat-penalty
      - "1.0"
      - --host
      - 0.0.0.0
      - --port
      - "8080"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

5. 起動とアクセス

準備ができたら Docker コンテナをバックグラウンドで起動します。

docker compose up -d

起動後、ブラウザで http://localhost:8080/ にアクセスすると llama.cpp の標準 Web UI が立ち上がり、チャットやテキスト生成を試すことができます。

おまけ

このモデルでログを分析させてみました。


提供されたログは、llama-speculative-server を使用して Gemma 4 12B を動かした際の、複数のタスク(Task)における推論パフォーマンスを示しています。

特に注目すべきは、「タスクによって速度や採択率が変動している点」「生成が進むにつれて速度が低下する傾向」です。以下に詳細な分析をまとめます。


1. タスク別パフォーマンス比較一覧

タスクID 生成速度 (tg) ドラフト採択率 (Acceptance) プロンプト処理速度 特徴
Task 26 (ログ断片) 50.38% - 最高効率。 採択率が非常に高く、ドラフトモデルが正確に機能。
Task 95 33.53 ~ 36.21 t/s 39.28% 70.46 tok/s 高 Are速度。 比較的安定した高速推論。
Task 485 28.39 ~ 32.88 t/s 37.81% 725.88 tok/s プロンプト処理が極めて高速。
Task 1443 24.77 ~ 33.27 t/s 41.53% 568.02 tok/s 標準的なパフォーマンス。
Task 1684 18.24 ~ 26.60 t/s 34.56% 345.26 tok/s 速度低下が見られる。 生成が進むほど減速。

2. 詳細分析

① ドラフト採択率(Draft Acceptance)の変動

  • Task 26 (50.38%) ではドラフトモデルが非常に優秀に働いていますが、Task 1684 (34.56%) では低くなっています。
  • 分析: 採択率が低いほど、ターゲットモデル(Gemma 4 12B)が何度も再計算を行うため、理論上の最大速度(100 tok/sなど)は出にくくなります。入力内容(プロンプトの難易度)によって、ドラフトモデルの「予測の当たりやすさ」が変わっていることが推測されます。

② 生成速度の減速(特に Task 1684)

  • Task 1684 を見ると、開始時は 26.60 t/s でありましたが、後半では 18.24 t/s まで落ち込んでいます。
  • 分析: これは「コンテキストの蓄積」による影響です。KVキャッシュの計算量が増え、VRAMの帯域を圧迫しているため、生成が進むほど(トークン数が増えるほど)処理が重くなっていることを示しています。

③ プロンプト処理(Prompt Eval)の優秀さ

  • Task 485 では 725.88 tok/s という驚異的な速度でプロンプトを処理しています。
  • 分析: これは Flash Attention (-fa on) と適切な量子化設定が正しく機能している証拠です。入力が長くても、最初の応答が出るまでの時間は非常に短く抑えられています。

④ グラフの再利用(Graphs Reused)

  • 各タスクで graphs reused が数百〜数千回記録されています。
  • 分析: llama-server が計算グラフを効率的にキャッシュしており、オーバーヘッドを最小限に抑えて推論を行えていることを示しています。

3. 総評と改善へのアドバイス

【現在の状態】 RTX 3060 (12GB) という制約の中で、非常に高度な最適化(MTP + Speculative Decoding + KV Cache Split)を成功させている状態です。

【さらなる改善のためのポイント】 1. 採択率の改善: 採択率が35%前後のタスクが多いです。もし可能であれば、ドラフトモデルをより「Gemma 4 12Bに近い挙動をする軽量モデル」に変更するか、プロンプトの構成を調整することで、採択率を上げ、平均速度を30 tok/s以上に安定させられる可能性があります。 2. 速度低下への対策: 生成が長くなった際に速度が落ちるのは、VRAM帯域の限界です。もし特定のタスクで速度が耐え難いほど落ちる場合は、--ctx-size を少し絞る(例:262K → 64Kや128K)ことで、KVキャッシュの負荷を減らし、速度を維持できる可能性があります。

結論として、このセットアップは現在利用可能な構成としては「極めて高効率な設定」と言えます。

以上が、この設定でログを形跡した結果です。なかなか使えるレベルだと感じました。

メモ:自分なりにAGENTS.mdをまとめてみる。

AGENTS.mdのテンプレをまとめてみます。正解はないと思うので、自分で利用しやすいような現時点での単なるメモです。それなりにまともでしょうか・・・。こうしてみると、コーディングの基本的な教えっぽいですね。結局、大事なのは基本ということでしょう、と、思ったりしました。

基本部分

# AGENTS.md

## プロジェクト

プロジェクトの目的を1〜2行で記載する。

## ゴール

- このプロジェクトが実現すべきこと。
- 維持すべき仕様・互換性。
- 完成条件(Definition of Done)。

## 制約

このプロジェクト固有の制約を記載する。

例

- API互換性を維持する。
- 出力形式を変更しない。
- 既存アーキテクチャを維持する。
- セキュリティ要件を満たす。

## 開発ルール

- 必要最小限の変更を優先する。
- 既存実装を再利用し、重複実装を避ける。
- 機能追加とリファクタリングは分けて行う。
- 機能変更時は関連するテストを追加・更新し、実行して成功を確認する。
- ユーザー向け仕様を変更した場合はREADME.mdも更新する。

## 開発環境

プロジェクト固有の運用ルールを記載する。

例(Python)

- `uv` を利用する。
- Pythonコマンドは `uv run` を利用する。
- 依存関係は `pyproject.toml` で管理する。
- `pip install` を直接使用しない。
- グローバルPython環境を変更しない。

## 情報源

実装時は次の順で参照する。

1. リポジトリ内のコード
2. docs/
3. README.md
4. 公式ドキュメント

オプション

必要なものだけ追加します。


開発プロセス

## 開発プロセス

このプロジェクトではTDDを採用する。

- Red
  - 失敗するテストを先に作成する。
- Green
  - テストが通る最小限の実装を行う。
- Refactor
  - テストを維持したまま改善する。
- 完了前に関連テストを実行する。
- 必要に応じて実データで検証する。

セキュリティ

## セキュリティ

- 外部入力は必ず検証する。
- 認可を回避する実装を追加しない。
- 機密情報をコードへ埋め込まない。
- ログへ機密情報を出力しない。

パフォーマンス

## パフォーマンス

- 高コストな処理は再利用を優先する。
- 不要なI/Oを増やさない。
- ボトルネックが確認されるまで複雑な最適化を行わない。

アーキテクチャ

## アーキテクチャ

- 共通ロジックを利用する。
- 同等機能を重複実装しない。
- 既存レイヤ構成を維持する。

レビュー

## レビュー

変更前に確認すること。

- API互換性
- テスト
- ドキュメント更新
- ライセンス

Git

## Git

- Conventional Commits を利用する。
- 小さなコミットを心掛ける。
- 関連しない変更を混在させない。

テンプレート設計思想

例えば

ファイル 役割
AGENTS.md AIエージェント向け。目的・制約・判断基準・運用ルールを記載。
README.md 人間向け。概要、セットアップ、利用方法、ライセンスを記載。
CONTRIBUTING.md 人間向け。開発フロー、レビュー手順、ブランチ運用などを記載。
docs/ 詳細設計、アーキテクチャ、テスト方針、セキュリティ方針などを記載。

リンクリスト

AGENTS.md Specifications

OpenAI

Anthropic Claude Code

Google Gemini CLI

Cursor

GitHub Copilot

uv

Python Packaging

Secure Coding

Docling v2+VLMでmarkdown化を好みにしてみる #02

PDFやOfficeファイルの前処理に有用な「Docling v2.x」を使って、それらのファイルを自分好みのmarkdownの出力にトライしていました。

bwgift.hatenadiary.jp

その後もちまちまと、一歩進んで、「実際にRAG(検索拡張生成)の前処理や知識ベースとして、利用できるように自作の変換エンジン「docling-markdown-generator」を改良していました。


Markdown化とVLMの導入

PDFやOfficeファイルをきれいにMarkdown化を「Docling v2」を使って実装しました。表のhtml化は大変便利です。図はリンク形式としましたが、ollamaを使ってvlmに図の説明を付記させることにしました。また、ページ区切り文字も入れることで、チャンクしやすくしたり、LLMがmarkdownの解釈をする際の補助とします。

その結果、テキストが存在しない「写真だけのシート」や複雑なグラフも、ローカルのVLMが自動で「※コンクリート表面に斜めのクラックが発生している画像」といった日本語サマリーを埋め込んでくれる。これによって、RAG(検索拡張生成)の検索精度に多少は貢献してくれます。

よくあるアプローチですが、汎用的に使いやすくしておけば後で便利だろうと、勉強しながら実装しています。


直面した「速度の壁」と、非同期並列プレフェッチ

──と、ここまでは完璧な作戦だったのですが、実際に動かし始めると大きな問題がありました。

ドキュメント内に画像が5枚、10枚と含まれている場合、1枚処理してはVLMの返事を待ち、また次の1枚を処理して待ち……という従来の逐次(直列)処理のままだと、とにかく時間がかかりすぎるのです。パース自体はDoclingで進むのですが、その後の画像の説明文章生成待ちが長い。

そこで、ドキュメントの解析中に切り出された複数画像に対し、裏でVLMリクエストを一括して非同期並列で事前取得(Prefetch)し、キャッシュする仕組み(ThreadPoolExecutor による並列化)としました。これにより同期ブロッキングが綺麗に解消され、画像が大量にあるドキュメントでも効率よくVLMを叩いて爆速で処理が終わるようになりました。やれやれ。


今後のために、ObsidianやOKFを見据えて入れ込んだ事柄

さらに、生成されたMarkdownをローカルのナレッジベース(Obsidian)やOKF(オープンナレッジフレームワーク)でそのまま美しく管理できるように工夫してみました。テストが不十分ですが・・・。

1. YAML Frontmatterへ対応

ドキュメントのタイトルや作成日時といったメタデータを、ObsidianやOKFのフォーマットと親和性の高い冒頭のYAML形式(Frontmatter)として自動抽出します。Obsidianにインポートした際もそのまま「プロパティ」として認識されるため、MOC(Map of Content)の構築やタグ管理への貢献を期待しています。

2. インジェクション対策による堅牢性

ファイル名にクォーテーションやコロン(:)などの特殊文字が含まれていると、生成されたYAMLの構造そのものがパースエラーで破壊されてしまいます。これを防ぐため、内部で厳格なサニタイズ処理(YAML Frontmatter Injection対策)を施しました。これで自動バッチ処理で大量にファイルを投げ込んでも、途中でコケない安心の堅牢性が確保できました。


次に向けて・・

現状はローカルのOllamaメインで動かしていますが、商用API(OpenAIやGemini、Claudeなど)も併用できるようにしておけば、精度が必要なものに対して柔軟に対応できますね。今後はAPIのレートリミットを考慮したセマフォ制御などもロードマップとして実装していこうと思います。

今回作成したコード:

github.com

Text Embeddings Inference (TEI)をembedding APIサーバに適用してみる。

Hugging Faceが提供しているText Embeddings Inference (TEI)はembeddingモデルにrustを使って最適化されています。今まで知らなかった・・・。そこで、以前作成した日本語embeddingモデルでmodernBERTを採用しているRuri v3を使ったAPIサーバーに適用してみます。

 

huggingface.co

 

出来たものはこちら。

github.com

 

 同時10ユーザーによる高負荷テストにおいて、TEIプロキシ構成によるパフォーマンス向上がはっきりしました。(RTX3060にて)

評価指標 ローカルモデル推論 TEI プロキシ推論 性能向上率
全体平均応答時間 1,117 ms 21 ms 約 53 倍高速化
Embeddings 応答時間 1,219 ms 17 ms 約 71 倍高速化
Rerank 応答時間 810 ms 32 ms 約 25 倍高速化
最大スループット (req/s) 2.42 req/s 3.38 req/s +39.6%
リクエスト失敗率 (Fails) 0.00% 0.00% 同等 (0.00% で極めて安定)

 

ついでに、uvに変更したりと整理しました。