WSL2 portproxyの自動修復でハマった三重苦+1 ― タスクスケジューラ・文字化け・Publicプロファイル、そしてRAGの「投函ボックス」取り込み


家庭内に分散AI推論クラスタ「Coordinator」を作っている記録の続きです。前回までで RAG 基盤(Qdrant + bge-m3 + 専用VM)まで載せました。今回は地味だけど一度ハマると気持ち悪い 「WSL2 の portproxy が再起動で壊れる問題の自動修復」 を仕上げ、ついでに RAG の運用資料取り込みを 「投函ボックス方式」 に整えます。

結論から言うと、portproxy のタスクスケジューラ登録という「5分で終わるはずの積み残し」が、4つの問題が連鎖して小一時間かかりました。ただ、どれも「家庭内クラスタあるある」なので、同じところで詰まる人の役に立つはずです。

背景:なぜ portproxy を自動で貼り直したいのか

GPU ワーカー(RTX 3070 Ti)は Windows + WSL2 上で Ollama を動かしています。LAN の他ノード(Coordinator が居る ai-core VM など)からこの Ollama に届かせるため、Windows 側で netsh interface portproxy を使い、0.0.0.0:11434 宛の通信を WSL2 の内部IP(172.28.138.1 など)へ中継しています。

問題は WSL2 の内部IPが再起動のたびに変わりうること。IPが変わると portproxy の中継先(connectaddress)が古いままになり、品質採点のバッチ処理が「エラーも出さずに無言で失敗」するという、いちばん厄介な壊れ方をします。

そこで前回、「WSL2 の現在のIPを取得して portproxy を貼り直す PowerShell スクリプト」を書いてありました。今回はそれをタスクスケジューラに登録して、ログオン時に自動実行させるだけ……のはずでした。

ハマり①:SYSTEM で実行すると WSL が動かない

最初は「常時動くように」と、実行ユーザーを SYSTEM にして登録しました。トリガーはログオン時と起動時の両方。一見きれいです。

ところが手動でタスクを走らせると LastTaskResult = 1(失敗)。SYSTEM が書き出したログを覗くと、こうなっていました。

2026-06-08 22:04:06 ERROR: invalid WSL2 IP: '0000'

原因は WSL ディストロはユーザー単位で登録されていること。SYSTEM アカウントには自分用の WSL ディストロが無いので、wsl.exe hostname -I がまともなIPを返せず、文字化けした出力になっていたのです。

幸い、このとき書いていたスクリプトには「取得したIPがIPv4の形式かどうか検証する」処理を入れてあったので、壊れたアドレスを portproxy に渡す前に ERROR で止まってくれました(このバリデーションは後述の②と③で効いてきます)。

対処はシンプルで、実行ユーザーを SYSTEM からログオンユーザーに変更し、トリガーもログオン時だけにしました。このマシンは「使うときは必ずログインする・常時起動はしない」運用なので、ログオン時に貼り直せば、Ollama を使う瞬間には必ず正しい portproxy が用意されている、という設計で十分です。

ハマり②:ユーザー名が「hogehoge」じゃなかった

では実行ユーザーを自分のアカウントに変えよう、とコンピュータ名+ユーザー名で登録したら、今度はこのエラー。

Register-ScheduledTask : アカウント名とセキュリティ ID の間のマッピングは実行されませんでした。
HRESULT 0x80070534

0x80070534 は「アカウント名から SID を解決できなかった」エラーです。ユーザー名を間違えている可能性が濃厚なので、whoami で正式名を確認したら……

PS> whoami
i5-12600kf\hoge

末尾の「a」がない。 Linux 側ではずっと hogehoge で運用しているのに、この Windows マシンのローカルアカウントは作成時に sagaw で切り詰められていたようです。完全に思い込みで hogehoge と打っていました。正しい hoge で指定したら、タスクは State: Ready で無事登録できました。

ローカルアカウント名は環境によって思わぬところで切り詰められていることがあるので、推測で打たず whoami で取った文字列をそのまま使うのが確実だと痛感しました。

ハマり③:portproxy の中継先が文字化けして「TCPは張れるが応答ゼロ」

タスクは動くようになり、ログにもそれらしい行が出ます。ところが ai-core 側から疎通を取ると、奇妙な状態でした。

$ curl -s -o /dev/null -w "http_code=%{http_code}\n" http://192.168.0.196:11434/api/tags
http_code=000

http_code=000 は「TCP接続はできたが、HTTPの応答が成立していない」状態です。タイムアウト(接続拒否)ではないので、portproxy のリスナー(0.0.0.0:11434)までは届いている。でもその先の中継が成立していない。

ヒントは netsh interface portproxy show の表示にありました。中継先アドレスが毎回違う文字列で文字化けしていたのです(・・ォ0・ のような表示)。表示だけの問題なら同じ化け方になるはずで、毎回違うのは妙でした。

真因はこうでした。スクリプトを実行するコンソールの文字コードが日本語環境の cp932 のままで、wsl.exe hostname -I の出力(UTF-8系)を取り込む過程でIP文字列に不正なバイトが混入。その壊れた文字列を netsh add に渡していたため、リスナーは立つけれど中継先が壊れていて HTTP 応答ゼロ、という症状になっていたわけです。

解決のため、スクリプトの先頭で出力エンコーディングを UTF-8 に固定し、取得したIPから数字とドット以外を除去したうえで形式を検証する、という二段構えにしました。

# --- エンコーディング固定(cp932混入によるconnectaddress破損を防ぐ)---
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
chcp 65001 | Out-Null

# WSL IP取得:ASCII以外の混入バイトを除去し、IPv4形式を検証
$raw = (& wsl.exe hostname -I) 2>$null
$wslIp = ($raw -join " ").Trim().Split(" ")[0]
$wslIp = ($wslIp -replace '[^0-9.]', '')

if ([string]::IsNullOrWhiteSpace($wslIp) -or $wslIp -notmatch '^\d{1,3}(\.\d{1,3}){3}$') {
    Log "ERROR: invalid WSL2 IP: '$wslIp'"
    exit 1
}

コンソールを UTF-8 にして手動で貼り直したら、show の中継先がちゃんと 172.28.138.1 と表示され、ホスト自身からの疎通も http_code=200 になりました。①で入れておいたIP検証が、化けた出力からは 0000 しか残らず正しく弾いてくれていた、という伏線回収でもありました。

ハマり④:家庭内LANなのにネットワークが「パブリック」だった

実は③の前に、もうひとつ壁がありました。ホスト内(localhost)では届くのに、ai-core から叩くと Connection timed out になる時間帯があったのです。ファイアウォールのルールを確認すると、許可ルールは Allow / Inbound / プロファイル=Any で正常。なのに通らない。

犯人はネットワークプロファイルでした。

PS> Get-NetConnectionProfile | Select InterfaceAlias, NetworkCategory
InterfaceAlias NetworkCategory
-------------- ---------------
イーサネット            Public

家庭内LANなのに Public 判定。Windows の Public プロファイルは未知のサービスへの inbound を既定で厳しく落とすので、ここで握り潰されていました。本来あるべき状態に直すのが筋なので、プロファイルを Private に変更しました。

Get-NetConnectionProfile | Where-Object NetworkCategory -eq "Public" |
    Set-NetConnectionProfile -NetworkCategory Private

最終確認:削除した状態から自力で貼り直せるか

仕上げに、いちばん大事な検証をしました。portproxy ルールをわざと削除した状態でタスクを実行し、スクリプトが新しいログを書いて正しいIPで貼り直せるか、です。これが通って初めて「再起動後の自動修復」が証明されます。

2026-06-08 22:13:30 WSL2 IP = 172.28.138.1
2026-06-08 22:13:30 portproxy set: 0.0.0.0:11434 -> 172.28.138.1:11434
2026-06-08 22:13:32 done

そして ai-core から最終疎通:

$ curl -s -o /dev/null -w "http_code=%{http_code}\n" http://192.168.0.196:11434/api/tags
http_code=200

4つの問題(SYSTEM不可・ユーザー名 sagaw・connectaddress文字化け・Publicプロファイル)をすべて潰し、自動貼り直し経路まで含めて完全にクローズできました。「5分の積み残し」がしっかり勉強になりました。

おまけ:RAG の運用資料を「投函ボックス」で取り込む

同じ日に、RAG の運用資料コレクション(server_ops)の取り込みも整えました。やりたいのは「引継ぎ資料やメモを Markdown で放り込んでおけば、夜中に勝手にベクトル化して検索できるようにする」ことです。

ここで素直に「ディレクトリにあるファイルを毎晩取り込む」とすると、変更がなくても毎晩フル再取り込みになります。うちの RAG 用VMは bge-m3 を CPU で回すので、サマリ版でもチャンク66個に約13分。毎晩やる処理ではありません。さらに「同じ資料の新旧バージョンがコレクションに両方残る」と、検索でバージョンが混ざる事故も起きます。

そこで採ったのが「投函ボックス方式」です。流れはこうです。

  1. inbox/ に Markdown を投函する
  2. 夜間のバッチが、まず同じファイル名の古いチャンクをベクトルDBから削除(doc単位のpurge)
  3. そのファイルだけを隔離ディレクトリに置いて取り込み直す
  4. 成功したら done/<日付>/ へ退避(監査用に残すが、検索対象には入れない)

削除のキモは、取り込み時にチャンクへ付けている file(ファイル名)メタデータです。これを条件にベクトルDB側で「このファイル由来のチャンクを全部消す」ができるので、再取り込みするとそのファイルの最新版だけがコレクションに残る。実際に同じ資料を投函し直すと、削除66件 → 追加66件で、コレクションの総数は66のまま。増殖しないことを確認できました。

この方式の良いところは、handover 専用の作り込みにせず「Markdownを放り込めば何でも最新版で管理される汎用の仕組み」になっていること。サーバ復旧手順やDB運用メモなど、これから増える運用ドキュメントもそのまま乗せられます。既存の取り込みスクリプト本体には一切手を入れず、purge と投函処理は別スクリプトに分離したので、検証済みのコードを壊さずに機能追加できました。

正直なところ:RAGの検索品質はまだ課題

取り込みと検索のパイプラインは動きましたが、検索の品質には明確な弱点が残りました。「Reaper はどんな故障を検出する?」と聞いても、肝心の説明チャンクが上位に来ず、たまたまキーワードを含む記事一覧テーブルの断片がトップに来たりします。スコアも全体に低め(0.57〜0.59)で、ドンピシャとは言えない「ニアミス」帯でした。

原因はおそらく、固定長でぶつ切りにするチャンク分割が、見出しや表や段落といった意味のまとまりを無視して切っていること。1つの説明が複数チャンクに割れたり、無関係な行と混ざったりしています。これは Markdown の構造(見出し・表ブロック)に沿った分割に作り直すべきテーマで、それ自体が一仕事なので、次回のメインに据えることにしました。「動く」ことと「使える品質」の間にはまだ距離がある、という良い宿題です。

まとめ

  • WSL2 の portproxy 自動修復は、SYSTEMでは動かない/ユーザー名の思い込み/文字コード混入/Publicプロファイルの4連鎖。どれも単体は些細だが重なると http_code=000 のような分かりにくい症状になる。
  • 切り分けの定石は「どの層まで届いているかを一段ずつ確認する」こと。localhost→WSL直叩き→LAN経由、と再現範囲を狭めていくと原因が絞れる。
  • RAG の運用資料は投函ボックス+doc単位purgeで、汎用かつ増殖しない取り込みに。ただし検索品質はチャンク分割の改善が次の課題。

「最強のAIを作る」のではなく「今ある機材で、自分が理解・制御できる実用的なAI環境を育てる」というこのプロジェクトの方針からすると、こういう泥臭い自動修復こそが本丸だな、と改めて思った回でした。