AI Engineer › พื้นฐาน LLM และ API
Token, ต้นทุน และ context window
ประมาณ 30 นาที
เป้าหมายของบท
- ประมาณจำนวน token และคำนวณต้นทุนของคำขอ LLM (input/output แยกกัน)
- เข้าใจ context window เป็นงบ token และเห็นว่าทำไมจึงไม่ควรยัดทุกอย่างเข้า prompt
- อธิบายแนวคิด subword/BPE ได้ด้วยตัวอย่างที่คำนวณเอง
LLM ไม่ได้อ่านตัวอักษร แต่อ่าน “token”
โมเดลภาษาตัดข้อความเป็น token (ชิ้นย่อยของคำ) ก่อนเสมอ และทุกอย่างที่เราจ่ายเงินหรือถูกจำกัด — ราคา ความเร็ว ขนาด context window — นับเป็น token ไม่ใช่ตัวอักษรหรือคำ ภาษาอังกฤษหนึ่ง token โดยประมาณคือราว 4 ตัวอักษร ส่วนภาษาไทยมักใช้ token ต่อข้อความยาวเท่ากันมากกว่า (ต้นทุนและ context จึงหมดเร็วกว่า)
ในเบราว์เซอร์ตัวนี้เราไม่มี tokenizer จริง จึงใช้ตัวประมาณสองแบบเพื่อฝึกคิดเชิงต้นทุน:
import math, re
def est_tokens(text): # กฎประมาณ: 4 ตัวอักษร ≈ 1 token
return math.ceil(len(text) / 4)
def toy_tokens(text): # tokenizer ของเล่น: คำ + เครื่องหมายแต่ละตัว
return re.findall(r"\w+|[^\w\s]", text)
ทั้งสองแบบให้ผลไม่ตรงกับ tokenizer จริง (เช่นเอกสาร d01 ในฐานความรู้ของเราได้ 64 กับ 54 ตามลำดับ) — ตอนใช้งานจริงให้อ่านค่าที่ API รายงานกลับมา (response.usage.input_tokens / output_tokens)
แล้วใช้ตัวประมาณเฉพาะตอนวางแผนงบก่อนเรียก
ต้นทุนของหนึ่งครั้งที่เรียก
ผู้ให้บริการคิดราคา input และ output แยกกัน (output แพงกว่า) ต่อหนึ่งล้าน token:
PRICE_IN = 3.0 # สมมติ USD ต่อ 1M input token
PRICE_OUT = 15.0 # สมมติ USD ต่อ 1M output token
cost = input_tokens * PRICE_IN / 1e6 + output_tokens * PRICE_OUT / 1e6
# 1,200 token เข้า + 300 token ออก = 0.0081 USD
ราคาข้างต้นเป็นตัวเลขสมมติสำหรับฝึกคำนวณ — ตรวจราคาจริงจากหน้าราคาของผู้ให้บริการเสมอ
Context window = งบ token ต่อหนึ่งคำขอ
Prompt + ประวัติสนทนา + เอกสารที่ดึงมา + คำตอบ ต้องรวมกันไม่เกินขนาด context window การยัดทุกอย่างเข้าไปจึงทั้งแพงและช้า: ฐานความรู้ 24 เอกสารของเรามีรวม ≈ 1,398 token ถ้ายัดทั้งหมดในทุกคำถามจะได้ ≈ 1,405 token ต่อคำขอ (≈ 0.0042 USD ที่ราคาสมมติ) แต่ถ้าดึงมาเฉพาะ 3 เอกสารที่เกี่ยวข้อง (แนวคิดของ RAG ใน module ถัดไป) จะเหลือราว 198 token (≈ 0.0006 USD) — ประหยัดประมาณ 7 เท่าและยังลดสิ่งรบกวนโมเดล
Subword: ทำไมคำแปลกๆ ยังอ่านได้
Tokenizer สมัยใหม่ใช้ subword (เช่น BPE): เริ่มจากตัวอักษร แล้ววนรวมคู่ที่อยู่ติดกันบ่อยที่สุดเป็นหนึ่ง token ใหม่ ทีละรอบ
from collections import Counter
corpus = [list(w) for w in ['low', 'low', 'lower', 'newest', 'newest', 'widest']]
pairs = Counter((a, b) for word in corpus for a, b in zip(word, word[1:]))
top = min(pairs, key=lambda p: (-pairs[p], p)) # นับมากสุด; เสมอให้เรียงตามตัวอักษร
คำที่พบบ่อยจะกลายเป็น token เดียว คำหายากถูกแตกเป็นชิ้นเล็กๆ — จึงไม่มีคำที่ “ไม่รู้จัก” แต่คำหายาก/ภาษาที่ข้อมูลเทรนน้อยจะมี token ต่อข้อความมากขึ้น
ตัวอย่างแก้โจทย์
กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้
ประมาณ token และต้นทุน
ใช้ตัวประมาณกับฐานความรู้ร้านค้า (kb_docs) แล้วคิดเป็นเงิน
ประมาณ token ของทุกเอกสารด้วยกฎ 4 ตัวอักษร — ผลรวม 1,398 เฉลี่ย ≈ 58 ต่อเอกสาร
ยัดทุกอย่าง vs ดึงเฉพาะที่เกี่ยว
เทียบขนาด prompt ตอนยัดทั้งฐานความรู้กับตอนใส่แค่ 3 เอกสาร
ยัดทุกเอกสารเข้า prompt ≈ 1,405 token ส่วนใส่ 3 เอกสารแรก ≈ 198 token (คำถามข้อแรกต่อท้าย)
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
คำนวณต้นทุนของคำขอที่ใช้ input 2,000 token และ output 500 token เก็บไว้ในตัวแปร call_cost เติม ___ ให้ครบ
แบบฝึกท้ายบท
บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม
🟢 พื้นฐาน
ฐานความรู้ kb_docs มีกี่คำรวมกันทุกเอกสาร (นับด้วย split() ของคอลัมน์ text) เก็บไว้ในตัวแปร total_words
ประมาณ token ของเอกสาร d01 ด้วยฟังก์ชัน est_tokens ที่ให้ไว้ เก็บไว้ในตัวแปร tokens_d01
คำถามแรกใน qs ถูกตัดเป็นกี่ token ด้วย toy_tokens เก็บไว้ในตัวแปร n_toy_tokens
คำขอหนึ่งครั้งใช้ input 1,200 token และ output 300 token ต้นทุนเป็น USD เท่าไร (ราคา PRICE_IN, PRICE_OUT ที่ให้ไว้ ปัด 6 ตำแหน่ง) เก็บไว้ในตัวแปร call_cost
🟡 ประยุกต์
ประมาณ token ของทุกเอกสาร (est_tokens) — ค่าเฉลี่ยต่อเอกสาร (ปัด 2 ตำแหน่ง) เก็บไว้ใน avg_tokens และ doc_id ของเอกสารที่ยาวที่สุดเก็บไว้ใน longest_doc
เทียบ input ต่อคำถามข้อแรก: (ก) ยัดทุกเอกสารรวมกับคำถาม (ข) ใส่เฉพาะ 3 เอกสารแรกรวมกับคำถาม (ต่อกันด้วยช่องว่าง) — เก็บ token ที่ประมาณได้ไว้ใน stuffed_tokens, rag_tokens และอัตราส่วน stuffed ÷ rag (ปัด 2 ตำแหน่ง) ไว้ใน saving_ratio
ใส่เอกสารตามลำดับ d01, d02, … เข้า prompt ทีละเอกสารโดยไม่ให้ผลรวม est_tokens เกิน 500 — ใส่ได้กี่เอกสาร (n_fit) และรวมกี่ token (used_tokens)
🔴 ท้าทาย
BPE หนึ่งรอบ: จาก corpus (คำแต่ละคำเป็น list ของตัวอักษร) นับความถี่คู่ตัวอักษรที่ติดกันทุกคำ (Counter) แล้วหาคู่ที่พบมากสุด — ถ้าเสมอให้เลือกคู่ที่เรียงตามตัวอักษรก่อน (tuple เล็กสุด) เก็บเป็นข้อความ "a+b" ไว้ใน top_pair และจำนวนครั้งไว้ใน top_pair_count
รวมคู่ที่พบมากสุดจากข้อ e8 (ใช้กฎเสมอเดียวกัน) เป็น token เดียวในทุกคำของ corpus แล้วนับจำนวน token ทั้งหมดก่อนและหลังรวม เก็บไว้ใน tokens_before, tokens_after (ไล่จากซ้ายไปขวา ไม่ซ้อนทับ)
Quiz
สรุป
- ราคา ความเร็ว และ context window นับเป็น token; input กับ output คิดราคาแยกกัน (ตัวเลขราคาในบทนี้เป็นค่าสมมติ)
- ตัวประมาณ (4 ตัวอักษร ≈ 1 token) ใช้วางแผนได้ แต่ของจริงให้อ่านจาก usage ที่ API รายงาน
- ดึงเฉพาะเอกสารที่เกี่ยวข้อง (≈198 token) แทนยัดทั้งฐาน (≈1,405 token) ลดต้นทุนและสิ่งรบกวน; BPE รวมคู่ที่พบบ่อยเป็น token ใหม่
คำถามสัมภาษณ์
อธิบายว่าคุณจะประเมินต้นทุนต่อเดือนของแชตบอตที่ใช้ LLM API อย่างไร
ประมาณ token เฉลี่ยต่อคำขอ (system prompt + ประวัติ + เอกสารที่ดึงมา = input, คำตอบ = output) คูณราคาต่อ token ของแต่ละฝั่ง แล้วคูณจำนวนคำขอต่อเดือน ตรวจกับ usage จริงจาก API ช่วงทดสอบ และหาวิธีลด เช่น ดึงเฉพาะเอกสารที่เกี่ยวข้อง จำกัดประวัติ cache คำถามซ้ำ และใช้โมเดลเล็กกับงานง่าย
ทำไมข้อความภาษาไทยกับภาษาอังกฤษที่ยาวเท่ากันจึงอาจมีต้นทุนต่างกัน
tokenizer แบบ subword ถูกเทรนมาจากข้อมูลที่ภาษาต่างกันในสัดส่วนไม่เท่ากัน ภาษาที่พบน้อยมักถูกแตกเป็นชิ้นเล็กกว่า จึงใช้ token ต่อข้อความมากกว่า ต้นทุนและการใช้ context เพิ่มขึ้น ควรวัดด้วย tokenizer/usage จริงกับข้อมูลของตัวเอง