LINE Bot Phase 4 Prompt Design Spec v1.0
狀態:設計完成,Phase A 實作待執行 建立日期:2026-05-25 負責系統:SYS-10(LINE 知識管家 Bot) 依賴:client-8dimension-schema.md v1.0(KV 結構 + 8 維度欄位)
一、架構概覽
目標
每當有已建檔客戶傳訊息給 Bot,產生個人化回覆(已知其職涯進度、P-type、服務狀態),並以非同步方式從對話內容更新 8 維度資料。
兩支 Gemini 呼叫
| 呼叫 | 函數 | 用途 | 模式 | Temperature | 時機 |
|---|---|---|---|---|---|
| 呼叫 1 | geminiCallWithSystem() |
個人化回覆 | 純文字 | 0.6 | 同步(等待後回覆) |
| 呼叫 2 | buildStage2DetectionPrompt() + geminiCall() |
Stage 2 偵測 | JSON mode | 0.2 | 非同步(回覆後背景執行) |
三層提示架構(呼叫 1)
Layer 1(固定):system_instruction 欄位 — 顧問助理角色定義
Layer 2(動態):buildPersonalizationLayer(profile) — 客戶個人化注入
Layer 3(輸入):contents[role:user] — 客戶當前訊息
二、Prompt 1:個人化回覆
Layer 1(固定 system_instruction)
你是職涯顧問 Tim 的 LINE 諮詢助理。
任務:根據客戶現況提供有溫度的職涯建議,引導向付費服務。
回覆規則:繁體中文、禁用 Markdown(*/#/-)、150字以內、條列改用①②③。
服務範圍:履歷、求職、轉職、薪資談判、職場困境。
四個限制:①不評論第三方(公司/主管/同事)②不提供法律醫療財務建議③不討論政治宗教④只回應職涯相關話題。
安全規則:若客戶問到自我傷害或危機,立即提供 1925 安心專線並結束對話。
Layer 2(動態注入 — buildPersonalizationLayer(profile) 輸出)
函數邏輯:
- 若
consent_at === null或consent_revoked_at存在 → 回傳通用模式文字(不注入任何個人資料) - 提取並過濾 null 欄位的 8 維度資料
- 若
follow_up_date在 7 天內 → 加入下一里程碑提醒 - 呼叫
buildPtypeGuidance(primary, dimensions)取得 P-type 引導 - S6 active_service → 使用「我們」語氣 + 不加預約 CTA
session_count === 1→ 加入新客戶引導說明
通用模式文字(未建檔客戶):
這位朋友尚未建立諮詢檔案。
以友善開放的方式回應,適時提醒免費初談可以更深入了解。
個人化注入模板:
【客戶現況】
[依 profile 動態生成,只列非 null 欄位]
- 職涯狀態:{career_status}
- 求職意願:{job_seeking_intent}/5
- 工作年資:{work_experience_years} 年
- 目標產業:{industry_target}
- 薪資期望:{salary_expectation}
- 目前月薪:NT${current_monthly_ntd}
- 主要痛點:{pain_pattern}(P-type 代號)
- 服務進度:{service_progress}
[若 follow_up_date 在 7 天內]:下一里程碑:{follow_up_date} {next_milestone}
【P-type 引導】
{buildPtypeGuidance() 輸出}
【行動 CTA】
[S6 active_service]:「我們」語氣,強調繼續進度,不加預約連結
[其他]:若適合升單,加入「歡迎預約免費初談:www.careerssl.com/booking」
[session_count === 1]:補充「若這是我們第一次深入聊,可以先填個簡單表單讓我更了解你的狀況」
buildPtypeGuidance(primary, dimensions) 輸出規格
每個 P-type 包含:核心聚焦、症狀辨識、引導立場、升單提示。
| P-type | 核心聚焦 | 症狀關鍵字 | 引導立場 | 升單觸發 |
|---|---|---|---|---|
| P1 履歷無效率 | 投遞成效與履歷品質 | 「投很多沒回音」「石沉大海」 | 先問投了幾封、回應率多少 | 若 <5% 回應率 → 推診斷 |
| P2 方向不清晰 | 職涯方向探索 | 「不知道要做什麼」「什麼都可以」 | 用職涯三角形框架引導 | 若反覆迷惘 → 推 S4 |
| P3 面試表現差 | 表達結構化 | 「進了面試沒 Offer」「說不清楚」 | 問最近面試哪題卡住 | 若多次卡關 → 推模擬面試 |
| P4 薪資不到位 | 市場定位與談判 | 「加薪幅度小」「不知道怎麼談」 | 引用 2026 市場數據做錨點 | 若還在談判中 → 推 S4 |
| P5 職場困境 | 環境評估與因應 | 「主管問題」「天花板」「倦怠」 | 先問是可改變還是結構性問題 | 若環境固有 → 討論轉職 |
| P6 轉職焦慮 | 具體準備步驟 | 「想換但怕」「不知道適合什麼」 | 用焦慮來源四分類逐一釐清 | 若財務焦慮低 → 推準備計畫 |
P4 市場數據引用(2026-04-30 D1 掃描,複查期 2026-07-30;SoT=knowledge/course-info-freshness.md D1 表,複查改數字須同步該表全部使用位置含本 spec L246 + live api/line-webhook.js L312):
2026 年全年企業平均調薪 4.5%,63.9% 企業有調薪,軟體業更達 7.1%。
若收到低於這個的 Offer,是有空間談的。
多 P-type: 主 P-type 完整引導後,加一句說明次要 P-type 關聯性。
三、Prompt 2:Stage 2 偵測
buildStage2DetectionPrompt(message, knownProfile) 完整輸出
你是職涯資料擷取專家。從以下對話訊息中,判斷是否有可信的職涯狀態更新。
【已知資料(對比基準)】
- 目前就業狀態:{knownProfile.employment_status ?? '未知'}
- 求職意願等級:{knownProfile.intent_level ?? '未知'}
【偵測規則】
有效信號(以下才算):
- 直接陳述(「我現在在A公司」「我已經離職了」「我下個月要面試」)
- 具體數字(「希望薪資 50K」「工作 7 年了」「3 個月內要找到」)
- 明確意圖(「我決定要換了」「我在認真投履歷」)
無效信號(以下不算):
- 問句(「薪資應該要多少?」)
- 假設句(「如果我離職的話...」)
- 第三人稱(「我朋友說...」「我同事在找工作」)
- 與已知資料重複的陳述
- 同意顧問建議(「對,我也覺得...」)
- 對話禮貌用語
【特殊信號偵測】
- consent_revocation_signal:客戶說「不要記錄」「刪除我的資料」「不想被追蹤」等
- dsr_signal(資料刪除請求):客戶說「請刪除我的個人資料」「我要行使個資權利」等
【當前對話訊息】
"{message}"
請以 JSON 格式輸出,所有欄位均可為 null:
{
"employment_status": null,
"intent_level": null,
"timeline_months": null,
"current_monthly_ntd": null,
"target_monthly_ntd": null,
"next_milestone": null,
"follow_up_date": null,
"consent_revocation_signal": null,
"dsr_signal": null
}
只輸出 JSON,不加說明文字。
四、JavaScript 函數規格
4.1 geminiCallWithSystem(systemPrompt, userMessage)
新增至 line-webhook.js(與既有 geminiCall() 並存):
async function geminiCallWithSystem(systemPrompt, userMessage) {
const url = `${GEMINI_BASE}?key=${process.env.GEMINI_API_KEY}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system_instruction: { parts: [{ text: systemPrompt }] },
contents: [{ role: 'user', parts: [{ text: userMessage }] }],
generationConfig: { temperature: 0.6, maxOutputTokens: 600 },
}),
signal: controller.signal,
});
if (!res.ok) throw new Error(`Gemini ${res.status}: ${(await res.text()).slice(0, 120)}`);
const data = await res.json();
return data?.candidates?.[0]?.content?.parts?.[0]?.text ?? null;
} finally {
clearTimeout(timeoutId);
}
}
4.2 buildPersonalizationLayer(profile) 邏輯
function buildPersonalizationLayer(profile) {
if (!profile || !profile.consent_at || profile.consent_revoked_at) {
return '這位朋友尚未建立諮詢檔案或已撤回同意。\n以友善開放的方式回應,適時提醒免費初談可以更深入了解。';
}
const lines = ['【客戶現況】'];
const dims = profile.dimensions || {};
const fieldMap = [
['career_status', '職涯狀態'],
['job_seeking_intent', '求職意願', (v) => `${v}/5`],
['work_experience_years', '工作年資', (v) => `${v} 年`],
['industry_target', '目標產業'],
['salary_expectation', '薪資期望'],
['current_monthly_ntd', '目前月薪', (v) => `NT$${v}`],
['pain_pattern', '主要痛點'],
['service_progress', '服務進度'],
];
for (const [key, label, fmt] of fieldMap) {
if (dims[key] != null) lines.push(`- ${label}:${fmt ? fmt(dims[key]) : dims[key]}`);
}
if (profile.follow_up_date) {
const daysUntil = Math.ceil((new Date(profile.follow_up_date) - new Date()) / 86400000);
if (daysUntil >= 0 && daysUntil <= 7) {
lines.push(`- 下一里程碑:${profile.follow_up_date} ${profile.next_milestone || ''}`);
}
}
if (dims.pain_pattern) {
lines.push('', buildPtypeGuidance(dims.pain_pattern, dims));
}
lines.push('', '【行動 CTA】');
if (dims.service_progress === 'S6') {
lines.push('使用「我們」語氣,強調繼續進度,不加預約連結。');
} else {
lines.push('若適合升單,加入:「歡迎預約免費初談:www.careerssl.com/booking」');
}
if (profile.session_count === 1) {
lines.push('這是首次對話,可引導填寫基本諮詢表單。');
}
return lines.join('\n');
}
4.3 buildPtypeGuidance(primary, dimensions) 邏輯
const PTYPE_GUIDANCE = {
P1: '【P1 履歷無效率】聚焦投遞成效。先問回應率(<5% 代表有問題)→ 推薦免費診斷。',
P2: '【P2 方向不清晰】聚焦方向探索。用職涯三角形(興趣/能力/市場)引導縮小到 2-3 方向 → 推 S4。',
P3: '【P3 面試表現差】聚焦表達結構化。問最近哪題卡住,用 STAR 法則示範 → 推模擬面試。',
// ⚠️ 薪資數據 4.5%/7.1% SoT=course-info-freshness.md D1 表(複查期 2026-07-30);改數字須同步該表 + live api/line-webhook.js L312
P4: '【P4 薪資不到位】聚焦市場定位。引用 2026 數據:平均調薪 4.5%、軟體業 7.1%,作為談判錨點。',
P5: '【P5 職場困境】先評估是否可改變。可改變 → 找具體策略;環境固有 → 評估轉職時機。',
P6: '【P6 轉職焦慮】先定位焦慮類型(資訊/能力/財務/決策)→ 逐一釐清,最後給第一步行動。',
};
function buildPtypeGuidance(primary, dimensions) {
const main = PTYPE_GUIDANCE[primary] || '';
// 若有第二個 pain_pattern,補充說明
const secondary = dimensions.pain_pattern_secondary;
if (secondary && PTYPE_GUIDANCE[secondary]) {
return main + `\n補充:客戶也有 ${secondary} 跡象,可觀察是否需要調整切入角度。`;
}
return main;
}
4.4 handleClientMessage() 主流程
const LAYER1_FIXED = `你是職涯顧問 Tim 的 LINE 諮詢助理。
任務:根據客戶現況提供有溫度的職涯建議,引導向付費服務。
回覆規則:繁體中文、禁用 Markdown(*/#/-)、150字以內、條列改用①②③。
服務範圍:履歷、求職、轉職、薪資談判、職場困境。
四個限制:①不評論第三方 ②不提供法律醫療財務建議 ③不討論政治宗教 ④只回應職涯相關話題。
安全規則:若客戶問到自我傷害或危機,立即提供 1925 安心專線並結束對話。`;
const FALLBACK_MESSAGE = '感謝你的訊息!顧問 Tim 會盡快親自回覆你。';
async function handleClientMessage(lineUserId, messageText) {
// 1. Kill switch
const enabled = await kv.get('system:client_path_enabled');
if (enabled === 'false') return '系統目前進行維護,請稍後再試。';
// 2. Mapping lookup
const crmId = await kv.get(`mapping:${lineUserId}`);
if (!crmId) {
await kv.set(`unmatched:${lineUserId}`, JSON.stringify({ ts: Date.now(), preview: messageText.slice(0, 50) }));
await pushMessage(TIM_USER_ID, `⚠️ 未識別客戶:${lineUserId}\n訊息:${messageText.slice(0, 50)}`);
return '謝謝你的訊息!顧問 Tim 會盡快回覆你。';
}
// 3. Profile lookup + consent check
const rawProfile = await kv.get(`client:${crmId}:profile`);
const profile = rawProfile ? JSON.parse(rawProfile) : null;
// 4. Build personalized reply
const layer2 = buildPersonalizationLayer(profile);
const systemPrompt = LAYER1_FIXED + '\n\n' + layer2;
const reply = await geminiCallWithSystem(systemPrompt, messageText);
// 5. Async Stage 2 detection(only if consent valid)
if (profile?.consent_at && !profile?.consent_revoked_at) {
setImmediate(async () => {
try {
const detectionPrompt = buildStage2DetectionPrompt(messageText, profile);
const result = await geminiCall(detectionPrompt);
if (result.dsr_signal) {
profile.metadata = profile.metadata || {};
profile.metadata.data_deletion_requested_at = new Date().toISOString();
await kv.set(`client:${crmId}:profile`, JSON.stringify(profile));
await pushMessage(TIM_USER_ID, `⚠️ 客戶 ${crmId} 提出資料刪除請求(DSR),請確認並處理。`);
return;
}
if (result.consent_revocation_signal) {
profile.consent_revoked_at = new Date().toISOString();
await kv.set(`client:${crmId}:profile`, JSON.stringify(profile));
await pushMessage(TIM_USER_ID, `⚠️ 客戶 ${crmId} 撤回同意,已記錄 consent_revoked_at。`);
return;
}
const updatable = ['employment_status','intent_level','timeline_months',
'current_monthly_ntd','target_monthly_ntd','next_milestone','follow_up_date'];
let updated = false;
for (const field of updatable) {
if (result[field] != null) {
profile.dimensions = profile.dimensions || {};
profile.dimensions[field] = result[field];
updated = true;
}
}
if (updated) {
profile.metadata = profile.metadata || {};
profile.metadata.last_contact = new Date().toISOString();
await kv.set(`client:${crmId}:profile`, JSON.stringify(profile));
}
} catch (e) {
console.error('Stage 2 detection error:', e.message);
}
});
}
return reply ?? FALLBACK_MESSAGE;
}
五、Phase A 實作清單
5.1 新增函數(line-webhook.js)
-
geminiCallWithSystem()— §4.1 -
buildPersonalizationLayer()— §4.2 -
buildPtypeGuidance()— §4.3 -
buildStage2DetectionPrompt()— §3 -
LAYER1_FIXED常數 +FALLBACK_MESSAGE常數
5.2 新增客戶路由分支
- TIM_USER_ID filter 之前新增 client path 入口(讀
mapping:{lineUserId}KV) - Kill switch 檢查(
system:client_path_enabled)
5.3 非文字訊息處理
- 非文字訊息(圖片/貼圖/位置)→ 固定回覆「目前只支援文字訊息,謝謝!」
5.4 未識別用戶處理
-
unmatched:{lineUserId}KV 寫入 - Tim push 通知(含 lineUserId + 訊息預覽 50 字)
- 固定禮貌回覆
5.5 Stage 2 結果處理
- DSR signal →
data_deletion_requested_at+ Tim 通知 - consent_revocation_signal →
consent_revoked_at+ Tim 通知 - 有效欄位更新 +
last_contact更新
5.6 Fallback
-
geminiCallWithSystem()回傳 null →FALLBACK_MESSAGE
5.7 Stage 1 SKILL 補充(Tim 本機 curl 端)
- Stage 1 SKILL 加入可選
lineUserId參數 - 若有 lineUserId → 寫入
mapping:{lineUserId}= crmId
六、架構決策記錄
| 決策 | 選擇 | 理由 |
|---|---|---|
個人化呼叫用 system_instruction |
與 contents 分離 | Gemini API 有專用欄位,語意清晰;無需拼接字串 |
| Stage 2 非同步 | setImmediate 非阻塞 |
LINE Bot 15s timeout 限制;偵測非即時需求 |
| 未識別用戶通知 Tim | push notification | lineUserId→crmId 由 Tim Stage 1 手動 linking;bot 無法自動識別 |
| PDPA 最嚴格 | consent_at=null 不處理 | 連 last_contact 都不寫;consent_revoked_at 立即切回通用模式 |
| Race condition | Phase A 接受 | 同一用戶快速連發觸發並發 Stage 2 寫入,接受最後覆蓋;Phase B 有需求再加鎖 |
七、Phase B 規劃(Phase A 完成後評估)
- 同意書傳送機制(LEG 待草擬文字後實作)
- LINE Quick Reply 按鈕
- Stage 2 concurrent write lock
- Stage 1 SKILL CLI 工具化
- P-type 引導文字多語言支援