用 XML 標籤騙 AI 把思考寫出來:native thinking 之前的 CoT 技巧
在模型還沒有內建思考通道的年代,prompt engineering 靠 XML 標籤把推理「寫出來」來降低幻覺。即使現在 native extended thinking 已經更好,這套用標籤分隔輸入、控制輸出的技巧,在寫 hook 與 agent 時依然好用。
- AI coding
- Claude
- Prompt Engineering
- Chain of Thought
- XML
最早看到用 XML 標籤要求 AI 做區塊分段,是在查 prompt engineering 資料的時候。那是一種觸發 chain of thought(CoT)的方法——透過強迫 AI 先把推理寫出來,來降低它憑空生成、產生幻覺的機率。
做法:把 thinking 和 answer 用標籤分開
最基本的做法,是用 XML 標籤把「思考」和「答案」兩個區塊分開:要求 AI 把推理過程寫進 <thinking> 標籤,再把推論出來的結論寫進 <answer> 標籤,分別列出。
請依照以下格式回答我的問題:
先在 <thinking> 標籤裡寫下你的完整推理過程,把每一步都列出來;
完成推理後,再把最終結論寫進 <answer> 標籤。
<thinking>
(在這裡逐步推理,可以自由探索、修正)
</thinking>
<answer>
(在這裡寫最終答案)
</answer>這樣分隔有兩個實際價值:<thinking> 像一塊草稿區,讓 AI 可以在裡面自由探索、列舉可能性、自我修正,不必擔心中間過程被當成最終答案;而標籤帶來的可解析性(parseability),則讓程式能輕鬆地把 <answer> 的內容單獨抽出來用。
為什麼「寫出來」這件事這麼關鍵
這裡有一個我一開始也誤解的點:這些標籤不是發生在 AI 內部某個看不見的思考階段,它們就出現在 AI 輸出的文字裡。
原因要從 LLM 的特性講起。LLM 是 autoregressive 模型,每個 token 的生成都依賴前面已經生成的 token。把推理寫出來,等於讓每一步都成為下一步的接續依據,後面的推論才有東西可以踩。所以對生成式 AI 來說,如果不輸出思考,就不會發生真正的思考。Anthropic 早期專講 chain of thought 的 prompt 文件裡,就有一句講得很直接:「Always have Claude output its thinking. Without outputting its thought process, no thinking occurs!」(這頁後來被併入了「提示最佳實踐」總頁,這句斬釘截鐵的話在新版裡已經收斂掉了,但概念仍在。)
也因此,減少幻覺的功勞其實歸於 CoT 本身(強迫逐步推理),而不是 XML 標籤。幻覺常來自模型跳過推理、直接給出一個看似合理的答案;CoT 逼它先把推理鏈寫出來,標籤只是讓這個結構更清晰、更好解析而已。
三種 CoT 層次
同樣是觸發 CoT,可以由淺到深分成三個層次。
基本:一句「逐步思考」。
請一步一步思考後,再回答這個問題。引導:給出具體步驟。
請依照以下步驟分析這段程式碼:
1. 先找出函式的輸入與輸出
2. 列出可能的邊界情況(空值、極大值、非預期型別)
3. 逐一檢查每個邊界情況是否有被處理
4. 根據以上分析,總結這段程式碼潛在的問題結構化:用 XML 標籤把推理和答案分離。
請分析這段程式碼。
<thinking>
依序檢查:輸入與輸出、邊界情況、潛在 bug。把推理過程寫在這裡。
</thinking>
<answer>
用條列式總結你找到的問題,以及對應的修正建議。
</answer>現在還需要這招嗎
老實說,純粹為了「觸發思考」的話,這個技巧現在已經不太需要了。像 Claude Opus 提供的 native extended thinking,本身就是更好的機制——模型有了真正的思考通道,不必再靠 prompt 騙它把推理寫出來。
但這套「native thinking 還不存在時的替代方案」並沒有因此失去價值。一來,當你用的底層模型還沒有真正的思考通道時,它仍是讓模型乖乖推理的有效手段;二來,XML 標籤的用途遠不只觸發 CoT。以下幾個場景,即使開了 native thinking 也還是很好用。
1. 分隔輸入區塊
當 prompt 裡混了好幾種性質不同的東西——指令、要處理的資料、範例、上下文——純文字很容易讓模型搞混「哪段是要我做的事、哪段是我要處理的料」。用標籤把它們框起來,邊界就清楚了:
<instructions>
把以下 JSON 的欄位名稱從 snake_case 轉成 camelCase,
並輸出轉換後的 TypeScript interface。
</instructions>
<context>
這個 interface 會用在前端,專案統一使用 camelCase 命名。
</context>
<example>
輸入:{ "user_name": "Sean" }
輸出:interface User { userName: string }
</example>
<data>
{
"user_id": 1,
"first_name": "Sean",
"created_at": "2026-06-28"
}
</data>2. 控制輸出結構
如果 prompt 的輸出要被後續程式接走,要求模型把答案包進固定標籤,會比叫它「請用 JSON」在某些情境更穩——尤其當答案本身含有會破壞 JSON 的字元(程式碼、引號、換行)時。
這比較像是你在寫一個 hook,或寫 agent 的時候:你需要請 AI 生成一段特定內容,而那段內容要被程式拿去做後續處理。這時 XML 標籤就能很方便地把需要引用的部分分離出來。
(AI 的輸出)
好的,這是我為你生成的設定檔:
<output>
{
"port": 3000,
"host": "localhost"
}
</output>
如果需要調整 port,再跟我說。程式端只要鎖定標籤把中間那段抓出來就好,前後那些給人看的客套話完全不影響解析:
const match = response.match(/<output>([\s\S]*?)<\/output>/);
const config = match ? JSON.parse(match[1].trim()) : null;3. 框出範例的邊界
給多個範例時用標籤框起來,模型比較不會把「範例的內容」誤當成「真正要處理的輸入」:
<example>
<input>...</input>
<output>...</output>
</example>4. 引導「分段輸出」
這點跟 CoT 沾邊但不同。即使開了 native thinking,有時你仍會想讓模型在最終輸出裡分區呈現,例如 <analysis> + <recommendation> + <risks>。這不是為了觸發推理(那交給 thinking),而是為了讓給人看的成品結構清楚:
<analysis>
目前這個查詢在每次 render 都會重新發送,沒有任何快取。
</analysis>
<recommendation>
改用 React Query,把 staleTime 設為 5 分鐘。
</recommendation>
<risks>
若資料需要即時更新,快取可能造成顯示延遲,需評估可接受的範圍。
</risks>延伸一下,也可以請 AI 在輸出時,把每段內容的信心程度用標籤包起來,區隔出哪些是確定的、哪些是驗證過的、哪些只是推論。這樣讀它的回答時,就比較容易注意到哪些內容風險較高、不能盲信:
<certain>
這個函式在 input 為空陣列時會回傳 undefined。(已從程式碼確認)
</certain>
<verified>
修改後測試全數通過。(已實際執行 test)
</verified>
<inferred>
這個改動「應該」不會影響效能,但我沒有實際做 profiling。(推論,未驗證)
</inferred>標籤本身只是個容器,但當你開始用它替 AI 的輸出標注「這句話有多可信」,它就從一個格式技巧,變成了一道幫你過濾風險的防線。