本文 AI 產出,尚未審核

LangChain 課程 – Prompts:提示工程

主題:ChatPromptTemplate 多輪對話提示


簡介

在使用大型語言模型(LLM)建置聊天機器人時,提示(Prompt)的設計往往決定了回應的品質與一致性。傳統的單句 Prompt 雖然簡潔,但在需要多輪對話、保持上下文或執行複雜任務時,往往會出現資訊遺失或回應不符合預期的問題。

ChatPromptTemplate 是 LangChain 提供的高階工具,讓開發者可以以結構化的方式定義 多輪對話的提示模板,自動管理系統訊息、使用者訊息與 AI 回應的角色切換。透過它,我們可以把對話流程抽象成「模板 + 變數」的形式,既提升可讀性,也方便在程式中重複使用與測試。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 ChatPromptTemplate,最後展示幾個實務應用情境,幫助你在專案中快速導入多輪提示工程。


核心概念

1. ChatPromptTemplate 的組成

ChatPromptTemplate 主要由三類 Message 組成:

類型 角色 用途
SystemMessagePromptTemplate system 設定模型的行為、語氣或全域指令。
HumanMessagePromptTemplate user 使用者的輸入或變數。
AIMessagePromptTemplate assistant 預先設定的 AI 回應(少見,用於示例或測試)。

這三種訊息會依照 順序 形成「對話歷史」傳遞給 LLM。透過 ChatPromptTemplate.fromMessages([...]) 可以一次性建立完整的對話模板。

2. 變數插值(Variable Interpolation)

模板內的變數使用 {variable_name} 表示,執行時透過 formatMessages({variable_name: value}) 進行插值。變數可以是單一字串、列表(多條訊息)或更複雜的 JSON,LangChain 會自動把它轉換成相應的 Message 物件。

3. 多輪對話的「滾動」機制

在實際聊天時,我們通常只保留最近 N 條訊息,以避免 Prompt 長度過長。ChatPromptTemplate 本身不會自動截斷,但結合 ConversationBufferMemory 或自行實作 滾動窗口 後,即可在每次呼叫 LLM 前只傳遞必要的上下文。


程式碼範例

以下範例使用 JavaScript(Node.js)與 LangChain 官方套件 @langchain/core@langchain/openai。請先安裝:

npm install @langchain/core @langchain/openai

範例 1:最簡單的系統訊息 + 使用者訊息

import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from "@langchain/core/prompts";

// 建立模板:系統訊息固定,使用者訊息使用變數 {question}
const prompt = ChatPromptTemplate.fromMessages([
  SystemMessagePromptTemplate.fromTemplate(
    "你是一位友善且專業的客服助理,請用中文回覆以下問題。"
  ),
  HumanMessagePromptTemplate.fromTemplate("{question}")
]);

// 渲染 Prompt
const rendered = await prompt.formatMessages({ question: "我的訂單為什麼還沒出貨?" });
console.log(rendered);

重點SystemMessagePromptTemplate.fromTemplate 只會在第一次呼叫時加入,之後的每輪對話都會保留此設定。


範例 2:加入多輪使用者訊息(列表變數)

import { ChatPromptTemplate, HumanMessagePromptTemplate } from "@langchain/core/prompts";

// 使用者訊息可能有多條,我們以陣列方式傳入
const prompt = ChatPromptTemplate.fromMessages([
  SystemMessagePromptTemplate.fromTemplate(
    "你是一位旅遊規劃師,請根據使用者提供的資訊給出行程建議。"
  ),
  // {history} 會被展開成多個 HumanMessage
  HumanMessagePromptTemplate.fromTemplate("{history}")
]);

// 假設已收集兩條使用者訊息
const userHistory = [
  "我想去日本三天,喜歡美食與文化。",
  "預算大約 5 萬台幣,想住在市中心。"
];

const rendered = await prompt.formatMessages({ history: userHistory });
console.log(rendered);

技巧:當變數是陣列時,LangChain 會自動把每個元素轉為 HumanMessage,省去手動迭代的步驟。


範例 3:結合 ConversationBufferMemory 實作滾動窗口

import { OpenAIChat } from "@langchain/openai";
import { ConversationBufferMemory } from "@langchain/core/memory";
import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from "@langchain/core/prompts";

const llm = new OpenAIChat({ temperature: 0.2, modelName: "gpt-3.5-turbo" });

const memory = new ConversationBufferMemory({
  // 只保留最近 3 條訊息
  returnMessages: true,
  memoryKey: "chat_history",
  inputKey: "input",
  outputKey: "output",
  maxTokenLimit: 1500
});

const prompt = ChatPromptTemplate.fromMessages([
  SystemMessagePromptTemplate.fromTemplate(
    "你是一位法律顧問,請針對使用者的問題給予簡潔且正確的法律建議。"
  ),
  // 先插入過去對話歷史
  HumanMessagePromptTemplate.fromTemplate("{chat_history}"),
  // 再插入本次使用者輸入
  HumanMessagePromptTemplate.fromTemplate("{input}")
]);

// 包裝成 chain
import { RunnableSequence } from "@langchain/core/runnables";

const chain = RunnableSequence.from([
  // 先把記憶寫入變數
  async (input) => {
    const history = await memory.loadMemoryVariables({});
    return { ...input, chat_history: history.chat_history };
  },
  prompt,
  llm,
  async (output) => {
    // 把 AI 回應寫回記憶
    await memory.saveContext({ input: input.input }, { output });
    return output;
  }
]);

// 呼叫一次對話
await chain.invoke({ input: "我在租屋合約中被要求提前解約,該怎麼辦?" });

說明ConversationBufferMemory 會自動管理 chat_history,我們只需要在每次呼叫前把它注入模板,讓 LLM 只看到最新的 N 條訊息。


範例 4:使用 AIMessagePromptTemplate 預設回應(測試用)

import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate, AIMessagePromptTemplate } from "@langchain/core/prompts";

const prompt = ChatPromptTemplate.fromMessages([
  SystemMessagePromptTemplate.fromTemplate("你是一位幽默的聊天機器人。"),
  HumanMessagePromptTemplate.fromTemplate("{question}"),
  // 先給模型一個範例回應,讓它學習風格
  AIMessagePromptTemplate.fromTemplate("哈哈,這個問題真有趣!讓我想想…")
]);

const rendered = await prompt.formatMessages({ question: "為什麼貓咪喜歡躲在箱子裡?" });
console.log(rendered);

應用:在開發階段可以先放入示範回應,觀察模型是否能夠延續相同語氣;正式上線時可移除此訊息。


範例 5:動態改變 SystemMessage(根據使用者角色)

import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from "@langchain/core/prompts";

function getPromptByRole(role) {
  const systemMap = {
    admin: "你是一位資深系統管理員,請以專業且直接的語氣回答。",
    guest: "你是一位熱情的客服,請以友善且耐心的方式說明。",
  };
  return ChatPromptTemplate.fromMessages([
    SystemMessagePromptTemplate.fromTemplate(systemMap[role] || systemMap.guest),
    HumanMessagePromptTemplate.fromTemplate("{question}")
  ]);
}

// 依照使用者身份產生不同提示
const adminPrompt = await getPromptByRole("admin").formatMessages({ question: "如何設定防火牆規則?" });
const guestPrompt = await getPromptByRole("guest").formatMessages({ question: "我忘記密碼了,怎麼重設?" });

console.log("Admin Prompt:", adminPrompt);
console.log("Guest Prompt:", guestPrompt);

彈性:透過程式化產生 SystemMessage,可在同一服務中支援多種角色或情境,提升 可維護性


常見陷阱與最佳實踐

陷阱 描述 最佳實踐
Prompt 太長 當對話歷史無限制累積,會超過模型的 token 限制。 使用 ConversationBufferMemory 或自行實作 滑動窗口,只保留最近 k 條訊息。
變數未正確插值 {variable} 名稱拼寫錯誤或傳入 undefined,會產生空白或錯誤訊息。 formatMessages檢查 所有必填變數,或使用 TypeScript 定義介面。
系統訊息位置錯誤 SystemMessage 放在對話中間,會讓模型把它當成普通訊息,改變行為。 始終SystemMessage 放在最前(第一條訊息)。
混用不同角色訊息 同時使用 HumanMessagePromptTemplateAIMessagePromptTemplate 產生同一輪對話,會讓模型產生自洽性問題。 在需要示範回應或測試時才加入 AIMessagePromptTemplate,正式流程中僅保留 System + Human
未考慮多語言或文字編碼 中文提示若包含特殊符號或全形空格,可能被模型誤解。 統一使用 UTF-8,避免不必要的全形字符,並在模板中加入說明文字(如「請使用繁體中文」)。

其他建議

  1. 使用模板檔案:將常用的 ChatPromptTemplate 定義於 .prompt.json 檔案,程式中只負責載入,方便團隊協作與版本控制。
  2. 加入測試:利用 Jest 或 Vitest 撰寫單元測試,驗證 formatMessages 後的結構是否符合預期。
  3. 監控 Token 使用量:在部署前加入 tokenCounter,確保每次呼叫不會超過模型上限,避免突發錯誤。

實際應用場景

  1. 客服機器人
    • 使用 SystemMessage 設定「只能提供公司政策範圍內的答案」;ConversationBufferMemory 保留最近 5 條對話,確保上下文連貫。
  2. 教育輔助系統
    • 依據學生的年級、科目動態切換 SystemMessage(如「以小學程度解釋」),並把歷史題目與解答作為 history 變數插入。
  3. 法律諮詢平台
    • 結合 AIMessagePromptTemplate 給出範例回應,讓模型學習正式且嚴謹的語氣;同時使用 ConversationBufferMemory 限制對話長度以符合合規要求。
  4. 多角色 RPG Chatbot
    • 透過 getPromptByRole 動態產生不同角色的系統訊息(冒險者、巫師、商人),讓同一模型根據使用者選擇的角色切換對話風格。
  5. 跨平台對話同步
    • 在手機與 Web 前端共用相同的 ChatPromptTemplate 定義,確保不同裝置的對話行為一致,並以 JSON 檔案作為統一來源。

總結

ChatPromptTemplate 是 LangChain 中 構建多輪對話 的核心工具。透過 System、Human、AI 三種訊息類型的組合,我們可以:

  • 明確分離 系統指令與使用者輸入,避免角色混淆。
  • 彈性插值 變數,支援單值、列表甚至 JSON 結構。
  • 結合記憶機制(如 ConversationBufferMemory)實現對話滾動,控制 token 數量。
  • 動態產生 不同情境或角色的提示,提升系統的可擴充性。

在開發過程中,注意 Prompt 長度、變數完整性與訊息順序,並遵循「先定義 SystemMessage、後插入歷史、最後加入本輪 HumanMessage」的慣例,即可打造出 穩定、可維護且高效能 的聊天應用。希望本篇文章能幫助你快速上手 ChatPromptTemplate,在未來的 LangChain 專案中發揮更大的創意與價值。祝開發順利!