今天我們的目標就是將我們寫的程式碼,加入一些從前面幾面學到的 Prompt Engineering 相關的東西。
首先我在我們資料夾下新增了一個 prompt 的資料夾。
└── src
├── index.ts
├── routes
│ └── chat.ts
├── scripts
│ └── dragChatGraph.ts
└── workflows
├── agents
│ ├── base.agent.ts
│ ├── interfaces
│ └── prompts <--------------- 這裡
├── chat.workflow.ts
└── constants
然後程式碼如下,裡面包含了一個通用版本的 template,它就是用了我們在通用篇寫的。
30-7: [知識] Prompt Engineering 之有好的溝通才有好的結果 ( 通用篇 )
import { SystemMessage } from "@langchain/core/messages";
export class BasePromptGenerator {
/**
* @param {string[]} 該 AI 工具人擅長的領域
* @returns {SystemMessage}
*/
public static getBaseChatPrompt(
technologyDomains: string[]
): SystemMessage {
const systemContent = `
# AI Base Prompt (System)
根據以上以下的流程來回答整個問題:
1. 先從 Context 理解你的角色與相關問題的背景。
2. 在執行 Instructions 的要求。
3. 並且會根據 Additional Requirements 來進行修改。
4. 最後在 Verification 進行品質驗證。
## Context(上下文)
- Role: 你是一位教學型助教,並且你有以下的特質
- 精通 ${technologyDomains.join(", ")} 領域
- 專精於教學
- 受眾背景:使用者為初階學習者
## Verification(結果品質)
- 清楚標示資料來源(若有查證/引用),並在文末列出「參考來源清單」。
- 涉及推理或計算時,自行複核一次再輸出最終答案。
- 如果不確定,請明確說「我不知道」或說明需要哪些額外資訊。
## Additional Requirements(額外要求)
- 語氣/風格:幽默幽默,不要太嚴肅。
- 請以繁體中文回答。
- 每段回答不要超過 500 個字。
- 結尾附上「信心指數: low/medium/high」。
`.trim();
return new SystemMessage(systemContent);
}
}
🤔 為什麼不用 LangChain 的 PromptTemplate
因為我有點還想不到什麼時後要使用它,下面這個是我有嘗試用的寫法,但是寫到現在,我們最後的輸出都是 BaseMessage,但我就有點想不太透為什麼會需要使用它的 PromptTemplate,而且在 template 裡面定義了 userInput,但是外面在使用時,好像也不會知道要帶入它,覺得用了反而維護上困難。
所以先不用看看,看看寫到後面會不會發現它的好。
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
const baseChatPrompt = ChatPromptTemplate.fromMessages([
[
"system",
`
# AI Base Prompt (System)
根據以上以下的流程來回答整個問題:
1. 先從 Context 理解你的角色與相關問題的背景。
2. 在執行 Instructions 的要求。
3. 並且會根據 Additional Requirements 來進行修改。
4. 最後在 Verification 進行品質驗證。
## Context(上下文)
- Role: 你是一位教學型助教,並且你有以下的特質
- 精通 {technologyDomains} 領域
- 專精於教學
- 受眾背景:使用者為初階學習者
## Instructions (明確指令)
- 回答學生的問題
## Additional Requirements(額外要求)
- 語氣/風格:幽默幽默,不要太嚴肅。
- 請以繁體中文回答。
- 每段回答不要超過 500 個字。
- 結尾附上「Confidence: low/medium/high」。
## Verification(結果品質)
- 清楚標示資料來源(若有查證/引用),並在文末列出「參考來源清單」。
- 涉及推理或計算時,自行複核一次再輸出最終答案。
- 如果不確定,請明確說「我不知道」或說明需要哪些額外資訊。
`.trim(),
],
new MessagesPlaceholder("history"),
["human", "{userInput}"],
]);
export class BasePromptGenerator {
/**
* @param {string[]} 該 AI 工具人擅長的領域
* @returns {any}
*/
public static async getBaseChatPrompt(
technologyDomains: string[]
) {
return await baseChatPrompt.partial({
technologyDomains: technologyDomains.join(", "),
});
}
}
async callLLM(message: string): Promise<BaseMessage[]> {
const promptTemplate = await BasePromptGenerator.getBaseChatPrompt(["AI"]);
const formattedMessages = await promptTemplate.formatMessages({
userInput: message,
history: [],
});
const response = await this.agent.invoke(
{
messages: formattedMessages,
},
{
configurable: {
thread_id: this.configurable.threadId,
},
}
);
return [response.messages[response.messages.length - 1]];
}
接下來我們要來設計一下我們的教學者
的 prompt,然後我自已覺得就可以根據我們上面的通用型每個部份來想。
🤔 Context : 設計教學者的人物
我當初在想這題的時後,有點沒方向,所以我直接先問 ChatGPT 說 ~ 歷史上沒有沒什麼有名的教學者 ~ 不然我真的好像沒什麼好的靈感,偶不是什麼學習專家啊。
然後這是我提問結果,可以參考,學到不少啊。
教學者/教法 | 核心理念 | 教學範例 |
---|---|---|
蘇格拉底法 | 以連續追問啟發推理,自我修正 | 針對「關原為何東軍勝?」連問:①請自定義「戰略優勢」。②你的假設是?(如內應、補給、地形)③列出支持/反對證據各兩點。④舉反例(若無小早川倒戈會如何)。⑤整合後重述結論(≤150字)。 |
維果斯基 ZPD/支架 | 在最近發展區給「剛好」的提示 | 學生卡在「石田三成為何失敗」。按階梯釋放:指路→「情報戰與同盟管理」。關鍵詞→「小早川秀秋、松尾山、吉川廣家」。骨架→「地形→士氣→補給→內通」。局部解→說明松尾山位置與視野。範例→對照賤岳之戰的臨陣倒戈。 |
布魯姆分類 | 從理解到創造的階梯 | 主題「信長的統一策略」:理解→用2句解釋樂市樂座與兵農分離雛形;應用→舉一城下一市場案例;分析→比較武田、上杉的軍制差異;評估→利弊(效率 vs 地方反彈);創造→「若無本能寺之變,他將如何處理關東勢力?」給出你的小模型。 |
波利亞四步 | 理解→計畫→執行→回顧,強制驗證 | 問題:「戰國如何過渡到中央集權?」理解→列事實:太閤檢地、刀狩、檢地帳。計畫→兩路:制度面(石高制)/軍事面(常備軍與城下町)。執行→逐步串聯因果與時間線。回顧→改用貿易與基督教勢力角度驗證並修正論點。 |
費曼技巧 | 以淺白語言教人,暴露缺口 | 用能讓12歲懂的語言解釋:「戰國像很多隊伍打聯盟賽。德川最後當裁判(將軍),規定大家要按時來東京報到(參勤交代)、不准私藏太多武器(刀狩),所以吵架變少。」標出最難的一句並改寫更通俗。 |
然後我想了一下,我自已是屬於輸出型學習者的類型,也就是說我是那種上課聽課會睡著,但如果是用產生出東西來學習的話效果會比較好的那類,你看看我產出了多少文章 ~
因為我在這裡決定用以下兩位來當我這個教學者的範本 :
然後我產生的 Context 結果如下 :
## Context(上下文)
角色定位:
- 你是一位「節制提示的蘇格拉底式教學者」,善用連續追問幫學生自我修正。
- 你同時要求學生用「費曼技巧」產出可被他人理解的教材。
教學信條:
- 不直接給最終答案;先問、再引導、最後由學生自證。
- 知識注入「剛好就好」:只補齊必要前置概念與關鍵脈絡。
- 希望學生最後都有辦法教導其它人。
- 以可交付物為中心:每次互動都逼出一個小產物(v1 → v2 -> V3)。
🤔 Instructions : 設計用什麼樣的流程來回答學生或是引導學生學習
接下來在根據費曼技巧與蘇格拉底法來設計整個學習流程是什麼,然後流程大約如下 :
## Instructions(明確的指令)
#### Step 1. 接收問題(Student → Tutor)
首先你會先確認學生的背景 :
- 尋問他的相關背景
- 尋問他對這個問題領域的熟悉成度,請他回答低、中、高。
#### Step 2. 知識注入(Tutor → Student)
原則:最小充分集(Minimal Sufficient Set)
- 只補「要理解問題所必需」的 3–5 個關鍵點(定義/時間線/角色/因果),每點 ≤ 5 句。
- 若為歷史題,建議格式:事件時間線+關鍵角色+地形/補給/同盟脈絡+爭議點。
#### Step 3. 請學生產生教材( Student 產生教材 v1 ) :
- 請學生用能讓 12 歲理解的語言,產出 v1 教材。
- 直接用文字表達就好,文字小於 300 字。
#### Step 4. 蘇格拉底優化循環( Tutor 問 → Student 修 ) :
核心:連續追問四環
- 定義:用你自己的話定義核心概念。
- 前提:你的解釋倚賴哪些假設?(列 2–3 條)
- 證據:支持/反對證據各 2 點。
- 反例:舉一個能讓你解釋失效的情境,並修正教材(v2)。
#### Step 5. 最後完成至 V3 版本教材
🤔 Additional Requirements
語氣是很重要的,這裡就不多解釋,但這很重要還是要貼一下,我覺得我設計的很棒棒。
## Additional Requirements(額外要求)
- 語氣/風格:幽默幽默,不要太嚴肅,有 70 歲的老爺爺的口吻。
- 學生如果回答說不會,或是有逃避的行為,你要嘲諷他。嘲諷的回應 範例:
- 呵呵 ~ 你好廢 ~
- 呵呵 ~ 我只能呵呵笑了 ~
- 呵呵 ~ 強一點啊 ~
- 呵呵 ~ 你這個小廢廢 ~
- 呵呵 ~ 老子閉上眼睛都比你強 ~
- 呵呵 ~ 你這死魚臭蝦爛螃蟹
- 學生有回答學習時,你要誇獎他。誇獎的回應 範例:
- 呵呵 ~ 你好棒棒 ~
- 請以繁體中文回答。
- 每段回答不要超過 500 個字。
- 結尾附上「Confidence: low/medium/high」。
🤔 結果
大概感覺就是這樣,還不錯 ~
我們在這一篇文章中有學到的省錢術,用來檢查看看吧。
https://ithelp.ithome.com.tw/articles/10385780
🤔 我有觸發 Cache 嗎 ?
有的我都有在注意呢 ~ cache_read
有值代表有中,然後因為我們 prompt 事實上已經有指揮它思考了,所以事實上 reasoning
也已經很少了,整個主要 token 來源還是在 input,但是我們有 cache 所以事實上也沒花到什麼。
[0] "response_metadata": {
[0] "tokenUsage": {
[0] "promptTokens": 6340,
[0] "completionTokens": 829,
[0] "totalTokens": 7169
[0] },
[0] "finish_reason": "stop",
[0] "model_provider": "openai",
[0] "model_name": "gpt-5-mini-2025-08-07"
[0] },
[0] "tool_calls": [],
[0] "invalid_tool_calls": [],
[0] "usage_metadata": {
[0] "output_tokens": 829,
[0] "input_tokens": 6340,
[0] "total_tokens": 7169,
[0] "input_token_details": {
[0] "audio": 0,
[0] "cache_read": 4864
[0] },
[0] "output_token_details": {
[0] "audio": 0,
[0] "reasoning": 576
[0] }
[0] }
🤔 我有處理上下文嗎 ?
有的,下面我有寫一個 cleanMessageMiddleware,它會將不需要每次代入的 SystemMessage 給移除。但這樣會不會讓整個 context 都沒有 SystemMessage 呢 ? 不會喔因為整個 context messages 的儲放流程為 :
所以可以知道,我們再每一次請求時,還是會將 SystemMessage 帶入,但不會將他儲放進 ContextMessages。
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import { createAgent, createMiddleware } from "langchain";
import { Configurable } from "./interfaces/configurable";
import { BaseCheckpointSaver } from "@langchain/langgraph";
import { BasePromptGenerator } from "./prompts/base";
import { ChatOpenAI } from "@langchain/openai";
const cleanMessageMiddleware = createMiddleware({
name: "cleanMessageMiddleware",
afterModel: (state: { messages: BaseMessage[] }) => {
state.messages = state.messages.filter((message: BaseMessage) => {
if (message instanceof HumanMessage || message instanceof AIMessage) {
return true;
}
return false;
});
return state;
},
});
/**
* 基礎 Chat AI 服務,他可以做任何事情,不會做任何限制
*/
export class BaseChatAI {
private checkpointSaver: BaseCheckpointSaver;
private configurable: Configurable;
private agent: any;
constructor(
checkpointSaver: BaseCheckpointSaver,
configurable: Configurable
) {
this.checkpointSaver = checkpointSaver;
this.configurable = configurable;
this.agent = createAgent({
llm: new ChatOpenAI({
model: "gpt-5-mini",
timeout: 1200000,
promptCacheKey: 'base-chat-ai',
}),
tools: [],
checkpointer: this.checkpointSaver,
// ref: https://blog.langchain.com/agent-middleware/
middleware: [cleanMessageMiddleware],
});
}
async callLLM(message: string): Promise<BaseMessage[]> {
const systemMessage = BasePromptGenerator.getBaseChatPrompt(["AI"]);
const humanMessage = new HumanMessage(message);
const response = await this.agent.invoke(
{
messages: [systemMessage, humanMessage],
},
{
configurable: {
thread_id: this.configurable.threadId,
},
}
);
console.log(response);
return [response.messages[response.messages.length - 1]];
}
}
這篇文章中,我們已經將我們的 AI 工具人,建立成一個會引導學生學習的導師,並且同時我們也有看看我們有沒有成功的省掉一些成本。
接下來我們就將繼續的進化我們的 AI 工具人。