03 · Context engineering — prompt caching, context rot, compaction, memory

Khi agent chạy nhiều vòng, thứ quyết định chất lượng và hoá đơn không còn là một câu prompt hay, mà là toàn bộ những gì nằm trong context ở mỗi vòng: system prompt, định nghĩa tool, lịch sử, kết quả tool, ghi chú. Bài này nói về cách quản lý "ngân sách chú ý" đó — và về một nghịch lý đo được trong lab: dọn context để tiết kiệm có thể làm hoá đơn tăng gần gấp đôi nếu không hiểu prompt cache.

Đã verify gì, chưa verify gì. Lab chạy thật: so byte tiền tố của hai request liên tiếp (PHP 8.4) để thấy các "silent invalidator" của prompt cache; mô hình chi phí 30 vòng agent theo bảng giá (Python). Mô hình chi phí là tính toán theo quy tắc giá công bố (ghi cache 1,25×, đọc cache $0,20/MTok trên Opus 5.5), không phải hoá đơn thật — chưa verify bằng API. Context rot, compaction, memory tool: dựa trên tài liệu Anthropic và báo cáo Chroma (nêu nguồn), chưa verify bằng model thật. Không có Context7 trong môi trường; dùng tài liệu tham chiếu Claude API cache 2026-09-25.

Câu trả lời 60 giây

"Context window là bộ nhớ làm việc có hạn của model: token nào cũng tranh sự chú ý với token khác, và chất lượng giảm dần khi context dài ra — kể cả khi còn xa giới hạn 1M. Tôi quản lý nó theo bốn thao tác: ghi ra ngoài (ghi chú, memory), chọn lọc (lấy đúng thứ cần qua tool/RAG khi cần), nén (compaction, dọn tool result cũ), cô lập (subagent có context riêng, trả về tóm tắt). Song song là prompt caching: cache là khớp tiền tố theo byte, nên phần ổn định phải đứng trước, phần thay đổi đứng sau. Lab của tôi cho thấy chỉ một dòng 'Bây giờ là 10:31' trong system prompt làm 98% prompt không dùng lại được cache; và dọn tool result cũ từng lượt làm phá cache liên tục, tốn gấp 1,7 lần so với không dọn — phải dọn theo lô."

Thuật ngữ

Thuật ngữNghĩa ngắn
context engineeringThiết kế và quản lý toàn bộ token đưa vào model ở mỗi lượt, không chỉ câu prompt.
prompt cachingNhà cung cấp giữ lại kết quả xử lý (KV cache) của phần đầu prompt; lần sau khớp tiền tố thì đọc lại, rẻ và nhanh hơn.
breakpoint (cache_control)Điểm đánh dấu "cache tới đây"; tối đa 4 điểm mỗi request.
context rotHiện tượng chất lượng giảm khi context dài ra, không chỉ khi gần giới hạn.
compactionTóm tắt phần lịch sử cũ thành một khối ngắn để tiếp tục phiên dài.
context editingXoá bớt (không tóm tắt) tool result / thinking cũ khỏi lịch sử.
memoryLưu trữ ngoài context (file, DB) mà agent đọc/ghi qua tool, sống qua nhiều phiên.
subagentAgent con chạy với context riêng cho một nhiệm vụ hẹp, trả về kết quả gọn cho agent cha.

1. Bài toán gốc và bối cảnh ra đời

Thời "prompt engineering" (2022–2024), bài toán là viết một câu lệnh tốt cho một lần gọi. Khi agent chạy hàng chục đến hàng trăm vòng, context tự phình lên bởi kết quả tool, và ba giới hạn xuất hiện cùng lúc:

  1. Chi phí và độ trễ: mỗi vòng gửi lại toàn bộ lịch sử → input cộng dồn tăng bậc hai (lab bài 02). Prefill dài → TTFT dài.
  2. Chất lượng: báo cáo "Context Rot" của Chroma (7/2025) thử 18 model (có Claude 4, GPT-4.1, Gemini 2.5): độ tin cậy giảm khi input dài ra, ngay cả với tác vụ đơn giản như tìm và chép lại; nhiễu gần nghĩa (distractor) làm tệ hơn. Trước đó, "Lost in the Middle" (Liu et al., 2023) cho thấy thông tin nằm giữa context dài bị dùng kém hơn thông tin ở đầu/cuối.
  3. Giới hạn cứng: vượt cửa sổ là request bị từ chối.

Thuật ngữ "context engineering" phổ biến từ giữa 2025 (Tobi Lütke, Andrej Karpathy); Anthropic hệ thống hoá trong bài "Effective context engineering for AI agents" (9/2025) với ba kỹ thuật cho tác vụ dài: compaction, ghi chú có cấu trúc, và kiến trúc subagent.

Tối ưu cho: agent nhiều vòng, phiên dài, dùng lại prompt lớn. Không cần cho một lần gọi ngắn — ở đó chỉ cần prompt rõ ràng.

2. Mô hình tư duy cốt lõi

Context là RAM có hạn và đắt; mục tiêu là tập token nhỏ nhất có tín hiệu cao nhất cho bước hiện tại. Prompt cache là một tầng cache theo tiền tố đặt trước RAM đó — nó chỉ giúp khi phần đầu giữ nguyên từng byte.
flowchart TB
  subgraph CTX["Context gửi mỗi lượt (thứ tự render: tools -> system -> messages)"]
    T["Định nghĩa tool (ổn định)"] --> S["System prompt (ổn định)"]
    S --> H["Lịch sử hội thoại (append-only, tăng dần)"]
    H --> R["Tool result mới (to, sống ngắn)"]
    R --> Q["Chỉ dẫn / câu hỏi lượt này (thay đổi)"]
  end
  M[("Memory / ghi chú ngoài context")] -.->|"đọc khi cần"| H
  K[("RAG / search qua tool")] -.->|"lấy đúng đoạn cần"| R
  SA["Subagent: context riêng"] -.->|"trả tóm tắt 1-2k token"| R
  CP["Compaction / dọn tool result"] -.->|"nén phần cũ"| H

Hai hệ quả trái chiều nhau — chính là trọng tâm của bài:

Thiết kế tốt là thiết kế mà hai lực này ít va nhau: phần ổn định ở đầu và không bao giờ đổi; phần thay đổi ở cuối; nén theo lô ở những mốc hiếm.

3. Internals và quyết định thiết kế

3.1 Prompt caching hoạt động thế nào

Nền tảng là KV cache ở bài 01: prefill một prompt tạo ra Key/Value cho từng token. Nếu request sau có cùng tiền tố, nhà cung cấp dùng lại KV đã tính. Các quy tắc (tài liệu Claude API, cache 2026-09-25):

Quy tắcChi tiếtVì sao lại thiết kế vậy
Khớp tiền tố theo byteKhác 1 byte ở vị trí N → mất cache từ N trở điKV của token phụ thuộc mọi token trước nó
Thứ tự rendertools → system → messagesĐổi tool = mất toàn bộ cache
BreakpointTối đa 4 cache_control; hoặc cache tự động ở cấp request (tự đặt ở block cuối)Giới hạn số entry phải ghi
Độ dài tối thiểu512 token trên Opus 5.5/Sonnet 5.5; 1024–4096 trên model cũ (Haiku 4.5: 4096). Ngắn hơn thì im lặng không cacheEntry quá nhỏ không đáng quản lý
TTL5 phút (mặc định) hoặc 1 giờ; mỗi lần đọc làm mới TTLRAM GPU/CPU của nhà cung cấp có hạn
GiáGhi: 1,25× (5 phút) hoặc 2× (1 giờ) giá input. Đọc: ~0,1× (Opus 5.5: $0,20 = 0,05×)Ghi tốn thêm chỗ, đọc bỏ qua prefill
LookbackMỗi breakpoint dò ngược tối đa 20 vị trí để tìm entry cũMột lượt thêm quá 20 block sẽ miss im lặng
Song songEntry chỉ đọc được sau khi request đầu bắt đầu streamN request song song cùng tiền tố đều trả giá đầy đủ
Phạm viTách theo workspace (Claude API), không chia sẻ giữa tổ chứcCô lập dữ liệu

Hoà vốn: TTL 5 phút hoà vốn từ lần đọc thứ nhất (1,25 + 0,1 = 1,35 < 2 lần giá không cache). TTL 1 giờ cần ít nhất 2 lần đọc. Chọn TTL theo khoảng cách giữa hai request cùng tiền tố: dưới 5 phút → 5 phút; 5–60 phút (người dùng trả lời chậm) → 1 giờ.

Kiểm tra cache có chạy không: đọc usage. Tổng prompt = input_tokens + cache_creation_input_tokens + cache_read_input_tokens. Vòng lặp khoẻ mạnh: cache_read tăng dần mỗi lượt, cache_creation chỉ bằng phần vừa thêm. Nếu cache_creation xấp xỉ toàn bộ context ở mọi lượt → có thứ gì đó đang sửa tiền tố.

3.2 Lab: "silent invalidator" — mất cache mà không có lỗi

Dựng request của lượt 1 và lượt 2 (65 giây sau) của cùng một hội thoại CSKH, render theo thứ tự API, tìm byte đầu tiên khác nhau:

A. system có giờ hiện tại               request1=  4468 byte, khớp tiền tố     90 byte =   2.0% dùng lại được
B. tools lấy từ DB không ORDER BY         request1=  4443 byte, khớp tiền tố     11 byte =   0.2% dùng lại được
C. system cố định, giờ đưa vào cuối messages request1=  4502 byte, khớp tiền tố   4468 byte =  99.2% dùng lại được

Cả A và B đều không báo lỗi gì — request vẫn thành công, chỉ là hoá đơn cao hơn. Những thủ phạm hay gặp trong code Laravel: now() trong system prompt; "Xin chào {$user->name}" ở system (mỗi user một tiền tố, không chia sẻ được); json_encode một mảng lấy từ query không ORDER BY; if ($feature) $system .= ... (mỗi tổ hợp flag một tiền tố); tool list thay đổi theo quyền user. Cách sửa: đưa phần động xuống cuối (hoặc vào message role: "system" giữa hội thoại — Opus 5.5 hỗ trợ, không phá cache), sắp xếp tool theo tên, giữ tool list cố định và kiểm quyền trong tool.

Anthropic có beta cache diagnostics (cache-diagnosis-2026-04-07): gửi id của response trước, API chỉ ra hai request lệch nhau ở đâu (model, system, tools hay messages) — chưa verify.

3.3 Bốn thao tác quản lý context

Thao tácKỹ thuậtĐượcMất
Ghi ra ngoàiFile ghi chú/todo, memory tool (memory_20250818, client-side, thư mục /memories), bảng DBSống qua compaction và qua phiên; context nhỏGhi chú có thể cũ/sai; là kênh bị đầu độc (prompt injection ghi vào memory)
Chọn lọcRAG, search tool, "just-in-time": giữ con trỏ (đường dẫn, id), chỉ đọc nội dung khi cầnChỉ trả tiền cho thứ liên quanThêm vòng gọi tool; chọn sai thì model không thấy thông tin
NénCompaction (tóm tắt), context editing (xoá tool result/thinking cũ)Phiên dài hơn cửa sổ; bớt nhiễuMất chi tiết; phá cache từ điểm sửa
Cô lậpSubagent với context riêng, trả về 1–2k token tóm tắtContext agent chính sạch; chạy song songToken tổng tăng nhiều lần; mất ngữ cảnh chung (bài 05)

Trên Claude API, ba cơ chế có sẵn (theo tài liệu, chưa verify):

3.4 Lab: dọn context vs prompt cache — con số ngược trực giác

Mô phỏng một phiên agent 30 lượt: tiền tố 8 000 token (system + tool), mỗi lượt thêm 300 token output và 2 500 token tool result. Bốn chiến lược, tính theo bảng giá:

Phiên 30 lượt, tiền tố 8000 token, mỗi lượt +300 output +2500 tool result

model              chiến lược                 tổng input đã gửi  context đỉnh   chi phí
claude-opus-5-5    không cache                        1,462,500        89,350    6.030$
claude-opus-5-5    prompt cache                       1,462,500        89,350    0.860$
claude-opus-5-5    cache + dọn mỗi lượt                 592,020        24,870    1.462$
claude-opus-5-5    cache + dọn theo lô >60k           1,026,020        58,550    0.825$

claude-sonnet-5-5  không cache                        1,462,500        89,350    3.015$
claude-sonnet-5-5  prompt cache                       1,462,500        89,350    0.568$
claude-sonnet-5-5  cache + dọn mỗi lượt                 592,020        24,870    0.766$
claude-sonnet-5-5  cache + dọn theo lô >60k           1,026,020        58,550    0.506$

claude-haiku-4-5   không cache                        1,462,500        89,350    1.507$
claude-haiku-4-5   prompt cache                       1,462,500        89,350    0.284$
claude-haiku-4-5   cache + dọn mỗi lượt                 592,020        24,870    0.383$
claude-haiku-4-5   cache + dọn theo lô >60k           1,026,020        58,550    0.253$

Đọc kết quả:

Giả định của mô hình: cache luôn còn sống (các lượt cách nhau dưới 5 phút), không có thinking token, mỗi lượt đọc được toàn bộ tiền tố đã ghi. Thực tế hit rate thấp hơn → khoảng cách giữa "không cache" và "có cache" sẽ nhỏ hơn. Mục đích là thấy hướng tác động.

3.5 Viết system prompt ở "đúng độ cao"

Hai kiểu hỏng đối xứng: quá cứng (một cây if-else dài dằng dặc trong prompt — giòn, model bám chữ, gặp tình huống ngoài danh sách là lúng túng) và quá mơ hồ ("hãy hữu ích" — không cho model tín hiệu cụ thể). Đích đến là: mô tả mục tiêu, ràng buộc cứng, cách xử lý tình huống mơ hồ, và vài ví dụ tiêu biểu (không phải danh sách mọi trường hợp). Chia phần bằng tiêu đề/thẻ XML để model và người đọc tìm được.

Progressive disclosure (tiết lộ dần) là mẫu chung của Claude Code và Agent Skills: chỉ mô tả ngắn của mỗi skill nằm sẵn trong context; nội dung đầy đủ (SKILL.md, script) chỉ được đọc khi nhiệm vụ cần. CLAUDE.md của repo này là ví dụ của "memory dự án" nạp sẵn — vì luôn nằm trong context, mỗi dòng trong đó phải đáng giá token của nó.

3.6 Ràng buộc mới: lịch sử phải append-only

Trên Opus 5.5, Sonnet 5.5, Fable 5.1, thinking block gắn với model và với chính hội thoại tạo ra nó ("preserved thinking"). Theo tài liệu, sửa một lượt cũ (xoá một reminder đã chèn, viết lại một tool result) làm các thinking block phía sau mất hiệu lực; tài khoản tạo từ 31/8/2026 bị 400 khi gửi lịch sử đã sửa. Hệ quả kiến trúc: harness phải append-only; muốn "xoá" thì dùng cơ chế API cung cấp (context editing, compaction, message system có clear_at) thay vì tự sửa mảng messages. Chưa verify bằng API thật.

4. Đảm bảo và giới hạn

Cơ chếĐảm bảoKhông đảm bảo
Prompt cacheNếu hit: đúng kết quả như không cache (cache không làm đổi output)Hit — có thể miss do TTL, song song, lookback, tiền tố quá ngắn; không có lỗi báo miss
Context windowRequest vượt giới hạn bị từ chối rõ ràngChất lượng đều ở mọi độ dài
CompactionPhiên chạy tiếp được sau ngưỡngGiữ lại đúng chi tiết quan trọng (tóm tắt là lossy)
Context editingXoá đúng loại block đã cấu hìnhModel không cần lại thứ đã xoá
MemoryDo bạn hiện thực → tuỳ bạnĐúng đắn, không cũ, không bị đầu độc; không có transaction giữa nhiều agent

Độ phức tạp: không nén thì tổng input của phiên n vòng là O(n²); nén theo lô đưa về gần O(n·C) với C là trần context mục tiêu.

5. Failure modes

LỗiHệ quảPhát hiệnPhục hồi / phòng
Silent cache invalidation (lab 3.2)Chi phí tăng 5–10 lần, TTFT tăng; không lỗicache_read_input_tokens ≈ 0 qua nhiều request; test tích hợp khẳng định request thứ 2 có cache_read > 0Sửa tiền tố; diff payload hai request liền nhau (bỏ cache_control trước khi diff)
Dọn context phá cache (lab 3.4)Tăng chi phí dù context nhỏ đicache_creation lớn mỗi lượtDọn theo lô, ngưỡng cao
Compaction làm mất ràng buộcNgười dùng dặn "không đụng production" ở lượt 3, sau compaction agent quênEval phiên dài; kiểm tra tóm tắt có chứa các ràng buộcRàng buộc cứng đặt ở system/tool (hạ tầng), không để trong hội thoại; ghi chú có cấu trúc cho quyết định quan trọng
Context rot / lost in the middleBỏ sót dữ kiện có trong contextEval với context dài + distractorChọn lọc chặt hơn; đặt thông tin then chốt gần cuối (gần câu hỏi)
Tràn cửa sổRequest 400, phiên chết giữa chừngĐếm token trước khi gửiCompaction, giới hạn kích thước tool result
Memory cũ / saiAgent hành động theo sự thật đã hết hạnGhi chú có ngày, nguồnTTL cho memory; ưu tiên dữ liệu sống từ tool
Memory bị đầu độcPrompt injection ghi chỉ dẫn độc vào memory, ảnh hưởng mọi phiên sauReview/diff memory; tách theo userMemory là dữ liệu không tin cậy; không cho nội dung web ghi thẳng vào memory
Song song không chia sẻ cacheFan-out N worker trả N lần giá ghicache_creation nhân NGửi 1 request, chờ token đầu, rồi mới bắn N−1

Lab tái hiện: hai dòng đầu đã tái hiện (mục 3.2, 3.4). Các dòng còn lại cần model thật — chưa verify.

6. Vận hành trong production

7. So sánh và ma trận quyết định

Chiến lượcChi phíChất lượngĐộ phức tạpHỏng kiểu gìDùng khi
Nhồi hết vào contextCao (trừ khi cache tốt)Tốt khi ngắn, giảm khi dài và nhiễuThấpContext rot, TTFTTài liệu vừa phải, dùng lại nhiều lần (cache)
RAG trước khi hỏiThấpPhụ thuộc retrievalTrung bình (index, chunking)Lấy sai đoạn → trả lời sai tự tinKho tài liệu lớn, câu hỏi độc lập (bài 04)
Agentic search (tool grep/search, đọc khi cần)Trung bình (nhiều vòng)Cao với dữ liệu có cấu trúc (code, log)ThấpChậm, nhiều vòngCodebase, dữ liệu thay đổi liên tục — cách Claude Code làm
CompactionGiảm dài hạnMất chi tiếtThấp nếu dùng của APIQuên ràng buộcPhiên rất dài, liên tục
Context editingTuỳ cách dọn (lab)Bớt nhiễuThấpPhá cache nếu dọn lắt nhắtNhiều tool result to, dùng một lần
SubagentTổng token tăng mạnhContext chính sạchCaoMất ngữ cảnh chung, mâu thuẫnNhiệm vụ con độc lập, đọc nhiều (bài 05)
Memory ngoàiThấpTốt cho kiến thức bềnTrung bìnhCũ, bị đầu độcSở thích người dùng, quyết định dự án qua nhiều phiên

Mặc định hợp lý: system + tool ổn định có breakpoint riêng; cache tự động cho đuôi hội thoại; tool result giới hạn kích thước; dọn theo lô khi vượt ngưỡng; compaction của API cho phiên dài. Tránh: tự viết lại lịch sử, và nhồi hàng trăm nghìn token "cho chắc".

8. Góc nhìn tech lead: tác động tổ chức

9. Anti-patterns và bài học thực tế

Điều người vận hành lâu năm đều biết: lỗi cache không bao giờ tự báo. Hãy có một dashboard cache hit ratio trước khi có dashboard nào khác cho tính năng AI — nó là metric rẻ nhất phát hiện regression chi phí.

10. Câu hỏi phụ interviewer hay hỏi tiếp

Prompt caching khác cache kết quả (response cache) thế nào?
Prompt caching lưu kết quả xử lý đầu vào ở phía nhà cung cấp; output vẫn được sinh mới mỗi lần. Response cache (Redis theo hash câu hỏi) trả lại output cũ — chỉ đúng khi câu hỏi lặp y hệt và câu trả lời không phụ thuộc thời gian.
Compaction hay RAG cho phiên dài?
Khác mục đích: compaction nén lịch sử của chính phiên; RAG lấy kiến thức ngoài. Phiên dài thường cần cả hai, cộng ghi chú có cấu trúc cho quyết định quan trọng.
Làm sao biết cache đang chạy?
Đọc usage.cache_read_input_tokens; có test tích hợp gửi hai request giống nhau và khẳng định lần hai đọc cache; theo dõi tỉ lệ theo route.
Subagent có tiết kiệm token không?
Không — tổng token thường tăng. Nó tiết kiệm context của agent chính (chất lượng) và cho chạy song song (thời gian).
Đặt thông tin quan trọng ở đâu trong prompt?
Chỉ dẫn và ràng buộc ổn định ở system (đầu, cache được); tài liệu dài đặt trước câu hỏi; câu hỏi/nhiệm vụ cụ thể ở cuối. Tránh để dữ kiện then chốt chìm giữa một khối dài.

Phụ lục: lab đã chạy

Môi trường: PHP 8.4.19; Python 3.11.15. Output gốc: labs/ai/out/03-cache-prefix.txt, labs/ai/out/03-context-cost.txt.

php labs/ai/php/cache_prefix.php        # tìm byte đầu tiên khác nhau giữa 2 request liên tiếp
python3 labs/ai/py/context_cost.py      # mô hình chi phí 30 lượt, 4 chiến lược, 3 model

Chưa verify: hit rate thật và hoá đơn thật (cần API key); compaction, context editing, memory tool, cache diagnostics, ràng buộc append-only với thinking block (lấy từ tài liệu tham chiếu API). Nguồn: Chroma, "Context Rot: How Increasing Input Tokens Impacts LLM Performance" (7/2025); Liu et al., "Lost in the Middle" (2023); Anthropic Engineering, "Effective context engineering for AI agents" (9/2025).

Bài trước: 02 · Tool use và agent loop · Bài tiếp: 04 · RAG production với pgvector.