模型只做一件事:預測下一個詞元
讓模型亮出每一步的候選詞元和機率,看一段回答是怎麼一個詞元一個詞元生成的;再講它從續寫機器變成助手的過程,以及幻覺從哪裡來。
- 約 35 分鐘
- 難度:入門
- 實測:2026-09-14 deepseek-flash
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
你大概聽過這個說法:"大模型本質上就是在預測下一個詞。"這句話是對的,但只聽這一句,很難想像一個"預測下一個詞"的程式怎麼能寫程式碼、解數學題、回答問題。更重要的是,這句話能解釋很多你以後會遇到的現象:為什麼它會一本正經地編造,為什麼同一個問題有時對有時錯,為什麼提示詞的寫法對結果影響那麼大。
這一課我們讓模型把它每一步的"想法"亮出來。
看它一步一步地選
上一課提到,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 的上限
每一步,模型都把到目前為止的全部內容(你的問題加上它已經寫出來的部分)當作輸入,只預測一個詞元。它寫出的每個字都會成為下一步的輸入。
這個機制帶來幾個直接的後果:
- 輸出越長越慢,越貴。生成 100 個詞元就要跑 100 步。這也是輸出比輸入貴的原因之一。
- 它沒法回頭改。前面寫錯了一個詞,後面只能順著這個錯誤往下寫,或者在後文裡自己圓回來。思考模式之所以有用,部分原因是給了模型一個"打草稿"的地方,草稿裡可以寫錯再糾正,正式回答只寫最後的結論。
- 前面的內容會影響後面。如果你讓模型"先給結論,再給理由",它會先寫結論,再為這個結論找理由;反過來"先分析,再給結論",結論是在分析的基礎上寫出來的。這兩種寫法的準確率可能差很多,02 模組第 3 課會做實驗。
從續寫機器到助手
一個只會"預測下一個詞"的模型,最初真的只會續寫。你給它"床前明月光",它會接著寫詩;你給它"怎麼學 Python?",它可能接著寫出另外十個類似的問題,因為網上的問題列表經常長這樣。
它能變成一個會回答問題的助手,大致經過了三個階段:
- 預訓練:在海量文本(網頁、書、程式碼)上練習"預測下一個詞元"。為了預測得準,模型不得不學會語法、事實、推理的模式。這一步決定了模型"知道"什麼,花掉了絕大部分算力。
- 指令微調:用大量"問題 → 好回答"的對話樣本繼續訓練,讓它學會"看到問題就回答問題"。
- 偏好對齊:讓人或者別的模型給回答打分,訓練它更傾向於給出有幫助、誠實、安全的回答。
第 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 連線池的學術論文,它也說沒有找到這樣的論文,並提醒我不要讓它編。問題出在那些看起來很正常的問題上:一個具體的版本號、一段具體的歷史。越具體、越冷門,越容易編。
所以要記住:
- 流暢和自信不代表正確。模型編造的內容和真實的內容讀起來一樣流暢。
- 越具體越要核實。版本號、日期、數字、人名、API 參數、引用出處,都是重災區。
- 最有效的辦法是讓模型"有據可查"。把真實的資料放進輸入裡,讓它照著資料回答,這就是 04 模組的 RAG;或者讓它能自己去查,這是 05 模組的智慧體。第一部分的貫穿專案 RepoBot,v1 版會故意保留這個問題,讓你親眼看到它答錯,再在 v2 裡解決。
練習
- 用
show_candidates試幾個不同的開頭:"我最喜歡的程式語言是"、"1+1="、"從前有座山"。哪個的候選最集中,哪個最分散? - 把
top_logprobs改成 20,看看"週末我打算去"的候選裡有沒有你意想不到的。 - 問模型一個你非常瞭解、但比較冷門的具體問題(你們公司的產品細節、你家鄉的一條小街、你熟悉的一個開源庫的某個版本),關掉思考,看它會不會編。再開著思考問一次,有沒有區別?把你的發現記下來。
自測
1. 模型怎麼知道一段回答應該在什麼時候結束?
它並不"知道"。詞表裡有一個特殊的"結束"詞元,模型在某一步預測出這個詞元的機率很高並抽中了它,程式就停止生成。另一種情況是達到了 max_tokens 上限,被強行截斷。
2. 為什麼模型寫到一半發現前面寫錯了,很難自己改正?
模型每一步都把之前寫出的所有內容當作輸入,只往後預測一個詞元,沒有回頭修改的機制。前面的錯誤會成為後面的輸入,影響後續的預測。思考模式在一定程度上緩解了這個問題:模型可以在思考過程裡打草稿、糾錯,最後只把結論寫進正式回答。
3. 模型為什麼會編造一個不存在的 CVE 編號?
模型每一步選的是"最像接下來會出現的內容",而不是"真實的內容"。它見過大量"某版本修復了某安全問題,編號 CVE-xxxx"這樣的文字,當它對具體事實不瞭解時,就會按這種模式生成一段看起來合理的內容。越具體、越冷門的事實越容易被編造,所以這類資訊一定要核實。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…