September
2nd,
2026
前情提要 我有一個自己每天在用的 LINE Bot,linebot-helper-python。丟網址給它會回摘要跟四個平台的社群文案,丟 YouTube 連結會回影片摘要,還有書籤、地點查詢、語音助理那些。跑在 Cloud Run 上,用 Vertex AI。 八月底 Google 發了一篇 Introducing agentic video in Gemini,看完我第一個念頭是:這東西能讓我的 bot 多做一件現在做不到的事——針對影片提問,而不是只給一份摘要。 結果做下來,公告裡沒寫的部分比寫了的更值得記。 Agentic video 是什麼 原本 Gemini 讀影片是「靜態」的:不管你問什麼,它都照固定的取樣率把整支影片的每一格畫面、每一段音訊全部塞進 context。一支兩小時的影片,光影片本身就佔掉幾十萬 token,而你可能只是想問「他有講到定價嗎」。 Agentic 模式把這件事反過來:讓模型自己決定要載入哪一段。先掃逐字稿,判斷答案可能在 1:24 附近,就只把那一段的畫面拉進來。 官方公告給的數字是長片場景 token 少 88%、成本降 66%、正確率高 7%。開啟方式就是在 Part 上多一個參數: video_part = types.Part( file_data=types.FileData(file_uri=..., mime_type="video/mp4"), media_processing="AGENTIC", # 或 "STATIC" ) 支援的模型有三個:gemini-3.7-flash、gemini-3.6-flash、gemini-3.5-flash-lite。輸入來源可以是 YouTube 網址、Cloud Storage URI,或內嵌的 base64。 它實際能做到什麼 我拿兩小時的 Google I/O ‘25 keynote 測,問「他有講到定價嗎?如果有,請給我幾分幾秒」,回來的是: 影片中有提到價格與訂閱方案的定價相關資訊。主要出現在影片的 1:24:40 到 1:26:10 左右⋯⋯ 然後正確描述了 Google AI Pro 與 Google AI Ultra 兩個方案的差異。再問 Android XR 眼鏡那段在哪,回 1:36:31 至 1:50:30,內容也對得上。 這種在兩小時影片裡精準定位的能力,是我覺得這個功能真正值錢的地方。摘要人人都會做,「這支長片裡他哪時候講到 X」目前沒什麼工具做得好。 前置條件:四個參數,少一個都不會報錯 這是這次最貴的一課,所以放前面講。 在 Vertex AI 上要讓 agentic 真的生效,四件事必須同時成立: # 條件 少了會怎樣 1 api_version="v1beta1" agentic 只在 v1beta1 提供,用 v1 會靜默退回 STATIC 2 media_processing="AGENTIC" 預設就是 STATIC,不設等於沒開 3 模型在支援名單內 不支援的模型會靜默降級為 STATIC 4 thinking_level 有設定 見下一節,這個最麻煩 關鍵字是靜默。少任何一個,API 都會回 200、答案照樣產出、看起來完全正常,只有帳單不一樣。沒有任何錯誤訊息會告訴你 agentic 沒生效。 還有兩個環境面的前提: SDK 版本:media_processing 這個欄位是 google-genai 2.20.0 才加進去的。我逐版下載驗證過,2.19.0 沒有、2.20.0 有。版本太舊的話這個參數會被當成未知欄位丟掉,一樣不報錯。 Vertex 專屬:官方文件的多輪對話範例是寫給 Gemini Developer API 的,Vertex 這條路上行為不同,後面會講。 我的作法是把這四項各寫一支測試,斷言送進 SDK 的參數: def test_thinking_level_is_low(captured): youtube_tool.summarize_youtube_video(VIDEO_URL) config = captured["call_kwargs"]["config"] assert...
繼續閱讀
September
1st,
2026
當訊息可以反悔:用 Edit 與 Unsend Webhook 打造會「變形」的 LINE 團購 Bot 作者:Evan Lin,LINE Taiwan Developer Relations Team Lead 2026 年 8 月 20 日,LINE 宣布在 LINE Labs 開放「編輯訊息」功能免費試用。 對一般使用者來說,這是一個很直覺的功能:訊息送出後才發現打錯字、日期寫錯,或語氣想再調整,不必再經過「收回、重打、重新送出」的流程,直接編輯原本的文字訊息就可以了。 但我看到這個功能時,第一個想到的是另一件事: 如果使用者改掉一則已經被 LINE Bot 處理過的訊息,Bot 知道嗎? 答案是:知道。LINE Messaging API 提供了 Edit event;而當使用者收回訊息時,也有 Unsend event 可以接收。 這篇文章想跟大家分享,我如何把這兩種 webhook event 做成一個有故事的 LINE Bot Demo,以及實作時幾個容易忽略、卻非常重要的細節。 完整範例程式碼放在 GitHub: https://github.com/kkdai/linebot-edit-unsend 先從 LINE Labs 的「編輯訊息」開始 目前在 LINE Labs 試用編輯訊息,需要先符合以下條件: 手機版 LINE 更新至 26.12.0(含)以上版本。 進入「主頁 → 設定 → LINE Labs」。 開啟「訊息編輯」。 在一般一對一與群組聊天室中,文字訊息送出後 15 分鐘內可以編輯;Keep 筆記則是 6 天內。照片、影片、語音、檔案與貼圖目前不能編輯。修改後,聊天室會顯示「已編輯」,但不提供舊版本查看或還原。 這裡還有一個跟 Bot 開發直接相關的限制:目前與 LINE 官方帳號的一對一聊天室不支援編輯訊息。因此,要測試 Messaging API 的 Edit event,必須把 Bot 加入群組聊天室。 功能與啟用方式可以參考 LINE 台灣新聞室公告。 從 UI 功能想到 Bot 的資料一致性 過去 Bot 收到文字訊息後,常見的處理流程是: 接收 message webhook。 解析文字。 寫入資料庫或觸發後續流程。 回覆處理結果。 如果訊息送出後不能修改,這個模型很單純。但當訊息可以被編輯,原本的文字便不一定是使用者最後的意圖。 例如使用者送出: 珍珠奶茶 / 半糖 / 少冰 / 1 Bot 已經把它記錄成一杯 60 元的飲料。幾秒後,使用者直接編輯原訊息: 珍珠奶茶 / 微糖 / 去冰 / 2 如果 Bot 沒有處理 Edit event,聊天室看到的是兩杯,但後端仍然記成一杯。畫面上的世界和 Bot 裡的世界就分開了。 這就是 Edit event 最實際的價值:它不只是通知「文字改過了」,而是讓服務有機會同步使用者的最新意圖。 團購變形記:讓 API Demo 有一個故事 為了同時展示 edit 與 unsend,我把 Bot 設定成「百變店長・單單」,負責拯救下午三點陷入低電量的辦公室。 Demo 流程如下: 團主輸入...
繼續閱讀
August
27th,
2026
前情提要 我有一個自己在用的 macOS App,gemini-live-translate-macos。它用 ScreenCaptureKit 直接抓指定 App 的音訊,不需要 BlackHole 那類虛擬音效卡,然後丟給 Gemini Live API 做即時翻譯,一邊出繁中字幕、一邊播中文語音。開發過程寫過兩篇,第一篇是用 AGY CLI 從零做出來,第二篇是 Claude Code 接力把它從能動變成好用。 這次要加的東西起點很單純:我看到 Live API 多了一份「即時逐字稿」的文件,想說既然已經在接 Live API 了,加個純逐字稿模式應該就是改幾個參數的事。 結果查完文件才發現,Google 這次一口氣放了兩顆名字很像但能力差很多的模型,而我真正想要的那個功能(語者分離)在我原本以為的那顆上面根本沒有。 兩個名字只差兩個字的模型 先把差異攤開,這是我花最多時間才搞清楚的部分: gemini-3.5-transcribe-live gemini-3.5-transcribe 走哪個 API Live API(WebSocket 串流) Interactions API(一般 HTTP 請求) 使用情境 邊講邊出字 錄完之後整份丟進去 語者分離 不支援 最多 8 位 字詞層級時間戳 不支援 支援 音訊長度 單次 session 10 分鐘 1 小時,開語者分離時 30 分鐘 智慧模式 SMART 可用 smart 與語者分離互斥 暫定字幕 有 interimInputTranscription 不適用 官方文件在 Live 那頁的限制章節寫得很直白: Speaker diarization is not supported in live streaming sessions. For speaker diarization, use the non-streaming Audio transcription endpoint. 所以「即時看到誰在講什麼」這件事,目前做不到。要語者分離就得錄下來、會後整份送出去。這個限制決定了我後面整個架構。 即時那顆:interim 是它的重點 setup 的形狀跟原本的翻譯模型不一樣,responseModalities 要是 TEXT,轉錄參數放在 setup.inputAudioTranscription 底下: { "setup": { "model": "models/gemini-3.5-transcribe-live", "generationConfig": { "responseModalities": ["TEXT"] }, "inputAudioTranscription": { "languageCodes": [], "mode": "SMART" }, "realtimeInputConfig": { "automaticActivityDetection": { "disabled": false } } } } languageCodes 留空是自動偵測語言,填 ["en-US"] 這種 BCP-47 代碼則是給它語言偏好。mode 有兩個值:VERBATIM 逐字保留所有內容,SMART 會拿掉 uh、um 這類填充詞並自動補上格式。會議記錄我選 SMART,讀起來乾淨很多。 還有一個 customVocabulary 可以塞專有名詞,上限 1000 條,但文件建議 100 條以內效果最好。這次我沒做,等真的遇到人名一直被聽錯再說。 回應端則多了一個原本翻譯模型沒有的欄位: interimInputTranscription:說話中的暫定結果,會被後面的內容覆蓋...
繼續閱讀
August
25th,
2026
前情提要 AI 時代下,連我自己都會將大部分得程式碼優化或是寫作的部分都會麻煩 AI 代勞。但是因為模型訓練的資料因素,有太多的寫作方式都太老舊了。這樣造成寫出來的程式無法應用到最新版本 Go 的一些功能,這樣相當的可惜。 還好, JetBrains 出了 go-modern-guidelines 這個很好用的 plugin 。可以讓你的 AI Agent 變得更加的聰明,並且知道該如何使用最新的程式語法來優化你的 Golang 程式碼。 什麼是 go-modern-guidelines? 它想解決的問題:模型的知識有截止日,Go 沒有 這個專案的定位寫得很直白:為 AI agent 提供當代的 Go 撰寫規範,讓它們不會因為知識截止日而寫出過時的 Go。 問題有兩層。第一層很好理解:模型訓練資料有截止時間,截止之後才進標準庫的東西,它沒看過就不會用。專案自己舉的例子是 errors.AsType[T](Go 1.26),模型沒見過,自然不會寫。 第二層比較微妙,專案稱之為 frequency bias:就算模型「知道」新寫法,訓練資料裡舊寫法出現的次數還是壓倒性地多。網路上十年份的 Go 程式碼裡,interface{} 的出現次數遠遠多於 any,sort.Slice 遠多於 slices.SortFunc。模型是在做機率預測,多數決贏的往往是舊的那個。 這第二點我在這次重構裡真的看到了。原本專案裡有這麼一段: // The oauth2 library can return an error containing "invalid_grant" // when the refresh token is expired, revoked, or otherwise invalid. if err != nil { errorStr := err.Error() // Basic substring check to avoid importing "strings" for i := 0; i <= len(errorStr)-13; i++ { if errorStr[i:i+13] == "invalid_grant" { return true } } } 一段手刻的字串搜尋,註解還特地解釋「為了避免 import strings」。strings 是標準庫,import 它的成本是零。這段程式碼要的其實就是一行 strings.Contains(err.Error(), "invalid_grant")。 它怎麼運作:兩個指令,一份隨 Go 版本增長的清單 工具本體是一支 CLI,只有兩個子指令: list [--go-version <version> | --file-path <path>] 回傳這個 Go 版本支援的規範清單,由新到舊排序。 explain <id>... 回傳特定規範的詳細說明與 before/after 範例。 list 的設計重點在於它會依 Go 版本回答不同的答案。你可以直接丟一個檔案路徑給它,它會自己往上找 go.mod、go.work,或退而求其次看本機的 Go toolchain: $ go-modern-guidelines list --file-path ~/Documents/linebot-file/main.go 我這個專案的 go.mod 寫 go 1.24.0,所以它回了 45 條。換個版本號,數字就跟著變: Go 版本 規範數 1.21 32 1.22...
繼續閱讀
August
22nd,
2026
前情提要 「網址擷取失敗,幫我看一下 log。」 這種回報我大概一兩週會收到一次,通常都是某個網站又加了一層防爬蟲,查一下讓爬取多一種備援方式繞過去就結束了。這次也是這樣開始的:使用者傳一個 acm.org 的網址進來,摘要功能吐了一個錯誤。 查下去才發現不是這麼一回事。網址擷取這個功能已經默默壞了三天,而且問題不在對方網站,是我自己的 Docker image 裡一個沒鎖版本的 npm 套件,某天悄悄升級到一個需要更新版 Node.js 才跑得動的新版本,容器裡的 Node.js 卻還停在半年前裝的那個。 先看 log: gcloud logging read 'resource.type="cloud_run_revision" AND \ resource.labels.service_name="linebot-helper-python" AND \ textPayload:"acm.org"' --limit=50 --freshness=2d \ --format="table(timestamp,severity,textPayload)" ERROR:loader.url:All methods failed for URL: https://www.acm.org/articles/people-of-acm/2026/russ-cox WARNING:loader.url:cloudscraper failed ...: 403 Client Error: Forbidden for url: ... WARNING:loader.url:httpx failed ...: Client error '403 Forbidden' for url: ... WARNING:loader.url:singlefile failed ...: SingleFile exited with code 1 ERROR:loader.singlefile:SingleFile loading failed ...: SingleFile exited with code 1 這個 Bot 抓網頁內容有個 fallback chain:先試 singlefile(headless Chromium 完整渲染),失敗換 httpx,再失敗換 cloudscraper(專門繞 Cloudflare 的工具),三種都失敗才報錯給使用者。httpx 跟 cloudscraper 拿 403 情有可原,acm.org 本來就有防護。但 singlefile 不該失敗,它是設計來繞過這種防護的最後一道防線。 往下翻,找到 singlefile 真正的錯誤: Error: WebSocket is not available, Node.js 22.4.0 or later is required at file:///usr/local/lib/node_modules/single-file-cli/lib/deno-polyfill.js:132:8 Node.js v18.20.4 容器裡是 Node 18,single-file-cli 卻要求 22.4 以上。查了一下這個錯誤第一次出現的時間是三天前,不是今天才發生。這代表過去三天,只要一個網址前面三種方法有任何一種被擋下來,就會全滅,使用者只會收到一句「無法從網址讀取內容」,完全看不出背後是這個原因。 為什麼要跑三次 Cloud Build,而不是改完就上線 這篇文章接下來會出現三次「改 Dockerfile → 用 Cloud Build 建置 → 實際跑起來看看」的循環。一開始我沒打算這麼做,覺得改個版本號、看 build 過了就能上線。後來發現這個假設每次都錯。 原因是 npm install -g single-file-cli 這種指令,build 成功只代表「npm 找得到這個套件、裝得進去」,不代表「這個套件跑起來的時候能動」。npm 對 engines 欄位的檢查預設只是警告,不會讓 build 失敗;而套件真正會不會 crash,要等它實際去啟動一個 headless Chromium、建一條 WebSocket...
繼續閱讀
August
13th,
2026
前情提要 我的 LINE Bot 一直有個摘要功能:丟一個網址進去,它爬完內容、產一段摘要,附上社群貼文草稿跟儲存書籤的按鈕。這個功能從 2024 年就在了,但它解決的始終是「這篇在講什麼」,而我常常想知道的是另外三件事: 這篇文章講的東西,背景脈絡是什麼?它的說法有沒有其他人反駁過?裡面那些數字,是有出處的還是作者自己講的? 摘要回答不了這些,因為摘要的輸入就只有那篇文章本身。模型手上沒有其他材料,你叫它「批判性分析」,它只能在原文裡繞圈圈,或者開始編。 Google Search Grounding 剛好補的就是這一塊。我在 之前那篇文章 裡用它做過搜尋助手,那時候用途是回答問題;這次我想試的是另一種用法:把一篇既有的文章丟給模型,讓它自己去搜尋文章外的資訊,然後回頭審視這篇文章。 成果是摘要卡片上多了一顆「📄 詳細研究報告」按鈕,按下去大約一到兩分鐘後,Bot 會推一個網頁連結給你。 主要 Repo:https://github.com/kkdai/linebot-helper-python 為什麼是 Grounding,而不是自己串搜尋 在 Grounding 之前,要讓模型讀到網路上的即時資訊,得自己搭一條管線:先請模型從文章裡抽出關鍵字,拿關鍵字去打搜尋 API,把搜尋結果的網頁一個個爬回來,塞進 prompt,再請模型總結。三次以上的 API 呼叫,每一段都可能斷,而且關鍵字抽得好不好,直接決定後面撈到的東西有沒有用。 Grounding 把這整段收進模型內部。你在 GenerateContentConfig 裡掛一個 google_search 工具,剩下的模型自己處理:它自己決定要不要搜、搜什麼、搜幾次,搜完自己判斷哪些結果值得用。 對「研究報告」這個題目來說,模型自己決定搜什麼這件事特別有價值。我在寫 prompt 的時候並不知道使用者會丟什麼文章進來,自然也寫不出該搜哪些關鍵字。但模型讀完文章之後知道,它會去找這個主題的來龍去脈,也會去找有沒有人持相反意見。 另一個我很在意的優點是引用來源會跟著回來。模型回應的 grounding_metadata 裡帶著它實際參考過的網頁,標題跟網址都有。這代表報告裡「根據其他報導」那幾句話,不是模型憑印象講的,而是有對應網頁可以點過去查。做資訊類的產品,這個差別很大。 抽來源的程式碼在 loader/langtools.py,寫得防禦一點,因為沒有觸發搜尋時這些欄位整串都不存在: def _extract_grounding_sources(response) -> list: """從 grounding metadata 抽引用來源(同 chat_session 的作法)。""" sources = [] try: if getattr(response, 'candidates', None): candidate = response.candidates[0] metadata = getattr(candidate, 'grounding_metadata', None) chunks = getattr(metadata, 'grounding_chunks', None) if metadata else None for chunk in chunks or []: web = getattr(chunk, 'web', None) if web: sources.append({ 'title': getattr(web, 'title', '') or '', 'uri': getattr(web, 'uri', '') or '', }) except Exception as e: logging.warning(f"Failed to extract grounding sources: {e}") return sources 系統架構 整條流程從摘要卡片上的按鈕開始,中間經過一次重新爬取跟一次 grounding 呼叫,最後以臨時網頁收尾。 graph TD A[使用者傳送網址] -->|摘要 Flex Bubble| B[📄 詳細研究報告 按鈕] B -->|Postback 帶 bookmark doc id| C[驗證書籤所有權] C -->|立即 Reply 研究中| D[LINE 聊天室] C -->|背景任務| E[load_url 重新爬取原文] E --> F[Gemini...
繼續閱讀