llm

5 thứ dev hay nhầm về LLM context window

Đang build LLM features và thấy cùng một misconceptions lặp đi lặp lại. Context window không phải bộ nhớ. 128k token không phải 128k từ. Đây là 5 cái sai phổ biến nhất.

Mình vừa debug một bug khá buồn cười: con chatbot của team cứ quên tên user sau mỗi page refresh. Dev viết nó tin rằng model “nhớ” session trước. Không, nó không nhớ gì hết.

Đây là những misconceptions mình thấy lặp đi lặp lại nhất khi review code LLM của anh em.

1. “Context window = bộ nhớ của AI”

Model không lưu trữ gì giữa các API call. Mỗi request là một tờ giấy trắng hoàn toàn.

Context window là cửa sổ model dùng để đọc input trong request hiện tại — không phải RAM, không phải storage. Call xong là mất hết. Request tiếp theo, model không biết request trước tồn tại.

import anthropic

client = anthropic.Anthropic()

# Call đầu tiên
response1 = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=100,
    messages=[{"role": "user", "content": "Tên mình là Huy."}]
)
print(response1.content[0].text)
# Output: "Xin chào Huy! Có thể giúp gì cho bạn?"

# Call thứ hai — model không biết "Huy" là ai
response2 = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=100,
    messages=[{"role": "user", "content": "Tên mình là gì vậy?"}]
)
print(response2.content[0].text)
# Output: "Tôi không biết tên của bạn. Bạn có thể cho tôi biết không?"

Muốn model “nhớ” conversation history, anh em phải tự pass lại messages vào mỗi request:

# Cách đúng: tự manage history
history = []

def chat(user_message: str) -> str:
    history.append({"role": "user", "content": user_message})

    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=500,
        messages=history  # pass toàn bộ history
    )

    assistant_message = response.content[0].text
    history.append({"role": "assistant", "content": assistant_message})
    return assistant_message

chat("Tên mình là Huy.")
chat("Tên mình là gì vậy?")  # Lần này model trả lời đúng

Cái “memory” trong ChatGPT là feature do OpenAI build ở application layer, không phải thứ gì built-in trong model.

2. “128k token = 128k từ”

Token không phải word. Tokenizer tách text thành các chunk nhỏ — có khi là cả từ, có khi chỉ là một vài ký tự, tùy model và tùy ngôn ngữ.

Rough benchmarks cho GPT/Claude tokenizers:

  • Tiếng Anh thông thường: ~0.75 words/token (1 token ≈ 4 chars)
  • Code: thường nhiều token hơn vì symbols
  • Tiếng Việt: tệ hơn đáng kể vì nhiều diacritics
# pip install tiktoken
import tiktoken

# cl100k_base là encoding của GPT-4/GPT-3.5 (OpenAI) — KHÔNG phải
# tokenizer của Claude. Anthropic không public một thư viện offline
# tương đương tiktoken; muốn biết chính xác số token Claude sẽ dùng,
# đọc field `usage` trong response hoặc gọi token-counting endpoint:
# https://docs.claude.com/en/docs/build-with-claude/token-counting
enc = tiktoken.get_encoding("cl100k_base")

text_en = "The quick brown fox jumps over the lazy dog"
text_vi = "Con cáo nâu nhanh nhẹn nhảy qua con chó lười biếng"
code = "def calculate_fibonacci(n: int) -> list[int]:\n    if n <= 0:\n        return []\n    result = [0, 1]\n    for i in range(2, n):\n        result.append(result[-1] + result[-2])\n    return result"

def analyze(label, text):
    tokens = enc.encode(text)
    words = len(text.split())
    print(f"{label}")
    print(f"  Words: {words}, Tokens: {len(tokens)}, Ratio: {len(tokens)/words:.2f} token/word")

analyze("English", text_en)
analyze("Vietnamese", text_vi)
analyze("Python code", code)

Số token thật anh em sẽ thấy phụ thuộc vào version encoding, nên mình không hardcode kết quả ở đây — chạy đoạn trên là biết ngay. Trong sample này, tiếng Việt tốn nhiều token hơn tiếng Anh; code cũng tốn nhiều token hơn con số “word count” gợi ý vì indentation, symbol và tên biến đều bị tokenizer tách riêng. Đừng biến kết quả của một tokenizer thành quy luật cho mọi model.

Hệ quả thực tế: đừng dùng word count để estimate context usage hay cost, và đừng dùng cl100k_base để đoán token count của Claude — hai tokenizer khác nhau, số ra sẽ lệch.

Một điều hay bị bỏ sót: “context” không chỉ là tin nhắn user gõ vào. Nó gồm system prompt, toàn bộ message history (cả lượt của assistant), tool definitions và tool results nếu dùng function calling — cộng thêm phần token model sẽ generate ra, vì input và output cùng chia sẻ chung một ngân sách context window. Input càng gần lấp đầy context window, phần còn lại cho output càng ít, bất kể max_tokens anh em set là bao nhiêu.

3. “Nhét càng nhiều context vào càng tốt”

Có một vấn đề tên là “lost in the middle” — được document trong nghiên cứu năm 2023 của Stanford. Kết quả: model attend tốt nhất vào đầu và cuối context, performance giảm đáng kể với thông tin ở giữa long context.

# Cấu trúc prompt kém — critical info bị chôn ở giữa
bad_prompt = """
Đây là toàn bộ documentation của hệ thống:
{200 trang docs không liên quan}

QUAN TRỌNG: User chỉ có quyền đọc, không được ghi dữ liệu.

{100 trang docs khác không liên quan}

Hãy generate code để thực hiện yêu cầu sau: {user_request}
"""

# Cấu trúc prompt tốt hơn — critical info ở đầu và cuối
good_prompt = """
CONSTRAINTS (bắt buộc tuân theo):
- User chỉ có quyền đọc, không được ghi dữ liệu.

Context liên quan:
{chỉ lấy docs liên quan, không dump tất}

Task: {user_request}

Nhắc lại: user chỉ có quyền đọc.
"""

Một số nguyên tắc mình đang dùng:

  • Đặt instructions và constraints ở đầu prompt
  • Đặt câu hỏi hoặc task cụ thể ở cuối
  • Context dài thì lặp lại thông tin quan trọng ở cả đầu lẫn cuối
  • Đừng kỳ vọng model tự tìm ra một câu quan trọng nằm giữa trang 50 của 100 trang docs

4. “System prompt không tốn token”

Tốn. System prompt count vào context window và tốn tiền y như bất kỳ token nào khác.

import anthropic

client = anthropic.Anthropic()

system_prompt = """
Bạn là một trợ lý kỹ thuật chuyên về Python và machine learning.
Khi trả lời, hãy:
1. Luôn đưa ra code example cụ thể
2. Giải thích từng bước rõ ràng
3. Mention potential pitfalls nếu có
4. Dùng tiếng Việt nhưng technical terms giữ tiếng Anh
"""

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=500,
    system=system_prompt,
    messages=[{"role": "user", "content": "Gradient descent là gì?"}]
)

usage = response.usage
print(f"Input tokens: {usage.input_tokens}")
print(f"Output tokens: {usage.output_tokens}")
# System prompt tokens được tính vào input_tokens

Đoạn system prompt ở trên chỉ vài chục token — minh họa cú pháp là đủ, nhưng system prompt kiểu này trả full price mỗi lần gọi cũng không đáng bao nhiêu. Cái thực sự tốn tiền là khi system prompt phình to: toàn bộ coding guideline, tool schema của cả chục function, vài few-shot example dài — dễ lên tới hàng nghìn token, lặp lại y hệt ở mọi call.

Fix thực tế là prompt caching. Nhưng caching không tự động có tác dụng với prompt ngắn: ngưỡng tối thiểu phụ thuộc model — chẳng hạn docs hiện tại ghi 1.024 token cho Claude Sonnet 4.5 và 4.096 token cho Claude Haiku 4.5. Danh sách này thay đổi theo model, nên luôn kiểm tra docs của Anthropic thay vì hardcode một ngưỡng chung. Hệ thống prompt ví dụ phía trên còn lâu mới tới ngưỡng đó. Cache cũng yêu cầu exact prefix match: nội dung trước điểm đánh dấu cache_control phải giống hệt giữa các lần gọi; đổi một chữ ở đầu có thể làm phần prefix đó cache miss.

# Anthropic prompt caching — system_prompt ở đây giả định đã đủ dài
# (coding guideline + tool schema + few-shot examples, ước chừng
# vài nghìn token) để vượt ngưỡng cacheable
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=500,
    system=[
        {
            "type": "text",
            "text": system_prompt,
            "cache_control": {"type": "ephemeral"}  # cache lại
        }
    ],
    messages=[{"role": "user", "content": "Gradient descent là gì?"}]
)

# Lần đầu (cache miss): ghi cache, tính phí cao hơn giá input thường
# Lần sau, trong thời hạn cache (cache hit): chỉ tính ~10% giá input thường
print(f"Cache creation tokens: {response.usage.cache_creation_input_tokens}")
print(f"Cache read tokens: {response.usage.cache_read_input_tokens}")

Vài chi tiết dễ hiểu nhầm về pricing và TTL (số liệu có thể đổi, luôn check docs chính thức trước khi tính toán budget):

  • Cache write (cache miss, ghi mới) đắt hơn giá input thường, không rẻ hơn — vì đây là chi phí xử lý và lưu cache.
  • Cache read (cache hit) mới là chỗ tiết kiệm — rẻ hơn đáng kể so với giá input thường.
  • Cache mặc định (ephemeral) có TTL ngắn — vài phút — và được refresh mỗi lần cache hit. Anthropic cũng có tùy chọn TTL dài hơn cho use case gọi thưa hơn, với cache-write cost cao hơn tương ứng.
  • Caching chỉ đáng dùng khi cùng một prefix được gọi lại nhiều lần trong khung thời gian TTL đó — gọi một lần rồi thôi thì caching chỉ tốn thêm tiền ghi cache, không tiết kiệm gì.

5. “Context window đủ lớn thì không cần RAG”

Kỹ thuật mà nói thì nhét hết codebase vào context là được. Production thì đừng.

Ba lý do:

Latency: Context càng dài thì time-to-first-token càng lâu. Nhét 200k tokens vào mỗi request khi user hỏi một câu đơn giản là waste.

Cost: Token usage tỷ lệ thuận với chi phí. Large context + high traffic = hóa đơn to bất ngờ.

Precision: RAG retrieve đúng đoạn liên quan, giúp model focus. Dump toàn bộ docs vào thì rơi đúng vào cái “lost in the middle” ở mục 3.

# Ví dụ so sánh cost estimate (không phải số chính xác)
import anthropic

PRICE_PER_1M_INPUT = 3.0  # USD, Claude Sonnet

# Approach 1: Dump toàn bộ docs
full_docs_tokens = 150_000  # 150k tokens docs
user_question_tokens = 50   # câu hỏi ngắn
total_without_rag = full_docs_tokens + user_question_tokens

# Approach 2: RAG retrieve top-k relevant chunks
retrieved_chunks_tokens = 3_000  # chỉ lấy đúng phần liên quan
total_with_rag = retrieved_chunks_tokens + user_question_tokens

cost_without_rag = (total_without_rag / 1_000_000) * PRICE_PER_1M_INPUT
cost_with_rag = (total_with_rag / 1_000_000) * PRICE_PER_1M_INPUT

print(f"Without RAG: {total_without_rag:,} tokens = ${cost_without_rag:.4f}/request")
print(f"With RAG:    {total_with_rag:,} tokens = ${cost_with_rag:.4f}/request")
print(f"At 10k requests/day: ${(cost_without_rag - cost_with_rag) * 10000:,.0f} saved/day")

# Without RAG: 150,050 tokens = $0.4502/request
# With RAG:      3,050 tokens = $0.0092/request
# At 10k requests/day: ~$4,410 saved/day

Chênh lệch ở scale này không nhỏ — và đó là còn chưa tính latency: time-to-first-token với 150k token input luôn cao hơn đáng kể so với 3k.

Có exception: codebase nhỏ, one-off analysis, hoặc khi retrieval precision quá kém khiến RAG bỏ sót context quan trọng. Không có công thức chung đúng cho mọi trường hợp — quyết định nên dựa trên benchmark thực tế: đo latency, cost, và độ chính xác câu trả lời (so với việc nhét full context) trên một tập câu hỏi đại diện của chính hệ thống, rồi chọn theo số đo được, không theo mặc định.

Kết

  • Luôn pass history vào messages array — model không tự nhớ gì giữa các call.
  • Dùng tokenizer thực tế để estimate cost — đặc biệt với Vietnamese text và code, word count không reliable.
  • Critical info vào đầu hoặc cuối prompt, không chôn ở giữa context dài. Và bật prompt caching cho system prompt nếu call volume cao.