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 放在最前(第一條訊息)。 |
| 混用不同角色訊息 | 同時使用 HumanMessagePromptTemplate 與 AIMessagePromptTemplate 產生同一輪對話,會讓模型產生自洽性問題。 |
僅 在需要示範回應或測試時才加入 AIMessagePromptTemplate,正式流程中僅保留 System + Human。 |
| 未考慮多語言或文字編碼 | 中文提示若包含特殊符號或全形空格,可能被模型誤解。 | 統一使用 UTF-8,避免不必要的全形字符,並在模板中加入說明文字(如「請使用繁體中文」)。 |
其他建議
- 使用模板檔案:將常用的
ChatPromptTemplate定義於.prompt或.json檔案,程式中只負責載入,方便團隊協作與版本控制。 - 加入測試:利用 Jest 或 Vitest 撰寫單元測試,驗證
formatMessages後的結構是否符合預期。 - 監控 Token 使用量:在部署前加入
tokenCounter,確保每次呼叫不會超過模型上限,避免突發錯誤。
實際應用場景
- 客服機器人
- 使用
SystemMessage設定「只能提供公司政策範圍內的答案」;ConversationBufferMemory保留最近 5 條對話,確保上下文連貫。
- 使用
- 教育輔助系統
- 依據學生的年級、科目動態切換
SystemMessage(如「以小學程度解釋」),並把歷史題目與解答作為history變數插入。
- 依據學生的年級、科目動態切換
- 法律諮詢平台
- 結合
AIMessagePromptTemplate給出範例回應,讓模型學習正式且嚴謹的語氣;同時使用ConversationBufferMemory限制對話長度以符合合規要求。
- 結合
- 多角色 RPG Chatbot
- 透過
getPromptByRole動態產生不同角色的系統訊息(冒險者、巫師、商人),讓同一模型根據使用者選擇的角色切換對話風格。
- 透過
- 跨平台對話同步
- 在手機與 Web 前端共用相同的
ChatPromptTemplate定義,確保不同裝置的對話行為一致,並以 JSON 檔案作為統一來源。
- 在手機與 Web 前端共用相同的
總結
ChatPromptTemplate 是 LangChain 中 構建多輪對話 的核心工具。透過 System、Human、AI 三種訊息類型的組合,我們可以:
- 明確分離 系統指令與使用者輸入,避免角色混淆。
- 彈性插值 變數,支援單值、列表甚至 JSON 結構。
- 結合記憶機制(如
ConversationBufferMemory)實現對話滾動,控制 token 數量。 - 動態產生 不同情境或角色的提示,提升系統的可擴充性。
在開發過程中,注意 Prompt 長度、變數完整性與訊息順序,並遵循「先定義 SystemMessage、後插入歷史、最後加入本輪 HumanMessage」的慣例,即可打造出 穩定、可維護且高效能 的聊天應用。希望本篇文章能幫助你快速上手 ChatPromptTemplate,在未來的 LangChain 專案中發揮更大的創意與價值。祝開發順利!