iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
生成式 AI

練習AI系列 第 4

提示工程(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(更嚴格的結構化輸出與驗證)
系列文
練習AI11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言