iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
AI 自動化

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

Day 10|AI 回傳格式壞掉怎麼辦:JSON 解析與驗證

  • 分享至 

  • xImage
  •  

昨天把 classify() 的 system prompt 拆完了,最後留了一個尾巴:prompt 裡明明寫死「只輸出 JSON,不要有任何前言、說明或 markdown 標記」,但免費模型不見得會聽話。今天就是要看,AI 沒照規格回話的時候,程式怎麼接住那個爛掉的回應。

先看 AI 回應拿到手之後,到真的變成一個可用物件之前發生了什麼事:

const raw = await callAI(env, system, user);
const cleaned = raw
  .replace(/```json/gi, '')
  .replace(/```/g, '')
  .trim();

let parsed;
try {
  parsed = JSON.parse(cleaned);
} catch {
  // 有些模型會夾帶前後文字,退而求其次抓出第一個 JSON 物件
  const m = cleaned.match(/\{[\s\S]*\}/);
  try {
    if (!m) throw new Error('no braces found');
    parsed = JSON.parse(m[0]);
  } catch {
    // 真的解析不出來時,把原始回應片段附在錯誤訊息裡,方便直接在 LINE 上看到問題出在哪
    const snippet = cleaned.slice(0, 300) || '(AI 回傳空白內容)';
    throw new Error(`AI 回傳格式無法解析。原始回應:${snippet}`);
  }
}

第一步是無條件清理:不管 AI 有沒有夾 code fence,先把 ```json``` 這兩種標記整段拿掉再說。免費模型很愛把 JSON 包在 code fence 裡面交差,明明 prompt 已經講過不要這樣做,但擋不住它就是想照 Markdown 的習慣輸出。與其等 parse 失敗才處理,不如先清乾淨再說,反正對正常輸出來說這行 replace 也不會動到任何東西。

清乾淨之後才是真正的三層容錯。第一層是直接 JSON.parse(cleaned),這是最理想的狀況,多數時候(尤其是走 Mistral 那條路徑時)就直接過了。第一層失敗,代表清乾淨之後那串文字還是不是一個乾淨的 JSON 物件,最常見的原因是模型在 JSON 前後多講了幾句廢話,比如「好的,以下是分類結果:」。這時候第二層用一個貪婪的正則 /\{[\s\S]*\}/ 去抓第一個 { 到最後一個 } 之間的內容,等於是把前後的廢話切掉,只留中間看起來像 JSON 的那一塊,再試著 parse 一次。

如果連這樣都抓不到或還是 parse 失敗,程式就放棄治療,直接丟出錯誤。但丟錯誤的方式特別值得看一眼:不是丟一句「AI 回傳格式錯誤」就算了,而是把清理過的原始回應截前 300 字塞進錯誤訊息裡。這個錯誤最後會被 handleTextMessage 的 catch 接住,原封不動回到 LINE 對話框裡。等於是不用開 wrangler tail 看 log,光看 LINE 上收到的錯誤訊息就知道 AI 到底回了什麼鬼東西——是整個回應是空的、還是夾了安全審查提示、還是格式對了但少了某個欄位。這個「失敗也要給診斷資訊」的原則,是我自己踩過幾次「錯誤訊息只寫一句話、只能重現看 log」的坑之後才學到要顧的事。

parse 成功不代表資料就能直接拿去寫 Notion,classify() 最後回傳的物件還有一層防呆:

return {
  title: parsed.title || url,
  category: CATEGORIES.includes(parsed.category) ? parsed.category : '生活其他',
  tags: Array.isArray(parsed.tags) ? parsed.tags.slice(0, 4) : [],
  summary: parsed.summary || '',
};

這四行才是真正的「驗證」,跟前面的「解析」是兩件事:解析只保證拿到一個合法的 JSON 物件,不保證裡面的欄位長得對。title 沒給就退回用網址頂著,至少 Notion 頁面標題不會是空的。category 特別用 CATEGORIES.includes() 檢查過,因為 Day 5 提過 Notion 那個欄位是 Select 型別,如果 AI 亂造一個不在清單裡的分類名稱直接寫進去,Notion 會自動幫你建立一個新的 Select 選項,久了分類軸就跟標籤一樣發散掉、失去分類的意義,所以這裡沒過白名單的一律砍成「生活其他」。tags 檢查是不是陣列——萬一 AI 心情好把標籤寫成一個字串而不是陣列,Array.isArray 會抓到,抓到就直接給空陣列,不硬轉型去猜它原本想表達什麼;就算是陣列,也再切一次 slice(0, 4),prompt 裡雖然要求 2-4 個,但這只是「請求」,不是保證,實際擋住那個上限的是這行程式碼,不是 system prompt 裡的那句話。summary 沒給就是空字串,不會讓整個流程炸掉。

解析跟驗證其實是同一件事的兩個階段:解析是把一坨文字變成程式看得懂的物件,能不能過全看格式對不對;驗證是物件格式對了之後,還要問裡面的值合不合規矩。兩層都做完,才算是真的把 AI 這個不可控的輸出源,收攏成一個可以放心塞進 Notion API 的乾淨資料。明天要往回走一步,看 getExistingTags 抓來的既有標籤,是怎麼餵給 AI、又為什麼常常還是攔不住標籤越生越亂的問題。


上一篇
Day 9|分類 Prompt 設計:固定分類、2–4 個標籤、2–3 句摘要
系列文
用 LINE Bot 和 Agent Skill 自動化整理筆記與 IG 發文10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言