嗨大家,我是 Debuguy。
經過前面的努力,我們的 ChatBot 已經能思考、能協作、能使用工具。但今天要來面對一個很現實的問題:「解決一個 issue 要多少錢?」
我們來複習一下年代久遠的梗
我使用 AI ChatBot 的原則是:「MDFK 用爆」。
沒錯,就是 MDFK 用爆,老子才不管甚麼 token 成本的,每次有什麼事都 @ bot 就對了。簡單問題也要 ultra think,普通 query 也懶得寫直接 @bot 然後 ultra think 用爆。Context window 塞好塞滿,thinking tokens 是什麼我不知道。
我還記得,那天上班剛滿一個月,主管跑來跟我說:「這個月 Gemini API 帳單爆了,你有頭緒嗎?」
我 TMD 怎麼會知道。
作為一名負責任的工程師,是不應該讓這種事發生的
但如果沒有從使用者體驗的角度思考
很有可能拿到帳單才發現問題大條了
當 LLM 對話結束後,使用者看到的只有結果:
🤖 Bot: @Debuguy 根據人事總處網站的最新資訊,今天台北市因颱風宣布停班停課...
但背後實際發生的成本消耗:
「使用者爽爽問,但這都是需要支付費用的...」
讓我們看看 GenKit Flow 的關鍵改動:
async ({ messages }, { sendChunk }) => {
const { newMessages, history } = organizeMessages(messages);
const tools = await host.getActiveTools(ai);
const resources = await host.getActiveResources(ai);
+
+ // 關鍵:設置 token 計算變數
+ let inputTokens = 0;
+ let thoughtsTokens = 0;
+ let outputTokens = 0;
const { stream, response } = ai.prompt('chatbot').stream({
botUserId: process.env['SLACK_BOT_USER_ID']!,
prompt: newMessages,
}, {
messages: history,
tools,
resources,
toolChoice: 'auto',
+ // middleware 攔截 token 使用量
+ use: [
+ async (req, next) => {
+ const result = await next(req);
+ inputTokens += result.usage?.inputTokens ?? 0;
+ thoughtsTokens += result.usage?.thoughtsTokens ?? 0;
+ outputTokens += result.usage?.outputTokens ?? 0;
+ return result;
+ }
+ ],
});
const result = await response;
- return (await response).text;
+ return {
+ text: result.text,
+ usage: {
+ inputTokens,
+ thoughtsTokens,
+ outputTokens,
+ },
+ };
}
);
需要這樣設計的原因:
use
攔截每次 API 調用,累計 token 使用量原本簡陋的回覆升級成專業的 Slack Block Kit 格式:
- await say({ text: response, thread_ts: event.thread_ts || event.ts });
+ await say({
+ text: response.text,
+ blocks: [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": response.text
+ }
+ },
+ {
+ "type": "divider" // 分隔線
+ },
+ {
+ "type": "rich_text",
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {
+ "type": "text",
+ "text": "usage:\n"
+ }
+ ]
+ },
+ {
+ "type": "rich_text_list",
+ "style": "bullet",
+ "indent": 0,
+ "elements": [
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {
+ "type": "text",
+ "text": "input tokens: " + response.usage.inputTokens
+ }
+ ]
+ },
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {
+ "type": "text",
+ "text": "thoughts tokens: " + response.usage.thoughtsTokens
+ }
+ ]
+ },
+ {
+ "type": "rich_text_section",
+ "elements": [
+ {
+ "type": "text",
+ "text": "output tokens: " + response.usage.outputTokens
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ thread_ts: event.thread_ts || event.ts
+ });
Slack Block Kit 的好處:
如果想要格式化的更美
可以先用 Slack Block Kit Builder 測試
👤 Debuguy: @bot 幫我查一下今天天氣怎樣?
🤖 Bot: 今天台北多雲,溫度 25-28 度,降雨機率 30%。
👤 Debuguy: 那明天呢?
🤖 Bot: 明天晴朗,溫度 26-30 度...
👤 Debuguy: 後天呢?
🤖 Bot: 後天小雨...
使用者體驗:😐 想問就問,沒感覺
👤 Debuguy: @bot 幫我查一下今天天氣怎樣?
🤖 Bot: 今天台北多雲,溫度 25-28 度,降雨機率 30%。
───────────────────
usage:
• input tokens: 1,240
• thoughts tokens: 890
• output tokens: 156
👤 Debuguy: 能不能一次給我未來三天的預報?
🤖 Bot: 未來三天天氣預報:
今天:多雲 25-28°C...
明天:晴朗 26-30°C...
後天:小雨 23-26°C...
───────────────────
usage:
• input tokens: 1,380
• thoughts tokens: 650
• output tokens: 245
使用者體驗:🤔 開始學會問更有效率的問題
行為改變:
現在我們能清楚知道:
這是一個很關鍵的技術決策。你可能會想:「response
裡面不是本來就有 usage
嗎?」
// 看起來很合理的做法(但有大坑)
const response = await ai.generate({...});
- const tokens = response.usage; // 看起來很正常?
+ const tokens = response.usage; // ❌ 只包含最後一輪!
但這裡有個陷阱!
還記得前面幾篇文章的 trace 畫面嗎?當 ChatBot 使用 MCP 工具時,實際上會發生這樣的流程:
第1輪:LLM 分析使用者請求
第2輪:LLM 決定要用 Playwright 工具
第3輪:執行工具調用
第4輪:LLM 整合工具結果,生成最終回覆
問題來了: response.usage
只會包含最後一輪的 token 使用量!
實際測試一下:
// 當使用者問「幫我查颱風假」時
console.log(response.usage);
- // 期望:完整的 token 使用量
+ // 實際:{ inputTokens: 245, outputTokens: 156, thoughtsTokens: 89 }
+ // ❌ 但前面還有分析階段、工具選擇階段的 token 消耗!
所以如果只看 response.usage
,你會發現:
「奇怪,查個颱風假怎麼只花這麼少 token?感覺不對啊...」
- // 錯誤的做法:只拿最後一輪
- const response = await ai.generate({...});
- const tokens = response.usage;
+ // 正確的做法:攔截每一輪的 API 調用
+ use: [
+ async (req, next) => {
+ const result = await next(req);
+ // 每一輪都累計!
+ inputTokens += result.usage?.inputTokens ?? 0;
+ thoughtsTokens += result.usage?.thoughtsTokens ?? 0;
+ outputTokens += result.usage?.outputTokens ?? 0;
+ return result;
+ }
+ ]
這樣做的好處:
- // 純文字版本:
- 回覆內容...
-
- Token usage: Input 1240, Thinking 890, Output 156
+ // Block Kit 版本:
+ 🤖 Bot: 回覆內容...
+ ───────────────────
+ usage:
+ • input tokens: 1240
+ • thoughts tokens: 890
+ • output tokens: 156
Block Kit 版本的優勢:
今天我們成功實現了成本透明化:
技術層面:
產品層面:
文化層面:
成本透明化不是為了限制使用,而是為了讓使用變得更有效益。
當使用者能看到每次對話的真實成本時,他們不是變得吝嗇,而是變得更有策略。這種「數據驅動的使用者體驗設計」,可能是 AI 產品設計的一個重要方向。
明天我們要來處理下一個的挑戰:部署策略。
完整的原始碼在這裡,現在每次對話都能看到確切的成本消耗!
AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。
也歡迎追蹤我的 Threads @debuguy.dev