Day 18 - 一輪對話要花多少錢

Day 18 - 一輪對話要花多少錢

約 13,754 字

前面好幾天一直在講「這個很花錢」,但 KeSi 本身到現在都還不知道自己會花多少錢。今天幫 KeSi 裝一個電表,每跑完一輪就把這一輪的花費印出來,再實際跑一場八輪的對話,看看帳單長什麼樣子。

GitHub Repo:https://github.com/kaochenlong/KeSi

token 怎麼算?

API 的費用是照 token 算的,模型讀文字的時候不是一個字一個字讀,而是先把文字切成一小塊一小塊,每一塊就是一個 token,負責切的那個東西叫做 tokenizer。例如 Hello, how are you today? 這句英文,Claude 的 tokenizer 會算成 7 個 token,跟 5 個單字加 2 個標點的數量剛好一樣,不過 Anthropic 沒有公開它實際切在哪裡,我們只量得到總數。中文也不是一個字就是一個 token,我用同樣的方法單獨量了幾個字,扣掉每個請求固定會有的 token 之後:

內容 字數 token
你 1 2
檔 1 3
鑰 1 4
你好 2 3
檔案 2 4
你好,你今天過得如何? 11 12
請讀取這個檔案並告訴我它在做什麼。 17 22

單獨一個字就可能算成 2 到 4 個 token,每個字不一樣。而且 token 數不能一個字一個字加起來,「你」跟「好」單獨量都是 2,合在一起的「你好」卻只有 3,tokenizer 會看前後文決定怎麼切。整句來看,這兩句的 token 數都比字數多一些,但多多少要看句子裡是哪些字。

你送進去的內容算 input token,模型寫出來的回答算 output token,兩種 token 的計價方式不一樣。以待會跑的第一輪為例,我問 KeSi「這個專案在做什麼?」,它開了幾次工具讀檔案才回答,這一輪送出去的 system prompt、工具說明書、我的問題跟讀進來的檔案內容,加起來是 16,227 個 input token,KeSi 寫出來的回答跟工具呼叫是 530 個 output token。照 Haiku 4.5 的價錢,input 每百萬 token 1 美元、output 每百萬 token 5 美元,這一輪就是 0.0162 加 0.0027,大約 0.0189 美元。

同樣的意思,換一種語言寫,切出來的 token 數可能就不一樣。我在 examples/day18/demo.py 用官方免費的 Token Counting API 量了三組中英文,這支程式的後半段也會拿來跑整場八輪對話:

uv run --env-file .env examples/day18/demo.py
# 1. 同樣的意思,中文跟英文各算幾個 token
​
  每個請求的包裝:7
  [英] Hello, how are you today?  →  7
  [中] 你好,你今天過得如何?  →  12
​
  [英] Please read the file and tell me what it does.  →  11
  [中] 請讀取這個檔案並告訴我它在做什麼。  →  22
​
  [英] def add(a, b):\n    return a + b  →  13
  [中] def 相加(甲, 乙):\n    return 甲 + 乙  →  23

第一行的 7 是不管內容是什麼,每個請求都會多出來的固定 token,只送一個字母 x 也要算 8 個 token,扣掉 x 本身就是 7,後面每一組都已經先扣掉這 7 個。在這三組短句裡,中文大約是英文的 1.7 到 2 倍。第三組是同一段程式碼,只是把函式名稱跟參數名稱從英文換成中文,token 就從 13 變成 23。

不過這個倍數不用背,換成長一點的文件或是程式碼,比例都可能不一樣。要估某一份 system prompt 或某一段程式碼要花多少 token,最準的做法還是拿實際的內容去量。

量的時候,不要拿別家的 tokenizer 來算。OpenAI 出的 token 計算套件 tiktoken 很方便,但它切字的方式跟 Claude 不一樣,算出來的數字對不上。Claude 的話就用官方的 Token Counting API,文件是這樣寫的:

Token counting is free to use but subject to requests per minute rate limits based on your usage tier.

也就是用它量 token 不用錢,只是每分鐘能呼叫的次數有限制。

不過它算出來的是估計值,文件裡有這一段:

Token counts may include tokens added automatically by Anthropic for system optimizations. You are not billed for system-added tokens. Billing reflects only your content.

意思是量出來的數字裡,可能包含 Anthropic 自動加上的 token,這些 token 不收錢,所以量出來的數字可能比實際收費多一點。真正的帳要看每次 API 回應裡附的用量紀錄 usage,後面裝電表就是用它。

另外,在不同的模型下,一樣的內容 token 數不一定一樣。官方定價頁有這一段:

Claude 4.7 and later models and Claude Mythos Preview use a newer tokenizer that contributes to their improved performance on a wide range of tasks. This tokenizer produces approximately 30% more tokens for the same text. The exact increase depends on the content and workload shape. Claude Sonnet 4.6 and earlier models use the previous tokenizer.

這段是說,Claude 4.7 之後的模型換了一套新的 tokenizer,同樣一段文字交給新的 tokenizer 切,切出來的 token 大約會多三成,但實際多多少要看內容。KeSi 用的 Haiku 4.5 是比 4.7 還早的模型,不在這段說的範圍裡。

token 多了,就算每百萬 token 的單價一樣,同一段內容要付的錢也跟著變多。我拿同樣的內容,分別用 Haiku 4.5 跟用新 tokenizer 的 Claude Opus 5 去量:

內容Haiku 4.5Opus 5差多少
kesi.py 原始碼17,73421,046+18.7%
KeSi 的 README.md813941+15.7%
第 17 天的文章(中文為主)8,4168,977+6.7%

三份內容都變多了,但沒有一份剛好是三成,程式碼多了將近兩成,中文為主的文章只多了不到一成,這就是文件說的 The exact increase depends on the content。所以前面量的那些數字只適用 Haiku 4.5,換了模型就得重新量過,不能直接拿來套。

價目表

截至 2026 年 10 月,KeSi 一路在用的 Claude Haiku 4.5 在官方定價頁上是這個價:

項目 每百萬 token
input 1 美元
output 5 美元
5 分鐘快取寫入 1.25 美元
1 小時快取寫入 2 美元
快取命中 0.1 美元

表格下面那三項跟 prompt caching 有關,不過今天還用不到。prompt caching 是把每次都一樣的開頭內容先存在 Anthropic 那邊一段時間,5 分鐘或 1 小時,在這段時間裡再送一樣的內容就叫命中,算比較便宜的價錢,細節等幫 KeSi 裝快取那天再講。我把價格寫進程式:

# Claude Haiku 4.5 的費率(美元 / 百萬 token),價格會變,用之前先查官方定價頁
PRICE = {
    "input": 1.0,
    "output": 5.0,
    "cache_write": 1.25,  # 5 分鐘的快取寫入,是 input 的 1.25 倍
    "cache_read": 0.1,    # 快取命中,是 input 的一折
}

價格會變,所以註解裡寫了要先去查官方定價頁。程式裡只放了 5 分鐘的快取寫入價,因為之後要用的就是這一種。

每一輪花了多少?

每一次 API 回應都會帶著一份 usage,裡面寫著這次用了多少 input 跟 output token。我寫了一個 Meter 類別當 KeSi 的電表,做的事很簡單,就是把每一份 usage 記下來,需要的時候照價目表算錢:

class Meter:
    """記帳用的電表,每一次 API 請求的 usage 都記一筆。"""
​
    def __init__(self):
        self.calls = []
​
    def record(self, usage, *, partial=False):
        if usage is None:
            return
        self.calls.append({
            "input": usage.input_tokens or 0,
            "output": usage.output_tokens or 0,
            "cache_write": getattr(usage, "cache_creation_input_tokens", 0) or 0,
            "cache_read": getattr(usage, "cache_read_input_tokens", 0) or 0,
            "partial": partial,
        })
​
    def totals(self, since=0):
        total = {key: 0 for key in PRICE}
        for call in self.calls[since:]:
            for key in PRICE:
                total[key] += call[key]
        return total
​
    def cost(self, since=0):
        total = self.totals(since)
        return sum(total[key] / 1e6 * PRICE[key] for key in PRICE)
​
    def report(self, since=0):
        total = self.totals(since)
        requests = len(self.calls) - since
        partial = sum(call["partial"] for call in self.calls[since:])
        parts = [f"input {total['input']:,}", f"output {total['output']:,}"]
        if total["cache_read"] or total["cache_write"]:
            parts.append(f"快取讀 {total['cache_read']:,}")
            parts.append(f"快取寫 {total['cache_write']:,}")
        note = f",其中 {partial} 次只算到中斷前收到的部分" if partial else ""
        return (
            f"(本輪 {requests} 次請求,{'、'.join(parts)},"
            f"${self.cost(since):.4f}{note};累計 ${self.cost():.4f})"
        )
​
​
METER = Meter()

這裡要注意的是,電表記的是「一次 API 請求」,不是「一輪對話」。你按一次 Enter 是一輪,但模型如果先開工具、看完結果再回答,這一輪裡面就會送出好幾次請求,每一次都是一筆帳。since 參數就是為了這件事,main() 函式在送出問題之前,先記下電表目前已經記了幾筆(len(METER.calls)),跑完之後從那個位置開始算,就是「這一輪花了多少」。

usage 從哪裡來?第 17 天改成串流之後,stream_turn() 函式在收完整則回應時會呼叫 get_final_message(),帳就從它回傳的那則訊息的 usage 拿:

final = active.get_final_message()
METER.record(final.usage)
return final, finished, False

不過如果回答印到一半按下 Ctrl-C,就拿不到完整的回應了。串流文件有提到:

The token counts shown in the usage field of the message_delta event are cumulative.

意思是 message_delta 帶的是累計到那時候的總數,而這個事件要到回應快結束才會來,中途按下 Ctrl-C 就收不到了。所以這時候只能記快照裡目前的數字,並標成 partial,report() 會在那一行補一句「其中 1 次只算到中斷前收到的部分」。至於伺服器最後實際算了多少,電表不會知道,要對正式的帳還是得到 Anthropic 的網頁後台 Claude Console 看。

main() 把 run_agent() 包在 try/finally 裡,不管這一輪是正常結束還是被中斷,只要有收到 usage,最後都會把這一輪的帳印出來:

before = len(METER.calls)
try:
    # 顯示全部交給 run_agent,正常回應在串流時就已經印出來了
    run_agent(client, history)
except KeyboardInterrupt:
    seal_dangling_tool_use(history)
    print("\n(已中斷,可以接著問下一句)")
finally:
    # 這一輪有收到 usage 才印,請求在拿到 usage 之前就失敗的話不會多記一筆
    if len(METER.calls) > before:
        print(f"  {METER.report(before)}")

這樣每一輪的最後就會多一行帳單,下面是 demo.py 跑的第一輪,回答內容省略:

你 > 這個專案在做什麼?
KeSi > 我先看一下這個專案的結構。
  [執行工具] list_files({})
KeSi > 讓我看看 README 和主要的 Python 檔案。
  [執行工具] read_file({'file_path': 'README.md'})
  [執行工具] read_file({'file_path': 'pricing.py'})
  [執行工具] read_file({'file_path': 'rules.py'})
KeSi > ## 專案概況 ...
  (本輪 3 次請求,input 16,227、output 530,$0.0189;累計 $0.0189)

每輪一行帳單只看得到這一輪,想看整場加起來花了多少,就得自己往上捲去加。所以我在 main() 函式裡,跟 /reset、/permissions 這些指令放在一起,多加了一個 /cost 指令,輸入之後會印出這個 session 到目前為止的請求次數、token 數跟總金額,名字是照 Claude Code 看帳單的指令取的,後面會講到:

if user == "/cost":
    total = METER.totals()
    print(f"  這個 session 共 {len(METER.calls)} 次請求")
    print(f"  input {total['input']:,}、output {total['output']:,}")
    if total["cache_read"] or total["cache_write"]:
        print(
            f"  快取讀 {total['cache_read']:,}、"
            f"快取寫 {total['cache_write']:,}"
        )
    print(f"  合計 ${METER.cost():.4f}")
    continue

METER.totals() 不給 since 就是從第一筆算起,也就是整個 session 的總帳。快取那兩項現在都是 0,所以不會印出來。下面是接上第 12 天的假 API server 跑的,數字是 mock 固定回的 100 跟 20,不是真的用量:

你 > /cost
  這個 session 共 1 次請求
  input 100、output 20
  合計 $0.0002

跑到一半斷線了?

第一次跑八輪對話的時候,第一輪順利跑完,但後面某一輪在串流途中,整個程式就當掉了,最後一行錯誤訊息是:

httpx2.ReadError: [Errno 54] Connection reset by peer

這行的意思是 API 那一端把連線切斷了。網路連線偶爾斷掉是很正常的事,比較好的處理方式是告訴使用者連線斷了,然後讓他可以接著問下一句,在第 17 天處理 API 錯誤的時候就是這樣做的。但這次 KeSi 沒有處理到這個錯誤,整個程式直接結束,前面累積的對話日記也跟著不見了。第 17 天的 stream_turn() 只處理 anthropic.AnthropicError,但串流已經開始之後才斷線,丟出來的是 SDK 底層負責連線的 httpx2 套件的錯誤(httpx2.ReadError),SDK 沒有把它包成自己的錯誤,於是一路往上丟到程式外面。

所以我改了 stream_turn() 處理錯誤的那一段。原本它只接 anthropic.AnthropicError 這一種錯誤,現在改成不管是哪一種錯誤都接下來,也就是 except Exception。只要串流已經開始,就照第 17 天的做法處理:已經印完的文字留在日記裡,還沒收完的部分丟掉,再告訴使用者發生了什麼錯誤,然後回到等你輸入下一句的地方:

    except Exception as exc:
        # 串流開始之後斷線,丟出來的是底層 HTTP 套件的錯誤,不是 AnthropicError
        if stream is None:
            raise
        # 後面跟第 17 天一樣,包成 StreamFailure 往外丟

如果是串流還沒開始就失敗,例如一開始就連不上,stream 還是 None,錯誤會照舊往上丟給 run_agent() 處理。

改完之後要驗證它有沒有用,但網路什麼時候會斷線沒辦法控制,不能等它剛好斷給我看,所以我另外寫了 examples/day18/check.py,用一個假的串流在第一段文字收完之後丟出錯誤。這裡用 Python 內建的 ConnectionResetError 代替,重點是它跟 httpx2.ReadError 一樣都不是 AnthropicError。這支程式順便也驗電表的加總跟價錢,下面只節錄斷線那一段:

uv run examples/day18/check.py
# 3. 串流途中斷線,KeSi 不會當掉
KeSi > 我來讀一下
KeSi > 錯誤:ConnectionResetError:[Errno 54] Connection reset by peer
PASS 沒有往外丟錯誤
PASS 日記只留下完成的文字  留下 1 塊
PASS 最後一則告訴使用者斷線了  錯誤:ConnectionResetError:[Errno 54] Connection reset by peer
PASS 斷線前收到的 usage 有記下來,標成 partial  1 筆
​
# 9/9 通過

這支不會呼叫真的 API,不花錢。改完之後重新跑一次八輪對話,這次沒有斷線,八輪都跑完了,下面的數字就是這一次的結果。

八輪對話的帳單

這是 demo.py 的後半段,這裡我用第 13 天那個訂單試算專案,連續問八個問題,問題寫在 QUESTIONS 裡,從「這個專案在做什麼?」問到「整理一下你到目前為止看到的東西。」,模型要不要開工具由它自己決定:

 輪  請求     input   output       本輪       累計
  1     3    16,227      530  $  0.0189  $  0.0189
  2     2    12,873      543  $  0.0156  $  0.0345
  3     2    14,612      316  $  0.0162  $  0.0507
  4     1     7,757      414  $  0.0098  $  0.0605
  5     2    16,588      434  $  0.0188  $  0.0792
  6     2    17,860      317  $  0.0194  $  0.0987
  7     1     9,354      418  $  0.0114  $  0.1101
  8     1     9,792      584  $  0.0127  $  0.1228

日記一輪比一輪厚,照理每一輪的 input 應該越來越多,可是第 6 輪是 17,860,第 7 輪反而只剩 9,354。這是因為 input 那一欄是「這一輪所有請求加起來」,而每一輪的請求次數不一樣。第 6 輪送了兩次請求,模型先開工具、看完結果再回答,兩次請求各自都帶著當時整本日記。第 7 輪模型直接回答,只送了一次。

要看出日記越來越厚,得拿請求次數一樣的輪次來比。只送一次請求的是第 4、7、8 輪,input 從 7,757、9,354 漲到 9,792;送兩次的是第 2、3、5、6 輪,從 12,873、14,612、16,588 漲到 17,860。請求次數一樣,每一輪都比前一輪多一點,因為日記一直在變厚。

在這場對話裡,某一輪特別貴,是因為那一輪多送了請求,模型每多開一次工具,就多送一次整本日記。

錢花在哪裡?

整場算下來的錢錢差不多是這樣:

# 總計
​
  日記 28 則,API 請求 14 次
  input 105,063 token、output 3,556 token
  費用 $0.1228
    其中 input $0.1051(86%)、output $0.0178(14%)
  最後一次請求的 input:9,792
  固定開銷 5,092 × 14 次 = 71,288,佔全部 input 的 67.9%

input 佔了帳單的 86%。output 的單價是 input 的五倍,照理說應該是 output 比較貴,但是這場對話送出去的 input 有 105,063 個 token,output 只有 3,556 個,數量差了將近 30 倍,所以就算單價貴五倍也追不上。

最後一次請求的 input 是 9,792,這是送出第 8 題時模型收到的全部內容,包含 system prompt、工具說明書跟當時的整本日記。而整場累計送出去的 input 是 105,063,大約是它的 10.7 倍。如果每段內容只付一次錢,整場大約只要付 9,792 個 token 的 input,但每一次請求都要把當時的整本日記重新送一次,就變成十倍多。

這 105,063 裡面有一大塊是固定的。我用 Token Counting API 量了工具說明書加上 system prompt,是 5,092 個 token,不管問什麼,每一次請求都要帶。這是估計值,而且 system prompt 裡有日期跟作業系統,你量出來的數字會有一點不同。14 次請求就是 71,288 個 token,佔全部 input 的 67.9%,換算成錢是 0.0713 美元,佔整張帳單的 58%。

之後要裝的 prompt caching,處理的就是這一塊。照價目表粗估,第一次寫入快取要付 1.25 倍,之後 13 次命中都只要一折,5,092 × 1.25 + 5,092 × 13 × 0.1 等於大約 12,985 個 token 的 input 價,也就是 0.013 美元左右,前提是每次請求都在 5 分鐘內送出,快取還沒過期。這樣比原本的 0.0713 美元少了 0.058 美元,差不多是整張帳單的 47%。這只是照這次的數字算出來的估計,實際能省多少,等真的裝上快取再量。

剩下 input 的 32% 是一直在變的日記,讀進來的檔案內容、指令的輸出都在裡面,全部都會留在日記裡,每一次請求都跟著重送一次。

這兩節的數字是我在 2026 年 10 月 1 日同一次實際跑的結果,八輪加起來大約 0.12 美元。模型每次要不要開工具、回答多長都不一樣,你自己跑的請求次數跟金額都可能跟我的不同。

這樣算便宜嗎?

八輪對話 0.12 美元,聽起來很便宜,但這是很理想的情況,因為 KeSi 用的是比較便宜的 Haiku、專案只有幾個小檔案、八輪就結束了。

同樣的 token 數換成 Claude Opus 5(定價頁上 input 每百萬 5 美元、output 25 美元)來算就是大約 0.61 美元,而且 Opus 5 用的是前面講的新 tokenizer,同樣的內容 token 數還會再多一些。真實的專案檔案大得多,讀一個檔案就可能好幾千個 token,對話也不會八輪就結束。

KeSi 目前不會刪減或整理日記,每一次請求都帶著當時整本日記,日記會一直變長,所以每一次請求都會比前一次再貴一點。Claude Code 就不是這樣,文件有提到:

Claude Code manages context automatically as you approach the limit. It clears older tool outputs first, then summarizes the conversation if needed.

也就是 context window(模型一次讀得下的上限)快滿的時候,會先清掉比較舊的工具輸出,需要的話再把對話整理成摘要。

正版怎麼看帳單?

Claude Code 是用 /usage 指令看目前這個 session 用了多少 token,/cost 是它的別名。文件對裡面的金額有一段說明:

The Session block in /usage shows API token usage and is intended for API users. Claude Max and Pro subscribers have usage included in their subscription, so the session cost figure isn't relevant for billing purposes.

意思是那個金額是給用 API 計費的人看的,如果你是用 Max 或 Pro 訂閱,用量已經包含在訂閱裡,那個數字跟你實際付的錢沒有關係。同一頁還寫著:

Claude Code computes the dollar figure locally from token counts at list price, unless a modelPricing table is in effect.

也就是那個金額是在你的電腦上照定價算出來的,除非你自己設定了另一張價目表,要對正式的帳還是要回 Claude Console 看。

另外社群也有像 ccusage 這樣的工具,直接讀 Claude Code 存在 ~/.claude/projects/ 底下的 .jsonl 檔,也就是一行一筆 JSON 的對話紀錄,整理出每天、每個 session 的用量跟估算金額。這些紀錄就放在你自己的硬碟上,可以自己拿來整理,只是算出來的一樣是估計值,不是正式帳單。

小結

今天幫 KeSi 裝上了電表,每一次 API 請求的 usage 都會記下來,每一輪結束印一行帳單,/cost 可以看整場的總帳。也順便把串流中途斷線會讓程式當掉的問題補起來了。

這場 0.12 美元的對話看得出三件事:

  1. input 佔了 86%。output 單價貴五倍,但 input 的量多了將近 30 倍。

  2. 整場送出去的 input 是最後一次請求的 10.7 倍,因為每一次請求都要重送整本日記。

  3. input 裡有 67.9% 是每次都一樣的工具說明書跟 system prompt。

明天先處理日記裡的工具結果,讀進來的檔案內容會一直留在日記裡跟著重送,看看能不能讓它小一點。

咱們下集見 :)

合作夥伴

留言討論