跳到主要內容
技術

Harmony vs ChatML:兩代對話協定差在哪

很多人預設開源模型都吃 ChatML,但 OpenAI 為推理設計的 Harmony 是另一套協定。本文拆解兩者的世代差異——多 channel、角色階層、控制 token——以及為什麼你用 OpenAI-compatible API 呼叫時根本碰不到這層。

你大概看過 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 裡塞了 toolfunction 角色來接工具呼叫,但那是後來才補上的;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 的指令蓋過 developerdeveloper 蓋過 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——對你來說兩種格式都是透明的。

你的 Agent 程式透過 OpenAI API 收發 JSON,底層由模型自己跑 ChatML 或 Harmony


什麼時候你會真的碰到 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 自己跑模型時,你知道該往哪一層去找答案。