這一課完成後:你會建立 POST /api/ask,由 backend 使用 server-side API key 呼叫模型,回傳文字結果;並能解釋 provider API、model、input、output、latency、usage 與 error 各自在整個 AI product 裡的位置。

先把 AI 功能畫成一般 Web App 資料流

模型不是魔法元件。它只是你的 backend 會呼叫的另一個外部服務。

User
↓
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-...";

正確邊界:

Browser → 只能呼叫你自己的 /api/ask
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 已經學過的工作:

Parse body → Validate → Authenticate if needed → Call model adapter → Handle error → Return JSON

例如:

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。

模型輸出不是「資料庫真相」

這堂課最重要的觀念之一:

Database row → deterministic stored data
Model output → generated result

模型可能:

回答不完整。

格式不一致。

產生事實錯誤。

同一輸入在不同時間出現不同表述。

所以不要因為 API 回 200 就把 output 當成已驗證事實。後面的 Structured Output、RAG、Tool Calling 都是在處理這個問題。

Latency:模型 call 通常比一般 database query 慢

Frontend 不應該在按下按鈕後像什麼都沒發生。

至少要有:

Idle → Loading → Success / Error

例如:

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,例如:

user id
model id
request id
input / output token usage
latency
status

但不要把使用者完整 prompt、secret、session token 無腦全部塞進 log。

Provider error 和你的產品 error 要分開

可能失敗的地方很多:

你的 validation 400
未登入 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 而異,但架構上要有:

Start model request → deadline / abort → success OR controlled timeout error

不要讓一個 client request 無限制佔著資源。

不要把 prompt 和 instructions 混成一大坨

這一課先只傳 input,下一課會正式拆:

Application instructions → 產品規則
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 才負責:

收到 prompt → 呼叫 provider → 收到 model response → 萃取需要結果 → 回 frontend

最後收斂:AI 功能首先還是一個系統功能

User input
↓ 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 資料流。