AI GOAL GRID COURSE — 第 03 章
留空的格子交給 AI,自己填的格子一字不動 — prompt 設計與程式驗收的雙保險。
產品立場
null 是授權,有字是主權。
AI 是教練,不是代筆。
訪客瀏覽器 ──POST /fill──> Cloudflare Worker(GROQ_API_KEY 藏在這)──> Groq API GitHub Pages 順便做:每日額度、失敗退款、結果驗收
跟前課模組 6 同一個理由:key 放前端等於把信用卡貼在店門口。差別:這次 Worker 不只代打電話,還驗證輸入、扣額度、驗收輸出 — 一個 Worker,多個職責。
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${env.GROQ_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-oss-120b",
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" }, // 跟 AI 約好:只准回 JSON
max_tokens: 600,
temperature: 0.8,
}),
});
response_format: json_object 是 OpenAI 相容 API 的標準參數,強制模型只輸出 JSON。
export function parseJSONLoose(text) {
const s = String(text ?? "");
try { return JSON.parse(s); } catch { /* 繼續 */ }
const m = s.match(/```json\s*([\s\S]*?)```/i) || s.match(/```\s*([\s\S]*?)```/);
if (m) {
try { return JSON.parse(m[1]); } catch { /* 落空 */ }
}
return null;
}
模型偶爾會把 JSON 包進 ```json 程式碼框 — 先直接 parse,失敗再去框裡撈一次。
你是目標設定教練,使用「九宮格目標法」(源自原田メソッド,
即大谷翔平用過的目標達成表的方法)。
使用者的 3×3 九宮格:中央(位置4)是核心目標,周圍 8 格(位置0-3、5-8)是子目標。
${核心目標;留空時改為:請先推斷一個具體可衡量的核心目標}
已填的格子:
${每個已填格一行:位置i:「文字」;全空時寫(其餘全空)}
請補滿所有空格。規則:
1. 子目標要具體、彼此不重複、共同支撐核心目標;涵蓋「心・技・體・生活」四面向。
2. 每格 2-8 個字,繁體中文(台灣用語),不用標點結尾,不用 emoji。
3. 已填的格子原樣保留,一字不改。
只輸出 JSON:{"cells":["位置0文字",...,"位置8文字"]},長度必為 9。
原始碼:worker-deploy/src/lib/prompts.js 的 corePrompt(cells)。
// 先扣後跑:額度先 -1 再呼叫 AI(防同時送兩發繞過上限)
const quota = await takeQuota(env.DB, { kind: "fill", ip, limit: 12, day });
if (!quota.ok) return json({ error: "quota_exceeded", resetAt: taipeiResetAt() }, 429, cors);
let out = null;
try { out = await chatJSON(env, prompt, 600); } catch { out = null; }
// 驗收:必須是長度 9 的字串陣列,否則退款 + llm_failed
const arr = Array.isArray(out?.cells) ? out.cells : null;
if (!arr || arr.length !== 9 || arr.some((x) => typeof x !== "string" || !x.trim())) {
await refundQuota(env.DB, { kind: "fill", ip, day }); // 確定失敗 → 退還額度
return json({ error: "llm_failed" }, 502, cors);
}
const cells = arr.map((x) => x.trim());
normCells.forEach((c, i) => { if (c) cells[i] = c; }); // 已填格被 AI 改了?原值蓋回去
← → 翻頁