昨天我們建立了錯誤處理與狀態回報機制,讓系統變得更加可靠,但是在會議的內容裡經常有「週三前要完成」、「下週二交付」這種的自然語言的時間描述。
因此今天的目標是訓練 AI 將這些自然語言時間表達可以轉換為標準的日期格式,並且自動填入 Notion 的「完成期限」欄位,讓任務管理變得更加智慧化。
首先要在「會議記錄」資料庫中新增日期相關的欄位。
進入「會議記錄」資料庫,新增以下欄位,若已有則不用
設計一個專門用於日期解析的 Prompt。
在設計 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":"未提及具體時間,預設明天"}
這個 Prompt 我的設計分成以下幾點
現在要修改現有的「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;
// ===== 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 }];
這次重構的重點在於 Prompt 工程的最佳實踐
Promise.all
同時執行兩個不同的 Prompt 任務0.1
,確保輸出一致性現在要在兩個「寫入會議紀錄」的節點,加入新的日期欄位。
Properties
區域,點擊 Add Property
,新增以下欄位
{{ $('AI 摘要與任務提取').first().json.meeting_info.deadline_date }}
{{ $('AI 摘要與任務提取').first().json.meeting_info.original_time_expression }}
Properties
區域,新增相同的兩個欄位
{{ $('AI 摘要與任務提取').first().json.meeting_info.deadline_date }}
{{ $('AI 摘要與任務提取').first().json.meeting_info.original_time_expression }}
好的 Prompt 需要不斷測試和最佳化。
建立包含各種時間表達的測試內容
請提取會議摘要與行動任務,特別注意時間相關資訊
檢查 Notion「會議記錄」資料庫中的結果
根據測試結果調整 Prompt
// 好的做法:專用 Prompt 函式
function buildSpecificPrompt(params) {
return `專門設計的 Prompt 內容...`;
}
// 不太好的做法:將所有功能塞在一個 Prompt 裡
const messyPrompt = `你要同時做摘要、提取任務、解析日期、判斷專案...`;
可以在程式碼中加入 Prompt 版本註記
// 日期解析 Prompt v1.2 - 新增模糊時間處理
function buildDateParsingPrompt(todayStr, dayOfWeek) {
return `你是一個專業的時間解析專家...`;
}
可以設計不同版本的 Prompt 進行比較
// 可以透過參數切換不同的 Prompt 版本
const usePromptVersion = 'v1.2'; // 或 'v1.1'
const prompt = usePromptVersion === 'v1.2' ?
buildDateParsingPromptV12() :
buildDateParsingPromptV11();
✅ 完成項目
今天透過精心設計專用的 Prompt,我們讓 AI 具備了理解自然語言時間表達的能力,這個過程展現了 Prompt 設計的重要性,好的 Prompt 就像給 AI 的精確說明書,能大幅提升任務執行的準確性。
我也建立了模組化的 Prompt 架構,讓不同功能的 Prompt 可以獨立設計、測試和最佳化,這種設計模式為未來更複雜的 AI 功能奠定了良好基礎。
🎯 明天計劃
設計「參與者辨識」專用 Prompt,從會議對話中自動辨識參與夥伴,並與 Notion 使用者資料庫進行匹配,實現會議記錄的智慧標記與歸檔。