AI.run() 與常見任務:一個方法跑遍模型目錄 | Cloudflare 完整教學
上一篇《Workers AI 入門》,我們跑通了第一個
env.AI.run()文字生成推論。但這個方法遠不只「丟 prompt、拿回答」這一招——同一個env.AI.run(),換個模型識別符、換個輸入,就能跑遍整個模型目錄。這一篇我們深入AI.run()本身:把文字生成的進階參數(messages/prompt、temperature、max_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)和吐出什麼(輸出形狀)。搞懂「每種任務的插頭和輸出形狀」,你就掌握了整個模型目錄。
讀完你會掌握:
- 文字生成進階——
messagesvsprompt、systemprompt 定調、temperature與max_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-instruct | messages / prompt + temperature、max_tokens | result.response(字串) |
| 文字生成(串流) | 同上 + stream: true | 同上 | ReadableStream(SSE) |
| text embedding | @cf/baai/bge-base-en-v1.5 | text(字串或字串陣列) | result.data(number[][]) |
| 影像生成 | @cf/black-forest-labs/flux-1-schnell | prompt、num_steps | 二進位 PNG(ReadableStream) |
| 語音辨識(ASR) | @cf/openai/whisper | audio(位元組陣列) | result.text(逐字稿) |
| function calling | @cf/meta/llama-3.1-8b-instruct | messages + tools | result.tool_calls(陣列) |
看懂這張表的訣竅:input 的形狀由「任務類型」決定,不是由 AI.run() 決定。 LLM 吃 messages、embedding 吃 text、語音吃 audio、影像吃 prompt——你只要記住「這個模型是哪一類任務」,就知道該塞什麼、該讀哪個欄位。搞混欄位(例如對 embedding 讀 .response)只會拿到 undefined。
提醒:確切的模型識別符(model ID)與各模型支援的參數,請以官方模型目錄為準;本文列出的是最常用、最穩定的幾個代表,實際新增/更名以官方頁面為準。
三、關鍵參數:temperature 與 max_tokens
文字生成有兩個最常用、也最容易被忽略的參數:
| 參數 | 型別 | 作用 |
|---|---|---|
temperature | number | 控制隨機性/創意度,約 0–1。越高越發散、越有創意;越低越穩定、越可重現。要結構化輸出(JSON、分類)設 0,要創意寫作可調高 |
max_tokens | number | 限制最大輸出 token 數。不設容易讓模型話太長,既慢又吃 Neurons;務必依場景設一個合理上限 |
一個實務直覺:做「確定性任務」(抽取、分類、JSON)時把 temperature 設 0,可大幅提高一致性、減少重試;做「開放式生成」(創作、腦力激盪)時才調高。而 max_tokens 幾乎是每次都該顯式設定的——它同時是成本閘門和延遲閘門。為什麼?因為文字生成是逐 token 生成的:每多生一個 token 就多一次計算、多一點延遲,也多消耗一點 Neurons。不設上限,遇到模型「話癆」時,一個簡單問題可能被回成長篇大論,既拖慢回應、又白白吃掉配額。反過來,把 max_tokens 設太小也會讓答案被硬生生截斷。所以正確做法是依場景估一個合理上限:簡短回覆抓 256、一般對話抓 512、摘要或長生成才給到 1000 以上。
四、messages 陣列的角色分工
再多說一句 Chat 格式的 messages,因為它是文字生成與 function calling 共用的骨架。messages 是一個依時間排列的訊息陣列,每則訊息有 role 與 content 兩個核心欄位,常見的 role 有三種:
role | 用途 |
|---|---|
system | 定調:設定 AI 的角色、語氣、語言、規則與限制。通常放在陣列最前面,只出現一次 |
user | 使用者輸入:實際的問題或指令 |
assistant | 模型回覆:多輪對話時,把先前模型的回答放回陣列,模型才「記得」上下文 |
換句話說,多輪對話不是 Workers AI 幫你記住的,而是你每次把完整的對話歷史(含先前的 user 與 assistant)一起送進去。這也是為什麼長對話會愈來愈吃 token——歷史愈長、送進去的輸入愈多。實務上常見的做法是只保留最近幾輪、或先做摘要壓縮,避免撞上模型的上下文視窗上限。
實作範例
觀念地圖畫好,接下來把上面那張表的每一列各自跑一遍。所有範例以 TypeScript 撰寫,型別 Ai、VectorizeIndex、ExportedHandler 來自 @cloudflare/workers-types,並假設你已在 wrangler.jsonc 設好 "ai": { "binding": "AI" }(不熟的話回看上一篇《Workers AI 入門》)。開發別忘了 --remote。
1. 文字生成:messages、system 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-utils 的 runWithTools,把「呼叫工具 → 回填結果 → 再問模型」自動包起來,大幅減少樣板程式碼——尤其當你有多個工具、或模型連續呼叫好幾輪時,手寫循環很容易出錯。
常見錯誤與最佳實踐
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: true回ReadableStream,直接放進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》見。