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) แล้วคิดเป็นเงิน

  1. ประมาณ token ของทุกเอกสารด้วยกฎ 4 ตัวอักษร — ผลรวม 1,398 เฉลี่ย ≈ 58 ต่อเอกสาร

    Python

ยัดทุกอย่าง vs ดึงเฉพาะที่เกี่ยว

เทียบขนาด prompt ตอนยัดทั้งฐานความรู้กับตอนใส่แค่ 3 เอกสาร

  1. ยัดทุกเอกสารเข้า prompt ≈ 1,405 token ส่วนใส่ 3 เอกสารแรก ≈ 198 token (คำถามข้อแรกต่อท้าย)

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

คำนวณต้นทุนของคำขอที่ใช้ 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

1. ทำไมต้นทุนและขีดจำกัดของ LLM จึงนับเป็น token ไม่ใช่ตัวอักษร
2. ในการคิดต้นทุนต่อหนึ่งคำขอ ควรทำอย่างไร
3. ในบทนี้ การใส่เฉพาะ 3 เอกสารแทนทั้ง 24 ลดขนาด input จาก ≈1,405 เหลือ ≈198 token ข้อสรุปที่ถูกคือ
4. BPE สร้าง token ใหม่อย่างไร

สรุป

  • ราคา ความเร็ว และ 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 จริงกับข้อมูลของตัวเอง