AI.run() 與常見任務:一個方法跑遍模型目錄 | Cloudflare 完整教學

2026/09/04
AI.run() 與常見任務:一個方法跑遍模型目錄 | Cloudflare 完整教學

上一篇《Workers AI 入門》,我們跑通了第一個 env.AI.run() 文字生成推論。但這個方法遠不只「丟 prompt、拿回答」這一招——同一個 env.AI.run(),換個模型識別符、換個輸入,就能跑遍整個模型目錄。這一篇我們深入 AI.run() 本身:把文字生成的進階參數(messages/prompttemperaturemax_tokens)、串流輸出(stream: true + SSE)、給 RAG 用的 text embedding影像生成Whisper 語音轉文字、以及 function calling 工具呼叫,各自跑一遍。

前言

上一篇《Workers AI 入門》把地基打好了:我們知道 Workers AI 是無伺服器 GPU 推論、模型部署在全球邊緣、透過 env.AI binding 呼叫,也寫出了第一個 env.AI.run("@cf/meta/llama-3.1-8b-instruct", { messages }) 的文字生成。那一篇刻意只示範文字生成一種任務,把 AI.run() 的完整威力留給了這一篇。

這一篇的主角就是 env.AI.run() 這個方法本身。它的方法簽名很簡單——env.AI.run(model, input, options?)——但這份簡單背後藏著一個關鍵設計:它是所有 AI 任務的統一入口。文字生成、embedding、影像、語音、function calling,全部都是同一個 AI.run(),差別只在你傳哪個模型識別符、以及對應的 input 物件長什麼樣

打個比方。env.AI.run() 就像一個萬用插座轉接頭:插座(GPU 網路)只有一個,但你可以插上不同的電器——吹風機(文字生成)、充電器(embedding)、印表機(影像生成)、錄音筆(語音辨識)。轉接頭本身不變,變的是你插什麼、以及那個電器需要什麼樣的插頭(input schema)和吐出什麼(輸出形狀)。搞懂「每種任務的插頭和輸出形狀」,你就掌握了整個模型目錄。

讀完你會掌握:

  • 文字生成進階——messages vs promptsystem prompt 定調、temperaturemax_tokens 怎麼調
  • 串流輸出——stream: true 回傳 ReadableStream,如何以 SSE 正確回給前端(呼應第 007 篇)
  • text embedding——把文字轉向量、批次處理、維度與 RAG 的關係
  • 影像生成與語音轉文字——prompt 生圖、Whisper 吃音訊吐逐字稿,各自的輸入/輸出形狀
  • function calling——讓 LLM 回傳結構化工具呼叫、完成多輪對話循環

核心概念

在動手之前,先把一張「心智地圖」建立起來:AI.run() 是一個,任務是多個,每種任務有自己的 input 形狀與回傳形狀。 這一節先把這張對照表講清楚,下一節再逐一寫程式碼。

一、AI.run() 的方法簽名

先重溫一次方法簽名——這是理解一切的起點:

async env.AI.run(
  model: string,   // 模型識別符,如 "@cf/meta/llama-3.1-8b-instruct"
  input: object,   // 「模型特定」的輸入物件 ← 各任務差在這
  options?: {      // 選用:執行期選項
    gateway?: { id: string; metadata?: Record<string, string | number | boolean | null> };
  }
): Promise<Response | ReadableStream>

三個參數裡,model 決定你跑哪個模型、input因任務而異的輸入物件(整篇文章的重點就在這裡)、options 選填(gateway 留給下一篇的 AI Gateway 主題)。回傳型別寫成 Promise<Response | ReadableStream>,實際上要看模式:

  • 一般模式:回傳一個物件(如文字生成的 { response: string }、embedding 的 { data: number[][] })
  • 串流模式(stream: true):回傳 ReadableStream(SSE / EventStream 格式)
  • 影像生成:回傳原始二進位資料(PNG,ReadableStream / Uint8Array,沒有 .response 可讀)

二、常見任務與 input / 輸出對照表

這是全篇最該記住的一張表。把它印在腦子裡,你就知道每種任務「餵什麼、讀哪裡」:

任務類型代表模型input 主要欄位回傳讀取
文字生成(Chat)@cf/meta/llama-3.1-8b-instructmessages / prompt + temperaturemax_tokensresult.response(字串)
文字生成(串流)同上 + stream: true同上ReadableStream(SSE)
text embedding@cf/baai/bge-base-en-v1.5text(字串或字串陣列)result.data(number[][])
影像生成@cf/black-forest-labs/flux-1-schnellpromptnum_steps二進位 PNG(ReadableStream)
語音辨識(ASR)@cf/openai/whisperaudio(位元組陣列)result.text(逐字稿)
function calling@cf/meta/llama-3.1-8b-instructmessages + toolsresult.tool_calls(陣列)

看懂這張表的訣竅:input 的形狀由「任務類型」決定,不是由 AI.run() 決定。 LLM 吃 messages、embedding 吃 text、語音吃 audio、影像吃 prompt——你只要記住「這個模型是哪一類任務」,就知道該塞什麼、該讀哪個欄位。搞混欄位(例如對 embedding 讀 .response)只會拿到 undefined

提醒:確切的模型識別符(model ID)與各模型支援的參數,請以官方模型目錄為準;本文列出的是最常用、最穩定的幾個代表,實際新增/更名以官方頁面為準。

三、關鍵參數:temperaturemax_tokens

文字生成有兩個最常用、也最容易被忽略的參數:

參數型別作用
temperaturenumber控制隨機性/創意度,約 0–1。越高越發散、越有創意;越低越穩定、越可重現。要結構化輸出(JSON、分類)設 0,要創意寫作可調高
max_tokensnumber限制最大輸出 token 數。不設容易讓模型話太長,既慢又吃 Neurons;務必依場景設一個合理上限

一個實務直覺:做「確定性任務」(抽取、分類、JSON)時把 temperature 設 0,可大幅提高一致性、減少重試;做「開放式生成」(創作、腦力激盪)時才調高。而 max_tokens 幾乎是每次都該顯式設定的——它同時是成本閘門延遲閘門。為什麼?因為文字生成是逐 token 生成的:每多生一個 token 就多一次計算、多一點延遲,也多消耗一點 Neurons。不設上限,遇到模型「話癆」時,一個簡單問題可能被回成長篇大論,既拖慢回應、又白白吃掉配額。反過來,把 max_tokens 設太小也會讓答案被硬生生截斷。所以正確做法是依場景估一個合理上限:簡短回覆抓 256、一般對話抓 512、摘要或長生成才給到 1000 以上。

四、messages 陣列的角色分工

再多說一句 Chat 格式的 messages,因為它是文字生成與 function calling 共用的骨架。messages 是一個依時間排列的訊息陣列,每則訊息有 rolecontent 兩個核心欄位,常見的 role 有三種:

role用途
system定調:設定 AI 的角色、語氣、語言、規則與限制。通常放在陣列最前面,只出現一次
user使用者輸入:實際的問題或指令
assistant模型回覆:多輪對話時,把先前模型的回答放回陣列,模型才「記得」上下文

換句話說,多輪對話不是 Workers AI 幫你記住的,而是你每次把完整的對話歷史(含先前的 userassistant)一起送進去。這也是為什麼長對話會愈來愈吃 token——歷史愈長、送進去的輸入愈多。實務上常見的做法是只保留最近幾輪、或先做摘要壓縮,避免撞上模型的上下文視窗上限。

實作範例

觀念地圖畫好,接下來把上面那張表的每一列各自跑一遍。所有範例以 TypeScript 撰寫,型別 AiVectorizeIndexExportedHandler 來自 @cloudflare/workers-types,並假設你已在 wrangler.jsonc 設好 "ai": { "binding": "AI" }(不熟的話回看上一篇《Workers AI 入門》)。開發別忘了 --remote

1. 文字生成:messagessystem prompt 與參數

文字生成有兩種輸入格式。Chat 格式(messages) 用角色分工,是主流做法;Completion 格式(prompt) 是單純的字串補全,適合最簡單的情境。

export interface Env {
  AI: Ai;
}

// 【Chat 格式】用 system 定調 + user 提問(推薦)
async function chat(env: Env, question: string) {
  const result = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
    messages: [
      { role: "system", content: "你是一位專業技術顧問,只用繁體中文簡潔回答。" },
      { role: "user", content: question },
    ],
    temperature: 0.7, // 0–1,越高越有創意
    max_tokens: 512,  // 限制輸出長度(成本 + 延遲閘門)
  });
  return result.response; // 文字生成的答案在 result.response(字串)
}

system 訊息用來「定調」——設定語氣、語言、角色與規則;user 才是實際問題。這個角色分工是 Chat 模型的核心,寫得好的 system prompt 幾乎決定了輸出品質:與其在 user 裡反覆叮嚀「請用繁體中文、請簡潔」,不如把這些穩定的要求一次寫進 system,讓每一輪都自動生效。若只是想要簡單補全、不需要角色設定,可以改用 prompt:

// 【Completion 格式】單純字串補全
const result = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
  prompt: "用一句話解釋什麼是邊緣運算",
});
console.log(result.response);

需要模型輸出結構化 JSON 時,一個穩定的做法是把 temperature 設 0、並用 system 明確要求「只輸出有效 JSON」:

// 讓模型穩定輸出 JSON:temperature=0 + system 約束
const result = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
  messages: [
    { role: "system", content: "你只輸出有效的 JSON 物件,不要有任何其他文字或說明。" },
    { role: "user", content: "提取重點:John 在台北的蘋果公司工作,月薪 10 萬。" },
  ],
  temperature: 0, // 關鍵:設 0 提高輸出一致性
});

try {
  const data = JSON.parse(result.response);
  console.log(data); // { name: "John", company: "蘋果", location: "台北", salary: 100000 }
} catch {
  console.error("JSON 解析失敗:", result.response);
}

2. 串流輸出:stream: true + SSE

當回應較長(摘要、長文生成),讓使用者逐字看到輸出遠比等整段生成完才一次吐出體驗好。傳入 stream: true,AI.run() 就改回傳 ReadableStream,以 Server-Sent Events(SSE) 格式串流(SSE 的細節可回顧第 007 篇)。

最簡單的做法:把 stream 直接放進 Response,加上正確的 SSE 標頭:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const stream = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
      messages: [{ role: "user", content: "寫一篇關於邊緣運算的短文" }],
      stream: true, // ← 回傳 ReadableStream,而非物件
    });

    // 直接把 stream 交給 Response,補上 SSE 專用標頭
    return new Response(stream as ReadableStream, {
      headers: {
        "Content-Type": "text/event-stream", // ← 必須!否則前端無法正確解析
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
      },
    });
  },
} satisfies ExportedHandler<Env>;

若你想在中途加工每個 chunk(例如只轉發文字、加上自訂事件),用 TransformStream 逐塊處理,並自行補上 data: 前綴與結尾 [DONE]:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const stream = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
      messages: [{ role: "user", content: "介紹三個 Cloudflare 產品" }],
      stream: true,
    });

    const { readable, writable } = new TransformStream();
    const writer = writable.getWriter();
    const encoder = new TextEncoder();

    // 非同步逐塊處理,不阻塞回應
    (async () => {
      for await (const chunk of stream as AsyncIterable<{ response?: string }>) {
        if (chunk.response) {
          await writer.write(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
        }
      }
      await writer.write(encoder.encode("data: [DONE]\n\n")); // 收尾訊號
      await writer.close();
    })();

    return new Response(readable, {
      headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
    });
  },
} satisfies ExportedHandler<Env>;

注意:AI Gateway 的快取不支援串流。若你需要靠快取省成本,就得改用非串流呼叫——這是 stream 模式的一個取捨。

3. text embedding:給 RAG 用的向量

Embedding 把文字轉成高維數值向量,語意相近的文字向量距離更近。它是 RAG(Retrieval-Augmented Generation,檢索增強生成) 與語意搜尋的基礎。輸入用 text,回傳的向量在 result.data:

// 單筆:回傳一個 768 維向量
const single = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
  text: "What is machine learning?",
});
const vector = single.data[0]; // number[],長度 768

// 批次(推薦!):一次送多筆,更省 Neurons、更快
const batch = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
  text: [
    "Machine learning is a subset of AI",
    "Cloudflare Workers runs at the edge",
  ],
});
const [v1, v2] = batch.data; // data 是 number[][],順序對應輸入
console.log(`向量維度:${v1.length}`); // 768

維度是關鍵:bge-base-en-v1.5 是 768 維、bge-large-en-v1.5 是 1024 維、bge-small-en-v1.5 是 384 維、bge-m3 適合多語言。選模型 = 選維度,而你的 Vectorize 索引維度必須與之完全一致。下面把 embedding 接上 Vectorize,湊出一個最小 RAG:

export interface Env {
  AI: Ai;
  VECTORIZE: VectorizeIndex;
}

// 先建索引(注意 --dimensions 要對準模型維度):
// npx wrangler vectorize create kb --dimensions=768 --metric=cosine

async function ask(env: Env, question: string) {
  // 1. 問題轉 embedding
  const q = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: question });

  // 2. 向量搜尋 top-5 最相關文件
  const hits = await env.VECTORIZE.query(q.data[0], { topK: 5, returnMetadata: "all" });

  // 3. 過濾低相關度,組成上下文
  const context = hits.matches
    .filter((m) => m.score > 0.7)
    .map((m) => m.metadata?.text as string)
    .join("\n\n");

  // 4. 交給 LLM 依上下文作答
  const answer = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
    messages: [
      { role: "system", content: `請根據以下內容回答,找不到就如實說明。\n\n${context}` },
      { role: "user", content: question },
    ],
    max_tokens: 512,
  });
  return answer.response;
}

4. 影像生成與語音轉文字(Whisper)

影像生成prompt,回傳二進位 PNG(沒有 .response),直接以 image/png 回給瀏覽器即可:

// 文字生成影像:回傳的是二進位圖片,不是物件
const image = await env.AI.run("@cf/black-forest-labs/flux-1-schnell", {
  prompt: "a cute robot coding at the edge of a network, isometric",
  num_steps: 20, // 步數(此模型上限約 20)
});

return new Response(image as ReadableStream, {
  headers: { "Content-Type": "image/png" }, // ← 用圖片 MIME,別回 JSON
});

語音轉文字(Whisper)audio——把音訊檔轉成位元組陣列(Array.from(new Uint8Array(...))),逐字稿在 result.text:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const form = await request.formData();
    const audioFile = form.get("audio") as File;

    // 音訊 → ArrayBuffer → 位元組陣列
    const bytes = Array.from(new Uint8Array(await audioFile.arrayBuffer()));

    const transcription = await env.AI.run("@cf/openai/whisper", { audio: bytes });

    // 逐字稿在 result.text(不是 .response!)
    return Response.json({ text: transcription.text });
  },
} satisfies ExportedHandler<Env>;

這兩個例子再次印證那句話:回傳讀哪個欄位,由任務類型決定——影像沒有欄位可讀(直接是二進位)、語音讀 .text、文字讀 .response。順帶提醒兩個實務細節:影像生成是特別吃 Neurons 的任務(一張圖動輒上萬 Neurons,和一次 embedding 只吃幾個天差地別),別在免費額度上狂生圖;而 Whisper 有音訊大小的隱性上限(約數十 MB),長音訊建議先切段再逐段轉錄,或搭配前一條主線學過的 Queue/Workflow 做非同步批次處理。

5. function calling:讓 LLM 呼叫工具

Function calling 讓 LLM 在需要外部資料時,不自己瞎編,而是回傳一個結構化的函式呼叫請求(tool_calls),由你的程式碼實際執行、再把結果餵回去。這是 AI Agent 模式的地基。支援的模型有 @cf/meta/llama-3.1-8b-instruct@hf/nousresearch/hermes-2-pro-mistral-7b 等。

先定義工具(用 JSON Schema 描述參數),再跑「兩輪」對話:

const tools = [
  {
    type: "function" as const,
    function: {
      name: "getWeather",
      description: "取得指定城市的即時天氣",
      parameters: {
        type: "object",
        properties: { city: { type: "string", description: "城市名稱,如:台北" } },
        required: ["city"],
      },
    },
  },
];

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const userMsg = "台北現在天氣怎麼樣?";

    // === 第一輪:模型決定「要不要」呼叫工具 ===
    const first = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
      messages: [{ role: "user", content: userMsg }],
      tools,
    });

    // 沒有 tool_calls 就是直接回答了
    if (!first.tool_calls?.length) {
      return Response.json({ response: first.response });
    }

    // === 第二輪:實際執行工具,把結果餵回模型 ===
    const call = first.tool_calls[0];
    const args = JSON.parse(call.function.arguments);
    const weather = await fetchWeather(args.city); // 你真正的實作

    const final = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
      messages: [
        { role: "user", content: userMsg },
        { role: "assistant", content: JSON.stringify(first.tool_calls) },
        { role: "tool", content: JSON.stringify(weather) },
      ],
      tools,
    });

    return Response.json({ response: final.response });
  },
} satisfies ExportedHandler<Env>;

async function fetchWeather(city: string) {
  return { city, temperature: 28, condition: "晴天" }; // 模擬 API
}

這裡的關鍵是理解 function calling 的兩輪本質:第一輪模型不會真的去查天氣——它沒有連網能力,只會判斷「這個問題需要 getWeather 這個工具」並回傳結構化的呼叫請求;真正執行工具、拿到真實資料的是你的程式碼;第二輪才把工具結果連同對話歷史一起送回,由模型把結果組成一句自然語言回答。這個「模型出主意、你出手」的分工,正是 AI Agent 能安全接上真實世界(資料庫、API、外部服務)的基礎。

若不想手寫這套多輪循環,可用官方 @cloudflare/ai-utilsrunWithTools,把「呼叫工具 → 回填結果 → 再問模型」自動包起來,大幅減少樣板程式碼——尤其當你有多個工具、或模型連續呼叫好幾輪時,手寫循環很容易出錯。

常見錯誤與最佳實踐

AI.run() 用起來直覺,但幾個坑幾乎人人踩過。以下是最高頻的。

坑一:忘了設 max_tokens,回應又慢又吃 Neurons。

不設 max_tokens,模型可能一路生成到自然停止,既拉長延遲、又多花 Neurons。幾乎每次文字生成都該顯式設一個合理上限。

// ❌ 不設上限,可能生成過長
await env.AI.run(model, { messages });

// ✅ 依場景設 max_tokens(摘要 512、簡短回覆 256…)
await env.AI.run(model, { messages, max_tokens: 512 });

坑二:串流回傳的 stream 卻當成物件讀,或漏了 text/event-stream 標頭。

stream: true 回的是 ReadableStream,不能再讀 result.response;而且回應標頭的 Content-Type 必須text/event-stream,否則前端 SSE 解析器會失敗。

// ❌ 對串流結果讀 .response → undefined
const s = await env.AI.run(model, { messages, stream: true });
return Response.json({ answer: s.response });

// ✅ 直接把 stream 放進 Response,帶正確 SSE 標頭
return new Response(s as ReadableStream, {
  headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
});

坑三:embedding 維度和 Vectorize 索引維度不符。

bge-base 是 768 維,若你的索引卻建成 1024 維(或反過來),upsert/query 會直接失敗。先定模型、再依它的維度建索引,兩邊必須一致。

# ✅ bge-base-en-v1.5 是 768 維,索引就建 768 維
npx wrangler vectorize create kb --dimensions=768 --metric=cosine

坑四:讀錯回傳欄位。

這是最低級也最常見的錯:對 embedding 讀 .response、對語音讀 .response、對文字讀 .data……全都會拿到 undefined。牢記那張對照表:文字讀 .response、embedding 讀 .data、語音讀 .text、影像是二進位、function calling 讀 .tool_calls

坑五:embedding 用迴圈逐筆呼叫。

一筆一筆送不只慢,還多算 Neurons。embedding 一律批次——text 傳陣列一次送多筆,回傳的 data 順序對應輸入順序。

// ❌ 逐筆:慢又貴
for (const t of texts) await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: t });

// ✅ 批次:一次送完
const res = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: texts });

最佳實踐小結:每次文字生成都設 max_tokens;串流時把 ReadableStream 直接交給 Response 並用 text/event-stream 標頭;確定性任務把 temperature 設 0;embedding 批次化、維度對準索引;牢記每種任務的回傳欄位;需要多輪工具循環時考慮 runWithTools

小結

上一篇《Workers AI 入門》我們跑通了第一個推論;這一篇,我們把 env.AI.run() 這個統一入口徹底攤開,把常見 AI 任務各跑了一遍:

  • AI.run() 是統一入口——run(model, input, options?),input 的形狀與回傳讀取欄位由任務類型決定,不是由方法決定。
  • 文字生成——messages(system 定調 + user 提問)或 prompt;temperature 控創意(確定性任務設 0)、max_tokens 控長度;答案在 result.response
  • 串流——stream: trueReadableStream,直接放進 Response 並帶 text/event-stream 標頭;快取不支援串流。
  • embedding——text 進、result.data(number[][])出;維度必須對準 Vectorize 索引;務必批次。
  • 影像/語音——影像吃 prompt、回二進位 PNG;Whisper 吃 audio 位元組、逐字稿在 result.text
  • function calling——messages + tools,模型回 tool_calls,你執行工具再回填,完成多輪循環。

到這裡,你已經能用同一個 AI.run() 跑遍文字、向量、影像、語音與工具呼叫了。但當這些 AI 呼叫進到生產環境,新問題就浮現:重複的 prompt 每次都重算很浪費、成本無從觀測、失敗沒有重試、也可能想同時代理 Workers AI 和 OpenAI/Anthropic……這些正是下一篇《AI Gateway》要解決的。我們會把一層代理放在應用與模型之間,替所有 AI 請求加上快取、限流、重試、可觀測性與多供應商路由——讓你的邊緣 AI 從「能跑」邁向「可上線」。

想隨時查閱各任務的完整 input schema 與可用模型,可以參考 Cloudflare Workers AI 模型目錄。把這篇的六種任務都跑過一遍後,整個模型目錄對你來說就只是「換模型、換 input」而已,我們下一篇《AI Gateway》見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →