你大概看過 ChatML 那套 <|im_start|> / <|im_end|> 的標記,很多開源模型都沿用,久了你可能以為對話格式就那一種。
不是。OpenAI 為了推理重新設計了一套 Harmony,控制 token、角色、輸出結構全都不一樣。
這兩個不是同一個東西的不同寫法,是不同世代的對話協定。
ChatML:一則訊息 = 角色 + 內容
ChatML 是 GPT-3.5 / GPT-4 早期的對話格式,後來大量開源模型沿用。結構很單純:
<|im_start|>system
You are a helpful assistant.<|im_end|>
<|im_start|>user
台北今天天氣如何?<|im_end|>
<|im_start|>assistant
我沒有即時資料……<|im_end|>
一則訊息就是「角色 + 一段內容」,角色通常只有 system / user / assistant 三種。後期也有些模型在 ChatML 裡塞了 tool 或 function 角色來接工具呼叫,但那是後來才補上的;ChatML 設計之初就只有這三個。
它沒有原生的「思考 vs 答案」分流概念。模型如果要做推理鏈(chain-of-thought),思考過程跟最終回答全部混在同一段 content 裡——你拿到的是一坨文字,要自己想辦法切。
Harmony:為推理與工具設計的協定
Harmony 是 OpenAI 為了推理(reasoning)加工具呼叫(tool use)重新設計的格式。它跟 ChatML 的差異不在語法細節,而在它預設模型會「邊想邊用工具邊回答」。
多 channel:思考、工具、答案分流
這是最關鍵的差異。同一則 assistant 訊息,可以同時輸出到三個 channel:
analysis:思維鏈,模型的內部推理commentary:工具呼叫、或給工具的前導說明(preamble)final:真正要給使用者看的答案
ChatML 沒有這個概念。Harmony 把「模型在想什麼」跟「模型要回什麼」從協定層就拆開了。
更多角色,而且有階層
ChatML 通常只有三個角色,Harmony 有五個,而且是有順序的:
system > developer > user > assistant > tool
這個順序不是裝飾——指令衝突時靠它決定誰說了算。system 的指令蓋過 developer,developer 蓋過 user,依此類推。
不同的控制 token
Harmony 不用 <|im_start|> / <|im_end|>,改用一組更細的控制 token:
| token | 用途 |
|---|---|
| `< | start |
| `< | message |
| `< | channel |
| `< | end |
| `< | call |
| `< | return |
<|call|> 跟 <|return|> 是一對,它們決定模型「停下來」的意思。模型生成到一個段落停手時,吐的若是 <|return|>,代表答案講完了;吐的若是 <|call|>,代表它不是要收尾,而是要呼叫工具、等結果回來再繼續。inference server 就是看這兩個 token 決定該把回應交還給你,還是先去跑工具。
整套設計刻意對齊 OpenAI Responses API 的結構——這不是巧合,Harmony 本來就是為了那套 API 量身打造的。
但這對你幾乎沒影響
講完差異,重點來了:如果你是透過 API 呼叫,上面這些你都碰不到。
Harmony 的 raw token 是 inference server(vLLM、Ollama 這類)內部處理的東西。透過 API 或這類 provider 使用模型時,格式由 inference 端渲染,你不必碰。
你送進去的,是結構化的 messages 陣列:
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "台北今天天氣如何?"},
]
你拿回來的,是已經解析好的欄位:
# 從 response 拿到的結構,不是 raw token
response.choices[0].message.content # final channel
response.choices[0].message.reasoning_content # analysis channel
response.choices[0].message.tool_calls # commentary 裡的工具呼叫
response.choices[0].finish_reason
這裡 reasoning_content 要特別提一句:它不是 OpenAI 官方 API 的標準欄位,是 vLLM 這類自架 server 額外幫你把 analysis channel 解出來、塞進回應裡的。OpenAI 自家對 gpt-oss 的思維鏈多半藏起來不回傳,很多商用 provider 也一樣。所以這欄拿不拿得到、叫什麼名字,得看你打的是哪一家——別預設它一定在。
中間那段「把 messages 渲染成 Harmony 或 ChatML 的 raw token、再把模型吐出來的 raw token 解析回欄位」——完全是 inference server 的責任。JSON ↔ raw token 的轉換被 server 封裝掉了,你永遠停在 JSON 這一層。
這也是為什麼同一套 messages 邏輯可以打不同模型。你的呼叫端不需要知道後面是 Harmony 還是 ChatML——對你來說兩種格式都是透明的。
什麼時候你會真的碰到 raw 格式
透明歸透明,還是有兩種情況會讓你掉到 raw token 那一層:
一是完全不經 server,自己對 base 權重做 inference。 例如直接用 transformers 的 model.generate,沒有任何 inference server 幫你處理格式。這時你得自己套 Harmony renderer 或 chat template,否則模型行為會壞掉。
二是你想 debug「模型實際被餵了什麼」。 例如把 vLLM 開 verbose、或自己呼叫 tokenizer 的 apply_chat_template,印出渲染後的 raw prompt。但這是你主動去翻的,不是 API 正常會回給你的東西。
除了這兩種,你都停在 JSON。
順帶一提:這個道理不只 Harmony
Harmony 不是唯一一套被 API 包在底下的格式。Claude 也是——它不吃 ChatML 也不吃 Harmony,Anthropic 有自己的一套,但對呼叫 LLM API 的開發者來說並沒有影響:raw 格式它幫你包掉,你停在 JSON。
形狀當然不同。Messages API 的角色只有 user / assistant 兩種輪流,system 不是角色,是獨立的頂層參數;工具結果則靠 user 訊息裡的 tool_result block 傳回去。Harmony 那套「channel」,在這裡換成 content block 型別——文字是 text,工具呼叫是 tool_use,思維鏈是 thinking。
舉這個只是要說明:Harmony 跟 ChatML 的差別再大,也都被擋在 server 那一層。換家 provider、換套格式,你的 messages 邏輯照樣不用知道底下發生什麼事。
小結
Harmony 跟 ChatML 確實有差別——多 channel、角色階層、不同的控制 token,反映的是「對話格式」從單純收發訊息,演進到內建推理與工具呼叫的這段路。
但這個差別放在哪一層,決定了它跟你有沒有關係。如果你在寫 inference server,或自己跑 base 權重,這層你躲不掉。如果你只是拿 OpenAI-compatible API 接 LLM,那它就是 server 內部的實作細節——你看不到,也不需要看到。
知道底下是 Harmony 不是 ChatML,價值不在於你要去操作它,而在於當有一天模型行為怪怪的、或你決定繞過 server 自己跑模型時,你知道該往哪一層去找答案。