バックエンドを載せ替えたら bot が沈黙した——でも犯人は移行じゃなかった

個人で運用している「自動ラジオ動画生成システム」の、ある2日間の記録です。LLMのバックエンドを載せ替えた直後に Discord bot が黙り込み、そこから派生した一連の「警告の正体」を追いかけました。

技術ネタではありますが、核にあるのはもっと地味な教訓です。

「症状に飛びつかず、証拠で切り分ける」。

移行の直後に壊れたからといって、移行が原因とは限らない。警告が大量に出たからといって、品質が落ちたとは限らない。その2つを、実際のログと数字で確かめていきます。


はじめに:このシステムについて

このシステムは、リサーチ → 台本生成 → 音声・動画生成 → YouTube投稿までを自動化するパイプラインです。3つのリポジトリに分かれていて、Mac Studio がリサーチと台本生成を、Windows機が動画生成を担当します。LLMの推論は別のマシン(小型のGB10搭載機)で vLLM を回しています。

運用スタイルにひとつ特徴があります。設計判断(なぜ・何を)は私と対話AIが担い、実装(どうやって)はローカルのコーディングAIセッションに委ねる、という分業です。私は最終意思決定者兼レビュワー。この記事に出てくる「調査」「実装」も、その分業の上で回っています。

そして最近、推論バックエンドを Qwen 系から DeepSeek 系の2ノード構成に載せ替えました。話はここから始まります。


第1章 温度通知は届くのに、コマンドが無応答

ある日、Discordで台本生成をキックする /run コマンドを叩いたら、「アプリケーションが応答しませんでした」と返ってきました。

反射的に「バックエンドを替えたせいだ」と思いました。タイミングが近すぎる。でも、この手の直感はたいてい罠です。

まず症状を正確に読みます。「アプリケーションが応答しませんでした」は、Discordの3秒ACKタイムアウトです。つまり、コマンドを受けたbotが3秒以内に「受け付けたよ」の一次応答を返せていない。これはLLMの応答品質がどうこうという以前の、もっと手前の問題です。botが詰まっているか、そもそも動いていないか。

決め手になったのは、同じチャンネルに「GPU温度は正常です」という監視メッセージが届き続けていたことでした。監視系は生きている。でもbotのコマンドは無応答。この非対称が仮説を絞ってくれます。

調査の結果はこうでした。

  • 温度通知は、bot本体とは別の独立したWebhookから直接投稿されていた
  • bot本体は、数日前のMac再起動のあと復帰しておらず、プロセスとして存在していなかった

つまり「受け付ける者が誰もいない」状態だったわけです。

バックエンド載せ替えと障害の時間的近接は、ただの偶然でした。因果はゼロ。


第2章 launchd の落とし穴——「直したつもり」の罠

再発防止として、botをOSの常駐管理(launchd)に載せることにしました。ここで危うく穴を残すところでした。

Macの常駐化には2種類あります。

  • LaunchAgent — ユーザーがGUIにログインしたときに起動する
  • LaunchDaemon — ログイン前、システム起動時に起動する

今回の障害は、まさに「再起動したのにbotが上がってこない」でした。ここで LaunchAgent を選ぶと、自動ログインを有効にしていないヘッドレス運用では、再起動してもログインが発生せず、結局起動しない——元の障害モードを塞げていないことになります。

確認は1コマンドで済みました。自動ログインの設定を読むだけ。そこが埋まっているかどうかで、そのままクローズか追加対応かが分かれる。「直したつもり」が一番怖いので、ここは証拠で潰しました。


第3章 旧モデル向けの対策を棚卸しする

bot復旧のあとが本題です。バックエンドを替えた以上、前のモデル向けに入れていた各種の対策を見直す必要がありました。

ここで大事なのは、一括で消さないことです。過去の対策には2種類が混ざっています。「そのモデル固有のバグ回避」と、「推論モデル全般への防御」。後者を勢いで消すと、新しいモデルでも壊れます。

そこで、コード・設定・コメントを全部grepして、3つに分類しました。

  • 固有 — 撤去候補。ただし実測で非再現を確認してから
  • 一般 — 推論モデル全般への防御。維持
  • 不明 — 要検証。触らずに保留

たとえば「出力に混ざる思考タグを剥がす処理」は、今のモデルでは思考タグ自体が出ない設定なので実質休眠状態です。でも害はゼロなので、あえて残す。動いているものを書き換えるのは、リスクだけ増えて得がない。

逆に、旧モデル名がハードコードされたまま「プロキシが握り潰しているから無害」で放置されていた箇所は、プロキシの挙動が将来変わった瞬間に静かに壊れる時限爆弾です。ここは実値に直しました。

このとき見つかった収穫がひとつ。旧モデル時代は文脈長の上限が厳しく、そのために「出力トークン数を削る」「入力を切り詰める」といった設計上の我慢がいくつも入っていました。ところが新構成では上限が大幅に緩んでいた。つまり、我慢の前提そのものが消えていた。制約に合わせて縮こまっていた設計を、堂々と元に戻せる。

移行はコストであると同時に、過去の妥協を棚卸しする機会でもありました。


第4章 警告が32件。でも本物の問題は1件だった

新バックエンドで実運用を数本流し、成果物を検査しました。ここで一番の学びがありました。

台本には自動の品質チェックがかかっていて、「台本中の数値が、リサーチで集めた根拠データと一致するか」を機械照合します。あるrunでは、この照合で警告が32件出ました。数字だけ見ると「品質が落ちた」と青ざめます。

でも、1件ずつ根拠データと突き合わせていくと、話がまったく違いました。

32件のうち、本当に根拠のない数値(いわゆるハルシネーション)は1件だけ。残りは全部、照合器そのものの偽陽性(false positive)でした。

偽陽性のパターンは、こう分類できます。

1. レンジ/符号 根拠データが「−0.72〜−0.14」のような範囲や符号込みで格納されているのに、台本は自然な日本語で「0.72」と分解して書く。照合器が範囲や符号を扱えず不一致にする。引用は正しいのに警告が出る、一番理不尽なやつです。

2. 丸め 根拠が「95.5万」で、台本が「95万」と丸める。人間なら同じと分かる数値を、桁や端数の違いで不一致にする。

3. 歴史的年号 「2016年に設立」のような過去の出来事の年号を、「情報の鮮度に関する主張」と誤認して、ソースの発行日と一致しないと警告する。

4. スケール表記 「290万」を照合の手前で数値化しようとして失敗し、根拠データ集合から丸ごと消えていた。しかも黙って消えていた。これが厄介で、消えたことすら見えなかった。

5. 信頼区間の境界 これだけは毛色が違いました。台本が引用した「95%信頼区間の上限・下限」の値が、そもそも根拠データ側に存在しなかった。これは照合器の問題ではなく、根拠データを作る前段(リサーチ抽出)の問題です。照合器をいくら直しても、根拠側に値がない限り永久に一致しません。

つまり、「警告が多い」の正体は「モデルの質が落ちた」ではなく、「照合器の取りこぼしが、数値の多いテーマで顕在化した」でした。

オオカミ少年です。警告がノイズだらけだと、本当に見るべき1件が埋もれる。だから、偽陽性を1つずつ潰して、警告を「本当に見るべきものだけ」に戻していく——これが以降の作業の主軸になりました。


第5章 偽陽性を直すときの、地味だけど大事な話

丸めの偽陽性を直そうとして、面白い数理に行き当たりました。

素朴には「一定の割合(たとえば5%)以内なら同じ数値とみなす」としたくなります。でもこれは丸めには使えません。「小数を整数に丸めたときの最大誤差」は、数字の大きさに反比例するからです。

  • 95.5 を 95 に丸める → 誤差 0.5%
  • 1.4 を 1 に丸める → 誤差は最大 50% 近く

固定の割合だと、小さい数の丸めを拾うには閾値を大きくするしかなく、大きくすると今度は大きい数で緩みすぎて、本物の「取り違え」まで見逃してしまう。

正しいのは「割合」ではなく「表示精度に連動した丸め判定」でした。台本が書いた桁数のぶんだけ丸めて一致するかを見る。こうすると許容の幅が数字の大きさに自動で追従します。

  • 「95.5万 → 95万」 …… 通す(正当な丸め)
  • 「95.5万 → 100万」 …… 不一致のまま(粗すぎる概数)
  • 「290万 → 300万」 …… 不一致のまま(取り違えの疑い)

もうひとつ徹底したのは、「直したら他を壊していないこと」を必ず数字で確かめることです。修正を入れる前と後で照合を再実行し、

  • 狙った1件だけが一致に転じた
  • 増えた警告はゼロ
  • スコープ外の別バグはちゃんと残存している

ことを、ディスク上の成果物で確認する。過去に「完了しました」の通知が実態と食い違った経験があるので、通知やログの見た目ではなく、成果物そのものを根拠にする。この規律だけは崩しませんでした。


第6章 全体を通しての教訓

技術の細部よりも、判断の型のほうが後から効きます。今回持ち帰ったのはこの6つです。

1. 時間的近接は因果ではない 移行の直後に壊れても、まず症状を正確に読む。「無応答」は「品質が悪い」ではなく「手前で詰まっている」かもしれない。

2. 「直したつもり」を証拠で潰す 再発防止策が本当に元の障害モードを塞いでいるか、1コマンドでいいから確かめる。

3. 過去の対策は「固有/一般/不明」で仕分けてから触る 一括撤去はしない。動いている無害なものは、あえて残す。

4. 警告が多いことは、品質が悪いことと同じではない ノイズと本物を分けないと、本物が埋もれる。偽陽性は1つずつ潰す。

5. 計測してから最適化する 「たぶん過剰だから減らそう」は、新しい思い込みを作るだけ。前提が変わったなら、まず実測を取ってから動く。

6. スコープを1つずつ切る 1回の修正で1つの問題だけ。あれもこれもと欲張ると、単純な修正が大改修に化ける。


おわりに

移行は「成功」で判定できる状態になり、派生したバグと偽陽性も、いくつかは処理し、残りは設計を確定させたうえで「実装待ち」として棚に整理しました。緊急の火はもう消えています。

次にやるべきは、リサーチ抽出側の課題(信頼区間の境界が根拠データに入らない件)です。ただしこれは根拠データの品質全体に触る変更なので、通常運用を数本挟んで安定を確かめてから——というところで、ひと区切りです。

派手な機能追加の話ではありません。でも、こういう「地味に切り分けて、地味に潰す」の積み重ねが、個人運用のシステムを静かに信頼できるものに変えていくのだと思います。