iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
生成式 AI

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

Day 13 Prompt 架構升級 — 智慧日期解析與模組化設計

  • 分享至 

  • xImage
  •  

昨天我們建立了錯誤處理與狀態回報機制,讓系統變得更加可靠,但是在會議的內容裡經常有「週三前要完成」、「下週二交付」這種的自然語言的時間描述。

因此今天的目標是訓練 AI 將這些自然語言時間表達可以轉換為標準的日期格式,並且自動填入 Notion 的「完成期限」欄位,讓任務管理變得更加智慧化。

今天的目標與挑戰

  • 確保 Notion 的「會議記錄」存在「完成期限」的日期欄位
  • 設計專用的日期解析 Prompt
  • 在「AI 摘要與任務提取」節點中重構現有 Prompt 架構,並整合日期解析功能
  • 自動將解析後的日期填入 Notion 的「完成期限」日期欄位

Step 1:更新 Notion 資料庫結構

首先要在「會議記錄」資料庫中新增日期相關的欄位。

1-1 新增完成期限欄位

進入「會議記錄」資料庫,新增以下欄位,若已有則不用

  1. 完成期限
    • 屬性:日期 (Date)
    • 設定:包含時間
    • 用途:儲存 AI 解析後的標準化任務截止日期
  2. 原始時間描述
    • 屬性:文字 (Text)
    • 用途:保留會議中原始的時間表達方式,方便日後對照與 Prompt 調整

Step 2:設計專用的日期解析 Prompt

設計一個專門用於日期解析的 Prompt。

2-1 分析中文時間表達模式

在設計 Prompt 前,我們先來整理常見的中文時間表達,大致分為以下幾點

  • 絕對時間:「9月30日」、「這個月底」、「年底前」
  • 相對時間:「明天」、「後天」、「下週」、「下個月」
  • 週期性時間:「週三前」、「這週五」、「下週二」
  • 模糊時間:「儘快」、「最近」、「一週內」

2-2 設計日期解析專用 Prompt

你是一個專業的時間解析專家。你的任務是仔細分析會議內容中的時間相關資訊,並將其轉換為標準日期格式。

[當前時間資訊]
- 今天日期:${todayStr}
- 今天是週${dayOfWeek}
- 明天日期:${tomorrowStr}

[必須識別的時間表達模式 - 重要!]
請特別注意以下常見的時間表達:

1. **數字 + 時間單位**:
   - 「X天後」、「X天內」= 今天 + X天
   - 「一週後」、「一週內」、「1週後」= 今天 + 7天
   - 「兩週後」、「兩週內」、「2週後」、「兩周後」、「兩周內」、「2周後」= 今天 + 14天
   - 「三週後」、「3週後」、「三周後」、「3周後」= 今天 + 21天
   - 「一個月後」、「1個月後」= 今天 + 30天

2. **週期性時間**:
   - 「這週X」= 本週的週X
   - 「下週X」= 下一週的週X
   - 「下下週X」= 下兩週的週X

3. **期限類表達**:
   - 「月底前」、「月底」= 當月最後一天
   - 「年底前」、「年底」= 當年12月31日
   - 「季底前」= 當季最後一天

4. **模糊時間預設值**:
   - 「儘快」、「盡快」、「盡速」、「趕快」= 明天
   - 「最近」、「近期」= 3天後
   - 「不久」= 5天後

[解析步驟 - 按順序執行]
1. **仔細掃描文本**:逐字檢查是否包含任何時間相關詞彙
2. **模式匹配**:對照上述模式列表進行精確匹配
3. **數字識別**:特別注意中文數字(一、二、三...)和阿拉伯數字(1、2、3...)
4. **同義詞識別**:「週」和「周」是相同的,「後」和「內」表示期限

[計算規則]
- 今天是:${todayStr}
- 明天是:${tomorrowStr}
- 兩週後是:計算今天+14天
- 如果有多個時間點,選擇最早的截止時間

[重要提醒]
- **絕對不能遺漏任何時間表達**
- 「兩週後」、「兩周後」、「2週後」、「2周後」都是相同意思
- 即使時間表達在句子中間或末尾,都必須識別
- 保留找到的原始時間描述文字

[輸出格式]
如果找到任何時間表達,嚴格按照以下 JSON 格式回傳:
{"deadline_date":"YYYY-MM-DD","original_time_expression":"找到的原始時間描述"}

如果確實沒有找到任何時間表達,回傳:
{"deadline_date":"${tomorrowStr}","original_time_expression":"未提及具體時間,預設明天"}

2-3 Prompt 設計要點說明

這個 Prompt 我的設計分成以下幾點

  • 專業角色定位:明確定義 AI 為「專業的時間解析專家」,建立專業權威感和任務聚焦
  • 完整上下文資訊:不僅提供當前日期和星期,還包含明天日期,讓 AI 能進行精準的相對時間計算
  • 結構化模式識別:將時間表達分為四大類別(數字+時間單位、週期性時間、期限類表達、模糊時間),每類都有具體範例和計算規則
  • 標準化解析流程:設計解析四步驟(掃描→匹配→識別→處理),確保 AI 按邏輯順序處理複雜的時間語義
  • 容錯與邊界處理:透過「重要提醒」區塊處理同義詞(週/周)、多時間點選擇、位置無關識別等邊界情況
  • 防錯機制設計:採用「預設明天」策略完全避免 null 值,解決了 Notion API 相容性問題
  • 嚴格格式控制:明確規定 JSON 輸出格式,並提供兩種情境的範本,確保系統整合的穩定性

Step 3:重構 AI 摘要與任務提取節點的 Prompt 架構

現在要修改現有的「AI 摘要與任務提取」節點,建立模組化的 Prompt 系統。

3-1 建立 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;

// ===== Prompt 設計區域 =====

// 日期解析專用 Prompt(強化版)
function buildDateParsingPrompt(todayStr, dayOfWeek) {
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  const tomorrowStr = tomorrow.toISOString().split('T')[0];
  
  return `你是一個專業的時間解析專家。你的任務是仔細分析會議內容中的時間相關資訊,並將其轉換為標準日期格式。

[當前時間資訊]
- 今天日期:${todayStr}
- 今天是週${dayOfWeek}
- 明天日期:${tomorrowStr}

[必須識別的時間表達模式 - 重要!]
請特別注意以下常見的時間表達:

1. **數字 + 時間單位**:
   - 「X天後」、「X天內」= 今天 + X天
   - 「一週後」、「一週內」、「1週後」= 今天 + 7天
   - 「兩週後」、「兩週內」、「2週後」、「兩周後」、「兩周內」、「2周後」= 今天 + 14天
   - 「三週後」、「3週後」、「三周後」、「3周後」= 今天 + 21天
   - 「一個月後」、「1個月後」= 今天 + 30天

2. **週期性時間**:
   - 「這週X」= 本週的週X
   - 「下週X」= 下一週的週X
   - 「下下週X」= 下兩週的週X

3. **期限類表達**:
   - 「月底前」、「月底」= 當月最後一天
   - 「年底前」、「年底」= 當年12月31日
   - 「季底前」= 當季最後一天

4. **模糊時間預設值**:
   - 「儘快」、「盡快」、「盡速」、「趕快」= 明天
   - 「最近」、「近期」= 3天後
   - 「不久」= 5天後

[解析步驟 - 按順序執行]
1. **仔細掃描文本**:逐字檢查是否包含任何時間相關詞彙
2. **模式匹配**:對照上述模式列表進行精確匹配
3. **數字識別**:特別注意中文數字(一、二、三...)和阿拉伯數字(1、2、3...)
4. **同義詞識別**:「週」和「周」是相同的,「後」和「內」表示期限

[計算規則]
- 今天是:${todayStr}
- 明天是:${tomorrowStr}
- 兩週後是:計算今天+14天
- 如果有多個時間點,選擇最早的截止時間

[重要提醒]
- **絕對不能遺漏任何時間表達**
- 「兩週後」、「兩周後」、「2週後」、「2周後」都是相同意思
- 即使時間表達在句子中間或末尾,都必須識別
- 保留找到的原始時間描述文字

[輸出格式]
如果找到任何時間表達,嚴格按照以下 JSON 格式回傳:
{"deadline_date":"YYYY-MM-DD","original_time_expression":"找到的原始時間描述"}

如果確實沒有找到任何時間表達,回傳:
{"deadline_date":"${tomorrowStr}","original_time_expression":"未提及具體時間,預設明天"}`;
}

// 會議分析專用 Prompt
function buildMeetingAnalysisPrompt(projectList) {
  return `你是一個頂尖的會議分析師。你的任務是根據「會議逐字稿」和一份「已知的專案清單」來提取資訊。

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

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

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

// ===== AI 呼叫函式區域 =====

// 日期解析函式(強化版)
async function parseDateFromText(text, instruction) {
  const today = new Date();
  const todayStr = today.toISOString().split('T')[0];
  const dayOfWeek = ['日','一','二','三','四','五','六'][today.getDay()];
  
  // 加入預處理,檢查是否包含時間關鍵字
  const timeKeywords = ['天後', '天內', '週後', '週內', '周後', '周內', '兩週', '兩周', '2週', '2周', 
                       '一週', '1週', '三週', '3週', '月後', '月內', '年後', '年內', 
                       '明天', '後天', '下週', '下周', '儘快', '盡快', '最近', '不久'];
  
  const foundKeywords = timeKeywords.filter(keyword => text.includes(keyword));
  console.log(`[${sessionId}] 在文本中找到的時間關鍵字:`, foundKeywords);
  
  const payload = {
    model: "qwen2.5-taiwan-7b-instruct-i1",
    messages: [
      {
        role: "system",
        content: buildDateParsingPrompt(todayStr, dayOfWeek)
      },
      {
        role: "user",
        content: `請仔細分析以下文本中的時間資訊:

[會議逐字稿]
${text}

[使用者指令]
${instruction}

請特別注意:我已經預先掃描到可能的時間關鍵字:${foundKeywords.length > 0 ? foundKeywords.join(', ') : '無'}

請逐步分析並回傳結果。`
      }
    ],
    temperature: 0.1,  // 低溫度確保穩定性
    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;
      console.log(`[${sessionId}] AI 日期解析原始回應:`, content);
      
      const match = content.match(/\{[\s\S]*\}/);
      if (match && match[0]) {
        const result = JSON.parse(match[0]);
        console.log(`[${sessionId}] 解析結果:`, result);
        return result;
      }
    }
    
    // 如果解析失敗,回傳預設值
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    const defaultResult = { 
      deadline_date: tomorrow.toISOString().split('T')[0], 
      original_time_expression: "AI解析失敗,預設明天" 
    };
    console.log(`[${sessionId}] AI解析失敗,使用預設值:`, defaultResult);
    return defaultResult;

  } catch (error) {
    console.error(`[${sessionId}] 日期解析失敗:`, error);
    // 錯誤時也回傳預設值
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    const errorResult = { 
      deadline_date: tomorrow.toISOString().split('T')[0], 
      original_time_expression: "系統錯誤,預設明天" 
    };
    console.log(`[${sessionId}] 系統錯誤,使用預設值:`, errorResult);
    return errorResult;
  }
}

// 會議資訊提取函式 (保持原有邏輯)
async function extractMeetingInfo(text, instruction, projectList) {
  const payload = {
    model: "qwen2.5-taiwan-7b-instruct-i1",
    messages: [
      {
        role: "system",
        content: buildMeetingAnalysisPrompt(projectList)
      },
      {
        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: "無法提取行動項目"
    };
  }
}

// ===== 資料驗證函式區域 =====

// 日期格式驗證函式(保證不回傳 null)
function validateAndFormatDate(dateString) {
  // 計算明天的日期作為預設值
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  const tomorrowStr = tomorrow.toISOString().split('T')[0];
  
  // 如果沒有日期資訊,直接回傳明天
  if (!dateString || dateString === 'null' || dateString === null) {
    console.log(`[${sessionId}] 沒有日期資訊,預設為明天: ${tomorrowStr}`);
    return tomorrowStr;
  }
  
  try {
    // 嘗試解析日期
    const date = new Date(dateString);
    
    // 檢查是否為有效日期
    if (isNaN(date.getTime())) {
      console.warn(`[${sessionId}] 無效日期格式: ${dateString},使用預設值明天: ${tomorrowStr}`);
      return tomorrowStr;
    }
    
    // 確保格式為 YYYY-MM-DD
    const validatedDate = date.toISOString().split('T')[0];
    console.log(`[${sessionId}] 日期驗證成功: ${dateString} -> ${validatedDate}`);
    return validatedDate;
    
  } catch (error) {
    console.warn(`[${sessionId}] 日期格式化失敗: ${dateString},使用預設值明天: ${tomorrowStr}`, error);
    return tomorrowStr;
  }
}

// 處理原始時間描述
function validateTimeExpression(originalExpression) {
  if (!originalExpression || originalExpression === 'null' || originalExpression === null) {
    return "預設期限(1天內)";
  }
  return originalExpression;
}

// ===== 主流程:並行執行多個 Prompt =====
console.log(`[${sessionId}] 開始並行執行 AI 分析...`);

const [meetingInfo, dateInfo] = await Promise.all([
  extractMeetingInfo(text, instruction, projectList),
  parseDateFromText(text, instruction)
]);

console.log(`[${sessionId}] AI 分析完成,開始資料驗證...`);

// 驗證和格式化日期(保證有有效日期)
const validatedDeadline = validateAndFormatDate(dateInfo.deadline_date);
const validatedTimeExpression = validateTimeExpression(dateInfo.original_time_expression);

console.log(`[${sessionId}] 最終結果: deadline_date=${validatedDeadline}, original_time_expression=${validatedTimeExpression}`);

// 合併結果
const combinedResult = {
  session_id: sessionId,
  meeting_info: {
    ...meetingInfo,
    deadline_date: validatedDeadline,  // 永遠不會是 null
    original_time_expression: validatedTimeExpression  // 永遠不會是 null
  }
};

return [{ json: combinedResult }];

3-2 重構要點說明

這次重構的重點在於 Prompt 工程的最佳實踐

  1. 模組化設計:將不同功能的 Prompt 分開設計和管理
  2. 可維護性:每個 Prompt 都有專門的建構函式,便於調整和優化
  3. 並行處理:使用 Promise.all 同時執行兩個不同的 Prompt 任務
  4. 溫度參數調整:日期解析使用更低的溫度 0.1 ,確保輸出一致性
  5. 錯誤隔離:日期解析失敗不會影響主要的會議摘要功能

Step 4:更新寫入會議紀錄節點

現在要在兩個「寫入會議紀錄」的節點,加入新的日期欄位。

4-1 修改 True 分支的寫入節點

  1. 開啟「寫入會議紀錄」節點(True 分支,有專案關聯的那個)
  2. Properties 區域,點擊 Add Property,新增以下欄位
    • 完成期限{{ $('AI 摘要與任務提取').first().json.meeting_info.deadline_date }}
    • 原始時間描述{{ $('AI 摘要與任務提取').first().json.meeting_info.original_time_expression }}

4-2 修改 False 分支的寫入節點

  1. 開啟「寫入會議紀錄」節點(False 分支,沒有專案關聯的那個)
  2. 同樣在 Properties 區域,新增相同的兩個欄位
    • 完成期限{{ $('AI 摘要與任務提取').first().json.meeting_info.deadline_date }}
    • 原始時間描述{{ $('AI 摘要與任務提取').first().json.meeting_info.original_time_expression }}

Step 5:Prompt 效果驗證與調整

好的 Prompt 需要不斷測試和最佳化。

5-1 準備測試語料

建立包含各種時間表達的測試內容

5-2 執行 Prompt 測試

  1. 開啟 Gradio 前端介面
  2. 上傳測試語料檔案
  3. 在使用者指令中輸入:請提取會議摘要與行動任務,特別注意時間相關資訊
  4. 點擊「開始處理」

5-3 驗證 Prompt 效果

檢查 Notion「會議記錄」資料庫中的結果

  • 完成期限:顯示具體日期
  • 原始時間描述:包含「這週五前、下週二、月底前」等原文
  • 摘要與任務:保持原有品質,不受新 Prompt 影響

5-4 Prompt 最佳化迭代

根據測試結果調整 Prompt


Step 6:Prompt 工程最佳實踐總結

6-1 模組化 Prompt 設計

// 好的做法:專用 Prompt 函式
function buildSpecificPrompt(params) {
  return `專門設計的 Prompt 內容...`;
}

// 不太好的做法:將所有功能塞在一個 Prompt 裡
const messyPrompt = `你要同時做摘要、提取任務、解析日期、判斷專案...`;

6-2 Prompt 版本管理

可以在程式碼中加入 Prompt 版本註記

// 日期解析 Prompt v1.2 - 新增模糊時間處理
function buildDateParsingPrompt(todayStr, dayOfWeek) {
  return `你是一個專業的時間解析專家...`;
}

6-3 A/B 測試 Prompt

可以設計不同版本的 Prompt 進行比較

// 可以透過參數切換不同的 Prompt 版本
const usePromptVersion = 'v1.2'; // 或 'v1.1'
const prompt = usePromptVersion === 'v1.2' ? 
  buildDateParsingPromptV12() : 
  buildDateParsingPromptV11();

今天的成果總結

完成項目

  • 在 「會議記錄」 建立了「完成期限」與「原始時間描述」兩個欄位
  • 設計了專用的日期解析 Prompt,支援了各種中文時間的說法
  • 重構了 Prompt 架構,實現模組化和可維護性
  • 最佳化 Prompt,提升 AI 理解準確度
  • 最佳化了 AI 呼叫策略,使用並行處理提升效率

心得

今天透過精心設計專用的 Prompt,我們讓 AI 具備了理解自然語言時間表達的能力,這個過程展現了 Prompt 設計的重要性,好的 Prompt 就像給 AI 的精確說明書,能大幅提升任務執行的準確性。

我也建立了模組化的 Prompt 架構,讓不同功能的 Prompt 可以獨立設計、測試和最佳化,這種設計模式為未來更複雜的 AI 功能奠定了良好基礎。

🎯 明天計劃

設計「參與者辨識」專用 Prompt,從會議對話中自動辨識參與夥伴,並與 Notion 使用者資料庫進行匹配,實現會議記錄的智慧標記與歸檔。


上一篇
Day 12 系統可靠性升級 - 錯誤處理與狀態回報
下一篇
Day 14 打造參與者辨識核心 - Notion 成員資料庫建置與配對邏輯原型
系列文
打造基於 MCP 協議與 n8n 工作流的會議處理 Agent17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言