iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
生成式 AI

打造基於 MCP 協議與 n8n 工作流的會議處理 Agent系列 第 11

Day 11 智慧會議辨識類型與專案關聯

  • 分享至 

  • xImage
  •  

昨天我們成功整合了 Gradio 前端與 MCP Agent ,今天的目標是讓它變得更加智慧,能夠自動從會議內容中提取「會議類型」和「專案名稱」,並在 Notion 中建立正確的關聯,同時為每個會議生成唯一的 Session ID 以便日後追蹤。

今天的目標與挑戰

  • 新增 Session ID 功能
  • 在 Notion 會議記錄資料庫中新增 Session ID 欄位
  • 升級 AI Prompt,從會議內容中提取「會議類型」和「專案名稱」
  • 建立智慧專案關聯功能,自動查找並連結 Notion 專案資料庫
  • 測試智慧分類與關聯流程

為什麼需要 Session ID?

雖然我們已經可以成功處理會議音訊並且生成摘要,但系統還缺乏「脈絡記憶」,每次處理都是獨立的。
Session ID 的存在就像是每次會議的「身分證」,它能夠

  1. 可追溯性:每次處理都有唯一識別碼,即使名稱相同也能區分,方便後續查詢與除錯
  2. 系統整合:在不同系統(Gradio、n8n、Notion、LINE)間傳遞時保持一致性
  3. 歷史追蹤:建立完整的處理歷程,支援分析與最佳化
  4. 問題除錯:當出現問題時,能快速定位是哪次會議的摘要處理出現異常

Step 1:實作 Session ID 生成機制

1-1 選擇適合的 UUID 類型

在 Python 裡,可以使用 uuid 模組來生成全域唯一識別碼,針對 Session ID 選擇 uuid4() 是因為它具有以下優勢

  • 完全隨機:基於隨機數生成,不會洩露任何主機資訊
  • 高唯一性:碰撞機率極低,適合分散式系統
  • 安全性佳:無法預測或反推,適合作為會話標識

1-2 修改 MCPAgent 類別

src/mcp_agent.py 中新增 Session ID 生成功能

import requests
import uuid
from src.whisper_service import WhisperService
from datetime import datetime


class MCPAgent:
    def __init__(
        self, model="medium", webhook_url="http://localhost:5678/webhook/m2a-test"
    ):
        self.whisper = WhisperService(model)
        self.webhook_url = webhook_url

    def session_id(self):
        # 生成唯一的 Session ID,結合時間戳和 UUID。
        # 格式: session_YYYYMMDD_HHMMSS_UUID8

        session_uuid = uuid.uuid4()
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        # 回傳結合時間戳與部分 UUID 的字串
        return f"session_{timestamp}_{str(session_uuid)[:8]}"

    def process_audio(self, audio_path, instruction):

        # 0. 生成唯一的 Session ID
        session_id = self.generate_session_id()
        print(f"本次處理的 Session ID:{session_id}")

        # 1. 呼叫 WhisperService 轉錄
        result = self.whisper.transcribe(audio_path)
        if result.get("error"):
            # 轉錄失敗,回傳錯誤
            return {"error": f"轉錄失敗:{result['error']}"}

        # 2. 準備要送給 Webhook 的資料
        payload = {
            "session_id": session_id,
            "text": result["text"],
            "instruction": instruction,
        }

        # 3. 呼叫 n8n Webhook
        try:
            resp = requests.post(self.webhook_url, json=payload)
            resp.raise_for_status()
            return resp.json()
        except Exception as e:
            return {"error": f"Webhook 呼叫失敗:{e}"}

1-3 Session ID 格式說明

我們設計的 Session ID 格式為 session_YYYYMMDD_HHMMSS_UUID8位

例如session_20250923_143056_a7b8c9d2

這種格式具有以下優點

  1. 可讀性:包含時間資訊,從 ID 就能看出會議摘要處理的日期時間
  2. 排序性:依時間順序自然排列,按字母排序就等於按時間排序
  3. 唯一性:UUID 確保不會重複
  4. 簡潔性:只取 UUID 前8位,保持辨識度的同時避免過長

Step 2:在 Notion 資料庫中新增 Session ID 欄位

2-1 更新 Notion 資料庫結構

進入「會議記錄」資料庫,新增或確保有以下欄位

  1. Session ID

    • 類型:Text(文字)
    • 用途:儲存每次會議的唯一標識符
  2. 會議類型

    • 類型:Select(選擇)
    • 選項:專案討論、技術討論、需求分析、進度報告、創意發想、決策會議、一般會議
    • 用途:自動辨識並分類會議類型
  3. 關聯專案

    • 類型:Relation(關聯)
    • 關聯到:GIGI 工作室專案庫
    • 用途:自動關聯到現有的專案資料庫的相關專案
  4. 建立時間

    • 類型:Create Time(建立時間)
    • 用途:記錄會議處理的確切時間

Step 3:在工作流中新增「取得專案清單」 Notion 節點

要讓 AI 知道我們有哪些專案,首先就是要讓 n8n 把專案清單從 Notion 讀取出來。

3-1 節點配置

在「Webhook」節點後面,新增 Notion 節點 Actios 底下的「Get many database pages」。

  • Display Name:取得專案清單
  • DatabaseGIGI工作室專案庫
  • Return ALlON

Step 4:在工作流中新增「格式化專案列表」 Code 節點

再來我們需要將上一步輸出的結果,轉換為 AI 最容易理解的純文字格式。

4-1 節點配置

在「取得專案清單」節點後面,新增 Code 節點。

  • Display Name:格式化專案列表
  • JavaScript
    // 取得上一個節點的所有輸出項目
    const projects = $input.all();
    
    // 從每個專案物件中,只取出「專案名稱」
    // 從 simplified 格式的 json.property_ 屬性中取得名稱
    const projectNames = projects.map(item => item.json.property_);
    
    // 過濾掉空的專案名稱
    // Notion 資料庫可能會有空白標題的頁面
    const filteredProjectNames = projectNames.filter(name => name && name.trim() !== '');
    
    // 將有效的專案名稱陣列轉換成一個字串
    const projectListString = filteredProjectNames.join(', ');
    
    // 回傳一個包含這個清單的物件
    return [{
      json: {
        project_list: projectListString
      }
    }];
    

Step 5:修改 AI Prompt 進行資訊提取

我們要來賦予它從會議內容中辨識「會議類型」和「專案名稱」的智慧。

5-1 最佳化「AI 摘要與任務提取」節點

在 n8n 的「AI 摘要與任務提取」節點中修改 Prompt,讓 AI 不只能生成摘要和提取任務,還能辨識「會議類型」和「專案名稱」。

以下是修改節點後的內容

// 從 Webhook 拿原始資料 (明確指定拿第一筆)
const webhookData = $('Webhook').first().json.body; 
// 從「格式化專案列表」拿專案列表 (明確指定拿第一筆)
const projectListData = $('格式化專案列表').first().json; 

const sessionId = webhookData.session_id;
const text = webhookData.text;
const instruction = webhookData.instruction;
const projectList = projectListData.project_list;

// ===== AI 提取函式 =====
async function extractMeetingInfo(text, instruction, projectList) {
  const payload = {
    model: "qwen2.5-taiwan-7b-instruct-i1",
    messages: [
      {
        role: "system",
        content: `你是一個頂尖的會議分析師。你的任務是根據「會議逐字稿」和一份「已知的專案清單」來提取資訊。

[已知的專案清單]
${projectList}

[你的任務]
1. 仔細閱讀「會議逐字稿」。
2. 判斷逐字稿的內容主要是在討論「已知的專案清單」中的哪一個專案。
3. 如果找到完全匹配的專案,請在 project_name 欄位回傳該專案的確切名稱。
4. 如果逐字稿內容與任何已知專案都無關,或者不明確,請務必在 project_name 欄位回傳 null。
5. 完成會議類型、摘要和行動任務的提取。

[回傳格式]
請務必嚴格按照以下 JSON 格式回傳,僅包含指定的四個鍵:
{"project_name":"從清單中匹配到的專案名稱或 null","meeting_type":"會議類型(如專案討論、技術討論、需求分析、進度報告、創意發想、決策會議、一般會議)","summary":"會議摘要 (Markdown 格式)","tasks":"行動任務 (Markdown 格式)"}`
      },
      {
        role: "user",
        content: `[會議逐字稿]\n${text}\n\n[使用者指令]\n${instruction}`
      }
    ],
    temperature: 0.3,
    max_tokens: 2048
  };

  try {
    const response = await this.helpers.httpRequest({
      method: 'POST',
      url: 'http://host.docker.internal:1234/v1/chat/completions',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });

    const responseData = typeof response === 'string' ? JSON.parse(response) : response;

    if (responseData.choices && responseData.choices[0] && responseData.choices[0].message) {
      const content = responseData.choices[0].message.content;
      const match = content.match(/\{[\s\S]*\}/);
      if (match && match[0]) {
        return JSON.parse(match[0]);
      }
    }
    
    throw new Error('AI 回應格式異常,找不到有效的 JSON 內容');

  } catch (error) {
    console.error(`[${sessionId}] AI 資訊提取失敗:`, error);
    return {
      project_name: null,
      meeting_type: "錯誤",
      summary: `AI 分析失敗: ${error.message}`,
      tasks: "無法提取行動項目"
    };
  }
}

// ===== 主流程 =====
const meetingInfo = await extractMeetingInfo(text, instruction, projectList);
return [{ json: { session_id: sessionId, meeting_info: meetingInfo } }];

5-2 核心配置原則

  1. 集中管理: Prompt 都放在此節點,後續只需要在這裡調整就好。
  2. 嚴格格式:強制 AI 回傳純的 JSON,使用正規表示法 /\{[\s\S]*\}/ 抓取 JSON,避免任何多餘文字干擾解析。
  3. 一致性低溫度temperature: 0.3 保持回應穩定、一致,避免過度創意導致格式錯亂。
  4. 清晰命名:將回傳的 key 定為 project_namemeeting_typesummarytasks,保持前後端欄位名稱一致。

Step 6:在工作流中新增「If」 條件判斷節點

AI 給出答案後,我們需要一個 「If」 節點根據是否找到專案,來執行後續流程走向不同的路徑。

6-1 節點配置

在「AI 摘要與任務提取」節點後面,新增 「If」 節點。

  • Display Name:If
  • Conditions
    • Value{{ $('AI 摘要與任務提取').item.json.meeting_info.project_name }}
    • Operationis not empty

6-2 True 分支:查詢並關聯專案

當成功匹配到專案後,要在建立「會議記錄」時寫入關聯,因此要

  1. 新增「專案查詢」Notion 節點
    在「If」節點結果為 True 的輸出端,新增 Notion 節點 Actios 底下的「Get many database pages」。
    • Display Name:專案查詢
    • DatabaseGIGI工作室專案庫
    • Limit:1
    • Filter:Build Manually
    • Must Match:Any filter
    • Filters:Add Condition
      • Property Name or ID:專案名稱
      • Condition:Equals
      • Title{{ $('AI 摘要與任務提取').item.json.meeting_info.project_name }}
  2. 修改「寫入會議紀錄」節點
    1. 將此節點連接到「專案查詢」之後。
    2. 在原有的摘要、任務等屬性之外,新增
      • 「會議類型」
        • Key Name or ID:會議類型
        • Option Names or IDs{{ $('AI 摘要與任務提取').item.json.meeting_info.meeting_type }}
      • 「GIGI工作室專案庫」
        • Key Name or ID:GIGI工作室專案庫
          Add item
        • Relation IDs{{ $('專案查詢').first().json.id }}
      • 「Session ID」
        • Key Name or ID:Session ID
        • Text{{ $('AI 摘要與任務提取').item.json.session_id }}

6-3 False 分支:僅建立會議紀錄

當未匹配到專案時,就不建立專案關聯了。

  1. 複製「寫入會議紀錄」節點, 並且重新命名為「寫入會議紀錄(無關聯)」
  2. 將複本連接到 「If」 節點的 「False」 輸出端
  3. 打開此節點,刪除 Key Name or ID 名為「GIGI工作室專案庫」屬性,但保留其他欄位

Step 7:修改「Markdown 格式化處理」節點內容

最後無論流程走了哪條分支,都需要一個統一的「打包」的節點,將結果整理成前端介面與 LINE 通知所需的最終格式。

7-1 節點內容

以下為修改後的節點內容

const aiNodeData = $('AI 摘要與任務提取').first();
const summary = aiNodeData.json.meeting_info.summary;
const tasks = aiNodeData.json.meeting_info.tasks;

const notionUrl = $input.item.json.url;

function markdownToPlainText(markdown) {
  if (!markdown || typeof markdown !== 'string') {
    return '';
  }
  let text = markdown;
  text = text.replace(/^#{1,6}\s+/gm, '');
  text = text.replace(/(**|__)(.*?)\1/g, '$2');
  text = text.replace(/(*|_)(.*?)\1/g, '$2');
  text = text.replace(/~~(.*?)~~/g, '$1');
  text = text.replace(/!\[.*?\]\(.*?\)/g, '');
  text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
  text = text.replace(/^\s*[*\-\+]\s+/gm, '• ');
  text = text.replace(/^\s*\d+\.\s+/gm, '');
  text = text.replace(/^(---|___|***)\s*$/gm, '');
  text = text.replace(/``````/g, '');
  text = text.replace(/`([^`]+)`/g, '$1');
  text = text.replace(/\n{3,}/g, '\n\n');
  return text.trim();
}

const plainSummary = markdownToPlainText(summary);
const plainTasks = markdownToPlainText(tasks);

const lineMessageText = `🎯 會議處理完成通知

- 專案:GIGI 工作室內部專案
- 日期:${new Date().toLocaleDateString('zh-TW')}
- Notion 頁面:${notionUrl}

--- 會議摘要 ---
${plainSummary}

--- 行動任務 ---
${plainTasks}`;

const lineApiPayload = {
  "messages": [
    {
      "type": "text",
      "text": lineMessageText
    }
  ]
};

const finalOutput = {
  summary: summary,
  tasks: tasks,
  url: notionUrl,
  lineApiPayload: lineApiPayload
};

return [finalOutput];

Step 8:端到端流程驗證

8-1 驗證前的準備

先確認所有服務都已處於「待命」狀態

  • n8n 工作流:確認 M2A Agent 工作流處於 Active 的狀態。
  • Gradio 前端:在專案根目錄終端機中,執行 python app.py,並確認沒有任何錯誤訊息,且程式正在運作中。

8-2 最終測試步驟

  1. 打開介面:在瀏覽器中打開 Gradio 提供的本地網址(http://127.0.0.1:7860)。
  2. 上傳音訊:在「會議音訊」區塊,上傳音訊檔案。
  3. 輸入指令:在「處理指令」輸入框中,輸入指令。
  4. 點擊送出:點擊「開始處理」按鈕。

執行結果

Gradio 前端頁面
Successful
LINE 通知
Line
Notion
Notion
終端機
Terminal
n8n workflow
WorkFlow

確認過以上的結果後,代表我們成功了!!


今天的成果總結

完成項目

  • 為處理流程加入了唯一的 Session ID,提升了可追蹤性
  • 實現了 n8n 動態讀取 Notion 資料庫作為 AI 的「知識庫」
  • 透過升級 Prompt,賦予 AI 匹配專案與處理 null 情境的能力
  • 利用 If 節點成功實現了工作流的條件分支,讓 Agent 能根據不同情況執行不同路徑
  • 打通了 Notion 的 Relation 功能,實現了會議記錄與專案的自動化關聯

心得

今天的 M2A Agent 開始邁向「智慧化」了,我成功地使用 RAG (Retrieval-Augmented Generation),也就是「檢索增強生成」提供「已知專案清單」給 AI,讓我的 Agent 擁有了「理解上下文」,還有透過 If 節點根據輸出結果進行條件分支擁有了「決策」的能力,讓原本線性的流程,進化成了具備初步判斷能力的智慧系統。

這樣實作的成功讓我感到我的 Agent 更厲害了,我也更開心了!

🎯 明天計劃
導入錯誤處理與狀態回報機制,並在失敗時透過 LINE 主動通知,提升系統穩定性。


上一篇
Day 10 Gradio 前端開發與標準化專案結構
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言