📚職涯停看聽・知識庫← 總部儀表板
📅最後更新:2026/06/23
📑 目錄

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) 輸出)

函數邏輯:

  1. consent_at === nullconsent_revoked_at 存在 → 回傳通用模式文字(不注入任何個人資料)
  2. 提取並過濾 null 欄位的 8 維度資料
  3. follow_up_date 在 7 天內 → 加入下一里程碑提醒
  4. 呼叫 buildPtypeGuidance(primary, dimensions) 取得 P-type 引導
  5. S6 active_service → 使用「我們」語氣 + 不加預約 CTA
  6. 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 引導文字多語言支援
← 返回 操作 SOP