iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI 自動化

用 LINE Bot 和 Agent Skill 自動化整理筆記與 IG 發文系列 第 12 篇

Day 12|免費模型隨時會掛:三層 fallback 設計

  • 分享至 

  • xImage
  •  

昨天結尾提到,抓網頁內文跟 AI 分類都要打外部服務,免費額度說掛就掛,今天就把 callAI 這支函式攤開來看,看 line-notion-bot 怎麼把這件事的風險降到最低。

先看整支函式的骨架:

async function callAI(env, system, user) {
  if (env.MISTRAL_API_KEY) {
    try {
      return await callMistral(env, system, user);
    } catch {
      // 掉回 OpenRouter,不中斷整個分類流程
    }
  }
  return await callOpenRouter(env, system, user);
}

邏輯很直白:有設定 MISTRAL_API_KEY 才會先試 Mistral,試了失敗(或根本沒設定這個 secret)就掉到 callOpenRouter。這裡的 catch 是空的,沒有 rethrow,代表 Mistral 那條路只要出事,不管是什麼原因,一律靜默吞掉、換下一條路繼續跑,不會讓整個分類流程在這裡就中斷掉。

Mistral 這邊呼叫很單純:

async function callMistral(env, system, user) {
  const res = await fetch('https://api.mistral.ai/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${env.MISTRAL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'mistral-medium-latest',
      messages: [
        { role: 'system', content: system },
        { role: 'user', content: user },
      ],
      temperature: 0.2,
    }),
  });

  if (!res.ok) throw new Error(`Mistral 分類失敗(${res.status})`);

  const data = await res.json();
  return data.choices?.[0]?.message?.content ?? '';
}

打的是 Mistral 官方 API,用自己帳號申請的免費額度,model 寫死 mistral-medium-latest,失敗就 throw,讓 callAI 那層的 catch 接住。

callOpenRouter 長得很像,但關鍵差在 body 那個欄位:

async function callOpenRouter(env, system, user) {
  const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${env.OPENROUTER_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      models: env.AI_MODELS,
      messages: [
        { role: 'system', content: system },
        { role: 'user', content: user },
      ],
      temperature: 0.2,
    }),
  });

  if (!res.ok) {
    const detail = await res.text();
    throw new Error(`AI 分類失敗(${res.status})${detail.slice(0, 120)}`);
  }

  const data = await res.json();
  return data.choices?.[0]?.message?.content ?? '';
}

Mistral 那邊傳的是單數 model,這裡傳的是複數 models,一個陣列。env.AI_MODELS 對應 wrangler.toml 裡的設定:

AI_MODELS = ["meta-llama/llama-4-maverick:free", "meta-llama/llama-4-scout:free", "openrouter/free"]

OpenRouter 看到 models 這個欄位,會照陣列順序自己去試,第一個掛了換第二個,不用自己寫重試邏輯。這個陣列有個硬限制,最多只能放 3 個,超過的話 API 直接回 400(錯誤訊息是 'models' array must have 3 items or fewer.),這也是為什麼陣列長度剛好卡在三。

所以標題講的「三層」,對到的就是這三層:第一層是 Mistral 官方 API,用自己帳號的額度打頭陣;第二層是 OpenRouter 陣列裡排在前面的免費模型,目前是 llama-4-maverick 跟 llama-4-scout;第三層固定放 openrouter/free,讓 OpenRouter 自動選一個當下還活著的免費模型頂著,當最後的保底。三層疊起來,前面兩層不管哪個下架、哪個超額度,都還有下一層可以接。

會多加 Mistral 這一層,是後來才補上去的設計,理由是 Day 4 提過的那個錯誤畫面:免費 Llama 模型偶爾不照指示輸出 JSON,會回一些類似安全審查標記的字串,像是 User Safety: safe,這種情況下 Day 10 講的那三層 JSON 解析會全部失敗。Mistral 官方 API 在這方面穩定不少,設定了 MISTRAL_API_KEY 就優先打它,能把「回傳格式壞掉」這件事的機率壓低一截。

也因為 Mistral 跟 OpenRouter 這兩家的 API 規格不一樣,才拆成 callMistral 跟 callOpenRouter 兩支獨立函式:Mistral 官方 API 只吃單一 model 字串,沒有 OpenRouter 那種多模型 fallback 陣列的機制,硬要共用一套呼叫邏輯反而要寫一堆 if-else 判斷是誰在跑,不如兩支函式各自單純。MISTRAL_API_KEY 這個 secret 本身也是選填的,沒設定就直接跳過整段 Mistral 邏輯,callAI 一開始的 if (env.MISTRAL_API_KEY) 就是在做這個判斷。

三層都試過還是失敗,callOpenRouter 丟出的錯誤會一路往上,被 Day 10 那套「失敗也要給診斷資訊」的錯誤處理接住,回到 LINE 對話框裡讓我看到到底是哪一層、哪個狀態碼出的問題。明天要換個方向,講抓不到內文的網站(像 Facebook、Instagram 這種擋爬蟲的),line-notion-bot 怎麼讓筆記還是能憑網址跟備註存進去。


上一篇
Day 11|標籤越長越亂?先讀既有標籤,讓 AI 重用同一個詞
下一篇
Day 13|抓不到內文的網站:只憑網址和備註也要存得進去
系列文
用 LINE Bot 和 Agent Skill 自動化整理筆記與 IG 發文 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言