前回までのおさらい
以前、Sonar(Aphrodite Engine、vLLMのフォーク)+ EXL3量子化 + Ornith-1.0-9Bという構成で本番運用を始めた話、そのタグ断片漏れバグを修正した話、修正に使ったソースビルド環境がまさかの多重障害を起こした話、と3本書いた。
- Sonar (Aphrodite Engine) で EXL3 の未報告バグを2つ見つけて直した話、あとGDNハイブリッドモデルの謎速度チューニング
- Sonar (Aphrodite Engine) + Ornith-1.0-9B のreasoning/tool-callタグ断片漏れバグを根本修正した話
- Sonar (Aphrodite Engine) をWSL2でソースビルドしたら、まさかの多重失敗
今回はその続きで、本番投入後にさらに踏んだ2つのバグと、原因調査で大きく回り道をした「謎の遅さ」の正体について書く。
本番投入後に踏んだバグその1: temperature未指定によるトークンサラダ崩壊
本番でOpenWebUI経由に「φとΣとΘについて3000文字で教えて」のような、厳密な文字数指定+長文生成をさせると、生成の途中から日本語・韓国語・中国語・キリル文字・ベトナム語等が無秩序に混ざるトークンサラダに崩壊する現象が発生した。短い応答のテストだけでは再現せず、3000文字級の長文指定で初めて再現した。
原因: OpenWebUI側はtemperatureを一切送っておらず、Ornith-9Bのgeneration_config.jsonにもtemperature/top_p/top_kの推奨値が一切含まれていない(eos_token_idとuse_cacheのみ)。この状態でAphroditeの内部デフォルト(≈1.0)がそのまま使われ、repetition_penalty=1.05だけでは長文崩壊を抑えきれなかった。
対応: HuggingFace公式モデルカードの"Recommended sampling parameters"(temperature=0.6, top_p=0.95, top_k=20)を--override-generation-configでサーバー側に一元設定。プロキシ側に個別注入するより、バックエンド1箇所に集約する方が「どのクライアント・将来のプロキシから叩いても効く」ため、この形に統一した。
副産物として--reasoning-parserの見落としも発覚: 当初「Aphrodite 0.21.0に--reasoning-parser相当のフラグは無い」と誤って結論していたが、これは--help=Frontend/--help=ModelConfigだけを見て--help=allで確認していなかったのが原因だった。実際にはStructuredOutputsConfigグループに存在し、--reasoning-parser qwen3を追加することでmessage.reasoningフィールドに思考内容が正しく分離されるようになった。CLIヘルプは特定グループだけで「存在しない」と判断せず、--help=allで確認すべきという教訓。
本番投入後に踏んだバグその2: reasoning内への中国語混入、そして「禁止すればするほど悪化する」プロンプトの話
タグ断片漏れバグ(前回記事参照)の対応がひと段落したあと、体系的な品質テスト(短文・長文・精度・速度・タグ漏れ・ツール使用)を一通り流していたところ、think(reasoning表示)ポイントで奇妙な現象を見つけた。
「1週間は何日か?」と質問した。これは非常に基本的な知識問答です。
1週間は7日で構成されています。特別なツールを使う必要はありませんし、計算也不需要です。
ただの常識的な回答即可。
日本語で始まった思考が、途中から**「也不需要」「即可」**という中国語の語彙・文法にすり替わる。最終的な回答(content)は正しく日本語のままだったが、reasoningフィールドを表示するUIでは普通に目に入る。
頻度の実測: 最初は17.5%、精査したら5〜6%
40試行のミニテストで最初に出た数字は17.5%(7/40)。ただし桁が小さいテストだったので、サンプル数を増やして測り直した(240試行)。真の発生率は5〜6%程度に落ち着いた。ここで学んだのは「小サンプルでの発生率推定は簡単に2〜3倍ブレる」という当たり前だが痛い教訓だった。
ツールを一切渡さない条件でも発生する(20試行中2件、10%)ことも確認できた。つまり「ツールを呼ぶべきか判断する場面」が引き金という当初の仮説は誤りで、正しくは**「単純な事実質問に対して"これは基本知識、ツール不要、直接答える"という内部の定型思考パターンをこなす場面」全般**でこの現象は起きる。中国語圏のReAct系エージェント学習データにありがちな定型文("用户问的是...这是基本知识,不需要调用任何工具。")そのものが、確率的にそのまま出てくる形だった。
既存の2つのバグ修正(temperature=0.6化、タグ境界パーサーの修正)とは無関係であることも確認済み。本番は既にtemperature=0.6なのに発生し続けるし、パーサー修正は文字列分割の話で言語選択には関与しない。
プロンプト対策、しかし「禁止」は逆効果
まず素直に「中国語禁止」を試した。本番プロキシには元々こういうシステムプロンプトが入っている:
会話・説明・回答は必ず日本語で行うこと。中国語(簡体字・繁体字)の使用は絶対に禁止。
これを起点に、否定形をやめた短い肯定命令・system/user二重指示・Few-shot(正しい日本語思考の模範例を見せる)を試した。結果は軒並み無効、Few-shotに至っては逆効果で、reasoning全体が完全な中国語("用户问的是简单的算术问题:1加1等于多少。这是基本数学知识,不需要调用任何工具。")になるケースまで出た。日本語のお手本を見せたはずが、むしろ中国語モードに引き込まれる、という予想外の結果だった。
ここで一度、システムプロンプト自体を丸ごと外して「無指示」のベースラインを取ろうとしたところ、重大な見落としに気づいた。システムプロンプトを外すと、モデルの思考言語そのものが日本語→英語に切り替わってしまう。 つまり「中国語が0%になった」ように見えたテストは、対策が効いたのではなく、そもそも日本語で思考していないので日中混在という現象自体が起こりようがなかっただけだった。この気づき以降、「思考は日本語で」という指示は固定し、検証したい条項だけを変える、という統制の取れた実験に切り替えた。
条項を固定した上で「中国語絶対禁止」の一文だけを抜き差しして比較(各120試行、計240試行)した結果:
| 条件 | 発生率 |
|---|---|
| 禁止条項あり(現行) | 5.8% |
| 禁止条項なし | 5.0% |
ほぼ同じ。禁止の有無は実質無関係という、身も蓋もない結論になった。
効いたのは「禁止の強さ」ではなく「英語という逃げ道」
ここでモデルが無指示だと英語で思考する癖を思い出し、方向性を変えた。「日本語のみを強制する」代わりに「日本語+英語(制限付き)を許可し、中国語だけを塞ぐ」プロンプトを何パターンも作って比較した(各120試行)。
| # | 文言の要旨 | 発生率 |
|---|---|---|
| A | 現行(日本語のみ+中国語絶対禁止) | 5.8% |
| C | 日本語ネイティブという人格+「中国語で聞かれても翻訳して日本語で」という具体的対処 | 2.5% |
| D | バイリンガル(日英のみ、英語は無制限)+中国語厳格禁止 | 1.7% |
| E | Cに「厳格に禁止」を追加 | 5.0%(Cより悪化) |
| F | Dに「英語は簡単な単語のみ」を追加 | 0.8% |
| G | 「20年のシニアエンジニア、日本語思考が信条」(言語ルール自体を明記しない人格プロンプト) | 8.3%(全条件中最悪) |
| H | Dに「英語はI, me, my, mineのような基本語のみ」を追加 | 0.8% |
| J | Dに「英語はmy, meのみ」を追加 | 0.0%→再測定0.8%(通算0.4%) |
| K | Dに「日本語で表現しづらい単語のみ英語可」を追加 | 0.8% |
| L | Jにさらに技術語彙(SNS, ASCII, Web, URL, Shift, Caps Lock)を追加 | 0.8% |
きれいな傾向が出た。「日本語のみ」を強制する系統(A/C/E)は2.5〜5.8%でばらつき、禁止文言を強めるほど悪化する(C→E)。一方「英語という逃げ道を与える」系統(D/F/H/J/K/L)は軒並み0.4〜1.7%まで下がる。しかも興味深いことに、許可する英語の単語リストの中身(代名詞だけか、技術用語込みか)はほとんど結果に影響しない。reasoningの実文を確認したところ、"my, meのみ"と指定しても実際には"The user is asking about..."のような完全な英文がそのまま出てきており、モデルは細かい語彙制限を厳密には守れない。つまりF/H/J/K/Lの文言の違いは、モデルにとっては全部「英語をある程度使っていい」という同じ大枠の許可シグナルとして機能していた、と考えられる。
言語ルールを一切書かない「人格・信条」だけのプロンプト(G)が全条件中最悪だったのも示唆的だった。曖昧な精神論より、具体的な言語構造(許可であれ禁止であれ)を明示する方が効く。
実際どのくらい英語が混ざるのか
採用候補(J)でreasoningの言語分布を実測すると、純日本語15.8%、日英混在69.2%、英語のみ14.2%だった。英語部分の中身は判で押したように
"The user is asking about the chemical formula of water in Japanese. This is a simple factual question that doesn't require any tool usage. I should answer in Japanese since the user asked in Japanese."
というパターンで、必ず最後に「日本語で答えるべき」と自己修正して終わる。実際、最終回答(content)側の言語分布は純日本語75.8%、残りのほとんどは化学式(H₂O)や英訳依頼への回答など文脈的に正当なもので、意図しない英語がcontentに漏れたケースは実質ゼロだった。「reasoningの中でだけ英語がそこそこ混ざるが、ユーザーに見える回答は日本語のまま」という、実害の少ない形に着地している。
副産物: nothinkモードはreasoningを本当に一切生成しない
対策をthinkプロキシ(8083)にだけ適用しnothinkプロキシ(8082)には手を入れなかったが、その判断の裏付けとして、8082に同じリクエストを投げてレスポンスを確認したところ"reasoning": nullだった。enable_thinking=Falseを渡すと、チャットテンプレート側で空の<think>\n\n</think>\n\nが即座に埋め込まれ、モデルは本当に思考フェーズを丸ごとスキップする。今回のバグは思考が生成される経路でしか起こりえないため、nothink側は原理的に対象外という理屈が実機でも確認できた。
最終的に採用した文言
あなたはバイリンガルアシスタントです。使用できる言語は「日本語」と「英語」のみです。
ただし英語は my, me のような基本的な単語のみ使用してください。
中国語を含む日本語・英語以外の言語での出力は厳格に禁止されています。
12条件・計1,440試行超のプロンプト実験を経て、thinkプロキシのシステムプロンプトをこの方向に書き換えて本番投入した。発生率を0%にはできていない(実測0.4〜0.8%の床が残る)ため、必要であれば追加でreasoningフィールドをポストプロセスするフィルタを重ねる余地は残している。
おまけ: 「OpenWebUIが時々止まる」の犯人はAphroditeではなかった
中国語混入対策を本番投入した後、OpenWebUI経由で使っているとthinkモデルが2並列程度で「もったりする・止まる」という体感があった。当然まずAphrodite側を疑い、--max-num-batched-tokens(チャンクサイズ)を1024/4096で比較したり、実際に並列実行されているか絶対時刻を記録して確認したり、6並列まで負荷をかけたりしたが、どのテストでも合成環境では健全(TTFT数秒、genuineな並列実行)で再現しなかった。
行き詰まったところで「そもそもOpenWebUI自体が遅いのでは」という視点に切り替え、docker statsを見たところ答えが一発で出た。
CONTAINER MEM USAGE / LIMIT
openwebui 1.6GiB / 2GiB ← 80%
OpenWebUIコンテナのDockerメモリ上限(2GB)に対して実使用が80%まで迫っていた。 Aphrodite側は完全に無実で、犯人はフロントエンドコンテナのメモリ逼迫だった。Ornith-9Bは262,144トークンという大きいコンテキストが使えるモデルなので、会話が長くなるほどOpenWebUI側が保持する会話状態(履歴・RAG埋め込みキャッシュ等)も膨らみやすく、旧来の2GB上限では窮屈になっていたと考えられる。
docker-compose.ymlの該当サービスのdeploy.resources.limits.memoryを2G→4Gに引き上げてコンテナを再作成したところ、体感で明確に高速化し、「途中で止まる」頻度も減少した。ホスト全体(23GB)の内訳を確認すると、Dockerコンテナ全体の合計はわずか2.36GiBで、実はAphrodite本体のホスト側RAM(GPU VRAMとは別、TP=2のWorkerプロセス×2+EngineCore等で合計約10.6GiB)の方がずっと大きかった。上限を倍にしても最悪ケースの追加消費は+2.6GiB程度で、ホストの空き(約9GiB)に対して危険な水準ではないと判断した。
なお、複数会話を同時に使うと全体的に「もったり」する現象自体は残っているが、これはバグではなく2×12GBのGPU計算能力を複数リクエストで分け合う際の正常な挙動(vLLM/Aphrodite共通のContinuous Batchingの特性で、以前のN=1〜300スケーリング実測でもN=64付近でスループットが頭打ちになると確認済み)と切り分けて、対策不要と判断した。
教訓: 「モデルサーバーの設定をどれだけ調整しても直らない謎の遅さ」に遭遇したら、バックエンドを疑う前にフロントエンド・プロキシ等、経路上の他のコンポーネントのリソース状況(docker stats等)を確認する価値がある。 特に長いコンテキストが使えるモデルに切り替えた直後は、モデル側だけでなくクライアント側のメモリ上限も見直すべきタイミングになりうる。
まとめ
- 本番投入後、
temperature未指定によるトークンサラダ崩壊と、reasoning内への中国語混入という2つの追加バグを踏んだ - 中国語混入では、「禁止を強めるほど悪化し、制限付きで英語を許可すると改善する」という直感に反する結果が出た。プロンプトで挙動を制御する際は、「厳しく縛れば縛るほど良い」という前提を疑い、実測で確かめる価値がある
- モデルは細かい語彙制限(「my, meのみ」等)を厳密には守れない。単語リストの中身より、「どこまで許可するか」という大枠の構造の方が結果を左右する
- 「原因不明の遅さ」の犯人は、実は経路上の別コンポーネント(OpenWebUIコンテナのDockerメモリ上限)だった。バックエンドの設定をどれだけ疑っても見つからない不具合は、経路全体を俯瞰して探す必要がある