iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
生成式 AI

30 天生成式 AI 實戰挑戰:從基礎到應用系列 第 3

Day 3:提示工程(Prompt Engineering)

  • 分享至 

  • xImage
  •  

1) src/promptBuilder.js(新增)

可組裝化的提示引擎。

// src/promptBuilder.js

/**
 * 一個可組裝的 Prompt Builder:
 * - role / goal / audience / constraints / style
 * - formatHint(輸出格式說明)
 * - fewShots({user, assistant} 陣列)
 * - toMessages() 轉成 Chat Completions 可用 messages
 */

export class PromptBuilder {
  constructor() {
    this.role = "";
    this.goal = "";
    this.audience = "";
    this.constraints = [];
    this.style = "";
    this.formatHint = "";
    this.fewShots = []; // [{ user, assistant }]
    this.userInput = "";
    this.jsonSchema = null; // 若要結構化輸出
  }

  setRole(text) { this.role = text; return this; }
  setGoal(text) { this.goal = text; return this; }
  setAudience(text) { this.audience = text; return this; }
  addConstraint(text) { if (text) this.constraints.push(text); return this; }
  setStyle(text) { this.style = text; return this; }
  setFormatHint(text) { this.formatHint = text; return this; }
  setFewShots(pairs = []) { this.fewShots = pairs; return this; }
  setUserInput(text) { this.userInput = text; return this; }
  setJsonSchema(schema) { this.jsonSchema = schema; return this; }

  buildSystemPrompt() {
    const parts = [];
    if (this.role) parts.push(`角色:${this.role}`);
    if (this.goal) parts.push(`任務目標:${this.goal}`);
    if (this.audience) parts.push(`目標讀者:${this.audience}`);
    if (this.style) parts.push(`表達風格:${this.style}`);
    if (this.constraints.length) {
      parts.push(
        "限制條件:\n" +
        this.constraints.map((c, i) => `  ${i + 1}. ${c}`).join("\n")
      );
    }
    if (this.formatHint) parts.push(`輸出格式指引:${this.formatHint}`);

    // 若要求 JSON 格式,追加嚴格說明
    if (this.jsonSchema) {
      parts.push(
        "請務必輸出 **純 JSON**(不含多餘文字、註解或 Markdown),且鍵名及資料型別需符合 schema。"
      );
    }
    return parts.join("\n");
  }

  toMessages() {
    const messages = [
      { role: "system", content: this.buildSystemPrompt() },
    ];

    // few-shot
    for (const pair of this.fewShots) {
      if (pair.user) messages.push({ role: "user", content: pair.user });
      if (pair.assistant) messages.push({ role: "assistant", content: pair.assistant });
    }

    // 最後放真實使用者輸入
    if (this.userInput) messages.push({ role: "user", content: this.userInput });

    return messages;
  }
}

2) src/jsonGuard.js(新增)

輕量處理模型常見 JSON 輸出問題(前後多了字、或缺逗點)。

// src/jsonGuard.js

/**
 * 嘗試從模型輸出中抽出 JSON 並 parse。
 * - 會移除前後的 Markdown 區塊或多餘文字
 * - 若 parse 失敗,嘗試微調修復(非常保守)
 */
export function extractJson(text) {
  if (!text) throw new Error("空的模型輸出");
  // 移除 ```json ... ``` 或 ``` ... ``` 包裹
  const codeBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
  const raw = codeBlock ? codeBlock[1] : text.trim();

  // 嘗試第一輪 parse
  try { return JSON.parse(raw); } catch (_) {}

  // 簡單修復:去除多餘換行、尾逗點
  const cleaned = raw
    .replace(/\n/g, " ")
    .replace(/,\s*}/g, "}")
    .replace(/,\s*]/g, "]")
    .trim();

  return JSON.parse(cleaned); // 若失敗會直接 throw,讓上層捕捉
}

/**
 * 極輕量 schema 驗證(只檢查必要欄位是否存在與型別)
 * schema 範例:
 * { type: 'object', required: ['label','confidence'], properties: { label:{type:'string'}, confidence:{type:'number'} } }
 */
export function validateBySchema(obj, schema) {
  if (!schema) return { ok: true, errors: [] };
  const errors = [];

  if (schema.type === "object") {
    if (typeof obj !== "object" || Array.isArray(obj)) {
      errors.push("根型別應為 object");
    } else {
      for (const key of (schema.required || [])) {
        if (!(key in obj)) errors.push(`缺少必要欄位:${key}`);
      }
      if (schema.properties) {
        for (const [k, def] of Object.entries(schema.properties)) {
          if (k in obj && def.type && typeof obj[k] !== def.type) {
            errors.push(`欄位 ${k} 型別應為 ${def.type}`);
          }
        }
      }
    }
  }
  return { ok: errors.length === 0, errors };
}

3) src/day3_prompt_engineering.js(新增)

三個任務示範+JSON 產出。

// src/day3_prompt_engineering.js
import { openai } from "./aiClient.js";
import { PromptBuilder } from "./promptBuilder.js";
import { extractJson, validateBySchema } from "./jsonGuard.js";

/** 任務 A:英文老師(糾正文法,並解釋) */
export async function englishTeacher(input) {
  const pb = new PromptBuilder()
    .setRole("你是一位嚴謹且耐心的英文老師")
    .setGoal("糾正句子的文法與用字,並以簡潔中文解釋原因")
    .setAudience("具備國高中程度的學習者")
    .addConstraint("以條列方式呈現修改建議")
    .addConstraint("給出 1~2 個替代說法")
    .setStyle("專業、清楚、避免過度口語")
    .setFormatHint("使用 Markdown,分為『修正句子』『說明』『替代說法』三段")
    .setUserInput(`請幫我修正並說明這句話:${input}`);

  const res = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    temperature: 0.4,
    messages: pb.toMessages(),
  });

  return res.choices?.[0]?.message?.content?.trim();
}

/** 任務 B:程式碼審查(指出風險、修正範例) */
export async function codeReview(snippet, language = "javascript") {
  const pb = new PromptBuilder()
    .setRole("你是經驗豐富的資深工程師與安全審查者")
    .setGoal("審查程式碼品質、可維護性、安全風險,並提出具體修正")
    .addConstraint("請使用分段:風險、原因、修正建議、重構後範例")
    .addConstraint("避免廢話,指出可量化的問題(時間複雜度、可能例外、邊界條件)")
    .setStyle("專業、直接、重視可讀性")
    .setFormatHint("以 Markdown 呈現,重構碼需標註語言")
    .setUserInput(`語言:${language}\n請審查以下程式碼:\n${snippet}`);

  const res = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    temperature: 0.3,
    messages: pb.toMessages(),
  });

  return res.choices?.[0]?.message?.content?.trim();
}

/** 任務 C:文字分類(few-shot)→ 要求純 JSON 輸出 */
export async function sentimentClassify(text) {
  const schema = {
    type: "object",
    required: ["label", "confidence", "reasons"],
    properties: {
      label: { type: "string" },       // positive | neutral | negative
      confidence: { type: "number" },  // 0~1
      reasons: { type: "string" }
    },
  };

  const fewshots = [
    {
      user: "我今天升遷了,超級開心!",
      assistant: `{"label":"positive","confidence":0.92,"reasons":"語氣正向,含正面事件(升遷、開心)"}`
    },
    {
      user: "好像也還好,沒有什麼特別的事。",
      assistant: `{"label":"neutral","confidence":0.72,"reasons":"描述平淡,缺少情緒色彩"}`
    }
  ];

  const pb = new PromptBuilder()
    .setRole("你是嚴謹的文字情緒分類器")
    .setGoal("判定給定中文句子的情緒傾向")
    .addConstraint("標籤僅能為 positive / neutral / negative 之一")
    .addConstraint("confidence 介於 0 與 1 之間")
    .setJsonSchema(schema)
    .setFewShots(fewshots)
    .setUserInput(text);

  const res = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    temperature: 0.2,
    messages: pb.toMessages(),
  });

  const raw = res.choices?.[0]?.message?.content ?? "";
  const obj = extractJson(raw);
  const check = validateBySchema(obj, schema);
  if (!check.ok) {
    throw new Error("JSON 不符合 schema:" + check.errors.join("; "));
  }
  return obj;
}

4) index.js(更新為可跑 Day 3 範例)

// index.js
import { englishTeacher, codeReview, sentimentClassify } from "./src/day3_prompt_engineering.js";

const args = Object.fromEntries(
  process.argv.slice(2).reduce((acc, cur, i, arr) => {
    if (cur.startsWith("--")) {
      const key = cur.replace(/^--/, "");
      const val = arr[i + 1] && !arr[i + 1].startsWith("--") ? arr[i + 1] : true;
      acc.push([key, val]);
    }
    return acc;
  }, [])
);

async function main() {
  const task = args.task || "teacher";

  if (task === "teacher") {
    const input = args.text || "He go to school every day.";
    const out = await englishTeacher(input);
    console.log("\n=== 英文老師 ===\n");
    console.log(out);
  } else if (task === "review") {
    const sample =
`function sum(arr){
  let s = 0;
  for (let i=0;i<arr.length;i++){
    s += arr[i]
  }
  return s
}`;
    const out = await codeReview(sample, "javascript");
    console.log("\n=== 程式碼審查 ===\n");
    console.log(out);
  } else if (task === "sentiment") {
    const text = args.text || "今天心情糟透了,事情一團亂。";
    const out = await sentimentClassify(text);
    console.log("\n=== 情緒分類(JSON) ===\n");
    console.log(out);
  } else {
    console.log("未知任務,請使用 --task teacher | review | sentiment");
  }
}

main().catch((e) => {
  console.error("發生錯誤:", e.message);
  process.exit(1);
});

5) package.json(新增 Script)

只需在原本 scripts 內加以下三條(保留你之前的)。

{
  "scripts": {
    "day3:teacher": "node index.js --task teacher --text \"This sentence have a error.\"",
    "day3:review": "node index.js --task review",
    "day3:sentiment": "node index.js --task sentiment --text \"我覺得今天很棒,效率超好!\""
  }
}

如何執行

# 英文老師(糾正+說明)
npm run day3:teacher --silent

# 程式碼審查
npm run day3:review --silent

# 情緒分類(JSON 結構輸出)
npm run day3:sentiment --silent

上一篇
文字生成(Text Generation)
下一篇
文字 → JSON(更嚴格的結構化輸出與驗證)
系列文
30 天生成式 AI 實戰挑戰:從基礎到應用5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言