退役ノードの静かな穴と、AIが書いた「最新」の一言がキーワード分類器を騙した話

結論から。今回は2つの独立した不具合を1回のセッションで見つけて塞いだ。ひとつは「退役させたはずのノードが、実はまだキューを聴き続けていた」というインフラの話。もうひとつは「AIが自動生成した文章の中の『最新』という一言が、別のAIの分類器を誤作動させ、自宅サーバの質問にAWSやWindowsの話を混ぜ込んできた」というルーティングの話。どちらも派手な障害ではなく、定期点検と、たまたま見つけた副産物を放置しなかったことで表に出てきた。

1. 退役させたノードは、本当に退役していたか

結論: していなかった。ただし実害はゼロだった。

Coordinator の構成では、役目を終えたノードは sanity_models = [](保有モデルを空にする)ことで「もう推論を受け付けない」扱いにしている。7月3日のセッションで ai-core にこの設定をしていたにもかかわらず、実際には旧バージョンの worker が動き続け、共有キューからタスクを奪って処理していたことが発覚し、修正済みだった。

今回はその横展開として、同じ穴が残る2台——moon(ThinkCentre)と esxi(VM)——を点検した。結果は次のとおり。

moon:  ollama-worker.service   active (running)   enabled
esxi:  coordinator-worker.service  active (running)   enabled

両方とも、退役設定をしたはずなのに現役で動いていた。原因は worker の共通ロジック(worker_base.py)の設計上の欠陥で、sanity_models を空にしても、メインループ自体は止まらない。空であることを確認して “登録” だけをスキップし、キューを聴き続けてしまう。もし共有キューにタスクが流れてくれば、退役したはずのノードがそれを奪って処理し、失敗する可能性があった。

幸い、7月3日以前に確認したログを遡っても、実際にタスクを奪った形跡は見つからなかった。今の構成では共有キューにタスクが流れること自体がほとんどないためだ。「設定上は退役している」と「実際に何もしていない」は別のことだと確認できた、という話に留まった。

直した内容

ロジックを2段階に直した。

  • 意図的な退役(sanity_models = [])なら、起動時に自分の情報を掃除して、以後はキューに一切触らない永久休眠状態に入る。
  • 設定はあるのにモデルが立ち上がっていない(Ollama がまだ起動していない等の一時的な状態)なら、一定間隔でリトライして自動復帰する。

この2つを分けたのがポイントだった。「退役」と「一時的な起動待ち」を同じ扱いにすると、前者まで復帰リトライしてしまい無駄なポーリングが残るし、後者を退役扱いにすると本当は動けるはずのノードが上がってこない。両ノードに配布して再起動し、ログに退役モードのメッセージが出ることを確認してから、サービス自体を無効化した。

[retired] sanity_models=[] → retired mode: never consuming queues

これで、退役済みの3台(ai-core・moon・esxi)は「設定でも退役」「動作でも退役」「サービス自体も無効化」という三重の防御が揃った。

副産物: 消えていた指名経路

点検のついでに Redis を覗くと、moon 宛の登録情報がそもそも存在しないことに気づいた。以前は「小型モデルを使いたいときは moon を名指しで呼び出す」運用にしていたはずだが、退役設定を入れた時点で登録自体がスキップされるようになっており、名指しの呼び出しはすでに機能していなかった。実害はない(代わりに moon の Ollama を直接叩けば済む)が、”設定を変えたつもりの副作用” は思ったより広く波及するという実例になった。

2. 「最新」の一言が、自宅サーバの質問にAWSとWindowsを持ち込んだ話

結論: 原因は「AIが分解して作った質問文」が別のAIの分類器を騙したこと。既定オフの制御フラグで塞いだ。

この基盤には、ひとつの依頼を複数の小タスクに分解して並列処理する「pipeline」という仕組みがある。依頼を受けると、まず司令塔役のAIが「これは3つの作業に分けられる」と判断してサブタスクを作り、それぞれを個別のワーカーに投げる。

先日、回帰テストの副産物としてこんな依頼を流した。

自宅サーバの定期メンテナンス計画を、OS更新・バックアップ・監視の3つの観点で簡潔にまとめてください

司令塔AIはこれを6つのサブタスクに分解したが、そのうち3つが「Web検索ワーカー」に着地していた。原因を辿ると、サブタスクの文面に含まれていた「最新版のリリースノート」「現在のバックアップ方法」「現状の監視体制」という言葉が、Web検索を自動判定するキーワードリストにヒットしていた。

実際どんな回答が返ってきたか

「じゃあWeb検索を使えば正確な情報が手に入っていいのでは」と一瞬思ったが、実際の中身を見て考えを変えた。

  • OS更新について: サブタスク文には「対象OSの種類(例: Linuxディストリビューション名)」と明記されていたのに、返ってきたのは Windows 11 の話。しかもリリース年を2019年と誤って記載していた。
  • バックアップについて: 自宅サーバの質問のはずが、AWS(EC2・RDS・S3)とAzure Backupと Microsoft 365 を混ぜた回答が返ってきた。「定期的オブジェクトスナップショット(SSO)」のような、聞いたことのない用語まで生成されていた。
  • 監視について: 一般論としてはそれらしいが、自宅サーバに特化した情報は皆無だった。

原因は明快だった。サブタスクの文面には親の依頼(「自宅サーバの」)という文脈が乗っていない。だから検索は無文脈の一般語で行われ、検索エンジンの上位に出てくる企業向け・クラウド向けの記事を拾ってしまう。「Web検索を自動で挟む」という機能自体は無罪で、「文脈を持たない機械生成の文章で検索してしまう」ことが問題だった。

直した内容

選択肢は3つ考えた。(A)このまま機能として使う、(B)サブタスクの自動分類を完全に禁止する、(C)既定オフの明示的なフラグで制御できるようにする。実際の出力を見た結果、(C)を採った。将来サブタスクの生成方法が改善されて親の文脈を引き継げるようになれば、フラグをオンにすればいい。今は品質面で有害と判断し、既定はオフにする。

実装は次の2つのフラグを足すだけで済んだ。

RouteRequest.auto_classify: bool = True   # 既存の呼び出しは全て互換
PipelineRequest.allow_auto_classify: bool = False   # サブタスクは既定でオフ

サブタスク側では、分解元のAIが指定したドメイン(コード/一般など)があればそれを尊重し、指定がなければ「一般」と明示する。これにより、Web検索の自動判定だけでなく、副次的に「ドメイン不明のときに別のAIへ問い合わせて分類する」という重い処理も丸ごと省略できるようになった(この重い処理は数十秒かかることがあり、以前の検証で連鎖的な遅延の原因になっていたものでもある)。

効果を同じ質問で確認する

変更前(7/3) 変更後(7/4)
サブタスク6本の着地 Web検索ワーカー×3・8B推論×3 8B推論×4(全数)
所要時間 連鎖的な遅延あり 6秒で全数完了
回答の傾向 Windows・AWS混入、事実誤り 自宅サーバの文脈内で一貫

既存の呼び出し方(単発の質問への自動Web検索・自動RAG判定)は一切変更していないので、影響範囲はサブタスク経路だけに閉じている。

まとめ

今回の2件はどちらも、派手な障害報告ではなく「ちゃんと点検したら見つかった」「たまたま見た副産物を放置しなかったら見つかった」という類のものだった。設定を変えたことと、実際にその通り動いていることは別物であり、AIが自動生成した文章は、それを受け取る別の仕組みにとっては人間が書いた文章と同じ重みを持って解釈される。どちらも当たり前のようでいて、実際にログと出力を見るまでは気づけなかった。