這一課完成後:你會建立 POST /api/ask,由 backend 使用 server-side API key 呼叫模型,回傳文字結果;並能解釋 provider API、model、input、output、latency、usage 與 error 各自在整個 AI product 裡的位置。
先把 AI 功能畫成一般 Web App 資料流
模型不是魔法元件。它只是你的 backend 會呼叫的另一個外部服務。
↓
Frontend
↓ POST /api/ask
Your Backend
↓ Model API request
Model Provider
↓ Model response
Your Backend
↓ safe JSON response
Frontend
最重要的是:Frontend 不直接持有模型 provider 的 secret API key。
為什麼不能直接從 browser 呼叫模型 API?
只要 secret 被放進送到瀏覽器的 JavaScript,使用者就能在 source、bundle 或 Network panel 找到它。
// 不要放在 frontend
const apiKey = "sk-...";
正確邊界:
Backend → 持有 provider secret → 呼叫模型
這和 Web App Lesson 05 的 secret 原則完全一樣。
先把 provider-specific code 限制在一小塊
AI Builder 會用 OpenAI Responses API 當一個具體例子,但你的產品邏輯不要寫成「整個 app 都依賴某一個 SDK object」。
先想成一個 function:
generateText({ input })
→ 回傳字串
之後換模型、換 provider、加 routing,都只需要改 adapter,而不是整個產品。
先準備 server-side environment
本機可以在不 commit 的 .dev.vars 或 .env 放:
OPENAI_API_KEY=replace-me
OPENAI_MODEL=replace-with-a-current-model-id
不要把 model ID 永久散落在十個檔案。用 environment 或集中設定,之後才能比較模型、切換成本與能力。
模型名稱會變。這堂課刻意不把某個 model ID 寫死成永遠正確;實作時從 provider 最新的 model guide 選目前可用的模型。
安裝官方 JavaScript SDK
npm install openai
在 server-side / Worker code:
import OpenAI from "openai";
function createModelClient(env) {
return new OpenAI({
apiKey: env.OPENAI_API_KEY
});
}
API key 由 runtime environment 注入,不出現在 frontend,也不 commit 進 Git。
第一次 model call:只有一個 input
async function generateText(env, input) {
const client = createModelClient(env);
const response = await client.responses.create({
model: env.OPENAI_MODEL,
input
});
return response.output_text;
}
目前 OpenAI 的 Responses API 可以用 input 建立模型回應;官方 SDK 也提供 output_text 這個方便欄位,避免你假設 output array 第一項一定是文字訊息。
OpenAI Responses API reference
Model call 不是你的 route 本身
Route 還是要做 Web App 已經學過的工作:
例如:
async function handleAsk(request, env) {
const body = await request.json();
if (
!body ||
typeof body.prompt !== "string" ||
body.prompt.trim() === ""
) {
return Response.json(
{
error: {
code: "INVALID_INPUT",
message: "prompt is required"
}
},
{ status: 400 }
);
}
const prompt = body.prompt.trim();
if (prompt.length > 4000) {
return Response.json(
{
error: {
code: "INPUT_TOO_LONG",
message: "prompt is too long"
}
},
{ status: 400 }
);
}
try {
const text = await generateText(env, prompt);
return Response.json({ text });
} catch (error) {
console.error("model request failed", error);
return Response.json(
{
error: {
code: "MODEL_ERROR",
message: "Model request failed"
}
},
{ status: 502 }
);
}
}
Provider 的完整 internal error 不需要原封不動送給使用者。
把它接成 POST /api/ask
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (
request.method === "POST" &&
url.pathname === "/api/ask"
) {
return handleAsk(request, env);
}
return new Response("Not found", {
status: 404
});
}
};
現在你的 AI backend 和前面的 CRUD API 沒有本質上的魔法差異:仍然是 request、validation、business logic、external dependency、response。
Frontend 只知道你自己的 API
const response = await fetch("/api/ask", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: promptInput.value
})
});
成功後:
const data = await response.json();
result.textContent = data.text;
Frontend 不需要知道 provider API key,甚至不一定要知道 backend 最後選的是哪個 model。
模型輸出不是「資料庫真相」
這堂課最重要的觀念之一:
Model output → generated result
模型可能:
回答不完整。
格式不一致。
產生事實錯誤。
同一輸入在不同時間出現不同表述。
所以不要因為 API 回 200 就把 output 當成已驗證事實。後面的 Structured Output、RAG、Tool Calling 都是在處理這個問題。
Latency:模型 call 通常比一般 database query 慢
Frontend 不應該在按下按鈕後像什麼都沒發生。
至少要有:
例如:
askButton.disabled = true;
status.textContent = "Thinking...";
完成或失敗後再恢復。
模型 latency 是產品體驗的一部分,不是「之後再優化」才需要知道。
不要讓使用者連點製造一堆昂貴 request
最小版本至少可以:
request 進行中暫時 disable submit。
限制 prompt 長度。
需要登入的 AI 功能就先驗證 session。
server-side 設計 rate limit / quota 的位置。
真正 production 還要考慮 per-user quota、abuse detection、provider limits 與預算。
Usage:AI request 不是免費抽象函式
模型 API 通常會產生可計量 usage。不要只記「成功 / 失敗」,還要建立成本意識。
可以在 server log 或 usage table 記錄非敏感 metadata,例如:
model id
request id
input / output token usage
latency
status
但不要把使用者完整 prompt、secret、session token 無腦全部塞進 log。
Provider error 和你的產品 error 要分開
可能失敗的地方很多:
未登入 401
quota / policy / provider reject
provider timeout
provider 5xx
你的 parsing / rendering bug
Frontend 最好只依賴你自己的穩定 error schema,不要把某個 provider 的原始 response shape 當成產品 API contract。
先做最小 timeout 心理模型
任何 external API 都可能卡住。你的 backend 應該知道「等多久算失敗」。
實作方式依 runtime / SDK 而異,但架構上要有:
不要讓一個 client request 無限制佔著資源。
不要把 prompt 和 instructions 混成一大坨
這一課先只傳 input,下一課會正式拆:
User input → 使用者這次要求
例如「你是一個學習助教」這類產品規則,不應該每次都假裝是使用者自己輸入的內容。
小挑戰:做一個 AI Explain 按鈕
不要先做聊天紀錄。做一個單次任務就好:
需求 1:Frontend 有一個 textarea 和「Explain」按鈕。
需求 2:Frontend 只呼叫自己的 POST /api/ask。
需求 3:Provider API key 只存在 server-side environment。
需求 4:空 input 與過長 input 會被 backend 拒絕。
需求 5:request 期間顯示 loading 並避免重複提交。
需求 6:provider 失敗時,frontend 顯示可理解 error。
需求 7:server log 不印出 API key 或 session token。
需求 8:model ID 從 environment / config 取得,不散落在 frontend。
用 DevTools Network 重新看一次資料流
Browser 應該只看得到:
POST /api/ask
它不應該拿到你的 provider API key。
Server-side 才負責:
最後收斂:AI 功能首先還是一個系統功能
↓ validate
Your backend
↓ secret-protected provider call
Model output
↓ normalize / error handling
Your API response
↓ UI state
如果這條線都還不穩,先不要急著加記憶、RAG、Agent 或十個 tools。
下一課會處理最常被低估的一層:Prompt / Instructions。你會把「產品規則」和「使用者輸入」拆開,開始真正控制模型任務。
完成條件:你能從自己的 backend 安全呼叫模型 API,知道 secret 為什麼不能進 frontend,能處理 input validation、loading、provider error 與 basic usage,並能畫出完整的 User → Backend → Model Provider → Backend → UI 資料流。