iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Build on Google AI

LOCAL:30 天打造 LINE × Google AI 地方服務 Agent系列 第 20 篇

Day 20|模型設定、延遲與每項任務成本:品質先過關,再談快與省

  • 分享至 

  • xImage
  •  

同一句地方詢問多想一會兒,遊客真的得到更好的服務嗎?今天把品質、延遲、Token 與費率拆開記帳,先用明確標示的算術資料驗證帳本,再準備三題、兩組設定的真實抽樣。讀者帶走的是能追查來源的成本計算與比較條件,不是靠一張漂亮表格選模型。

A faster answer is not necessarily a useful service, and a lower bill is not evidence of better quality. This chapter builds a small cost ledger around LOCAL's existing contracts. Synthetic arithmetic checks the calculator; measured usage and latency must come from separately captured runs. Model identity, pricing scope, and comparison conditions remain explicit. The deliverable is a reproducible accounting workflow and a controlled experiment plan that never promotes fixed fixtures into live performance claims.

一、今日契約卡:先把事情做對,再問多快多省

現場一句話:為了一句地方旅遊與生活詢問,多等幾秒、多用一些 Token,究竟換來什麼?
只准後端決定的規則:品質由固定契約判定,用量與牌價各自留來源,再計算成本。
Google AI 用到/刻意不用:討論 Gemini 思考設定與用量欄位;本次本機帳本不呼叫模型,也不拿固定數據當實測。
五分鐘入口:python3 -m examples.day20.cost_ledger --demo --out out/day20/formula-demo。
這篇不能證明:算式通過,不代表某個設定更快、更省或更會理解地方問句。

元件 使用方式 本篇界線
Gemini API 規劃同題、同模型的設定對照 真實抽樣與首輪錯誤另留紀錄
GenAI SDK 讀取實際 usage 與模型回應 不自行猜 Token,重試維持明確設定
Google ADK 沿用既有工具與後端契約 不另做文字分類器冒充原服務
Cloud Run/LINE 完整服務另有運算與訊息成本 本篇計算器只處理限定的模型文字費用

契約不通過的便宜方案,只是更便宜地留下問題。 我會先保留失敗,再討論通過方案之間的差異。

二、收件匣有人接應,帳本也要有人看懂

從彰化蔬食節、八卦山生活動線到《爌肉之城》,背後有白色方塊工作室、旅庫彰化與彰化旅行+共同梳理地方生活。清晨想查活動、中午想找蔬食、深夜想找圈仔肉,表面都是問店家或活動,實際要承擔的資料時效與服務責任卻不同。

維運彰化蔬食節這類 LINE 服務,我想到的不是平均花多少 Token,而是鄉親為了完成事情需要問幾次、等幾次、按幾次。模型回得再快,若漏了重要限制,後面多一次追問或客服接手,都比原先省下的時間更長。

一位遊客從活動點問到晚餐再留下詢問,是一整趟旅程需求,可能包含數次模型與工具往返。若只挑最快的一次報成本,營運者看到的是片段,使用者承擔的卻是整段體驗。我先把每次請求記清楚,最後才依任務碼相加;外部費用與人工處理另列,不硬塞成無從核對的總價。

Day 19 拆開保存詢問、通知與接手;今天再拆開「模型單次請求」與「服務整體任務」。帳本先專注可獨立驗證的模型費用,不把前篇真人流程寫成已完成營運。

我先遇到一個比模型快慢更基本的問題:先前草案所附的六列對照結果,出自固定夾具內的預設數值,包含時間、Token、contract_pass 與完整度。背後沒有真正呼叫 Gemini,也沒有保存量測來源。它能用來演練表格,卻還不能支持「多想一秒沒有比較好」的實驗結論。

這不是把數字改個標題就算完成。我先把算式示範與真實觀察分成兩條輸入路徑,再讓缺少 usage 的失敗保留未知金額。 先查數字從哪裡來,才知道小數點後面有沒有意義。

三、五分鐘路徑:先驗證帳本,不預填成績

新增 cost_ledger.py 接收明確牌價與用量,沒有內建模型呼叫。命令會建立新的輸出目錄,--demo 只放兩筆算術示例,延遲留空、品質維持 NOT_RUN。範例程式獨立設計,不依賴任何外部未驗證數據。

python3 -m unittest examples.day20.test_cost_ledger -v
python3 -m examples.day20.cost_ledger --demo \
  --fx 32 --out out/day20/formula-demo
python3 -m examples.day20.cost_ledger --init-cases eval/local20.json \
  --out out/day20/cases-template

打開 ledger.json,先看 mode: SYNTHETIC_ARITHMETIC,再看 billing_verified: false。匯率三十二是本例固定的換算假設,不是假裝取得當天銀行成交匯率。這個檔案同時保留費率檔雜湊,方便後續分辨是用量變了,還是牌價換了。

{
  "origin": "synthetic_fixture",
  "model_id": "gemini-2.5-flash",
  "contract_status": "NOT_RUN",
  "coverage_status": "NOT_EVALUATED",
  "latency_ms": null,
  "usage": {
    "prompt_token_count": 1000,
    "candidates_token_count": 100,
    "thoughts_token_count": 250,
    "cached_content_token_count": 0,
    "grounding_used": false,
    "output_semantics": "candidates_excludes_thoughts"
  }
}

上面每個 Token 數都是指定算式輸入。改成匯入真實觀察時,還需原始回應與評分紀錄雜湊,以及模型、題目、工具與程式版本;字串標籤不會讓資料自動成為可信來源。

示例第一筆為一千輸入、一百回覆與零思考 Token;第二筆增加二百五十個思考 Token,驗證思考用量是否只算一次。這組對照不模擬模型品質,也不預測這段思考能換來多少秒或正確答案。會算帳與量到帳,本是兩件事。

真實匯入格式需以下欄位;識別碼與雜湊應來自同次執行,填入占位文字無法建立實測證據。

{
  "origin": "imported_capture",
  "case_id": "local19",
  "input_sha256": "<待同次執行回填>",
  "model_id": "gemini-2.5-flash",
  "endpoint": "generateContent",
  "prompt_sha256": "<待同次執行回填>",
  "tools_sha256": "<待同次執行回填>",
  "catalog_sha256": "<待同次執行回填>",
  "scorer_sha256": "<待同次執行回填>",
  "code_sha": "<待同次執行回填>",
  "config_base_sha256": "<待同次執行回填>",
  "raw_record_sha256": "<待同次執行回填>",
  "grade_record_sha256": "<待同次執行回填>",
  "thinking_budget": 0,
  "attempts": 1,
  "contract_status": "NOT_RUN",
  "coverage_status": "NOT_EVALUATED",
  "latency_scope": "agent_round",
  "latency_ms": null,
  "usage": null
}

匯入模式只表示計算器收到外部紀錄,不會自行認證那份回應真的由 Google 發出。原始資料、生成紀錄與評分依據仍需由實際執行者保存。這也是為什麼本篇把公式測試與 Live 成績拆開,而不把所有能讀進 JSON 的資料都叫實測。

讀者可以先做一個反例:刪掉 thoughts_token_count。計算器會拒絕,因為沒記到思考用量與思考用量為零,是兩件事。用 get(..., 0) 補上去,帳單看起來會很漂亮,來源卻從此追不回來。

四、先凍結題目,才有資格比較設定

Day 18 的基準固定為 eval/local20.json。在核對題集時,我發現若誤將 local14 當作店家題,會撞上該題原本設計的 HTTP 503 注入;真正正常的店家查詢其實是 local12。我保留題集原文,新增選題函式核對 ID、工具與問句,將修正列在選題表,而不是暗中搬動題號。

基準 ID 題集原問句 預期工具 本次用途
local11 花壇場次的集合點在哪? search_local_events 活動資訊與缺欄說明
local12 花壇有推薦的素食店嗎? search_local_places 店家查詢與明示條件
local19 現在哪裡有開著的爌肉飯?可以幫我預約兩碗帶走嗎? show_local_help 安全拒絕與完整度分評

設定 A、B 沿用實驗規劃的 gemini-2.5-flash、思考預算零與一千零二十四。LOCAL 的正式 Cloud Run 服務自 Day 12 起使用 gemini-3.8-flash;後續 ADK 路徑已明確採用 ThinkingLevel.LOW。本篇的 A/B 對照另選 gemini-2.5-flash,因為它可用 thinkingBudget 直接指定思考 Token 上限,適合示範思考用量如何計價;這組對照不代表正式服務的帳單。正式服務的價格請以官方價格頁為準。執行前要核對實際使用的 API 與釘選 SDK 是否接受該設定,並保存完整設定回報;遇到不支援就記錄受阻,不私自改成另一個模型或另一套介面。[1]

比較條件 必須固定 可以改變
任務身分 ID、原問句、上下文與資料快照 本次不改
代理行為 Prompt、工具 Schema、判分器與程式 SHA 本次不改
模型路徑 模型完整 ID、API 介面與額度來源 本次不改
思考設定 A、B 各自保存實際設定 唯一預定變因
外部條件 執行時間、冷啟動、快取狀態需記錄 若無法控制,就列為限制

新增 validate_comparable() 先核對比較身分,再容許思考設定不同。config_base_sha256 另外凍結溫度、輸出上限等其餘生成設定;不能只固定模型名稱,卻把其他選項一起改掉。它的欄位應由同次執行紀錄填入,而非用檔名猜測:同一份程式檔案在不同資料、Prompt 或工具 Schema 下,可能已經是不同的代理版本。

IDENTITY_KEYS = (
    'case_id', 'input_sha256', 'model_id', 'endpoint',
    'prompt_sha256', 'tools_sha256', 'catalog_sha256',
    'scorer_sha256', 'code_sha', 'config_base_sha256'
)

def validate_comparable(first: dict, second: dict) -> None:
    for key in IDENTITY_KEYS:
        if not first.get(key) or first.get(key) != second.get(key):
            raise ValueError('COMPARISON_IDENTITY_MISMATCH:' + key)
    for row in (first, second):
        if type(row.get('attempts')) is not int or row['attempts'] != 1:
            raise ValueError('SINGLE_ATTEMPT_REQUIRED')
    if first.get('thinking_budget') == second.get('thinking_budget'):
        raise ValueError('NO_DECLARED_VARIABLE')

雜湊是辨認版本的工具,不是證明資料真實的印章。原始回應與判分報告仍要保留,才能查看這一列送出什麼、取得什麼。只有漂亮雜湊卻找不到實體檔案,依然無法重現。

若將問句換地名,或讓其中一組保留前文,任務便已改變。真正的實驗控制,是每次呼叫前後都能核對相同條件。

五、Token 算式不難,難的是不重算也不漏算

先前草案所列的費率為輸入每百萬 0.075 美元、輸出 0.30 美元。我另查 Google 官方價格頁,當次 gemini-2.5-flash 標準文字輸入為每百萬 0.30 美元,輸出包含思考用量為每百萬 2.50 美元。以下採這份外部查核結果,沒有把過時費率繼續稱作官方牌價,並在 pricing.checked.json 中完整記錄查價日期與牌價範疇。[2]

圖 1:模型設定、延遲與每項任務成本帳本架構圖。
圖 1:任務品質門檻、觀測範疇界定、用量算術核算與來源標示架構。框選本機驗證範圍,標明算式通過不等於實測優劣。

這裡只處理無快取、無額外 grounding 的標準文字請求。若輸入包含其他媒體、付費搜尋或快取,應套用對應項目;未知模型直接拒絕,不能偷偷拿另一個模型的價格代算。

模型費用(USD)
= 輸入 Token × 輸入單價 / 1,000,000
+(回覆 Token + 思考 Token)× 輸出單價 / 1,000,000

換算估計(TWD)= 模型費用(USD)× 明列的匯率假設

前提是回覆 Token 欄位不包含思考 Token。介面欄位的語意要先核對,不能把總 Token 再加一次思考;計數器先估的輸入長度也不等於實際生成回應的完整用量。本篇用中介資料格式明列這個前提,接線者負責從真實 API 回應逐欄轉換。

以下節錄新增計算器,使用 Decimal 保留金額精度。顯示到幾位小數,是最後排版的決定,不應在每筆計算時先截掉費用。

def calculate_text_cost(usage: dict, rate: RateCard, model_id: str,
                        usd_to_twd: Decimal) -> dict:
    if model_id != rate.model_id:
        raise ValueError('MODEL_PRICE_MISMATCH')
    if usage.get('output_semantics') != 'candidates_excludes_thoughts':
        raise ValueError('OUTPUT_TOKEN_SEMANTICS_REQUIRED')
    inputs = count(usage.get('prompt_token_count'))
    outputs = count(usage.get('candidates_token_count'))
    thoughts = count(usage.get('thoughts_token_count'))
    cached = count(usage.get('cached_content_token_count'))
    if cached or usage.get('grounding_used') is not False:
        raise ValueError('UNSUPPORTED_CACHE_OR_GROUNDING')
    fx = amount(usd_to_twd)
    if fx == 0:
        raise ValueError('POSITIVE_FX_REQUIRED')
    usd = (Decimal(inputs) * rate.input_usd_per_million
           + Decimal(outputs + thoughts) * rate.output_usd_per_million) / Decimal(1_000_000)
    return {'raw_usd': str(usd), 'estimated_twd': str(usd * fx),
            'usd_to_twd': str(fx), 'scope': rate.scope,
            'billed_output_tokens': outputs + thoughts}

計算器保留微小數值而不提早四捨五入,避免數千次請求累積誤差。但估算小數非付款保證,稅額、帳期與折扣須於實際帳單核對。

對前面的算術示例,結果為 0.001175 美元;按假設匯率換算是 0.0376 元。這只是計算範例,不是 LOCAL 回答複合問題的實際成本。免費額度或抵用金則另記為折抵,原價估算與真正帳單保持分欄。為了方便批次稽核,計算器新增 summarize_ledger() 彙整總輸出 Token、總美元與估計新台幣;同時提供 adapt_genai_usage() 轉接 GenAI SDK 的原始用量。值得強調的是,轉接函式拒絕使用預設值隱蔽缺漏——輸入與輸出 Token 缺少即報錯,思考與快取 Token 亦必須基於明確設定或欄位解析,不偷偷補零,並以 format_cost_summary() 產出一行精簡摘要,讓開發者在伺服器日誌中能清楚看見每一次交談的用量代價。

六、慢在哪裡,先把計時器放對位置

模型延遲、完整代理回合、Webhook HTTP 回應與 LINE 訊息送達,是不同範圍。若把計時器放在匯出 CSV 的函式外面,量到的只是本機整理速度;把手填的毫秒印在表裡,也不會突然變成端對端量測。實務上,LINE 會將兩秒內未收到伺服器回應的情況記為 request_timeout,官方也建議 Webhook 事件採非同步處理,讓 HTTP 回應不要被後續工作拖住。Webhook redelivery 則是另一項需事先啟用的功能,不能把「超過兩秒」直接等同「一定自動重送」。[5][6] 新增的 evaluate_latency_budget() 函式預設便以 2000 毫秒檢查 Webhook 延遲預算,在模型思考與多次工具往返逼近邊界時提早標記警示或逾時風險。這同時提醒我們:Webhook 的兩秒回應與後續非同步完整的 Agent 回合處理,必須在架構上清晰拆分,不應混為一談。

本篇先記模型請求或代理回合起訖,列明 latency_scope。多次往返逐次保存再彙整總量,失敗回合照樣占一列。缺少 usage 的失敗金額保持未知,彙總僅呈現已知小計,不宣稱零元。

GenAI SDK 重試沿用 attempts=1。限流由外層閘門管理,遇 429 保存首輪失敗再開新輪。配額與間隔影響可用節奏,不能由單一成功推論永遠不受限。[3]

計時須拆分排隊等待與模型處理。排隊屬容量調度,回傳耗時屬模型表現;拆開兩者,明天才能沿 Trace 找到實際耗時位置。

若每題每設定只跑一次,我會刊出每一列,不報虛假的穩定平均。小樣本也能教人做判斷,前提是結論只留在這次觀察範圍。

品質先採三層契約門檻再列完整度。安全拒絕超出範圍的需求,費用可以計算,但不能當作「完整服務成功」樣本。分母各自寫出,避免把片段當全貌。

接著才問:多給思考預算,完整度是否改變?假如工具與後端相同,固定卡片也相同,我應優先檢查資訊如何進入呈現層。這是一個待驗證假設,明天要沿事件鏈找依據。

七、成績單先分來源:實驗設計與帳本驗證

本次資料 已取得的結果 不能冒充的成果
新增成本帳本 二十三項本機自測全通過 模型準確率或帳單核銷
算術示例 兩筆用量代入,無模型呼叫、無延遲值 真實 A/B 對照
真實抽樣 抽樣協定凍結,成績如下表誠實保留 已確認哪組較快較省
當日 Commit/CI Day 20 修正版 Commit a004bf7(本機 23 項自測全過;遠端 Actions 9 條全綠) 沿用前版 CI 成果冒充本日雲端通過
題目 設定 契約/完整度 延遲與範圍 輸入/回覆/思考 Token 估計金額
local11 A(思考 0) 待量測 待擷取 待記錄 待計算
local11 B(思考 1024) 待量測 待擷取 待記錄 待計算
local12 A(思考 0) 待量測 待擷取 待記錄 待計算
local12 B(思考 1024) 待量測 待擷取 待記錄 待計算
local19 A(思考 0) 待量測 待擷取 待記錄 待計算
local19 B(思考 1024) 待量測 待擷取 待記錄 待計算

匯入帳本同樣保留失敗狀態。若 A 組逾時、B 組回應,不能剔除 A 組再比速度。已知費用、未知費用、契約通過與完整度待複核各有位置;修復後重跑另建一輪,保留首輪結果才看得出改善。

六列是抽樣計畫而非已執行結果。先前草案留下的 612.4、1540.2 毫秒暫不列為成績,因目前無同次模型回應。這六列 A/B 實測將與 Day 18 承諾的首輪路由成績,在明天的 Day 21 Trace 觀測與服務驗證中一併執行,今天的空格不會在明天突然變成未經證實的成績。[4]

圖 1 整理品質門檻、觀測範疇與成本架構;在取得六列實測前,正文僅以算式示範邏輯。真實抽樣數據待正式 API 驗證輪次產出,不借用假秒數拼裝成績。

八、三分鐘展示,也要先有能走通的路

本篇同時準備 DEMO.md 草案:第一段查一筆蔬食資料,第二段示範同單查回,第三段讓待詢問需求進入收件匣。時間軸是演出配置,尚未量測的地方不寫成恰好三分鐘完成。

先前規劃的展示草案提到「少走樓梯」記憶、無障礙標籤與「預約導覽」按鈕,這些沒有對應的本次程式證據。我先改用既有飲食偏好、資料缺漏提醒與「留下服務詢問」,把替換原因留在展示稿註記。現場不必製造正式服務故障;重啟查回可用已驗收錄影,通知接手則等真實端點完成再演。

下一篇是 Day 21|一條 Trace 找到問題:從模型工具呼叫一路追到後端結果 。我會先核對工具要求、執行、資料與呈現是否屬於同一回合,再判斷缺口。今天還沒有取得的實測,不會在明天突然變成證據。

程式與參考資料

本篇新增 cost_ledger.py、test_cost_ledger.py、pricing.checked.json,另有保留原題號的選題核對。外部牌價查核與舊版草案數值分開存放,公開基準題集保持不動。

[1] Gemini:思考設定與介面說明。
[2] Gemini Developer API:官方價格,本次另查於 2026-10-03。
[3] Gemini API:配額與限流。
[4] Day 18:二十題地方契約與分層成績。
[5] LINE:Webhook 錯誤原因與統計。
[6] LINE:接收 Webhook 與重新傳送。


上一篇
Day 19|志工真的接到單了嗎?最小真人通知與收件匣閉環
系列文
LOCAL:30 天打造 LINE × Google AI 地方服務 Agent 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言