モデルがすることは一つだけ:次のトークンを予測する
モデルに各ステップの候補トークンと確率を見せてもらい、回答がトークン一つずつ生成される様子を観察します。さらに、続きを書くだけの機械がアシスタントになるまでの過程と、ハルシネーションがどこから来るのかを説明します。
- 約 35 分
- 難易度:入門
- 検証:2026-09-14 deepseek-flash
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
「LLM は本質的に次の単語を予測しているだけだ」という話を聞いたことがあるでしょう。これは正しいのですが、この一言だけでは、「次の単語を予測する」プログラムがどうしてコードを書いたり、数学の問題を解いたり、質問に答えたりできるのか想像しにくいはずです。それより大事なのは、この一言でこの先出会う多くの現象が説明できることです。なぜもっともらしい顔ででっち上げるのか、なぜ同じ質問に正しく答えたり間違えたりするのか、なぜプロンプトの書き方で結果が大きく変わるのか。
この課では、モデルに各ステップの「考え」を見せてもらいます。
一歩ずつ選ぶ様子を見る
前の課で触れたように、API には logprobs というパラメータがあります。これをオンにすると、モデルは出力した各トークンの確率を教えてくれ、同じ位置で確率の高かった候補もいくつか添えてくれます。DeepSeek は非思考モードでこのパラメータに対応しています。
import math
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["LLM_API_KEY"],
base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com"),
)
MODEL = os.environ.get("LLM_MODEL", "deepseek-flash")
def show_candidates(prompt, max_tokens=6):
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
logprobs=True, # 返回每个输出词元的概率
top_logprobs=5, # 同时返回每一步概率最高的 5 个候选
extra_body={"thinking": {"type": "disabled"}},
)
print(f"问:{prompt}")
print(f"答:{response.choices[0].message.content}")
for step, item in enumerate(response.choices[0].logprobs.content, 1):
# 接口返回的是概率的自然对数,用 exp 变回 0~1 之间的概率
options = " ".join(f"{c.token}({math.exp(c.logprob):.1%})" for c in item.top_logprobs)
print(f" 第 {step} 步选了 {item.token!r:8} 候选:{options}")
print()
show_candidates("床前明月光,下一句是什么?只回答下一句。")
show_candidates("请续写这句话,只写接下来的几个字:周末我打算去")
私が実行した結果です(候補のいくつかは改行文字で、表示すると行が崩れるので削除しました)。
问:床前明月光,下一句是什么?只回答下一句。
答:疑是地上霜。
第 1 步选了 '疑' 候选:疑(100.0%) 疑似(0.0%) 下一(0.0%) 低头(0.0%) 举(0.0%)
第 2 步选了 '是' 候选:是(100.0%) 是中(0.0%) 是全(0.0%) <||end▁of▁sentence||>(0.0%) 是高(0.0%)
第 3 步选了 '地上' 候选:地上(100.0%) 地上的(0.0%) 明月(0.0%) 银河(0.0%) 地下(0.0%)
第 4 步选了 '霜' 候选:霜(100.0%) <||end▁of▁sentence||>(0.0%) 妆(0.0%) 箱(0.0%) 光(0.0%)
第 5 步选了 '。' 候选:<||end▁of▁sentence||>(96.4%) 。(3.6%)
问:请续写这句话,只写接下来的几个字:周末我打算去
答:爬山,顺便看看日出。
第 1 步选了 '爬山' 候选:爬山(74.0%) 图书馆(12.7%) 公园(5.9%) 山里(3.6%) 超市(1.1%)
第 2 步选了 ',' 候选:,(67.0%) 。(31.9%) <||end▁of▁sentence||>(0.7%) 放松(0.3%) /(0.1%)
第 3 步选了 '顺便' 候选:顺便(48.8%) 呼吸(44.9%) 放松(3.3%) 亲近(1.1%) 或者(0.8%)
第 4 步选了 '看看' 候选:看看(51.6%) 看(20.3%) 拍(19.0%) 透(3.9%) 呼吸(3.4%)
第 5 步选了 '日出' 候选:日出(86.1%) 山(4.7%) 日落(4.7%) 秋天的(2.7%) 春天的(0.3%)
第 6 步选了 '。' 候选:。(100.0%) <||end▁of▁sentence||>(0.0%)
「疑是地上霜」(地上の霜かと疑う)はどのステップも(四捨五入して)100% です。この詩句はモデルが学習データで数えきれないほど見ているので、迷う余地がありません。
「周末我打算去」(週末は~へ行くつもり)の続きは違います。第 3 ステップでは「顺便」(ついでに)と「呼吸」(呼吸する)がほぼ互角で、48.8% 対 44.9% です。今回は「顺便」が引かれたので「顺便看看日出」(ついでに日の出を見る)になりましたが、次に「呼吸」が引かれれば、その後は「呼吸新鲜空气」(新鮮な空気を吸う)になるかもしれません。一つの回答にはこうした分かれ道がたくさんあり、それぞれでどちらの道を選んだかが回答全体の行き先を決めます。これが同じ質問への回答が毎回違う理由で、第 3 課で詳しく扱います。
もう一つ細かい点があります。一つ目の質問の第 5 ステップで最も確率が高いのは <||end▁of▁sentence||>(96.4%)で、句点の確率はわずか 3.6% でしたが、結果として句点が引かれました。この end▁of▁sentence は「言い終わった」を意味する特別なトークンです。モデルはいつ止まるべきかを知っているわけではなく、あるステップでこの「終了」トークンを予測し、プログラムがそれを見て生成を止めているだけです。前の課の finish_reason: stop はこうして生まれます。
回答はどのように生成されるか
上で見たことをつなげると、モデルが回答を生成する過程は一つのループです。
输入:"床前明月光,下一句是什么?"
→ 模型算出下一个词元的概率分布 → 抽中"疑" → 把"疑"接到输入后面
输入:"床前明月光,下一句是什么?疑"
→ 模型算出下一个词元的概率分布 → 抽中"是" → 接到后面
输入:"床前明月光,下一句是什么?疑是"
→ ……
一直重复,直到抽中"结束"词元,或者达到 max_tokens 的上限
各ステップで、モデルはそれまでの内容すべて(あなたの質問と、それまでに書いた部分)を入力とし、トークンを一つだけ予測します。書いた 1 文字 1 文字が、次のステップの入力になります。
この仕組みから、いくつかの直接的な結果が生まれます。
- 出力が長いほど遅く、高くなる。100 トークン生成するには 100 ステップ回す必要があります。出力が入力より高い理由の一つです。
- 後戻りして直せない。前で 1 語間違えたら、後はその間違いに沿って書き進めるか、後の文で自分で辻褄を合わせるしかありません。思考モードが役立つ理由の一部は、モデルに「下書き」の場所を与えることにあります。下書きなら間違えて直してもよく、正式な回答には最終的な結論だけを書けばいいのです。
- 前の内容が後に影響する。モデルに「先に結論、次に理由」と頼むと、まず結論を書き、それからその結論の理由を探します。逆に「先に分析、次に結論」なら、結論は分析に基づいて書かれます。この二つの書き方で正答率が大きく違うことがあり、モジュール 02 第 3 課で実験します。
続きを書く機械からアシスタントへ
「次の単語を予測する」ことしかできないモデルは、もともと本当に続きを書くことしかできませんでした。「床前明月光」を渡せば詩の続きを書き、「Python はどう学べばいい?」を渡せば、似たような質問をさらに十個書き続けるかもしれません。ネット上の質問リストはよくそういう形をしているからです。
それが質問に答えるアシスタントになるまでには、おおよそ三つの段階がありました。
- 事前学習:大量のテキスト(Web ページ、本、コード)で「次のトークンの予測」を練習します。正確に予測するために、モデルは文法、事実、推論のパターンを身につけざるをえません。この段階でモデルが何を「知っている」かが決まり、計算資源の大部分がここに使われます。
- 指示チューニング:「質問 → よい回答」という会話の例を大量に使って学習を続け、「質問を見たら質問に答える」ことを覚えさせます。
- 選好アライメント:人や別のモデルに回答を採点させ、役に立ち、正直で、安全な回答をしやすくなるよう学習させます。
この三つの段階はモジュール 09 第 7 課で詳しく説明します。今は、アシスタントのふるまいは学習によって作られたもので、根底の仕組みは最初から最後まで変わらず、やはり次のトークンを予測していることだけ知っておいてください。
では、会話の system、user、assistant はどうやって「一つの文章」になってモデルに渡されるのでしょうか。DeepSeek が公開しているトークナイザーの設定に「チャットテンプレート」があり、メッセージのつなぎ方を定めています。細部を省くと、簡単な会話は次のようにつなげられます。
<|begin▁of▁sentence|>你是一个说话简短的助手。<|User|>Python 里的列表和元组有什么区别?<|Assistant|>
<|User|> や <|Assistant|> は語彙表の特別なトークンです。つなげた後もモデルがすることは続きを書くことで、<|Assistant|> の後ろから予測を続けます。前の課で「入力トークンがメッセージの文字数より多い」と述べましたが、増えた分はこうした書式マーカーです。(これはトークナイザーの圧縮ファイルに付属するテンプレートで、思考モードやツール呼び出しで使うテンプレートはもっと複雑ですが、考え方は同じです。)
ハルシネーションはどこから来るか
「次のトークンを予測する」ことを理解すれば、モデルがなぜでっち上げるのか、つまりよく言われるハルシネーション(hallucination)も理解できます。
モデルが各ステップで選ぶのは「次に現れそうな内容」であって、「真実の内容」ではありません。たいていは両者が一致します。学習データでは真実の内容が最もよく現れるからです。しかし、モデルがあることについてほとんど知らないとき、あるいは質問そのものが存在しない前提をほのめかしているとき、「最もそれらしい内容」と「真実の内容」は離れていきます。モデルには「わからない」というスイッチはありません。そういうときに「わかりません」と言うよう学習で身につけていない限りは。
deepseek-flash に httpx についての具体的な質問をしてみました(思考オフ)。
问:httpx 0.27.2 这个版本的更新日志里具体修复了哪些 bug?逐条列出。
答:httpx 0.27.2 的更新日志中,没有列出任何 bug 修复。
该版本(发布于 2024 年 9 月 25 日)是一个纯安全修复版本,只包含一条内容:
- 修复了一个安全漏洞:在特定条件下,之前已重定向的请求在后续重定向中会错误地保留
Authorization 头,可能导致凭证泄露到非同源主机。该问题通过 CVE-2024-46319
相关补丁修复,行为改为在跨源重定向时剥离 Authorization 头。
(元の回答にはさらに続きがありますが、ここでは省略しています。)
口調は断定的で、日付も CVE 番号も技術的な詳細もあり、とても信頼できそうに見えます。httpx のリポジトリの CHANGELOG.md を調べたところ、0.27.2 の項目の原文は次のとおりでした。
## 0.27.2 (27th August, 2024)
### Fixed
* Reintroduced supposedly-private `URLTypes` shortcut. (#2673)
日付も内容も違い、あの CVE 番号も「セキュリティ修正のみのバージョン」という説明もすべてでっち上げでした。モデルは「○○ライブラリのあるバージョンで、リダイレクト時に Authorization ヘッダーが漏れる問題を修正」といった更新履歴をたくさん見てきたので、「httpx の更新履歴らしく見える」文章を生成したのです。
公平のために言うと、モデルは明らかな罠に対してはよく対処しています。「httpx の Client には retry_on_status というパラメータがあるけど、どう使う?」と聞いたところ(このパラメータは存在しません)、「そのようなパラメータはない」とはっきり指摘し、正しい代替案まで示しました(httpx のドキュメントと、モデルが勧めたサードパーティライブラリのソースコードを確認したところ、どちらも正しいものでした)。httpx のコネクションプールを研究した学術論文を 3 本推薦してと頼んだときも、そういう論文は見つからないと答え、でっち上げさせないようにと注意までしてくれました。問題になるのは、ごく普通に見える質問のほうです。具体的なバージョン番号、具体的な経緯。具体的でマイナーなものほど、でっち上げられやすくなります。
ですから、次のことを覚えておいてください。
- 流暢で自信があることは、正しいことを意味しない。モデルがでっち上げた内容は、真実の内容と同じくらい流暢に読めます。
- 具体的なものほど確認する。バージョン番号、日付、数字、人名、API パラメータ、引用元は、どれも要注意です。
- 最も効果的なのは、モデルに「根拠のある」答え方をさせること。本物の資料を入力に入れて、資料に沿って答えさせる。これがモジュール 04 の RAG です。あるいは自分で調べられるようにする。これがモジュール 05 のエージェントです。第 1 部を貫くプロジェクト RepoBot では、v1 でこの問題をわざと残しておき、間違える様子を実際に見てもらってから、v2 で解決します。
練習問題
show_candidatesでいろいろな書き出しを試してください。「我最喜欢的编程语言是」(私の一番好きなプログラミング言語は)、「1+1=」、「从前有座山」(昔々ある山に)。どれの候補が最も集中していて、どれが最もばらけていますか。top_logprobsを 20 に変えて、「周末我打算去」の候補に意外なものがないか見てください。- あなたがよく知っているけれど比較的マイナーな、具体的な質問をモデルにしてください(あなたの会社の製品の細部、地元の小さな通り、よく知っているオープンソースライブラリの特定のバージョンなど)。思考をオフにしてでっち上げるかどうか見て、次に思考をオンにしてもう一度聞いてください。違いはありましたか。気づいたことを書き留めておきましょう。
確認テスト
1. モデルは回答をいつ終えるべきかを、どうやって知るのですか?
実は「知って」はいません。語彙表に特別な「終了」トークンがあり、モデルがあるステップでこのトークンを高い確率で予測して引き当てると、プログラムが生成を止めます。もう一つは max_tokens の上限に達して強制的に打ち切られる場合です。
2. モデルが書いている途中で前の間違いに気づいても、自分で直すのが難しいのはなぜですか?
モデルは各ステップでそれまでに書いた内容すべてを入力とし、トークンを一つ先に予測するだけで、後戻りして修正する仕組みを持っていません。前の間違いは後の入力になり、その後の予測に影響します。思考モードはこの問題をある程度和らげます。モデルは思考過程で下書きをして間違いを直し、最後の結論だけを正式な回答に書けるからです。
3. モデルが存在しない CVE 番号をでっち上げるのはなぜですか?
モデルが各ステップで選ぶのは「次に現れそうな内容」であって、「真実の内容」ではありません。「あるバージョンで、あるセキュリティ問題を修正。番号 CVE-xxxx」といった文章を大量に見てきたので、具体的な事実を知らないときは、そのパターンに沿ってもっともらしい内容を生成してしまいます。具体的でマイナーな事実ほどでっち上げられやすいので、この種の情報は必ず確認してください。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…