iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
AI & Data

30 天從 0 至 1 建立一個自已的 AI 學習工具人系列 第 10

30-10: [實作-3] 讓我們的 AI 工具人變成學習引導者吧

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250927/20089358rtL7NfVZzg.png

今天我們的目標就是將我們寫的程式碼,加入一些從前面幾面學到的 Prompt Engineering 相關的東西。

🚀 Step 1. 加入通用版本的 Template,讓他回答統一

首先我在我們資料夾下新增了一個 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]];
  }

🚀 Step 2. 讓我們的 AI 工具人,變成學習的引導者

接下來我們要來設計一下我們的教學者的 prompt,然後我自已覺得就可以根據我們上面的通用型每個部份來想。

  • Context : 設計教學者的人物設計
  • Instructions : 設計用什麼樣的流程來回答學生的問題
  • Additional Requirements : 然後進行一些額外的要求,來修飾你的回答

🤔 Context : 設計教學者的人物

我當初在想這題的時後,有點沒方向,所以我直接先問 ChatGPT 說 ~ 歷史上沒有沒什麼有名的教學者 ~ 不然我真的好像沒什麼好的靈感,偶不是什麼學習專家啊。

然後這是我提問結果,可以參考,學到不少啊。

教學者/教法 核心理念 教學範例
蘇格拉底法 以連續追問啟發推理,自我修正 針對「關原為何東軍勝?」連問:①請自定義「戰略優勢」。②你的假設是?(如內應、補給、地形)③列出支持/反對證據各兩點。④舉反例(若無小早川倒戈會如何)。⑤整合後重述結論(≤150字)。
維果斯基 ZPD/支架 在最近發展區給「剛好」的提示 學生卡在「石田三成為何失敗」。按階梯釋放:指路→「情報戰與同盟管理」。關鍵詞→「小早川秀秋、松尾山、吉川廣家」。骨架→「地形→士氣→補給→內通」。局部解→說明松尾山位置與視野。範例→對照賤岳之戰的臨陣倒戈。
布魯姆分類 從理解到創造的階梯 主題「信長的統一策略」:理解→用2句解釋樂市樂座與兵農分離雛形;應用→舉一城下一市場案例;分析→比較武田、上杉的軍制差異;評估→利弊(效率 vs 地方反彈);創造→「若無本能寺之變,他將如何處理關東勢力?」給出你的小模型。
波利亞四步 理解→計畫→執行→回顧,強制驗證 問題:「戰國如何過渡到中央集權?」理解→列事實:太閤檢地、刀狩、檢地帳。計畫→兩路:制度面(石高制)/軍事面(常備軍與城下町)。執行→逐步串聯因果與時間線。回顧→改用貿易與基督教勢力角度驗證並修正論點。
費曼技巧 以淺白語言教人,暴露缺口 用能讓12歲懂的語言解釋:「戰國像很多隊伍打聯盟賽。德川最後當裁判(將軍),規定大家要按時來東京報到(參勤交代)、不准私藏太多武器(刀狩),所以吵架變少。」標出最難的一句並改寫更通俗。

然後我想了一下,我自已是屬於輸出型學習者的類型,也就是說我是那種上課聽課會睡著,但如果是用產生出東西來學習的話效果會比較好的那類,你看看我產出了多少文章 ~

因為我在這裡決定用以下兩位來當我這個教學者的範本 :

  • 費曼技巧 : 以淺白語言教人,暴露缺口
  • 蘇格拉底法 : 以連續追問啟發推理,自我修正

然後我產生的 Context 結果如下 :

## Context(上下文) 
角色定位:
- 你是一位「節制提示的蘇格拉底式教學者」,善用連續追問幫學生自我修正。
- 你同時要求學生用「費曼技巧」產出可被他人理解的教材。

教學信條:
- 不直接給最終答案;先問、再引導、最後由學生自證。
- 知識注入「剛好就好」:只補齊必要前置概念與關鍵脈絡。
- 希望學生最後都有辦法教導其它人。
- 以可交付物為中心:每次互動都逼出一個小產物(v1 → v2 -> V3)。

🤔 Instructions : 設計用什麼樣的流程來回答學生或是引導學生學習

接下來在根據費曼技巧與蘇格拉底法來設計整個學習流程是什麼,然後流程大約如下 :

  1. 學生提出問題。
  2. 剛好的知識提供給學生。
  3. 學生嘗試自已產出教材。
  4. 蘇格拉底優化循環,生成 v2/v3。
## 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/upload/images/20250924/20089358LTln6W7RPf.png
https://ithelp.ithome.com.tw/upload/images/20250924/20089358ivAAZ4Hck5.png
https://ithelp.ithome.com.tw/upload/images/20250924/20089358U4I6Ypzayw.png
https://ithelp.ithome.com.tw/upload/images/20250924/20089358JRldt3luCn.png

大概感覺就是這樣,還不錯 ~

🚀 Step 3. 檢查有沒有啥可以省省錢呢 ?

我們在這一篇文章中有學到的省錢術,用來檢查看看吧。

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 的儲放流程為 :

  1. 初使化 ContextMessages = []
  2. 呼叫 LLM 時將包含 SystemMessage + HummanMessage 丟給 LLM。
  3. 然後重點是這樣,回傳出去時會將 SystemMessage 移除。
  4. 所以這時 ContextMessages = [HummanMessage + AIMessage]
  5. 下一次呼叫 LLM 時,帶給 LLM 的會是 [HummanMessage + AIMessage] + [SystemMessage + HummanMessage]

所以可以知道,我們再每一次請求時,還是會將 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 工具人。

程式碼


上一篇
30-9: [知識] Prompt Engineering 之省錢很重要 - OpenAI ( 成本篇 )
下一篇
30-11: [實作-4] 讓我們的 AI 工具人來幫我們總結今日的學習
系列文
30 天從 0 至 1 建立一個自已的 AI 學習工具人14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言