可組裝化的提示引擎。
// 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;
}
}
輕量處理模型常見 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 };
}
三個任務示範+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;
}
// 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);
});
只需在原本 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