我不再讓 GPT-5 幫我看信箱,整個工作流程變得更便宜也更好

Back
Category : News

我曾經認為電子郵件是 AI 的可怕戰場。

太雜亂、太人性化、充滿 2017 年的轉寄郵件,以及公司裡沒人知道是哪個軟體產生的 HTML。

後來我花時間閱讀信箱自動化的討論串,特別是 r/openclaw 上關於郵件流程的一篇好文,終於搞懂了模式:

如果你不再讓模型扮演郵件伺服器的角色,電子郵件其實是 AI 非常好的應用場景。

這聽起來很明顯。但許多信箱自動化仍然在做以下事情:

  • 收到新郵件
  • 問 GPT-5 這是不是客服信
  • 問 Claude 這是不是業務信
  • 問另一個模型這是不是垃圾信
  • 再問一次該屬於哪個別名信箱
  • 再問一次現在要回覆還是稍後回覆

這不是智能。

這是昂貴的失憶症。

更好的模式很簡單:

  • 程式碼擁有狀態、重試、排程、同步與驗證
  • LLM 只負責真正需要判斷的決策

這樣的區分讓我的信箱工作流程變得更便宜、更容易除錯,也遠比以前穩固。



我一直回想的規則

OpenClaw 工作流程討論中的一則留言,比大多數文件都說得更好:

如果你的工作流程在達到 LLM 使用上限時就停止運作,那 LLM 很可能做得太多了。

這原本是在談程式碼代理人,但它完全適用於信箱自動化。

如果你的郵件管線需要仰賴模型來記住信箱狀態、去除重複事件、處理重試,或每次執行都重新檢查路由規則,那你就是建錯系統了。

模型擅長判斷。

它們不擅長當管理員。



電子郵件感覺很混亂,但傳輸協定其實早已結構化

人類體驗郵件是混亂的。

機器不是。

每封郵件到來時都帶有有用的結構:

  • From
  • To
  • Reply-To
  • Subject
  • 討論串識別碼
  • 訊息 ID
  • 標頭
  • 時間戳記
  • 原始 MIME
  • 附件邊界
  • 別名地址

這很重要,因為許多路由決策根本不該交給 LLM。

如果發票永遠都要寄到 [email protected],GPT-5 就不該每天早上重新發現這條規則。

如果客服郵件永遠落在特定別名上,程式碼就該確定性地路由它。

如果某個討論串已經處理過,你的 worker 應該從資料庫知道,而不是從提示詞知道。



模型該做的事

把 GPT-5、Claude Opus 4.6、Grok、Qwen 或 Llama 用在真正需要推理的部分:

  • 分類模糊訊息
  • 總結長討論串
  • 從醜陋的轉寄郵件鏈中萃取意圖
  • 為人類審核草擬回覆
  • 判斷附件看起來像是合約、發票還是支援文件



程式碼該做的事

所有重複性工作:

  • 同步信箱變更
  • 保存同步 token
  • 執行寄件者與別名規則
  • 排程後續追蹤
  • 重試失敗
  • 抑制重複處理
  • 驗證討論串是否已處理
  • 記錄決策以利稽核

這種架構不像「AI 信箱代理人」那麼炫目。

但它也是下個月還能繼續運作的架構。



Gmail 的配額數字基本上就是在告訴你該怎麼建

這部分徹底改變了我對信箱管線的看法。

Google 公開了 Gmail API 方法的配額成本。

  • history.list = 2 個配額單位
  • messages.list = 5 個配額單位
  • messages.get = 20 個配額單位
  • threads.get = 40 個配額單位
  • messages.send = 100 個配額單位

這些數字不是 trivia。

它們是設計提示。

Google 正在告訴你應該這樣做:

  1. 監聽變更
  2. 只抓取變更的內容
  3. 套用確定性篩選器
  4. 只在邊緣案例呼叫 LLM

而不是這樣:

  1. 每分鐘輪詢一次收件匣
  2. 抓取所有未讀郵件
  3. 把整條討論串丟給 Claude
  4. 永遠重複

如果你的工作流程每分鐘醒來一次,就叫前沿模型檢查所有未讀郵件,那你不是在打造自動化。

你是在打造一張持續的帳單。



一個理性的 Gmail 管線

對 Gmail 來說,模式很直觀:

  1. 使用 users.watch 訂閱收件匣變更
  2. 透過 Cloud Pub/Sub 接收事件
  3. 使用 history.list 取得變更的訊息 ID
  4. 只抓取你真正需要的訊息
  5. 先執行確定性規則
  6. 將模糊訊息升級給 GPT-5 或 Claude



開始監聽信箱

POST https://www.googleapis.com/gmail/v1/users/me/watch
Content-Type: application/json

{
  "topicName": "projects/myproject/topics/mytopic",
  "labelIds": ["INBOX"],
  "labelFilterBehavior": "INCLUDE"
}

進入全螢幕模式

退出全螢幕模式



處理變更的最簡 Node 範例

import { google } from "googleapis";

const gmail = google.gmail({ version: "v1", auth });

async function processMailboxChange(startHistoryId: string) {
  const history = await gmail.users.history.list({
    userId: "me",
    startHistoryId,
    historyTypes: ["messageAdded"]
  });

  const messageIds = new Set<string>();

  for (const item of history.data.history ?? []) {
    for (const added of item.messagesAdded ?? []) {
      if (added.message?.id) messageIds.add(added.message.id);
    }
  }

  for (const id of messageIds) {
    const msg = await gmail.users.messages.get({
      userId: "me",
      id,
      format: "metadata",
      metadataHeaders: ["From", "To", "Subject", "Reply-To"]
    });

    const headers = Object.fromEntries(
      (msg.data.payload?.headers ?? []).map(h => [h.name!, h.value!])
    );

    const subject = headers.Subject || "";
    const to = headers.To || "";
    const from = headers.From || "";

    if (to.includes("[email protected]")) {
      await routeToAccountsPayable(id);
      continue;
    }

    if (from.endsWith("@trustedvendor.com") && subject.includes("Invoice")) {
      await routeToAccountsPayable(id);
      continue;
    }

    await sendToLLMForClassification({ id, subject, from, headers });
  }
}

進入全螢幕模式

退出全螢幕模式

重要的不是程式碼風格。

重要的是操作順序:

  • 先做便宜的信箱同步
  • 再做確定性路由
  • 最後才呼叫模型



Outlook 和 Microsoft 365 用不同名稱做同樣的事情

Microsoft Graph 有相同的架構,只是用 delta query 而不是 Gmail history。

GET https://graph.microsoft.com/v1.0/me/mailFolders/{id}/messages/delta

進入全螢幕模式

退出全螢幕模式

你需要保留:

  • @odata.nextLink 來分頁
  • @odata.deltaLink 給下一次同步週期

那個 token 就是你的記憶。

你的 worker 應該擁有它。

而不是模型。



Node 中的範例結構

async function syncOutlookFolder(deltaUrl?: string) {
  const url = deltaUrl ?? "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages/delta";
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` }
  });

  const data = await res.json();

  for (const msg of data.value ?? []) {
    if (msg.toRecipients?.some((r: any) => r.emailAddress?.address === "[email protected]")) {
      await routeToSupport(msg);
      continue;
    }

    await classifyIfNeeded(msg);
  }

  if (data["@odata.nextLink"]) {
    return syncOutlookFolder(data["@odata.nextLink"]);
  }

  if (data["@odata.deltaLink"]) {
    await saveDeltaLink(data["@odata.deltaLink"]);
  }
}

進入全螢幕模式

退出全螢幕模式

相同的模式。

不同的 API。



Cloudflare Email Workers 把邊界劃得非常清楚

這是我最喜歡的例子,因為區分非常乾淨。

import PostalMime from "postal-mime";

export default {
  async email(message, env, ctx): Promise<void> {
    const subject = message.headers.get("subject") || "";
    const from = message.from || "";
    const to = message.to || "";

    if (to === "[email protected]") {
      await message.forward("[email protected]");
      return;
    }

    if (subject.includes("Invoice") && from.endsWith("@vendor.com")) {
      await message.forward("[email protected]");
      return;
    }

    const parsed = await PostalMime.parse(message.raw);

    const llmResult = await classifyEmail({
      subject,
      from,
      to,
      text: parsed.text,
      html: parsed.html
    });

    if (llmResult.label === "support") {
      await message.forward("[email protected]");
    }
  }
};

進入全螢幕模式

退出全螢幕模式

第一個 if 陳述式,比很多「自主代理人」展示還要做更多有用的事。



工具:什麼適合做什麼

選項 最適合的用途
Gmail API 使用 users.watch 推送通知、history.list 增量同步,以及在呼叫 LLM 前的配額感知篩選
Microsoft Graph Mail API 資料夾層級的 delta 同步、使用 @odata.nextLink@odata.deltaLink 的持久信箱狀態,以及選擇性分類
Cloudflare Email Workers 直接在入站郵件上執行自訂邏輯,包含標頭、原始 MIME,以及 forward()reply()setReject() 等動作
OpenClaw 自託管編排,包含 cron、webhook、工具、記憶體,以及圍繞實際模型呼叫的多代理人路由
n8n / Make / Zapier 快速的無程式碼或低程式碼編排,適合希望在加入 LLM 步驟前先有規則、重試與整合的團隊

這些工具都不是行銷意義上的「AI 原生」。

這正是它們有用的原因。



醜陋的部分是真的

公平地說,電子郵件在實務上並不乾淨。

你還是得面對:

  • MIME 的奇怪行為
  • 只有 HTML 的內文
  • 巨大的轉寄郵件鏈
  • 行內圖片
  • 附件才是真正 payload 的情況
  • 收據、合約與法律討論串會弄壞天真的解析器

這就是為什麼純規則不夠。

但這也是為什麼純 LLM 管線是個錯誤。

正確的模式是硬邊界搭配軟性後備機制:

  • 確定性地解析標頭與 MIME
  • 用規則路由明顯案例
  • 把狀態存在模型之外
  • 把模糊內容升級給 GPT-5、Claude、Grok、Qwen 或 Llama
  • 高風險動作(如寄出最終回覆)保留人類審核

這種混合架構沒有「完全自主信箱代理人」那麼性感。

但這才是大人打造生產環境自動化的方式。



為什麼在按 token 付費時這件事更重要

這就是經濟面變得惱人的地方。

如果你的工作流程不斷把同樣的路由決策送給模型,你就是在付錢讓模型一次又一次重新發現你自己的商業邏輯。

這是偽裝成 AI 問題的糟糕架構問題。

對那些整天在 n8n、Make、Zapier、OpenClaw 或自訂 Node worker 中執行代理人的團隊來說,按 token 計費會讓情況快速惡化。

你最後看著使用量儀表板,而不是在出貨。

這正是我喜歡確定性編排優先、LLM 呼叫其次模式的原因,也說明了為什麼固定費率的 API 存取對自動化密集的工作負載如此吸引人。

使用 Standard Compute,你可以保留工作流程已經習慣的 OpenAI 相容 API 形態,卻不用把每次分類、重試和長討論串都當成計費事件。它是現有 SDK 與 HTTP 用戶端的即插即用替代方案,背後是 GPT-5.4、Claude Opus 4.6 與 Grok 4.20 的動態路由,價格則是可預測的月費。

當你的自動化 24/7 持續執行,而重點是要同時停止照顧信箱和 token 計量器時,這一點非常重要。



我現在的實務規則

如果我從頭開始打造信箱自動化,這是我信任的堆疊:

  • Gmail API 或 Microsoft Graph 負責信箱同步
  • Cloudflare Email Workers 或 Node worker 負責確定性處理
  • PostalMime 負責解析
  • 必要時使用 OpenClaw、n8n、Make 或 Zapier 進行編排
  • 只有在郵件提出真正問題時才使用 GPT-5 或 Claude

這就是整個轉變。

別再讓模型當迴圈。

讓模型回答問題。

在那之後,一切都變得更便宜。

一切也都變得更好。

https://dev.to/lars_winstand/i-stopped-letting-gpt-5-babysit-my-inbox-and-the-whole-workflow-got-cheaper-and-better-2bi5

https://www.worldprogramming.org/posts/i-stopped-letting-gpt-5-babysit-my-inbox-and-the-whole-workflow-got-cheaper-and-better-ptaljx