iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
AI & Data

30 天從 0 至 1 建立一個自已的 AI 學習工具人系列 第 9

30-9: [知識] Prompt Engineering 之省錢很重要 - OpenAI ( 成本篇 )

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250921/20089358d3Lq4nbK5w.png

同步至 medium

接下來我們這篇文章的重點就是 :

省錢 !

錢錢真的很重要,尤其當主管後每一件事情都要算錢錢,然後再 AI 的世界中省錢的重點就在於 :

節省 Token

至於 Token 是什麼,我自已是覺得的到了 2025 年時,應該已經不太需要說了……

🚀 策略 1. Prompt Cache Hit

首先 prompt cache 這個東西不敢保證每個 model 都有支援,但應該我們常用的是都有而且每個機制都有些不同,我這裡列出比較常用的 OpenAI 來看 :

🤔 省錢的機制 + 省多少 ?

它的省錢機制主要在於 :

Input 的 token 中,如果有部份是已經 cached 的 token,則會比正常價格更便宜。

https://platform.openai.com/docs/models/gpt-5

例如以 gpt-5 上面這個網站上來看,input 正常是 1.25$,然後右邊有寫 cache 的是 $0.125,所以是指 cache 的 token 只收 0.125$ 而不是 1.25$ 。

以 GPT-5 為例,Input 的花費最多可以省 90% 左右,如果 cache 都有命中。

🤔 要如何觸發呢 ? 條件如下,基本上每個都要中

條件有點多,但也不是說不好中。

  • 提示長度 ≥ 1024 tokens
    • 少於 1024 tokens 不會進快取,cached_tokens = 0。
    • 命中後以 128 tokens 為單位累積 (1024、1152、1280 …)。
  • 前綴 (prefix) 完全相同
    • 包含 system/developer 訊息、範例、工具列表、影像參數等。
    • 一個字、空白或換行不同都會造成 miss。
  • 使用相同的模型與 snapshot
    • 不同模型(gpt-5 vs gpt-5-mini)、不同日期快照都不共用快取。
  • 相同的 prompt_cache_key(建議設定) =⇒ 這裡官網沒強制要求,但我實測後發現加了後,才會中
    • 若沒設,系統會用 prefix hash;
    • 設定後可強制路由一致,提高命中率。
  • 請求頻率不超過快取分流限制
    • 同一組 prefix+key 約 ≤15 req/min;超過會分流到不同機器,降低命中。
  • 快取未過期
    • 一般 5–10 分鐘不使用會清除;
    • 低流量時最多可保留 ~1 小時。

🤔 程式碼實測

import OpenAI from "openai";

const client = new OpenAI();

// 一個固定的 system 指令(長度夠大才能觸發快取)
const longSystemPrompt = `
You are an expert math tutor.
Your job is to help students understand complex mathematical concepts
with clear explanations and step-by-step examples.
Always explain your reasoning thoroughly.
(This section is intentionally long to exceed 1024 tokens when combined.)
Repeat some filler text to simulate length.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
`.repeat(20); // 重複 20 次,保證超過 1024 tokens

async function run() {
  
  // 第一次請求 (Cache Miss, 會建立快取)
  const first = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: longSystemPrompt },
      { role: "user", content: "解釋畢氏定理" }
    ],
    // 啟用 prompt caching(Beta 功能)
    store: true
  });

  console.log("第一次請求 usage:", first.usage);

  // 等待一下確保快取生效
  await new Promise(resolve => setTimeout(resolve, 1000));

  // 第二次請求 (Cache Hit, 前綴相同,只換 user 問題)
  const second = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      { role: "system", content: longSystemPrompt }, // 前綴完全一樣
      { role: "user", content: "解釋牛頓第一運動定律" }
    ],
    store: true
  });

  console.log("第二次請求 usage:", second.usage);
}

run();

這個是有觸發的情況,可以看到第二次有 cached_tokens 為 2176。

第一次請求 cached_tokens: {
  input_tokens: 2319,
  input_tokens_details: { cached_tokens: 0 },
  output_tokens: 1311,
  output_tokens_details: { reasoning_tokens: 320 },
  total_tokens: 3630
}
第二次請求 cached_tokens: {
  input_tokens: 2322,
  input_tokens_details: { cached_tokens: 2176 },
  output_tokens: 1158,
  output_tokens_details: { reasoning_tokens: 384 },
  total_tokens: 3480
}

然後這是沒有觸發的情況,你看 cached_tokens 就為 0。

第一次請求 cached_tokens: {
  input_tokens: 134,
  input_tokens_details: { cached_tokens: 0 },
  output_tokens: 2142,
  output_tokens_details: { reasoning_tokens: 704 },
  total_tokens: 2276
}
第二次請求 cached_tokens: {
  input_tokens: 137,
  input_tokens_details: { cached_tokens: 0 },
  output_tokens: 1092,
  output_tokens_details: { reasoning_tokens: 320 },
  total_tokens: 1229
}

🚀 策略 2. 不要讓 LLM 想太多 ?

在剛剛上面測試的結果中,有沒有注意到 reasoning_tokens,我將上面的結果拉下來一個看,你看 reasoning_tokens 它會算在 output token 的 $$ 喔。

{
  input_tokens: 137,
  input_tokens_details: { cached_tokens: 0 },
  output_tokens: 1092,
  output_tokens_details: { reasoning_tokens: 320 },
  total_tokens: 1229
}

所以這裡的策略就是 :

不要讓它想太多 !

https://platform.openai.com/docs/guides/reasoning-best-practices

這裡我自已想了一下,有以下幾個手法:

🤔 在不需要動腦的情況下,強制降低智商

OpenAI 的 Model 事實上有一個參數可以用,如下範例程式碼中的reasoning,你設定後會強制降低它的思考。

import OpenAI from "openai";
const client = new OpenAI();

const response = await client.responses.create({
    model: "gpt-5-mini",
    reasoning: { effort: "high" },
    input: "我是馬克大人,你好",
});

console.log(response);

以下範例分別是highlow的差別,你看同一個問題 reasoning_tokens 差了多少。

// high high high

  usage: {
    input_tokens: 13,
    input_tokens_details: { cached_tokens: 0 },
    output_tokens: 420,
    output_tokens_details: { reasoning_tokens: 384 },
    total_tokens: 433
  },
// low low low low
  usage: {
    input_tokens: 13,
    input_tokens_details: { cached_tokens: 0 },
    output_tokens: 95,
    output_tokens_details: { reasoning_tokens: 64 },
    total_tokens: 108
  },

🤔 透過 Prompt Techniques 來讓他腦袋變的很思考很簡單

這個的概念就像是,有個老師引導你,不過讓你在碰到問題時,東想西想,然後最後結果還是很爛,還花了很多錢 ( 我指 reasoning token )。

例如,我們下面用上一章學到的 few-shot 和沒有用的比較,雖然我自已沒有做很嚴格的驗證,但我自已試了大約 10 次以上,不同問題,但結構一樣(在往上我懶)都是用 few-shot ( goodInput 那個 ) 的 reasoning_tokens 較底。

import OpenAI from "openai";
const client = new OpenAI();

const goodInput = `## Instructions (明確指令)
- 學習以下的範例
- 然後回答實際的問題: "人工智慧在醫療領域的最新應用這個是屬於那個類別"

## Example
標題:央行宣布調整利率政策
類別:財經

標題:新型疫苗研發取得重大突破
類別:健康

標題:NBA總冠軍賽精彩對決
類別:體育`

const badInput = '請回答以下問題: "人工智慧在醫療領域的最新應用這個是屬於那個類別,請回答 2 個字的"'

const response = await client.responses.create({
    model: "gpt-5-mini",
    reasoning: { effort: "low" },
    input: badInput,
});

console.log(response);
// goodInput
  usage: {
    input_tokens: 114,
    input_tokens_details: { cached_tokens: 0 },
    output_tokens: 74,
    output_tokens_details: { reasoning_tokens: 64 },
    total_tokens: 188
  },


// badInput
  usage: {
    input_tokens: 42,
    input_tokens_details: { cached_tokens: 0 },
    output_tokens: 200,
    output_tokens_details: { reasoning_tokens: 192 },
    total_tokens: 242
  },

🚀 策略 3 : LLM Model 類型選擇

這個應該很好理解,每個模型有不同的價格,也有不同的使用時機。

對了但是還有一種情況,那就是我們在和 AI 溝通時,事實上在不同的情境溝通,我們可能就會需要不同的 model,例如如果你只是簡單的翻譯,那是不是直接用最便宜的模型就好,但如果你是要深度推理的問題,那這時就會需要很強的模型。

所以這裡會用這種策略來打 :

  1. 先小後大
  2. 用意圖先分類後,再根據意圖來決定叫那個模型

先說第一個簡單的說就是一開始的問題會先進入到小模型,然後當小模型產生出來的結果信心度不足(你就叫他多回傳個信心度,最簡單的信心度做法,但這個是最簡單的),然後接下來我們就可以根據信心度,來判斷是否要在呼叫一次大模型。

第二種實作上就有點像 LangGraph 提出的這個的這個流程,會在 RouteAI 那來判斷要叫誰做,當然 RouteAI 就是先用最便宜的。

https://ithelp.ithome.com.tw/upload/images/20250923/20089358yWNd7mbthW.png
圖片來源: LangGraph 官網

以下為 LangGraph 的範例程式碼。

function routeDecision(state: typeof StateAnnotation.State) {
  // Return the node name you want to visit next
  if (state.decision === "story") {
    return "llmCall1";
  } else if (state.decision === "joke") {
    return "llmCall2";
  } else if (state.decision === "poem") {
    return "llmCall3";
  }
}


// Build workflow
const routerWorkflow = new StateGraph(StateAnnotation)
  .addNode("llmCall1", llmCall1)
  .addNode("llmCall2", llmCall2)
  .addNode("llmCall3", llmCall3)
  .addNode("llmCallRouter", llmCallRouter)
  .addEdge("__start__", "llmCallRouter")
  .addConditionalEdges(
    "llmCallRouter",
    routeDecision,
    ["llmCall1", "llmCall2", "llmCall3"],
  )
  .addEdge("llmCall1", "__end__")
  .addEdge("llmCall2", "__end__")
  .addEdge("llmCall3", "__end__")
  .compile();

// Invoke
const state = await routerWorkflow.invoke({
  input: "Write me a joke about cats"
});
console.log(state.output);

🚀 策略 4 : 用 Batch API

就 OpenAI 事實上有提到所謂的 Batch API,然後啊他可

https://platform.openai.com/docs/guides/batch

https://ithelp.ithome.com.tw/upload/images/20250923/20089358uDlu4HUlKI.png

重點是『 輸入 』與『 輸出 』都可以省 50%。

但他的缺點就是非即時,所以這個功能大部份的情況都是在非即時聊天的功能用到,像是如果我們 RAG 要用的 embeddings 啊,或是預期產生每堂課程的學習摘要之類的都很適合。

但他好像不能和上面的策略 1 一起用。

🚀 策略 5 ( Beta ) : Flex processing

OpenAI-Flex processing

OpenAI 好像大約在 2025.04 左右有出了一個叫 Flex processing 還在 Beta 中,簡單的說就是可以同時有 Batch 的價格,還可以使用 Cache 的價格優惠,但問題就是有以下的缺點 :

  • 較慢的回應時間
  • 偶爾出現資源不可用
  • 只適用這些模型 GPT-5、o3、o4-mini。

所以我自已想了一下,如果模型 OK,他應該是可以替代一部份的 Batch 的工作,例如一些前處理相關的東西,然後下面是我請 AI 產生適合的地方,我自已覺得都算合理,可以參考看看

  • 模型評測(evals / A/B):大量測例跑分、few-shot/指令變體對比、self-consistency 取多樣本統計。
  • 資料增豐(data enrichment):為海量紀錄補欄位(關鍵字、主題、情緒、實體)、摘要欄位、標註類別。
  • 內容大量處理:批量摘要/翻譯長文、會議逐字稿清理、FAQ 擷取、知識抽取(facts/triples)。
  • 合成資料產生:產測試案例、弱監督標註、資料增強(同義改寫、多語版)。
  • RAG 前處理:離線切塊、段落標註、段落標題/metadata 生成、向量前計算(若嵌入模型也支援類似費率更佳)。
  • 後臺報表:每日/每小時生成洞察報表、長文件分析(法規、書籍、技術規格)。
  • 風險較低的自動化腳本:如 SEO 初稿、產品描述草稿(有人審核即可)、程式碼註解生成。
  • 排程回填/重算:索引重建、歷史資料重跑、格式遷移時的文本重寫。

然後使用的方法很簡單,就是多個 flex 就好。


import OpenAI from "openai";
const client = new OpenAI({
    timeout: 15 * 1000 * 60, // Increase default timeout to 15 minutes
});

const response = await client.responses.create({
    model: "o3",
    instructions: "List and describe all the metaphors used in this book.",
    input: "<very long text of book here>",
    service_tier: "flex",
}, { timeout: 15 * 1000 * 60 });

console.log(response.output_text);

🚀 策略 6 : 限制輸出

就是不要讓他廢話太話,限制他的格式與大小

這個真的要限制,不讓真的 output 會噴很多錢錢。

🚀 策略 7 : 管理上下文

在這一篇文章我們有提到記憶的功能,就是我們會在 AI Application 管理整個對話的上下文,然後再每一次給 Prompt 時,再加入到它的裡面,這樣就可以讓 LLM 知道我們整個對話的脈絡,你就可以問他說,我剛剛供啥小之類的。

如果只記短時間當然沒問題,但如果時間很長那就真的會讓你的 Token 噴到和飛的一樣。

所以只要你有做記憶上下文功能,就一定要考慮選擇那些上下文 ( 例如前 N 筆之類 ? )

反正就是要處理就對了,手法就看你的情境了。

🚀 小總結

這篇文章中,咱們研究了幾個省錢大作戰的策略,然後上面沒寫但我這裡標注一下那些是其它 model 也通用的 :

  • 策略 1. Prompt Cache Hit => 其它 model 通用
  • 策略 2. 不要讓 LLM 想太多 ? => 其它 model 通用
  • 策略 3 : LLM Model 類型選擇 => 其它 model 通用
  • 策略 4 : 用 Batch API => ?
  • 策略 5 ( Beta ) : Flex processing => ?
  • 策略 6 : 限制輸出 => 其它 model 通用
  • 策略 7 : 管理上下文 => 其它 model 通用

所以事實上這篇文章要說事實上應該也適合大部份的模型 ~

🚀 參考資料


上一篇
30-8: [知識] Prompt Engineering 之 Prompting Techniques ( 技巧篇 )
系列文
30 天從 0 至 1 建立一個自已的 AI 學習工具人9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言