はじめに
みなさん、こんにちは。株式会社インプルのyotaです。
前回の記事では、無料で使えるローカルLLMを5つピックアップし、RAG(検索拡張生成)環境で「一番嘘をつかないモデルはどれか?」という比較検証を行いました。その結果、Gemma 2の優秀さが際立つ一方で、同じプロンプトを与えても結果が変化する「出力のブレ」がみられました。まだご覧になっていない方は、ぜひこちらの記事からお読みください。
今回は、前回の検証で高いポテンシャルを見せた上位3モデル(Gemma 2、Llama 3.1、Qwen 2.5)を主軸に、ブレないシステムの構築に挑戦します。その鍵となるのが「Temperature(温度)」と「Seed(シード値)」という生成パラメータの制御です。
前回の振り返り
前回の記事では、5つのローカルLLMを用いて「RAG環境下において、ドキュメントにない情報を捏造せず(嘘をつかず)、正確に抽出できるか」という検証を行いました。
結果として、Googleの「Gemma 2(9B)」が情報抽出の正確さと自制心の両面でトップの成績を収めました。また、Metaの「Llama 3.1(8B)」やAlibabaの「Qwen 2.5(7B-instruct)」も、優れた推論能力と高いポテンシャルを見せてくれました。
しかし、検証の過程で業務利用における重大な課題に直面しました。
全く同じ条件でテストを2回実行したところ、Qwen 2.5が1回目で正解した問題を間違えたり、逆にLlama 3.1が2回目は正解したりと、「実行のたびに回答がブレる」という現象が起きました。
この出力のブレは、モデルの読解力不足によるものではありません。LLMの内部にデフォルトで組み込まれている「次にどの単語を選ぶかのランダム性(確率的な揺らぎ)」が原因です。
一般的なアイデア出しなどであれば、この揺らぎは「AIの豊かな発想」として役立ちます。しかし、「社内規定を正確に案内する」といった嘘が許されないRAGシステムにおいて、サイコロの出目(機嫌)によって回答が変わる仕様は致命的です。
「優秀なモデルを選ぶだけでは、実用的なQAシステムは完成しない」
この考察から、優秀なローカルLLMをRAGのエンジンとして実戦投入するためには、このランダム性を司る生成パラメータ(TemperatureやSeed)をシステム側でコントロールし、AIを「ブレない関数」へと調教するステップが不可欠であるという結論に至りました。
生成パラメータの全体像
AIにドキュメントを読み込ませるRAGシステムにおいて、モデルの選定と同じくらい重要なのが「生成パラメータ」のチューニングです。LLMの内部には、次にどの単語を出力するかを決定するための様々な設定値が用意されています。まずは、ローカルLLMで調整可能な主要パラメータの全体像を見てみましょう。
| パラメータ名 | 主な機能 | 設定範囲 | デフォルト値 |
| Temperature | 出力のランダム性(冒険心)を制御 | 0.0〜2.0 | 0.7〜0.8 |
| Seed | 乱数生成の初期値を固定 | 任意の整数 | 未指定(ランダム) |
| Top_p | 累積確率をもちいて、出力候補の単語を絞り込む | 0.0〜1.0 | 0.9〜1.0 |
| Top_k | 確率が高い上位N個の単語のみに候補を絞り込む | 0〜無限大 | 40〜50 |
| Repetition Penalty | 同じ単語やフレーズの繰り返しを抑制 | 1.0〜2.0程度 | 1.1前後 |
| Num Predict | AIが生成する最大のトークン(単語)数を制限 | 任意の整数 | モデル・環境による |
なぜ最初に「Temperature」と「Seed」を調整するのか?
これだけ多くの設定項目がある中で、RAGの環境において真っ先に「Temperature」と「Seed」の2つを調整すべき明確な理由があります。それは、「決定論的な動作(常に同じ結果を返すこと)」と「検証の再現性」を確保するためです。
システム開発において、同じ入力を与えているのに毎回出力が変わってしまう状態(ランダム性が高い状態)では、プロンプトの改善が効いたのか、単なる偶然なのかを客観的に評価できません。Temperatureで根本的なランダム性を抑え込み、Seedで乱数を固定して初めて、他のパラメータ(Top_pなど)やプロンプトの微細な調整を行うための「正しい検証のスタートライン」に立つことができるのです。
では、この重要なパラメータである2つについて、さらに詳しく解説します。
Temperature(温度):AIの「候補の幅(ルーレットのマス目)」を調整する
Temperatureは、AIが次の単語を選ぶ際の「確率分布(ルーレットのマス目の広さ)」を歪めるパラメータです。
- デフォルト値(0.7〜0.8)が引き起こした前回のカオス
通常、LLMはチャットボットとして自然で多様な会話ができるよう、Temperatureが0.8前後(やや高め)に設定されています。前回の検証はこのデフォルト設定のまま実行していました。
この状態は、ルーレットにおいて「確率の低い珍しい単語」のマス目があえて少し広げられている状態です。その結果、AIが「マイナー単語を選んでみよう」と冒険してしまい、Qwen 2.5が突如情報を読み落としたり、Mistral Nemoが突如韓国語を話し出したりといった「ハルシネーション」を引き起こしてしまったのです。 - Temperature = 0.0(RAGの鉄則)
逆にこの数値を0.0に設定すると、ルーレットは「最も確率が高い(無難な)単語」の1色だけで100%塗り潰されます。一切の冒険をしないため、社内規定の検索など、絶対に嘘をついてほしくないRAGタスクにおいては「0.0」が基本設定となります。
Seed(シード値):AIの「サイコロの目」を固定する
Temperatureによってつくられたルーレットに対し、「最終的にどのマス目(単語)にボールを落とすのか」の乱数を決めるのがSeedの役割です。
- デフォルト値(未指定・ランダム)が引き起こした出力のブレ
デフォルトではSeed値は指定されておらず、実行するたびに毎回デタラメにボールが投げ込まれます。マス目が複数ある状態(Temperatureが0.0より大きい状態)で毎回違う投げ方をすれば、当然結果はブレます。前回の検証で、1回目と2回目でLlama 3.1の回答が「思考放棄」から「完璧な正解」へと全く違う結果にブレたのは、この「毎回違う出目が使われていた(Seedが固定されていなかった)こと」が根本的な原因です。 - Seedの固定(例:42)
このSeed値を特定の値(例えばseed: 42など)に指定しておくと、AIは毎回「全く同じ軌道でボールを投げ込む(同じ乱数を使う)」ようになります。これにより、ルーレットに複数のマス目があっても結果が固定され、LLMの挙動を客観的に評価し、「AとBのモデルで同じ状況を再現する」といった厳密な検証が可能になります。
TemperatureとSeedの関係
もしTemperatureが0.0の場合、ルーレットの盤面はたった1つの単語で100%埋め尽くされています。この状態であれば、Seedが固定されていようが、ランダムな数字を出そうが、ボールは必ず同じ単語のマスに入ります。つまり、Temperatureを0.0にした時点で、Seedの乱数は結果に影響を与えなくなるのです。
検証
パラメータの仕組みを理解したところで、いよいよ実際の検証に入ります。
前回のテストで高いポテンシャルを見せた上位3モデル(Gemma 2、Llama 3.1、Qwen 2.5)に対し、「ルーレットとボールの理論」が本当に正しいのか、2つの段階に分けてテストを行いました。
検証設定
今回使用したPythonのコードとプロンプトの構成を提示します。RAGのコンテキスト(参考資料)やシステムプロンプトは前回と同じ条件です。
import ollama
import time
# テスト対象のモデルリスト
MODELS=[
"gemma2:9b",
"llama3.1:8b",
"qwen2.5:7b-instruct",
]
# 既存の公的ドキュメント(労働基準法の実際の条文抜粋)を利用
CONTEXT_TEXT = """
【労働基準法】
第32条(労働時間)...(中略)...
第34条(休憩)...(中略)...
第39条(年次有給休暇)...(中略)...
"""
# テスト用のプロンプト
SYSTEM_PROMPT = "あなたは優秀な人事・法務アシスタントです。必ず提供された【ドキュメント】の内容のみに基づいて回答してください。ドキュメントに記載がない情報は絶対に推測せず、「記載がありません」とだけ回答してください。"
# テストケース(最も出力がブレやすかったテスト2を使用)
TEST_QUERIE = "従業員全員に一斉に休憩を与えなくても良いのは、どのような条件を満たした場合ですか?"
full_prompt = f"【ドキュメント】 \n{CONTEXT_TEXT}\n\n 【質問】 \n{TEST_QUERIE}"
def run_determinism_test(model, use_seed=False):
answers = []
options = {'temperature': 0.0} # Temperatureは常に0で固定
if use_seed:
options['seed'] = 42 # Seedありの場合のみ追加
print(f" [条件: Seed固定(42)]")
else:
print(f" [条件: Seedランダム(未指定)]")
for i in range(1, 4):
try:
response = ollama.chat(
model=model,
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': full_prompt},
],
options=options,
)
answer = response['message']['content'].strip()
answers.append(answer)
print(f"試行 {i}: {answer[:50]}..." if len(answer) > 50 else f"試行 {i}: {answer}")
except Exception as e:
print(f"エラー発生: {e}")
answers.append("ERROR")
# 一致判定
is_deterministic = (answers[0] == answers[1] == answers[2])
result_mark = "完全に一致" if is_deterministic else "ブレ発生(不一致)"
print(f"-> 判定: {result_mark}\n")
def run_phase1_tests():
for model in MODELS:
print(f"▼▼▼ 実行モデル: {model} ▼▼▼")
# 1. Seedランダム
run_determinism_test(model, use_seed=False)
# 2. Seed固定
run_determinism_test(model, use_seed=True)
print("-" * 50)
if __name__ == "__main__":
run_phase1_tests()
テスト1:絶対零度(Temperature = 0.0)でのSeedの無効化
まずは、AIの冒険心を完全にゼロにする「Temperature: 0.0」の設定で、Seed(乱数)の指定を「ランダム(未指定)」にした場合と「固定(例:42)」にした場合で挙動を比較しました。
- 検証設定:Temperature
0.0/ Seedランダムvs固定 - 結果:全モデルにおいて、Seedがランダムでも固定でも、出力結果(文字数・テキスト内容)が「一言一句、完全に一致」しました。
▼▼▼ 実行モデル: gemma2:9b ▼▼▼
[条件: Seedランダム(未指定)]
試行 1: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 2: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 3: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
-> 判定: 完全に一致
[条件: Seed固定(42)]
試行 1: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 2: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 3: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
-> 判定: 完全に一致
--------------------------------------------------
▼▼▼ 実行モデル: llama3.1:8b ▼▼▼
[条件: Seedランダム(未指定)]
試行 1: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
試行 2: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
試行 3: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
-> 判定: 完全に一致
[条件: Seed固定(42)]
試行 1: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
試行 2: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
試行 3: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
-> 判定: 完全に一致
--------------------------------------------------
▼▼▼ 実行モデル: qwen2.5:7b-instruct ▼▼▼
[条件: Seedランダム(未指定)]
試行 1: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 2: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 3: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
-> 判定: 完全に一致
[条件: Seed固定(42)]
試行 1: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 2: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 3: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
-> 判定: 完全に一致
--------------------------------------------------
これはまさに先ほどの理論の証明になっています。Temperatureを0.0にしたことで、ルーレットの盤面が「最も無難な単語の1色」で100%塗り潰されました。そのため、Seedを未指定にして毎回デタラメにボールを投げ込ませても、必ず同じマス目に入り、結果的にSeedの効果が完全に打ち消されたことが確認できました。
テスト2:微小な揺らぎ(Temperature = 0.1)とSeedの固定
次に、ルーレットにほんの少しだけ別のマス目を作ってあげるために、「Temperature: 0.1」に設定を上げて同じテストを行いました。
- 検証設定: Temperature
0.1/ Seedランダムvs固定 - 結果(Seedランダム時):Gemma 2とLlama 3.1の回答にブレが発生しましたが、Qwenでは出力が完全に一致していました。
- 結果(Seed固定時):ブレを見せていたGemma 2もLlama 3.1も、出力結果が一言一句一致するようになりました。
▼▼▼ 実行モデル: gemma2:9b ▼▼▼
[条件: Seedランダム(未指定)]
試行 1: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 2: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 3: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
-> 判定: ブレ発生(不一致)
[条件: Seed固定(42)]
試行 1: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 2: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
試行 3: 当該事業場に、労働者の過半数で組織する労働組合がある場合、その労働組合と書面による協定がある場合です...
-> 判定: 完全に一致
--------------------------------------------------
▼▼▼ 実行モデル: llama3.1:8b ▼▼▼
[条件: Seedランダム(未指定)]
試行 1: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、その...
試行 2: 労働組合が事業場内で過半数の権限を持つ場合、または労働者の過半数で組織する労働組合がない場合は、労働...
試行 3: 労働組合が事業場内に過半数のメンバーを持つ場合、または労働者の過半数で組織する労働組合がない場合は、...
-> 判定: ブレ発生(不一致)
[条件: Seed固定(42)]
試行 1: 労働組合が事業場内で過半数の権限を持っている場合、または労働者の過半数で組織する労働組合がない場合は...
試行 2: 労働組合が事業場内で過半数の権限を持っている場合、または労働者の過半数で組織する労働組合がない場合は...
試行 3: 労働組合が事業場内で過半数の権限を持っている場合、または労働者の過半数で組織する労働組合がない場合は...
-> 判定: 完全に一致
--------------------------------------------------
▼▼▼ 実行モデル: qwen2.5:7b-instruct ▼▼▼
[条件: Seedランダム(未指定)]
試行 1: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 2: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 3: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
-> 判定: 完全に一致
[条件: Seed固定(42)]
試行 1: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 2: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
試行 3: 一斉に休憩を与えなくてもよいのは、当該事業場に労働者の過半数で組織する労働組合があるときはその労働組...
-> 判定: 完全に一致
--------------------------------------------------
Temperatureを0.1にしたことで、ルーレットに「確率の低い別の単語」のマス目が生まれました。Seedがランダムな状態では、投げ込まれたボールが稀にそのマスに入り、GemmaやLlamaの回答に僅かなブレを生じさせたのです。
しかし、Seedを固定(毎回同じ軌道でボールを投げる設定)にした瞬間、他のマス目があろうと必ず同じ場所(特定のマス目)にボールが落ちるようになり、完全な再現性が担保されました。
考察:「出力の固定」と「正解」はイコールではない
今回の検証を通じて、TemperatureとSeedを適切にチューニングすることで、LLMの出力を完全に固定化・安定化できることが証明されました。
しかし、ここで一つ「Seedの罠」について触れておきます。
Seedを固定すれば、確かに回答は毎回同じになります。しかし、それはあくまで「ボールを投げる位置が固定されただけ」であり、「正しい回答が選ばれるようになった」わけではありません。
たとえばTemperatureが高い(例:0.8)状態でSeedを固定してしまうと、AIがたまたま選んだ酷い回答(例:前回の韓国語混じりの回答)が、何度実行しても一言一句変わらず出力され続けることになります。
そのため、プロンプトの改善やモデルの実力をテストする際、最初からむやみにSeedを固定するのは危険です。
まずは、Seedをランダムにして複数回実行し、「Temperatureの揺らぎの中で、どの程度正解を引けるのか」「そもそも正解が出力されることはあるのか」を把握すること。その上で、挙動を厳密にコントロールしたい場面で初めて「Temperature=0.0」や「Seed固定」のカードを切るのが、適切なパラメータチューニングの順番と言えるでしょう。
まとめ
いかがだったでしょうか?
今回は、ローカルLLMの「ランダム性」をシステム側から制御し、RAGシステムとして安心して使える「ブレない関数」へと調教する手法をご紹介しました。
本記事は「嘘をついてほしくないRAG環境」を想定していたため、Temperatureは0.0や0.1といった極端に小さな値のみを使用しました。しかし、一般的なチャットボットやアイデア出しの用途では、AIの豊かな発想力を引き出すために0.7〜0.8程度の設定がよく使われます。
先ほど提示したPythonコードを使えば、optionsのtemperatureの数値を0.8などに書き換えるだけで、AIの挙動がどう変化するのかを簡単に手元でテストすることができます(モデルのダウンロードは必要ですが…)。興味のある方はぜひ色々と試して遊んでみてください!
ローカルLLMは、パラメータという手綱の引き方次第で、真面目な事務員にもクリエイティブな作家にも姿を変えます。本記事が、みなさんのLLM開発の一助となれば幸いです。
最後までお読みいただき、ありがとうございました!
