2026/3/31
AI自動化 AI Agent n8n API串接如何用 n8n 打造 AI Agent 專屬記憶庫?Logging 實戰 | (EP.7) n8n 自動化 API 串接教學
為什麼 AI 需要 Logging?告別「金魚腦」的專屬黑盒子當我們將 LINE 等通訊軟體與 AI 結合時,常常會遇到一個災難現場:AI 沒有記憶。它無法記住上一秒的對話,導致每次回覆都像初次見面,甚至引發錯誤(Error)。 Logging 的本質,就是系統的專屬「黑盒子」。一句話講完:Logging 就是為了讓你在事後「看得懂發生過什麼事」。 在 n8n 或任何自動化流程中,Log 不僅僅是無聊的資料,它是拯救開發者的五大超能力: 除錯 (Debug): 沒紀錄只能瞎猜,有紀錄就能精準抓蟲。 重現 (Reproduce): 還原當下觸發錯誤的 Prompt。 監控 (Monitor): 追蹤系統的成功率與 API 呼叫狀況。 優化 (Optimize): 作為未來訓練 AI 的精華資料。 稽核 (Audit): 有紀錄才有證據,知道哪個環節出錯。 記憶的進化:從「給人看」到「給 AI 看」Logging 的應用可以分為兩個階段: 階段一:只做 Logging(給人看)為了事後查看、稽核執行結果。我們開始把資料寫入 Google Sheets,方便我們進行 Debug 與狀態確認。 階段二:Logging + 給 AI 查(AI 也要看)當 AI 需要記得前文、參考歷史紀錄,或是做進階的資料檢索(RAG)時,Log 就直接升級成了 AI 的 Context(上下文)。這能讓 AI 瞬間恢復記憶,甚至做到個人化預測與專屬知識庫的搭建。 踩雷警告!為什麼讓 AI 自動撈資料(Tool Calling)是一場災難?很多新手會有一個致命誘惑:「既然 AI 這麼聰明,不要在 Workflow 查了,直接寫個 Tool 讓 AI 自己去 Google Sheet 撈資料(Tool Calling)不是更簡單?」 想法很完美,但對於新手與固定規則的任務來說,這是一個巨大的陷阱! 失控的可控性: AI 可能亂查太多的資料、查錯條件,甚至決定「不查了」,看它心情做事。 成本超級高: 先讓 LLM 判斷 → 呼叫工具 → 再回傳 LLM,速度極慢且狂燒 Token。 除錯大地獄: 出錯時你根本不知道是 Prompt 寫壞、工具沒寫好,還是 AI 邏輯當機。 架構大 PK:Workflow 先查 vs. AI Agent 自己撈 比較項目 Workflow 先查 (Pre-fetch / 固定 Context) AI Agent 自己撈 (Tool Calling / 動態查詢) 適用情境 固定規則(例:每次都查最新 3 筆對話) 動態條件(例:查閱特定主題的歷史紀錄) 穩定度 極高 (100% 執行) 較低(看 AI 心情決定是否呼叫) 除錯難度 簡單清晰 複雜地獄 花費成本 低 高 專家建議: 在進入 AI 節點前,先由 Workflow 流程預先整理好必要資訊(固定 Context),確保 AI 每次都有穩定、可控的上下文。只有在遇到「不確定需求」或「延伸問題」時,才交由 Agent 進行動態查詢。 完整 Workflow 架構解析本次實作的完整流程共分為 三大區段,以下逐一拆解。 區段一:接收與解析 LINE 訊息1Webhook1 → Edit Fields1 Webhook1 接收 LINE Messaging API 傳入的 POST 請求(路徑:line-0324)。 Edit Fields1 負責從 LINE 事件結構中提取三個關鍵欄位: 欄位 來源路徑 說明 input_text body.events[0].message.text 使用者輸入的訊息 userId body.events[0].source.userId 識別使用者身份 replyToken body.events[0].replyToken LINE 回覆用的一次性 Token 區段二:建立固定 Context(Pre-fetch 記憶)1Get User History → Build History Context → Debug History Preview 這是整個架構的靈魂——在 AI 介入前,由 Workflow 自行整理好歷史記憶。 Get User History:從 Google Sheets 的 logging 工作表中讀取所有紀錄。 Build History Context:透過 Code 節點對資料進行篩選與格式化: const current = $('Edit Fields1').first().json; const currentUserId = current.userId || ''; const rows = $input.all().map(item => item.json); // 只取同一位使用者、狀態為 success 的最近 3 筆紀錄 const filtered = rows .filter(row => row.userId === currentUserId && row.status === 'success') .sort((a, b) => { const ta = new Date(a.timestamp || 0).getTime(); const tb = new Date(b.timestamp || 0).getTime(); return tb - ta; }) .slice(0, 3); const historyContext = filtered.length ? filtered.map((row, idx) => { return `${idx + 1}.\n時間:${row.timestamp || ''}\n輸入:${row.input_text || ''}\n摘要:${row.summary || ''}\n分類:${row.category || ''}\n關鍵字:${row.keywords || ''}`; }).join('\n\n') : '無歷史紀錄'; return [ { json: { ...current, history_context: historyContext, history_count: filtered.length } } ]; 核心邏輯: 過濾條件為「userId 相同」且 status === 'success',排除失敗紀錄後,取最新 3 筆,確保注入 AI 的都是可靠的高品質記憶。 Debug History Preview:在此節點新增 debug_ 前綴欄位(debug_userId、debug_history_count、debug_history_context 等),讓你在 n8n 執行面板中可以直接確認「AI 即將收到的歷史資料長什麼樣子」,是開發初期排查問題的透明窗口。 區段三:AI 分析、回覆 LINE 與寫入 Log1AI Agent1 → HTTP Request1 → Prepare Log1 → Google Sheets Log1 AI Agent1(搭配 Google Gemini)接收完整的 Context 後進行分析。 Prompt 設計如下: 請讀取以下內容,輸出摘要、主題分類、3 個關鍵字。 本次使用者輸入:{{$json.input_text}} 以下是此使用者最近的互動紀錄,僅供理解上下文與延續語意。 若與本次輸入無關,請以本次輸入為主: {{$json.history_context}} System Message 明確規範 AI 行為,防止幻覺與格式失控: 你是資料整理助手。 規則: 1. 使用繁體中文。 2. 不要捏造未提供的資訊。 3. 請只輸出 JSON。 4. 欄位必須包含 summary、category、language、keywords。 5. 若有歷史互動紀錄,僅可用來補足上下文,不可把過去內容誤當成這次輸入內容。 6. 若本次輸入與歷史紀錄無明顯關聯,請忽略歷史紀錄。 7. 若資訊不足,也必須輸出合法 JSON。 Structured Output Parser 確保 AI 輸出符合以下 JSON Schema,強制欄位驗證,防止格式亂跑: { "type": "object", "properties": { "summary": { "type": "string" }, "category": { "type": "string", "enum": ["AI工具", "程式開發", "商業", "教育", "其他"] }, "keywords": { "type": "array", "items": { "type": "string" } }, "language": { "type": "string" } }, "required": ["summary", "category", "keywords", "language"], "additionalProperties": false } HTTP Request1 呼叫 LINE Reply API,將 AI 分析結果回傳給使用者。 Prepare Log1 + Google Sheets Log1 將本次執行的完整資料寫回 Google Sheets,包含以下欄位: 欄位 說明 timestamp 執行時間($now) workflow Workflow 名稱 status 執行狀態(success) executionId n8n 執行 ID userId / replyToken 使用者識別資訊 input_text 本次輸入 summary / category / language / keywords AI 分析結果 history_count 本次帶入的歷史筆數 history_context 實際注入 AI 的歷史文字 設計亮點: 記錄 history_count 與 history_context 讓你未來可以回溯「AI 當時看到的是什麼」,是除錯 Hallucination 的關鍵利器。 🎁 附錄:完整 n8n Workflow JSON 腳本你可以直接複製以下 JSON 程式碼,並匯入至你的 n8n 專案中進行測試。匯入後請記得將 Google Sheets 文件 ID 替換為你自己的試算表 ID,並重新設定 Google Sheets 與 LINE Bearer Token 的憑證(Credentials)。 { "nodes": [ { "parameters": { "httpMethod": "POST", "path": "line-0324", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [-1392, -288], "id": "07a2efae-2d4c-4b63-ab6d-38366b22f782", "name": "Webhook1", "webhookId": "a6ff766a-b5f5-4d36-9066-963fc0e4407f" }, { "parameters": { "assignments": { "assignments": [ { "id": "820ce329-eb3e-46b7-8264-c13e6bab20e1", "name": "input_text", "value": "={{ $json.body.events[0].message.text }}", "type": "string" }, { "id": "b9d8194d-1c76-4ad3-a2ab-95fc0db6a001", "name": "userId", "value": "={{ $json.body.events[0].source.userId || '' }}", "type": "string" }, { "id": "6fbbf8c4-4044-41a6-9721-4f11ec8b0001", "name": "replyToken", "value": "={{ $json.body.events[0].replyToken || '' }}", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [-1152, -288], "id": "d03c1de9-2b49-4ed0-98a3-d79d0cf0bb96", "name": "Edit Fields1" }, { "parameters": { "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "list", "cachedResultName": "google sheet logging" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "logging" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4, "position": [-912, -288], "id": "0ff96efd-5db8-45c3-a43e-0b1db98e31b3", "name": "Get User History" }, { "parameters": { "jsCode": "const current = $('Edit Fields1').first().json;\nconst currentUserId = current.userId || '';\n\nconst rows = $input.all().map(item => item.json);\n\nconst filtered = rows\n .filter(row => row.userId === currentUserId && row.status === 'success')\n .sort((a, b) => {\n const ta = new Date(a.timestamp || 0).getTime();\n const tb = new Date(b.timestamp || 0).getTime();\n return tb - ta;\n })\n .slice(0, 3);\n\nconst historyContext = filtered.length\n ? filtered.map((row, idx) => {\n return `${idx + 1}.\\n時間:${row.timestamp || ''}\\n輸入:${row.input_text || ''}\\n摘要:${row.summary || ''}\\n分類:${row.category || ''}\\n關鍵字:${row.keywords || ''}`;\n }).join('\\n\\n')\n : '無歷史紀錄';\n\nreturn [\n {\n json: {\n ...current,\n history_context: historyContext,\n history_count: filtered.length\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-672, -288], "id": "fe67f1ce-515a-471c-8fda-cdabfe30b00b", "name": "Build History Context" }, { "parameters": { "assignments": { "assignments": [ { "name": "debug_userId", "value": "={{ $json.userId }}", "type": "string" }, { "name": "debug_input_text", "value": "={{ $json.input_text }}", "type": "string" }, { "name": "debug_history_count", "value": "={{ $json.history_count }}", "type": "string" }, { "name": "debug_history_context", "value": "={{ $json.history_context }}", "type": "string" } ] }, "includeOtherFields": true, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [-432, -288], "id": "61c5f27f-f9e9-4eb1-93eb-3f18ea2b586f", "name": "Debug History Preview" }, { "parameters": { "promptType": "define", "text": "=請讀取以下內容,輸出摘要、主題分類、3 個關鍵字。\n\n本次使用者輸入:{{$json.input_text}}\n\n以下是此使用者最近的互動紀錄,僅供理解上下文與延續語意。若與本次輸入無關,請以本次輸入為主:\n{{$json.history_context}}", "hasOutputParser": true, "options": { "systemMessage": "=你是資料整理助手。\n\n規則:\n1. 使用繁體中文。\n2. 不要捏造未提供的資訊。\n3. 請只輸出 JSON。\n4. 欄位必須包含 summary、category、language、keywords。\n5. 若有歷史互動紀錄,僅可用來補足上下文,不可把過去內容誤當成這次輸入內容。\n6. 若本次輸入與歷史紀錄無明顯關聯,請忽略歷史紀錄。\n7. 若資訊不足,也必須輸出合法 JSON。" } }, "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 3.1, "position": [-176, -288], "id": "e969d51a-4654-4d55-85ae-4c8efe0c3fa0", "name": "AI Agent1" }, { "parameters": { "options": {} }, "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", "typeVersion": 1, "position": [-224, -64], "id": "8454dd71-055e-43b5-b445-5289f37fc7f8", "name": "Google Gemini Chat Model1" }, { "parameters": { "schemaType": "manual", "inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\" },\n \"category\": {\n \"type\": \"string\",\n \"enum\": [\"AI工具\", \"程式開發\", \"商業\", \"教育\", \"其他\"]\n },\n \"keywords\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" }\n },\n \"language\": { \"type\": \"string\" }\n },\n \"required\": [\"summary\", \"category\", \"keywords\", \"language\"],\n \"additionalProperties\": false\n}" }, "type": "@n8n/n8n-nodes-langchain.outputParserStructured", "typeVersion": 1.3, "position": [16, -64], "id": "b9638a0d-bfc5-4af6-8dbc-91f225223b3a", "name": "Structured Output Parser1" }, { "parameters": { "method": "POST", "url": "https://api.line.me/v2/bot/message/reply", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [{ "name": "Content-Type", "value": "application/json" }] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"replyToken\":\"{{ $('Webhook1').item.json.body.events[0].replyToken }}\",\n \"messages\":[\n {\n \"type\": \"text\",\n \"text\": \"【summary】{{ $json.output.summary }} \\n【category】{{ $json.output.category }} \\n【language】{{ $json.output.language }} \\n【關鍵字】{{ $json.output.keywords.join('、') }}\"\n }\n ]\n}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [208, -288], "id": "371bad55-6ecb-4cc0-8ef2-3a8a0d9ffdc7", "name": "HTTP Request1" }, { "parameters": { "assignments": { "assignments": [ { "name": "timestamp", "value": "={{ $now }}", "type": "string" }, { "name": "workflow", "value": "={{ $workflow.name }}", "type": "string" }, { "name": "status", "value": "success", "type": "string" }, { "name": "executionId", "value": "={{ $execution.id }}", "type": "string" }, { "name": "userId", "value": "={{ $('Webhook1').item.json.body.events[0].source.userId || '' }}", "type": "string" }, { "name": "replyToken", "value": "={{ $('Webhook1').item.json.body.events[0].replyToken || '' }}", "type": "string" }, { "name": "input_text", "value": "={{ $('Edit Fields1').item.json.input_text || '' }}", "type": "string" }, { "name": "summary", "value": "={{ $('AI Agent1').item.json.output.summary || '' }}", "type": "string" }, { "name": "category", "value": "={{ $('AI Agent1').item.json.output.category || '' }}", "type": "string" }, { "name": "language", "value": "={{ $('AI Agent1').item.json.output.language || '' }}", "type": "string" }, { "name": "keywords", "value": "={{ $('AI Agent1').item.json.output.keywords ? $('AI Agent1').item.json.output.keywords.join('、') : '' }}", "type": "string" }, { "name": "history_count", "value": "={{ $('Debug History Preview').item.json.history_count || 0 }}", "type": "string" }, { "name": "history_context", "value": "={{ $('Debug History Preview').item.json.history_context || '' }}", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [448, -288], "id": "920dbd89-1a14-47ce-85f2-cf2bd86b93ba", "name": "Prepare Log1" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "list", "cachedResultName": "google sheet logging" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "logging" }, "columns": { "mappingMode": "defineBelow", "value": { "timestamp": "={{ $json.timestamp }}", "workflow": "={{ $json.workflow }}", "status": "={{ $json.status }}", "executionId": "={{ $json.executionId }}", "userId": "={{ $json.userId }}", "replyToken": "={{ $json.replyToken }}", "input_text": "={{ $json.input_text }}", "summary": "={{ $json.summary }}", "category": "={{ $json.category }}", "language": "={{ $json.language }}", "keywords": "={{ $json.keywords }}", "history_count": "={{ $json.history_count }}", "history_context": "={{ $json.history_context }}" } }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4, "position": [704, -288], "id": "89ea098f-51c3-4aa2-8632-8c0114eb5b92", "name": "Google Sheets Log1" } ], "connections": { "Webhook1": { "main": [[{ "node": "Edit Fields1", "type": "main", "index": 0 }]] }, "Edit Fields1": { "main": [[{ "node": "Get User History", "type": "main", "index": 0 }]] }, "Get User History": { "main": [[{ "node": "Build History Context", "type": "main", "index": 0 }]] }, "Build History Context": { "main": [[{ "node": "Debug History Preview", "type": "main", "index": 0 }]] }, "Debug History Preview": { "main": [[{ "node": "AI Agent1", "type": "main", "index": 0 }]] }, "AI Agent1": { "main": [[{ "node": "HTTP Request1", "type": "main", "index": 0 }]] }, "Google Gemini Chat Model1": { "ai_languageModel": [[{ "node": "AI Agent1", "type": "ai_languageModel", "index": 0 }]] }, "Structured Output Parser1": { "ai_outputParser": [[{ "node": "AI Agent1", "type": "ai_outputParser", "index": 0 }]] }, "HTTP Request1": { "main": [[{ "node": "Prepare Log1", "type": "main", "index": 0 }]] }, "Prepare Log1": { "main": [[{ "node": "Google Sheets Log1", "type": "main", "index": 0 }]] } } } 常見問答 (FAQ)Q:為什麼不直接使用 OpenAI 或 Gemini 內建的 Memory 功能?A:雖然部分 LLM 提供內建對話記憶,但對於自動化流程開發者來說,那就像一個無法受控的「黑盒子」。將 Memory 獨立存放在 Google Sheets 或資料庫中,能讓你隨時監控、修改、除錯(Debug),並確保 AI 不會產生難以追蹤的幻覺(Hallucination)。 Q:Google Sheets 適合拿來當作長期的大型資料庫嗎?A:對於新手測試、輕量級專案或概念驗證(PoC),Google Sheets 是完美且直觀的工具,因為它「視覺化且易懂」。但當你的系統上線且流量增大時,建議將 Logging 系統轉移至 PostgreSQL、MySQL 或 Supabase 等正規關聯式資料庫,以確保效能與穩定性。 Q:什麼時候才真正需要用到 Tool Calling(動態查詢)?A:當用戶的需求不確定或需要跨時間區間搜尋時。例如,當用戶問:「幫我整理『上個月』關於『商業策略』的所有新聞」,這種條件不固定的問題,Workflow 先查(固定 Context)無法預測,此時才適合讓 AI 透過 Tool Calling 自行下達條件去資料庫撈取資料。 Q:為什麼過濾歷史紀錄時要加上 status === 'success' 的條件?A:這是防止「垃圾記憶污染」的關鍵設計。如果 AI 曾因為網路錯誤、格式異常或 Token 不足而失敗,那筆紀錄的 input_text 或 summary 可能是空的或不完整的。把這些「失敗記憶」注入 AI,反而會讓 AI 困惑或輸出錯誤的摘要。只保留 success 的紀錄才能確保記憶品質。 Q:Debug History Preview 節點有什麼用?正式上線後可以刪掉嗎?A:Debug History Preview 是一個「透明窗口」節點,讓你在 n8n 執行面板中可以直接看到「AI 即將收到的歷史資料長什麼樣子」,在開發初期排查問題時非常有價值。正式上線後可保留(幾乎沒有效能開銷),作為日後維護時的快速診斷工具。若確定不再需要除錯,可刪除以精簡流程。 Q:Structured Output Parser 是什麼?為什麼要用它?A:Structured Output Parser 的作用是強制 AI 必須按照你定義的 JSON Schema 格式輸出,而不是隨意回傳文字。如果 AI 輸出不符合格式(例如缺少 summary 欄位),n8n 會自動觸發重試。這對於後續的 keywords.join('、') 等欄位操作至關重要——若格式不穩定,後面的節點就會直接報錯。 Q:category 欄位為什麼要用 enum 限制固定選項?A:這是維持資料一致性的核心手段。如果不加 enum,AI 可能今天回傳「AI 工具」、明天回傳「人工智慧工具」、後天回傳「AI Tools」,同一個意思三種寫法,導致你在 Google Sheets 篩選或後續統計時出現資料亂象。使用 enum: ["AI工具", "程式開發", "商業", "教育", "其他"] 強制統一格式,是設計健壯資料管線的基本功。 Q:為什麼要在 Log 中記錄 history_count 和 history_context?A:這是為了讓未來的你能「重現 AI 當時的視角」。假設某次 AI 回傳了奇怪的摘要,你打開 Log 可以直接看到:「當時 AI 帶了幾筆記憶(history_count)」以及「那些記憶的完整內容是什麼(history_context)」。沒有這兩個欄位,你只能看到輸出結果,完全無法判斷問題出在 Prompt、歷史資料,還是 AI 本身。 Q:Prompt 中說「若與本次輸入無關,請以本次輸入為主」,這樣真的有效嗎?A:這條規則能降低 AI 誤用歷史資料的機率,但不能完全保證。因此 System Message 中同時加入了「不可把過去內容誤當成這次輸入內容」的明確規則,雙重防護。若主題相近偶爾仍可能混淆,此時可考慮在 Prompt 中加入更明確的分隔標記(如 ---歷史紀錄開始---)來強化區隔。 Q:replyToken 是什麼?為什麼要記錄在 Log 裡?A:LINE 的 replyToken 是一個一次性、有時效的 Token(約 30 秒有效),用來對應「這次訊息的回覆權限」。HTTP Request 節點使用它呼叫 LINE Reply API 回傳訊息給使用者。記錄在 Log 中,是為了當回覆失敗時,你可以確認 Token 是否正確傳遞(雖然過了時效無法重新使用,但至少能確認資料流程正確)。 Q:如果使用者是第一次傳訊息(沒有任何歷史紀錄),流程會出錯嗎?A:不會。Build History Context 節點在 filtered.length 為 0 時,會將 history_context 設為 '無歷史紀錄' 字串,並將此值傳給 AI Prompt。AI 收到這個明確的提示後,會直接以本次輸入為唯一依據進行分析,完全不受影響。