การดู : 119

24/09/2026 13:42น.

ปกบทความ Prompt Caching แล็ปท็อปบนโต๊ะทำงานตอนกลางคืน พร้อมการ์ดเทียบค่าใช้จ่ายส่วน system prompt 100 request บน Claude Sonnet 5 ไม่ใช้ cache ประมาณ $2.00 ใช้ cache ประมาณ $0.22

Prompt Caching คืออะไร? ลดค่า Claude/OpenAI API ในระบบจริงด้วย Go

#Prompt Caching

#Golang

#Claude API

#OpenAI API

#LLM

#ลดต้นทุน API

#Backend

เวลามีน้องๆ ที่ต่อ Claude หรือ OpenAI เข้ากับระบบ Go ถามทีมเราว่า "บิล API เดือนนี้พุ่งขึ้นมาก ต้องเปลี่ยนไปใช้โมเดลถูกลงไหม" คำตอบที่เราตอบบ่อยที่สุดคือ "ยังไม่ต้อง ลองดูก่อนว่าส่งอะไรซ้ำไปทุกครั้งบ้าง" เพราะระบบส่วนใหญ่ส่ง system prompt ยาวๆ นิยาม tools และเอกสารอ้างอิงก้อนเดิมซ้ำไปทุก request แล้วจ่ายเต็มราคาทุกรอบ

ทั้งสองค่ายมีวิธีแก้เรื่องนี้แล้ว ชื่อว่า Prompt Caching แต่จากที่เราเห็น หลายระบบเปิดใช้แล้วก็ยังไม่ได้ลดอะไรเลย เพราะ cache ไม่เคย hit จริง

บทความนี้จะตอบคำถามเดียวให้ชัด: ต้องจัด request จากฝั่ง Go ยังไงให้ cache hit ทุกครั้งที่ควร hit และจะเช็กจากตัวเลขได้ยังไงว่ามันลดค่าใช้จ่ายได้จริง

Prompt Caching ทำงานยังไง

หลักการมีข้อเดียว คือ prefix match ผู้ให้บริการจะเก็บผลการประมวลผลของ "ส่วนต้น" ของ prompt ไว้ ถ้า request ถัดไปขึ้นต้นเหมือนเดิมทุกตัวอักษร ส่วนนั้นจะไม่ต้องประมวลผลใหม่ และคิดราคาถูกลงมาก

คำว่า "ทุกตัวอักษร" สำคัญที่สุดในบทความนี้ ถ้าข้อความข้างหน้าต่างไปแม้แต่ตัวเดียว เช่น timestamp ชื่อผู้ใช้ หรือลำดับ field ใน JSON ทุกอย่างที่อยู่หลังจุดนั้นจะ miss ทั้งหมด

ฝั่ง Claude เรียงลำดับ prefix ตายตัวเป็น tools แล้วตามด้วย system และ messages (เอกสาร Claude) ถ้าแก้นิยาม tool แม้เล็กน้อย cache ของทุกส่วนที่ตามมาจะหายไปหมด

Claude กับ OpenAI ต่างกันตรงไหน

  • วิธีเปิดใช้: Claude ต้องใส่ cache_control เอง ที่ระดับ request หรือราย block ได้สูงสุด 4 จุด ส่วน OpenAI ทำให้อัตโนมัติโดยไม่ต้องแก้โค้ด และมี breakpoint แบบกำหนดเองให้ใช้ด้วย

  • ขนาดขั้นต่ำ: Claude แล้วแต่รุ่น เช่น Sonnet 5 อยู่ที่ 1,024 tokens และ Opus 5.5 อยู่ที่ 512 tokens ส่วน OpenAI อยู่ที่ 1,024 tokens สำหรับ GPT-5.6 ขึ้นไป

  • ค่าเขียน cache: Claude คิด 1.25 เท่าของราคา input สำหรับ cache อายุ 5 นาที หรือ 2 เท่าสำหรับอายุ 1 ชั่วโมง ส่วน OpenAI ฟรีสำหรับรุ่นก่อน GPT-5.6 และคิด 1.25 เท่าตั้งแต่ GPT-5.6 ขึ้นไป

  • ค่าอ่าน cache: Claude คิด 0.1 เท่าของราคา input สำหรับรุ่นส่วนใหญ่ ส่วน OpenAI ลดได้สูงสุดประมาณ 90% แล้วแต่รุ่น

  • อายุ cache: Claude อยู่ที่ 5 นาทีและต่ออายุฟรีทุกครั้งที่ถูกใช้ หรือเลือกแบบ 1 ชั่วโมงได้ ส่วน OpenAI ระบบจัดการเอง

  • Rate limit: ฝั่ง Claude cache hit ไม่ถูกหักจาก rate limit ส่วนฝั่ง OpenAI token ที่อ่านจาก cache ยังนับรวมใน tokens-per-minute ถ้าระบบชนโควตาบ่อย ดูวิธีจัดการเพิ่มได้ใน EP.164 Rate Limiting AI Requests

ที่มา: เอกสาร Claude และ เอกสาร OpenAI (ข้อมูล ณ กันยายน 2026 ตัวคูณและรุ่นเปลี่ยนได้เมื่อมีโมเดลใหม่ ภาพรวมรุ่นล่าสุดของ Claude ดูได้ใน Claude Opus 5 ฉบับสาย Dev)

สรุปสั้นๆ คือ OpenAI ง่ายกว่าเพราะไม่ต้องทำอะไรก็ได้ cache แต่ Claude ให้ควบคุมได้ละเอียดกว่าว่าจะ cache ตรงไหน ซึ่งมีประโยชน์มากเมื่อ prompt มีหลายส่วนที่เปลี่ยนด้วยความถี่ต่างกัน

เปิด Prompt Caching ของ Claude จาก Go

ตัวอย่างนี้ใช้ net/http กับ struct ตรงๆ เพื่อให้เห็นว่า JSON ที่ส่งออกไปหน้าตาเป็นยังไง ถ้าอยากเทียบกับการเรียกฝั่ง OpenAI ผ่าน SDK ดูได้ใน EP.144 เชื่อมต่อ OpenAI API ด้วย Go SDK

package llm

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

type CacheControl struct {
	Type string `json:"type"`          // "ephemeral"
	TTL  string `json:"ttl,omitempty"` // "" = 5 นาที, "1h" = 1 ชั่วโมง
}

type TextBlock struct {
	Type         string        `json:"type"`
	Text         string        `json:"text"`
	CacheControl *CacheControl `json:"cache_control,omitempty"`
}

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type Request struct {
	Model     string      `json:"model"`
	MaxTokens int         `json:"max_tokens"`
	System    []TextBlock `json:"system"`
	Messages  []Message   `json:"messages"`
}

type Usage struct {
	InputTokens              int `json:"input_tokens"`
	CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
	CacheReadInputTokens     int `json:"cache_read_input_tokens"`
	OutputTokens             int `json:"output_tokens"`
}

type Response struct {
	Usage Usage `json:"usage"`
}

// systemPrompt ต้องเป็นข้อความคงที่ ห้ามมีเวลา ชื่อผู้ใช้ หรือค่าที่เปลี่ยนทุก request
func Ask(systemPrompt, question string) (*Response, error) {
	body := Request{
		Model:     "claude-sonnet-5",
		MaxTokens: 1024,
		System: []TextBlock{{
			Type:         "text",
			Text:         systemPrompt,
			CacheControl: &CacheControl{Type: "ephemeral"}, // จุดตัด cache อยู่ท้ายส่วนที่คงที่
		}},
		Messages: []Message{{Role: "user", Content: question}}, // ส่วนที่เปลี่ยนอยู่หลังจุดตัด
	}

	b, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(b))
	if err != nil {
		return nil, err
	}
	req.Header.Set("x-api-key", os.Getenv("ANTHROPIC_API_KEY"))
	req.Header.Set("anthropic-version", "2023-06-01")
	req.Header.Set("content-type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("anthropic: status %d", res.StatusCode)
	}

	var out Response
	return &out, json.NewDecoder(res.Body).Decode(&out)
}

หัวใจของโค้ดนี้อยู่ที่บรรทัดเดียว คือ CacheControl ต้องอยู่บน block สุดท้ายที่เหมือนเดิมทุก request ไม่ใช่บน block ที่เปลี่ยนทุกครั้ง

อ่านตัวเลขให้เป็น

ใน usage ของ response มี 3 ค่าที่ต้องดู

  • cache_creation_input_tokens คือจำนวน token ที่เพิ่งถูกเขียนลง cache ใน request นี้

  • cache_read_input_tokens คือจำนวน token ที่อ่านจาก cache ใน request นี้

  • input_tokens คือเฉพาะ token ที่อยู่หลังจุดตัด cache สุดท้าย ไม่ใช่ input ทั้งหมด

ถ้าจะหา input ทั้งหมดต้องรวมทั้งสามค่า และถ้า cache_creation_input_tokens กับ cache_read_input_tokens เป็น 0 ทั้งคู่ แปลว่าไม่ได้ cache เลย สาเหตุที่พบบ่อยคือ prompt สั้นกว่าขั้นต่ำของรุ่นนั้น ระบบจะข้ามไปเฉยๆ โดยไม่แจ้ง error (เอกสาร Claude)

ฝั่ง OpenAI ดูค่า cached_tokens ได้จาก usage.prompt_tokens_details ใน Chat Completions หรือ usage.input_tokens_details ใน Responses API ส่วนรุ่น GPT-5.6 ขึ้นไปจะมี cache_write_tokens เพิ่มมาด้วย (เอกสาร OpenAI)

คุ้มแค่ไหน: คำนวณจากราคาจริง

ลองคิดจากราคา Claude Sonnet 5 ต่อ 1 ล้าน token ตามเอกสาร ณ กันยายน 2026: input ปกติ $2 เขียน cache แบบ 5 นาที $2.50 และอ่าน cache $0.20

สมมติ system prompt ยาว 10,000 tokens และมี 100 request ที่เข้ามาถี่พอให้ cache ไม่หมดอายุ

  • ไม่ใช้ cache: 100 x 10,000 x $2 / 1,000,000 = ประมาณ $2.00

  • ใช้ cache: เขียนครั้งแรก 10,000 x $2.50 / 1,000,000 = $0.025 และอ่านอีก 99 ครั้ง 99 x 10,000 x $0.20 / 1,000,000 = $0.198 รวมประมาณ $0.22

ตัวเลขนี้คิดเฉพาะส่วน system prompt ยังไม่รวมคำถามของผู้ใช้และ output ซึ่งราคาเท่าเดิม และเป็นกรณีที่ hit ทุกครั้ง ระบบจริงจะลดได้น้อยกว่านี้ตามอัตรา hit

จุดคุ้มทุนมาเร็วมาก เพราะเขียน 1 ครั้งบวกอ่าน 1 ครั้งเท่ากับ 1.25 + 0.1 = 1.35 เท่า ขณะที่ไม่ใช้ cache สองครั้งเท่ากับ 2 เท่า แค่ hit ครั้งเดียวก็คุ้มแล้ว ฟังก์ชันนี้ใช้คำนวณจาก usage จริงได้ และต่อยอดกับระบบบันทึกค่าใช้จ่ายใน EP.149 Token Management ได้เลย

// ราคาต่อ 1 ล้าน token (USD) ดึงจากหน้าราคาของรุ่นที่ใช้จริง อย่า hardcode ถาวร
type Price struct {
	Input, CacheWrite5m, CacheRead float64
}

func CostUSD(u Usage, p Price) (withCache, withoutCache float64) {
	const m = 1_000_000.0
	withCache = float64(u.InputTokens)*p.Input/m +
		float64(u.CacheCreationInputTokens)*p.CacheWrite5m/m +
		float64(u.CacheReadInputTokens)*p.CacheRead/m

	total := u.InputTokens + u.CacheCreationInputTokens + u.CacheReadInputTokens
	withoutCache = float64(total) * p.Input / m
	return
}

ฟังก์ชันนี้คิดเฉพาะ input และสมมติว่าใช้ cache แบบ 5 นาที ถ้าใช้แบบ 1 ชั่วโมงต้องแยกคิดจาก cache_creation.ephemeral_1h_input_tokens

นอกจากเงิน ยังได้ความเร็วด้วย OpenAI ทดสอบเองแล้วพบว่า prompt สั้นระดับ 1,024 tokens เร็วขึ้นราว 7% แต่ prompt ยาว 150,000 tokens ขึ้นไปได้ time-to-first-token เร็วขึ้นราว 67% (OpenAI Cookbook) ยิ่ง prompt ยาว ยิ่งเห็นผลชัด

ใส่ cache_control ทุก request ไปเลยได้ไหม?

จริงๆ ทำได้ และ Claude มีโหมด automatic caching ที่ใส่ cache_control ครั้งเดียวที่ระดับ request แล้วระบบจะเลื่อนจุดตัดไปที่ block สุดท้ายให้เอง เหมาะมากกับแชตหลายรอบที่ประวัติยาวขึ้นเรื่อยๆ

แต่ถ้า block สุดท้ายเปลี่ยนทุกครั้ง เช่น แปะเวลาปัจจุบันหรือ context เฉพาะ request ไว้ท้าย prompt ระบบจะเขียน cache ใหม่ทุกรอบโดยไม่เคยได้อ่านเลย เท่ากับจ่ายค่าเขียนแพงขึ้น 25% ทุก request โดยไม่ได้อะไรกลับมา (เอกสาร Claude) กรณีนี้ต้องใช้ breakpoint ราย block และย้ายไปไว้ท้ายส่วนที่คงที่แทน

กฎจำง่ายๆ คือ ของที่ไม่เปลี่ยนไว้ข้างหน้า ของที่เปลี่ยนไว้ข้างหลัง ลำดับที่ปลอดภัยคือ tools, คำสั่งหลัก, เอกสารอ้างอิง, ประวัติแชต แล้วจึงเป็นคำถามล่าสุด

กับดักที่คนเขียน Go ต้องรู้

เอกสารของ Claude เตือนไว้ว่าบางภาษา รวมถึง Go อาจสลับลำดับ key ตอนแปลงเป็น JSON ทำให้ cache ไม่ match

ต้องแยกให้ชัดตรงนี้ encoding/json ของ Go เรียง key ของ map ให้เสมอตอน json.Marshal จึงปลอดภัย ความเสี่ยงจริงอยู่ที่จุดอื่น

  • ประกอบ prompt ด้วยการวน range บน map ลำดับการวน map ใน Go สุ่มทุกครั้ง ข้อความที่ได้จึงเรียงไม่เหมือนเดิม ควรใช้ slice ที่เรียงลำดับตายตัวแทน

  • ใช้ JSON library ตัวอื่น ที่ไม่รับประกันลำดับ key

  • input ของ tool_use ที่เก็บแล้วส่งกลับ ถ้าแปลงผ่าน map แล้วประกอบเองโดยไม่เรียง key

วิธีที่ปลอดภัยที่สุดคือใช้ struct เหมือนในตัวอย่างด้านบน เพราะลำดับ field ของ struct ตายตัวเสมอ

Cache ทำให้ AI ตอบเหมือนเดิม หรือใช้ข้อมูลเก่าไหม?

คำถามนี้ควรถามก่อนเปิดใช้ คำตอบคือไม่ Prompt caching ไม่มีผลต่อการสร้างคำตอบ response ที่ได้จะเหมือนกับตอนไม่ใช้ cache ทุกประการ (เอกสาร Claude) สิ่งที่ cache เก็บคือผลการประมวลผล input ไม่ใช่ตัวคำตอบ

แต่ caching ก็ไม่ได้ช่วยเรื่องความสดของข้อมูล ถ้าเอกสารอ้างอิงใน prompt เก่า คำตอบก็อิงของเก่าเหมือนเดิม ระบบที่ต้องใช้ข้อมูลล่าสุดจึงยังต้องมีกลไกอัปเดตเนื้อหาของตัวเอง และต้องยอมรับว่าทุกครั้งที่อัปเดต ส่วนนั้นกับทุกอย่างที่ตามมาจะ miss หนึ่งรอบ

อีกเรื่องที่ทีมมักถามคือความปลอดภัย cache ของ Claude แยกกันระหว่างองค์กร และบน Claude API ยังแยกระดับ workspace ด้วย ส่วนของ OpenAI แยกกันระดับองค์กร ผู้ใช้ต่างองค์กรจึงไม่ได้ใช้ cache ร่วมกัน


สรุป: เปิด cache อย่างเดียวไม่พอ ต้องออกแบบให้ hit

ตอบแบบตรงๆ ไม่อ้อม

ควรทำทันที ถ้าระบบมี system prompt นิยาม tools หรือเอกสารอ้างอิงยาวเกินขั้นต่ำของรุ่นที่ใช้ และมี request เข้ามาถี่พอให้ cache ไม่หมดอายุ แค่ hit ครั้งเดียวก็คุ้มค่าเขียนแล้ว

ยังไม่ต้องรีบ ถ้า prompt สั้นกว่าขั้นต่ำ หรือแต่ละ request ต่างกันทั้งก้อน กรณีนี้ caching แทบไม่ช่วยอะไร

ถ้าจะเริ่มวันนี้ ให้เริ่มจากเรื่องเดียว คือเอา cache_read_input_tokens หรือ cached_tokens ไปใส่ใน log ที่มีอยู่แล้ว ดูสักหนึ่งวันว่าระบบ hit จริงแค่ไหน ตัวเลขนั้นจะบอกเองว่าต้องย้ายอะไรไปไว้หน้า prompt และตรงไหนที่ยังรั่วอยู่

FAQ: คำถามที่พบบ่อยเกี่ยวกับบทความนี้

รวมคำถามและคำตอบที่ช่วยให้คุณเข้าใจเนื้อหาในบทความนี้ได้ดียิ่งขึ้น