Day 01 先把大型語言模型與 reasoning model 放回同一條訓練路徑上,Day 02 則暫時停在這條路徑的起點,讓 Qwen/Qwen3-0.6B-Base 接著寫完 The answer to 2 + 3 is。它確實選到了正確答案 5,卻沒有像我們習慣的 chatbot 一樣回答完就停下來,反而又開始替自己出下一道題。
依照這個順序,今天便輪到 Base、Instruct 與 Reasoning。這三個名稱看似像得像三種完全不同的模型,其實更接近同一條訓練路徑上的不同位置。在這裡我們可使用三條不同的生成路徑來仔細觀察其中差異:第一條仍然使用昨天的 Qwen/Qwen3-0.6B-Base,直接把原始文字交給模型續寫;後面兩條則使用官方的 Qwen/Qwen3-0.6B,先套用 chat template,再分別關閉與開啟 thinking mode。換句話說,我們實際載入的是兩組權重,並讓後訓練模型扮演了 non-thinking 和 thinking 兩種角色。
Base model 完成預訓練後,最熟悉的工作仍是預測文字接下來會長成什麼樣子;後訓練模型則學到該怎麼遵循使用者指令、像個稱職的助理一樣回答問題,也學過不同的回答形式。至於 Qwen3 的 thinking switch,並不是臨時替模型裝上一顆新的腦袋。enable_thinking=False 和 enable_thinking=True 使用相同的 post-trained checkpoint,只是 chat template 與生成模式告訴它,這次應該直接回答,或者先產生一段 <think>...</think>,再交出最後答案。
我一位睿智的好友在這幾年裡時常用不同的邏輯問題、語言問題去考驗和測試歷代 LLM,其中有些涉及符號邏輯運算就不在這系列的文章使用,我們姑且使用這個英文陷阱題來觀察模型表現:
A farmer has 17 sheep. All but 9 run away. How many sheep remain?
Explain briefly and finish with exactly `Answer: <number>`.
all but 9 run away 不是「有 9 隻跑掉」,而是「除了 9 隻以外都跑掉」,所以留在原地的就是 9 隻。這題沒有複雜算式,真正要做的是先讀懂句子;如果一開始把語意弄反,後面算得再仔細也只會得到 17 - 9 = 8。
Base model 沒有接收 chat template,也沒有經過讓它穩定遵守 user、assistant 角色的後訓練,我們仍然像 day 02 那樣把題目當成普通文字送進去。為了讓這條路徑可以固定重現,我也保留 greedy decoding:
base_inputs = base_tokenizer(
prompt,
return_tensors="pt",
).to(base_model.device)
base_ids = base_model.generate(
**base_inputs,
max_new_tokens=96,
do_sample=False,
)
到了後訓練模型,程式真正改變的地方是 apply_chat_template 裡的 enable_thinking。兩種模式都使用 sampling,而且各自沿用 Qwen model card 建議的 temperature、top-p 和 top-k;我沒有因為昨天使用 greedy,就把它硬套在 thinking model 上。
sampling = {
False: dict(max_new_tokens=128, temperature=0.7, top_p=0.8, top_k=20),
True: dict(max_new_tokens=512, temperature=0.6, top_p=0.95, top_k=20),
}
for enable_thinking in (False, True):
torch.manual_seed(42)
rendered = tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}],
tokenize=False,
add_generation_prompt=True,
enable_thinking=enable_thinking,
)
inputs = tokenizer(rendered, return_tensors="pt").to(model.device)
output_ids = model.generate(
**inputs,
do_sample=True,
**sampling[enable_thinking],
)
把 seed 固定為 42,實際得到的結果整理如下。Base model 用完了 96 個 token,文字裡算出 17 - 9 = 8,但沒有照指令產生 Answer: <number>,所以答案擷取程式沒有讀到合格的 final answer。關閉 thinking 的 Qwen3 只用了 39 個 token,迅速地交出錯誤答案;開啟 thinking 後,輸出增加到 374 個 token,答案卻沒有改變。
Base / 96 tokens
... 17 - 9 = 8
extracted answer: none
non-thinking / 39 tokens
Answer: <8>
thinking / 374 tokens
... But maybe there's something I'm missing here?
... 17 minus 9 is 8. Yep, that makes sense.
Answer: <8>
可以看到 thinking output 說「也許漏掉了什麼」時,一度以為它終於發現這是一道語意陷阱。它確實停下來重新檢查,也問了自己好幾次是不是還有別的解釋,問題是每一次檢查都從同一個錯誤前提開始:它先把句子改寫成「17 隻羊裡有 9 隻跑掉」,再確認一次 17 減 9 等於 8。想得更久沒有鬆動最初的誤解,只是把那個誤解重複得更有盲目的把握。
這個結果對反而比順利答對更有用,因為 reasoning model 不能靠輸出長短來辨認。Thinking mode 的確讓模型多使用一些 token,這些 token 可以拿來拆解問題、嘗試別條路或檢查答案,但也可能只是在錯誤方向上多走幾圈。況且我們看到的 <think> 仍然是模型生成出來的文字,可以分析它重複了哪些假設,卻不能直接當成模型內部運作的忠實錄影,更不能因為句子裡出現「等等」或「讓我再檢查一次」,就認定推理真的發生了。
當然,這一題也不能反過來證明 thinking model 比 base model 更差。Base 接到的是 raw completion,後訓練模型接到的是 chat template,兩種 thinking 設定又依照官方建議使用不同的 sampling 和 token budget;這次比較的目的,是看清楚權重階段與輸入方式如何改變可觀察的回答,而不是用一題排出模型排行榜。真的要比較 reasoning accuracy,至少得準備一批事先保留的題目,固定答案擷取與評分規則,最後才去看各自答對多少題。這件事會在後面的 frozen baseline 正式處理。
Sebastian Raschka 在《Build a Reasoning Model (From Scratch)》第一章先整理 Base、Instruct 與 Reasoning model 所在的訓練路徑,第二章再回到 pretrained Qwen3,從最基本的文字生成開始。本篇沿著相同順序,用官方 post-trained Qwen3 作為參照,把三個位置的輸入與輸出攤開來看;它不是我們訓練出來的成果,只是讓「後訓練會改變什麼」不必停留在名詞解釋。Qwen3 的 thinking switch 與建議 sampling 可以查閱官方 model card,原書的章節和程式安排則收在 companion hub 與官方 repository。
明天預計會重新回到 Qwen/Qwen3-0.6B-Base,既然這次主打了 From Scratch,那我們得仔細看看那些已經下載好的權重裡面,到底裝著什麼。