Featured image of post ローカルLLMでうまくいかない時にCheckする事

ローカルLLMでうまくいかない時にCheckする事

目次

背景

  • 最近仕事のロボット開発で、ローカルLLM on Jetsonをメインに使っている
  • ローカルLLMを使って開発していると、プロンプト通りにAIが指示を聞いてくれないことがある
  • フロンティアモデルと違って量子化されていたりパラメーター数に制限があるから素直に動かない
  • 特に、その原因を調査した際に、どこから疑って何をCheckすればいいのかを整理しておく
  • 実はLLMのモデルの事前のTuningの方法によって、意外にモデルの固有のくせがあったりもする

ローカルLLM特有の原因

ChatGPT・Claudeのような商用APIでは提供元が裏側で処理してくれている部分を、ローカルLLMでは自分で設定する必要がある。この設定ミスが、プロンプトそのものより先に疑うべき原因になっていることが多い。

チャットテンプレートの不一致

  • 最も見落とされやすく、かつ影響が大きい原因
  • 各モデルは、学習時に使われた特定のプロンプトの書式(チャットテンプレート)に従って会話を組み立てないと、本来の性能を発揮できない
  • 例えば、Llama系の[INST] ... [/INST]、ChatML系の<|im_start|>role\n...<|im_end|>、Alpaca系の### Instruction:のような、モデルごとに異なる書式がある
  • Google製のGemma系はさらに独自性が強く、以下のような形式を使う
1
2
3
4
<start_of_turn>user
{ユーザーの発言}<end_of_turn>
<start_of_turn>model
{モデルの応答}<end_of_turn>
  • ロール名がuser/modelで、他系統でよく使われるassistantではない
  • 独立したsystemロールを持たず、システム的な指示を入れたい場合は最初のuserターンの先頭に埋め込むのが一般的な運用
  • Llama系のテンプレートをそのまま流用すると、ロール名・終了トークンの両方でズレる
  • サービングフレームワーク(Ollama・llama.cpp・vLLM・text-generation-webuiなど)によって、正しいテンプレートを自動で当ててくれる場合と、自分で指定しないといけない場合がある
  • テンプレートがズレると、指示を無視する・system promptを読まない・意味不明な位置で出力を切るといった、一見「指示に従っていない」ように見える症状が出る

Baseモデルとinstructモデルの取り違え

  • instruction-tuning(指示に従うよう追加学習)されていない素のbaseモデルは、そもそも「指示に従う」という振る舞いを学習していない
  • モデル名に-instructや-chat、-itなどが付いているかを確認する

量子化による劣化

  • 4bit・3bitのような強い量子化をかけると、モデルサイズと引き換えに、指示追従性能が目に見えて落ちることがある
  • 同じプロンプトを量子化なし(またはより緩い量子化)のモデルで試し、症状が改善するか確認する
  • 同じビット幅でも、量子化の方法には大きく2種類あり、これによって品質が変わる
    • Post-Training Quantization(PTQ):
      • フル精度で学習済みのモデルを、学習後に量子化する方式
      • GPTQ・AWQ・GGUFのk-quantsなどが該当する
    • Quantization-Aware Training(QAT):
      • 量子化による誤差を学習時にシミュレートしながら学習・追加学習する方式
      • 同じビット幅でも、PTQより劣化が小さくなる傾向がある
  • 使っているモデルがPTQ版なのかQAT版なのかを確認し、可能であればQAT版を優先して試すか、同じ条件で両方を比較してみる

マルチモーダルProjectorの不整合

  • 画像を扱うマルチモーダルモデルは、画像用のエンコーダーが出した特徴量を、LLM側のトークン埋め込み空間に変換するProjector(小さな変換用ネットワーク)を経由している
  • Projectorと本体のLLMは別々のファイルとして配布されていることが多く、バージョンや量子化形式の組み合わせが正しくないと、画像から得られる情報が正しくLLMに渡らず、出力全体の質が落ちることがある
  • 画像を含むやり取りでだけ症状が出るのか、テキストのみのやり取りでも同じ症状が出るのかを切り分けて確認する
  • 画像を含む場合は、Projectorと本体モデルのバージョン・量子化形式の組み合わせが、配布元の想定通りかを確認する

System Promptの扱いの違い

  • サービングフレームワークやフロントエンドによって、system roleの扱われ方が異なる
  • system promptがuser promptと結合されて渡される、あるいは弱く扱われて実質無視される、といった実装差がある

停止トークン(Stop Token / EOS)の設定ミス

  • 想定と違うstop sequenceが設定されていると、出力が指示の途中で不自然に切れたり、逆に止まるべきところで止まらず喋り続けたりする
  • モデルカードに記載されている正しいEOSトークンと、サービング側の設定が一致しているか確認する
  • 例えば、ChatML形式のモデルはターンの終わりを<|im_end|>というトークンで示す
    • サービング側でこのトークンをstop sequenceとして登録し忘れると、モデルは<|im_end|>を出力した後も止まらず、続けて架空の<|im_start|>userターンまで生成し、それに自分で答え始めてしまうことがある。一見「指示を無視して関係ない話を続けている」ように見えるが、実際には止まるべきところで止まっていないだけのことがある
    • 逆に、本文中で自然に出てくる文字列(例えば\n\nなど)を誤ってstop sequenceに設定してしまうと、指示の途中で出力が突然打ち切られ、「指示に従っていない」ように見えることがある
  • 前述のGemma系のように、チャット上の実質的な終了シグナル(<end_of_turn>)と、トークナイザー上の形式的なEOSトークン(<eos>)が別物になっているモデルもある
    • サービング側が<eos>だけをstop sequenceに設定していると、<end_of_turn>が出ても止まらず、上記と同じ「架空のターンを生成して自問自答する」現象が起きうる

コンテキスト長の制限

  • ローカルLLMは、商用APIのモデルよりコンテキスト長が短いことが多い
  • 制限を超えた部分が、エラーも出さず黙って切り捨てられていることがある

直前のやり取りを無視する(マルチターンの文脈を見失う)

「あのXXXはどうなった?」のように、1つ前のメッセージにある内容を指す指示語を使うと、AIがそれを無視・無知として扱う、という症状も、ローカルLLM特有の原因が絡んでいることが多い。

  • 会話履歴が実際に正しく送られているか
    • アプリ側の実装ミスで、直前のターンだけ会話履歴の配列に含め忘れている、といったバグは意外とよくある
  • ターンの並びがuser→assistant→user→assistantのように、きちんと交互になっているか
    • ツールの実行結果や追加情報を、別のuserターンとして継ぎ足してしまい、assistant→user→userのように同じroleが連続していないか
    • 多くのモデルは、学習時に厳密に交互のターンで会話を学習しているため、この並びが崩れると、モデルが学習時に見たことのない構造として扱い、直前のターンをうまく参照できなくなることがある
    • 同じroleの発言は1つのターンにまとめて渡す(userの追加情報は、直前のuserターンに結合する、など)ことで解消できる場合がある
  • コンテキストの切り詰め戦略が意図通りか
    • 古いターンから削る想定が、実装ミスで直近のターンから欠落する、といった逆転が起きていないか
  • モデルサイズによる指示語解決の弱さ
    • 「あの」「それ」のような指示語が何を指すか(coreference resolution)は、モデルサイズが小さいほど弱くなる傾向がある
    • 商用のtop-tierモデルに比べて小さいサイズ帯のローカルモデルほど、この影響を受けやすい
  • モデルごとのチャットテンプレート事情
    • モデルファミリーによっては、独立したsystem roleを持たない、あるいはsystem promptをuserターンの先頭に埋め込む必要があるなど、独自のテンプレート仕様を持つものがある
    • 会話履歴を自前のコードで組み立てている場合、直近のターンだけ正しくこの形式でラップされていない、といった実装ミスが起きやすい
    • この現象自体は特定のモデルに限らず一般的に起こりうるが、system role非対応のようなテンプレート仕様を持つモデルでは、実装ミスによって症状が出やすい

タスク自体を疑う

プロンプトをいじる前に、そもそもそのタスクが解けるタスクなのかを確認する。

LLMが本質的に苦手なタスクではないか

  • 正確な四則演算・大きな数の計算
  • 最新情報や、学習データに含まれていない固有の事実
  • 文字数・単語数を厳密に数える作業
  • こうした作業は、プロンプトをどう工夫しても改善しにくいことが多く、外部ツール(電卓・検索・コード実行)に任せた方が早い場合がある

自分で解けるタスクか

  • 指示自体が曖昧・矛盾していないか、自分で一度解いてみる
  • 人間が読んでも複数の解釈ができる指示は、LLMにとっても曖昧
  • 期待する出力例を、まず自分の手で1つ書き下せるか確認する

プロンプト自体を疑う

指示の明確性

  • 何を、どんな形式で出力してほしいかが明示されているか
  • 出力フォーマット(JSON・箇条書き・表など)を指定しているか
  • 「良い感じに」「適切に」のような曖昧な形容詞に頼っていないか

Few-shot例の有無

  • ゼロショット(指示文だけ)で曖昧な結果になる場合、具体例(few-shot)を2〜3個追加してみる
  • 入力と出力のペアを見せることで、期待する粒度・フォーマットが伝わりやすくなる

指示の位置・順序

  • 長いコンテキストの中では、真ん中に置かれた情報ほど見落とされやすいという傾向が報告されている(lost in the middle)
  • 重要な指示は、プロンプトの先頭か末尾に置く方が効きやすい
  • 複数の指示がある場合、優先順位を明示する(「まず〜、次に〜」のように)

否定形より肯定形

  • 「〜しないでください」より「〜してください」という肯定形の指示の方が、意図通りに伝わりやすい傾向がある
  • 禁止事項が多い場合、代わりに何をしてほしいかを具体的に書く

Chain-of-Thought(思考過程の明示)

  • 複雑な推論が必要なタスクでは、いきなり答えを出させず、途中の思考過程を出力させる(Chain-of-Thought)と精度が上がることが多い
  • 「ステップごとに考えてから、最後に結論を出してください」のような指示を追加してみる
  • 逆に、単純なタスクでCoTを強制すると、かえって冗長になったり精度が落ちたりすることもあるので、タスクの複雑さに応じて使い分ける

コンテキスト・入力を疑う

入力データの質

  • 入力に誤字・脱字・フォーマット崩れが無いか
  • 入力自体が汚いと、モデルはその汚れごと学習・模倣してしまうことがある

コンテキストの長さ

  • コンテキストが長すぎて、本当に重要な情報が埋もれていないか
  • 不要な情報を削り、関連する情報だけに絞れないか検討する

無関係な情報(ディストラクタ)の混入

  • タスクに無関係な情報が紛れ込んでいると、モデルの注意がそちらに引っ張られることがある
  • 参考情報として渡す文書は、関係のある部分だけに絞り込む

モデル・パラメータを疑う

モデルの選択

  • タスクの難易度に対して、モデルの能力が不足していないか
  • 軽量・高速なモデルで試して不十分なら、より高性能なモデルに切り替えて同じ現象が起きるか確認する

サンプリングパラメータ

  • 決定的な出力がほしい場面で、temperatureが高すぎないか(0に近づけると出力が安定しやすい)
  • 逆に、多様なアイデアがほしい場面で、temperatureが低すぎて毎回似た出力になっていないか

System PromptとUser Promptの役割分担

  • 恒常的なルール(口調・制約・役割設定)はSystem Promptに、タスクごとに変わる内容はUser Promptに分けられているか
  • 両方に同じような指示が重複して書かれ、優先順位が曖昧になっていないか
  • ローカルLLMの場合、そもそもSystem Promptがサービング側で正しく扱われているか(前述の「System Promptの扱いの違い」)を先に確認しておく

出力の評価方法を疑う

評価基準自体が曖昧ではないか

  • 「良い出力」を自分の中でどう定義しているか、言語化できているか
  • 曖昧な基準のまま「なんか違う」と感じている場合、まず基準を明文化する

1回の出力だけで判断していないか

  • LLMの出力は確率的で、同じプロンプトでも毎回結果が変わりうる
  • 複数回(例えば5〜10回)出力させて、傾向として安定しているかを見る
  • 稀にしか失敗しないのか、毎回同じ失敗をするのかで、原因の切り分け方が変わる

再現性のあるデバッグ手法

最小構成に切り詰める

  • プロンプトを可能な限り削り、それでも同じ問題が再現するか確認する
  • 問題が再現しなくなった時点で、直前に削った部分が原因の可能性が高い

実例: タブラ・ラーサ法とpytest xfailでの切り分け

「直前のやり取りを無視する」症状を、実際に以下の手順で切り分けた。

  1. タブラ・ラーサ(余計な要素を一切含まない、最小限のSystem PromptとMessages)を用意する
  2. それをpytestのxfailマーカー付きテストとして書き、まずエラーの再現性を確保する(n=12とか温度を高めて同じpromptでも複数回行い、確率で判断する)
  3. タブラ・ラーサで再現するかどうかで、その後の切り分け方針が変わる
    • 再現しない場合: 元の複雑なプロンプトに対して二分法(プロンプトを半分に割り、どちらの半分に原因があるかを繰り返し絞り込む)を使えば、原因箇所のプロンプトを特定できる
    • 再現する場合: プロンプトの内容自体には原因が無く、instruction-tuningに由来するモデル自体のクセだと判断できる
  4. 今回はタブラ・ラーサでも再現したため、モデル自体のクセだと判明した
  5. 対策として、語気を強めたfew-shot例とChain-of-Thoughtをプロンプトに追加し、複数のケースで汎化することを確認した上で解決した
  6. ただし、これはあくまでプロンプト側の対症療法で、根本的に直すにはFine-tuningが必要になりそうだった

また、言葉が行けないのか、指示文がいけないのか、色々な切り分け方がある。

一箇所ずつ変更して比較する

  • 複数の変更を同時に行うと、何が効いたのか分からなくなる
  • 1回の試行で変える変数は1つに絞り、before/afterを比較する

試行ログを残す

  • どのプロンプト・どのパラメータで、どんな出力が返ってきたかを記録しておく
  • 後から「あの時の設定に戻す」「同じ問題が再発したか確認する」際に役立つ

まとめ

  • ローカルLLMでは、プロンプトの文面をいじる前に、チャットテンプレートの不一致のような配信側の設定ミスを先に疑うべき
  • 「直前のやり取りを無視する」症状は、会話履歴の受け渡し・切り詰め処理の実装ミスと、モデルサイズによる指示語解決の弱さの両方が絡みうる
  • system role非対応のようなモデル固有のテンプレート仕様があると、特に実装ミスが起きやすい
  • それでも解決しない場合、プロンプトの文面をいじる前に、タスク自体が解けるタスクかを疑う
  • プロンプト・コンテキスト・モデルパラメータ・評価方法という複数のレイヤーがあり、どこに原因があるかを切り分ける必要がある
  • 1回の出力だけで判断せず、複数回試して傾向を見ること、変更は1つずつ試すことが、再現性のあるデバッグにつながる

参考文献

Built with Hugo
テーマ Stack は Jimmy によって設計されています。