Một agent code không cần đọc lại toàn bộ 4.000 dòng của một file để biết nó nên gọi function nào với tham số gì. Nó cần đúng phần "bộ xương" — chữ ký hàm, kiểu dữ liệu, comment quan trọng — và bỏ qua phần thân hàm không liên quan đến tác vụ hiện tại.
Khi làm việc với coding agent (Claude Code, Cursor, Copilot Workspace, hay bất kỳ agent tự động hóa dòng chảy phát triển), phần context tốn token nhiều nhất thường không phải là hội thoại, mà là code: nội dung file, git diff, log của CI/CD (continuous integration/continuous deployment — tích hợp liên tục/triển khai liên tục), và output của test suite. Một repo cỡ trung bình có thể có hàng trăm nghìn dòng code; một pull request (PR) lớn có thể đụng vào 40-50 file; một lần chạy CI có thể sinh ra hàng chục nghìn dòng log. Nếu đưa nguyên tất cả vào context window, agent sẽ nhanh chóng chạm giới hạn token, tốn chi phí, và tệ hơn — bị "loãng" chú ý (attention) vào những thông tin không liên quan.
Bài này tập trung vào một nhóm kỹ thuật compression (nén ngữ cảnh) chuyên biệt cho code, diff, và test output — khác hẳn về bản chất so với tóm tắt hội thoại đã học ở các bài trước. Bạn sẽ học cách tạo code skeleton cho file, chuyển git diff thô thành semantic change log (nhật ký thay đổi theo ý nghĩa), viết template tóm tắt cho toàn bộ PR/feature branch, nén log CI khổng lồ thành bản briefing gọn, và cách phân bổ token budget hợp lý cho một session coding agent.
Vì sao Tóm tắt Code Cần một Cách Tiếp cận Khác
Tóm tắt hội thoại (conversation summarization) chấp nhận một mức độ mất mát (lossy) nhất định — bạn có thể diễn giải lại ý người dùng bằng câu chữ khác mà không đổi nghĩa. Với code, điều này gần như không được phép. Một dấu ? sai vị trí trong TypeScript (optional chaining), một tham số bị đảo thứ tự, hay một kiểu trả về bị tóm tắt nhầm từ Promise<User> thành "trả về user" đều có thể khiến agent sinh ra code sai ngay lập tức, hoặc tệ hơn — sinh ra code "trông đúng" nhưng compile lỗi hoặc runtime crash.
Vấn đề cốt lõi là: precision (độ chính xác) của phần khai báo/giao diện (interface) là bất khả xâm phạm, nhưng phần triển khai (implementation) bên trong lại thường là noise (nhiễu) đối với tác vụ hiện tại của agent. Nếu bạn đang yêu cầu agent thêm một field mới vào DTO (data transfer object) và cập nhật nơi gọi nó, agent cần biết chính xác:
- Chữ ký (signature) của mọi hàm liên quan — tên, tham số, kiểu, giá trị trả về.
- Cấu trúc class/interface — property nào, kiểu gì, optional hay required.
- Import và export — module nào phụ thuộc module nào.
- Docstring/comment mô tả hợp đồng (contract) của hàm — đặc biệt các ràng buộc không thể hiện qua kiểu (ví dụ "phải gọi sau khi
init()đã chạy").
Nhưng agent không cần biết chi tiết từng dòng logic bên trong một hàm helper không liên quan, hoặc cách một hàm khác implement thuật toán sort nội bộ — trừ khi tác vụ hiện tại liên quan trực tiếp đến logic đó.
Sự khác biệt này dẫn tới nguyên tắc thiết kế trung tâm của bài học: nén theo lớp cấu trúc (structural layer), không nén theo mức độ diễn giải lại (paraphrase). Với hội thoại, bạn rút gọn câu chữ nhưng giữ ý nghĩa. Với code, bạn giữ nguyên 100% phần cấu trúc/giao diện (không diễn giải lại một signature bằng lời — luôn giữ nguyên literal code), và loại bỏ toàn bộ phần thân hàm không cần thiết cho tác vụ.
Một hệ quả thực tế: các công cụ nén code tốt không dùng LLM để "viết lại" signature (rủi ro sai cao), mà dùng parser tĩnh (static parser — như AST của TypeScript compiler, hoặc ast module của Python) để trích xuất chính xác phần cấu trúc, rồi mới dùng LLM cho phần tóm tắt ý nghĩa (như tóm tắt diff hay PR).
Mẹo:
- Không bao giờ để LLM "chép lại từ trí nhớ" một function signature dài — luôn copy nguyên văn từ source, dù trong bản tóm tắt.
- Khi context bị cắt bớt tự động (do middleware compression), luôn kiểm tra xem phần bị cắt là docstring/comment hay logic bên trong — cắt sai chỗ (mất một dòng type annotation) nguy hiểm hơn cắt nhầm cả một khối logic.
- Với ngôn ngữ có type mạnh (TypeScript, Java, Go, Rust), ưu tiên giữ type annotation hơn cả comment, vì agent có thể suy luận hành vi từ type nhưng không thể suy luận type từ mô tả mơ hồ.
Tóm tắt ở Cấp độ File: Code Skeleton (Bộ khung Code)
Code skeleton là kỹ thuật lược bỏ phần thân hàm/phương thức (method body) trong khi giữ nguyên toàn bộ phần khai báo: signature, class structure, import/export, decorator, type annotation, và docstring/comment mức hàm. Kết quả là một phiên bản file "rỗng ruột" nhưng vẫn cho agent biết đầy đủ giao diện của module — đủ để agent gọi đúng hàm, truyền đúng tham số, và hiểu đúng những gì hàm trả về, mà không cần tốn token cho logic bên trong.
Đây là ví dụ một file TypeScript đầy đủ, dạng service quản lý order trong một hệ thống e-commerce:
// order-service.ts — FULL VERSION (187 tokens ước lượng)
import { Database } from "./db";
import { PaymentGateway } from "./payment-gateway";
import { InventoryClient } from "./inventory-client";
import { Order, OrderStatus, OrderItem } from "./types";
/**
* Handles the full lifecycle of an order: creation, payment,
* inventory reservation, and cancellation.
*/
export class OrderService {
constructor(
private db: Database,
private payment: PaymentGateway,
private inventory: InventoryClient
) {}
/**
* Creates a new order after validating stock availability.
* Throws InsufficientStockError if any item is out of stock.
*/
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
for (const item of items) {
const available = await this.inventory.checkStock(item.sku, item.qty);
if (!available) {
throw new InsufficientStockError(item.sku);
}
}
const order: Order = {
id: generateOrderId(),
userId,
items,
status: OrderStatus.PENDING,
createdAt: new Date(),
};
await this.db.orders.insert(order);
await this.inventory.reserve(items);
return order;
}
/**
* Charges the customer and transitions order to PAID.
* Rolls back inventory reservation if payment fails.
*/
async processPayment(orderId: string, cardToken: string): Promise<void> {
const order = await this.db.orders.findById(orderId);
try {
await this.payment.charge(order.total, cardToken);
order.status = OrderStatus.PAID;
await this.db.orders.update(order);
} catch (err) {
await this.inventory.release(order.items);
throw err;
}
}
/** Cancels an order and releases reserved inventory. */
async cancelOrder(orderId: string): Promise<void> {
const order = await this.db.orders.findById(orderId);
order.status = OrderStatus.CANCELLED;
await this.db.orders.update(order);
await this.inventory.release(order.items);
}
}
Và đây là skeleton tương ứng — giữ nguyên toàn bộ import, docstring, class structure, signature, chỉ thay body bằng một placeholder rõ ràng:
// order-service.ts — SKELETON (52 tokens ước lượng, giảm ~72%)
import { Database } from "./db";
import { PaymentGateway } from "./payment-gateway";
import { InventoryClient } from "./inventory-client";
import { Order, OrderStatus, OrderItem } from "./types";
/**
* Handles the full lifecycle of an order: creation, payment,
* inventory reservation, and cancellation.
*/
export class OrderService {
constructor(
private db: Database,
private payment: PaymentGateway,
private inventory: InventoryClient
) {}
/**
* Creates a new order after validating stock availability.
* Throws InsufficientStockError if any item is out of stock.
*/
async createOrder(userId: string, items: OrderItem[]): Promise<Order> { /* ... */ }
/**
* Charges the customer and transitions order to PAID.
* Rolls back inventory reservation if payment fails.
*/
async processPayment(orderId: string, cardToken: string): Promise<void> { /* ... */ }
/** Cancels an order and releases reserved inventory. */
async cancelOrder(orderId: string): Promise<void> { /* ... */ }
}
Với ví dụ nhỏ này mức giảm đã hơn 70%; trên các file thực tế có nhiều hàm dài (50-100 dòng mỗi hàm), tỷ lệ giảm token thường đạt 80-90%. Điều quan trọng là agent vẫn biết chính xác: OrderService cần 3 dependency gì, createOrder nhận userId và items trả về Promise<Order> và có thể throw InsufficientStockError, processPayment có side-effect rollback khi lỗi. Đây là toàn bộ thông tin cần để agent viết code gọi các hàm này đúng cách, hoặc để agent quyết định hàm nào cần đọc full source khi cần sửa logic bên trong.
Về công cụ: bạn không cần regex thủ công. Các thư viện parser như ts-morph (TypeScript), tree-sitter (đa ngôn ngữ), hay ast/libcst (Python) cho phép trích xuất chính xác node kiểu function/class declaration và bỏ node kiểu block statement bên trong — nhanh, rẻ, và không cần gọi LLM. Nhiều coding agent hiện đại (Claude Code, Cursor, Aider) đã có sẵn cơ chế build "repo map" hoặc "outline" dựa trên nguyên lý này khi index codebase.
Prompt pattern khi bạn cần agent tự tạo skeleton (trường hợp không có sẵn tool AST, ví dụ với ngôn ngữ ít phổ biến):
Convert the following source file into a "code skeleton":
- Keep all imports/exports exactly as written.
- Keep all class/interface/type declarations exactly as written.
- Keep all function/method signatures exactly as written (name, parameters,
types, return type, decorators).
- Keep all docstrings/comments attached to declarations.
- Replace every function/method body with a single-line placeholder: { /* ... */ }
- Do not paraphrase, rename, or reformat anything you keep.
- Do not add any explanation outside the code block.
Source file:
<paste file content>
Mẹo:
- Ưu tiên công cụ dựa trên AST/parser tĩnh hơn LLM để tạo skeleton — rẻ hơn, nhanh hơn, và không có rủi ro LLM "bịa" sai một ký tự trong signature.
- Với file có nhiều hàm private/helper nội bộ ít khi được gọi từ ngoài, có thể lược bỏ hoàn toàn (không chỉ rút gọn body) nếu tác vụ không liên quan — giữ lại comment "// N private helpers omitted" để agent biết chúng tồn tại.
- Luôn giữ decorator (@Injectable(), @Component(), @pytest.fixture) trong skeleton — chúng thường mang thông tin hành vi quan trọng (framework wiring) mà agent cần biết.
Tóm tắt Diff: Từ Git Diff Thô đến Semantic Change Log
Git diff thô (raw diff — output của git diff hay git show) có định dạng dòng-theo-dòng (line-based) với ký hiệu +/-, kèm theo context lines xung quanh. Định dạng này rất tốt cho con người review trực quan, nhưng khá kém hiệu quả về token cho agent, vì nó lẫn cả context không đổi, thông tin về số dòng (@@ -12,7 +12,9 @@), và không nói rõ ý nghĩa của thay đổi — agent phải tự suy luận lại từ các dòng +/-.
Semantic change log là bước biến diff thô thành một danh sách các câu mô tả ý nghĩa thay đổi, dạng "Đã làm gì, ở đâu, vì sao (nếu suy luận được)". Đây không phải là tóm tắt hời hợt kiểu "sửa vài chỗ trong file X" — mà là một danh sách có cấu trúc, chính xác về tên hàm/biến, giữ được các con số/điều kiện quan trọng.
Ví dụ một đoạn diff thô:
diff --git a/src/services/order-service.ts b/src/services/order-service.ts
index 3f2a1bc..9d81ef2 100644
--- a/src/services/order-service.ts
+++ b/src/services/order-service.ts
@@ -18,10 +18,17 @@ export class OrderService {
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
+ if (!items || items.length === 0) {
+ throw new InvalidOrderError("Order must contain at least one item");
+ }
+ if (items.length > MAX_ITEMS_PER_ORDER) {
+ throw new InvalidOrderError(
+ `Order exceeds maximum of ${MAX_ITEMS_PER_ORDER} items`
+ );
+ }
for (const item of items) {
const available = await this.inventory.checkStock(item.sku, item.qty);
if (!available) {
- throw new InsufficientStockError(item.sku);
+ throw new InsufficientStockError(item.sku, item.qty, available);
}
}
@@ -45,7 +52,6 @@ export class OrderService {
- /** @deprecated use processPayment instead */
- async chargeCustomer(orderId: string, cardToken: string): Promise<void> {
- return this.processPayment(orderId, cardToken);
- }
Và đây là semantic change log tương ứng — đây chính là dạng nên lưu vào context thay cho diff thô khi agent không cần chỉnh sửa thêm ở đúng những dòng này:
CHANGE LOG — src/services/order-service.ts
1. Added input validation to createOrder():
- Rejects empty item lists (throws InvalidOrderError).
- Rejects orders exceeding MAX_ITEMS_PER_ORDER (throws InvalidOrderError).
2. Extended InsufficientStockError signature: now takes (sku, qty, available)
instead of just (sku) — callers constructing this error elsewhere must update.
3. Removed deprecated method chargeCustomer() (was a thin wrapper around
processPayment()). Any remaining callers of chargeCustomer() will now
fail to compile — search codebase for usages before merging.
So với diff thô (khoảng 220 token trong ví dụ này), semantic change log chỉ mất khoảng 75-90 token nhưng giữ lại đầy đủ thông tin hành động mà một agent cần để quyết định bước tiếp theo (ví dụ: đi tìm và sửa các lời gọi chargeCustomer còn sót, hoặc kiểm tra các nơi khởi tạo InsufficientStockError). Điểm mấu chốt là semantic change log phải giữ nguyên literal tên hàm/biến/exception — không diễn giải chung như "đã sửa lỗi xử lý stock" mà phải cụ thể "InsufficientStockError signature đổi từ (sku) thành (sku, qty, available)".
Prompt pattern để agent tự sinh semantic change log từ diff thô:
You are converting a raw git diff into a compact semantic change log.
Rules:
- Group changes by file.
- For each file, list changes as short imperative bullet points
(e.g. "Added X", "Renamed Y to Z", "Removed deprecated W").
- Always keep exact identifier names (function, class, variable, error names)
— never paraphrase them into generic descriptions.
- If a function/method signature changed, state the OLD and NEW signature
explicitly.
- Flag anything that could break other callers (removed/renamed public
symbols, changed signatures, changed thrown error types) with "BREAKING:".
- Do not include unchanged context lines or line numbers.
- Do not include the raw diff in your output.
Diff:
<paste raw diff>
Kỹ thuật này đặc biệt giá trị khi agent cần giữ "trí nhớ" về nhiều commit trong một session dài — bạn có thể lưu semantic change log của từng commit thay vì diff thô, và chỉ full diff của commit gần nhất được giữ nguyên trong context (theo nguyên tắc "chi tiết đầy đủ cho cái mới nhất, tóm tắt cho cái cũ" — giống hierarchical summarization đã học ở bài trước).
Mẹo:
- Luôn thêm nhãn BREAKING: khi signature công khai (public) thay đổi — agent (và con người review) cần tín hiệu rõ để chủ động tìm caller bị ảnh hưởng.
- Đừng tóm tắt diff ở file test — giữ nguyên diff thô cho file test liên quan trực tiếp đến tác vụ hiện tại, vì logic test thường mang thông tin về hành vi mong đợi mà bạn không muốn agent suy luận sai.
- Với diff chỉ đổi format/style (Prettier, ESLint auto-fix), có thể tóm tắt thành một dòng duy nhất "Reformatted N files, no logic change" — không cần liệt kê từng file.
Change Set Context: Tóm tắt Toàn bộ PR hoặc Feature Branch
Khi agent cần hiểu bối cảnh của toàn bộ một pull request hoặc feature branch (thường gồm nhiều commit, chạm vào nhiều file, có thể trải dài vài ngày phát triển), tóm tắt từng file riêng lẻ là không đủ — cần một tài liệu tổng hợp ở tầng cao hơn, mô tả ý định của toàn bộ change set, không chỉ là danh sách các dòng đổi.
Đây là một template Markdown thực tế bạn có thể dùng làm chuẩn cho việc này — có thể tự tay viết khi mở PR, hoặc yêu cầu agent tự sinh ra từ toàn bộ diff + commit message của branch:
## Change Intent
<1-2 sentences: what problem does this PR solve, and why now?
e.g. "Add order size limits and richer stock-error reporting to prevent
the checkout abuse pattern reported in INC-4821.">
## Scope of Changes
- Files touched: <count> (<N> source, <N> test, <N> config)
- Modules affected: <list, e.g. order-service, inventory-client, api-gateway>
- Lines: +<additions> / -<deletions>
## Architectural Changes
<Only fill in if true — otherwise write "None.">
- New components/services introduced: ...
- Removed components: ...
- Changed dependency direction (who calls whom): ...
## Interface Changes
### Breaking
- <e.g. InsufficientStockError constructor signature changed:
(sku) -> (sku, qty, available). All construction sites updated in this PR;
external consumers of this error type (if any) must update.>
### Non-Breaking
- <e.g. Added optional field `notes?: string` to OrderItem — existing
callers unaffected.>
## Key Implementation Decisions
- <Decision + short rationale, e.g. "Chose to validate item count in
createOrder() rather than at the API layer, to keep the invariant
enforced regardless of entry point (REST, gRPC, background job).">
## Behavioral Changes
- <User/system-observable behavior differences, e.g. "Orders with more
than 50 items now return 400 instead of silently truncating.">
## Test Coverage
- New tests added: <count, files>
- Existing tests modified: <count, files, reason>
- Coverage gaps / manual verification needed: <e.g. "No test for concurrent
order creation hitting the new item-count limit — verified manually.">
Template này có 7 phần cố định, mỗi phần trả lời một câu hỏi khác nhau mà agent (hoặc reviewer con người) thường cần: "vì sao làm" (Change Intent), "đụng đến gì" (Scope), "có đổi kiến trúc không" (Architectural Changes), "có phá vỡ hợp đồng với bên ngoài không" (Interface Changes — tách rõ Breaking/Non-Breaking để agent biết mức độ rủi ro cần cẩn trọng), "vì sao chọn cách này" (Key Implementation Decisions — giá trị lớn khi agent cần tiếp tục phát triển theo đúng hướng đã chọn, tránh đi ngược lại quyết định trước), "người dùng thấy gì khác" (Behavioral Changes), và "đã test đến đâu" (Test Coverage).
Trong thực tế, bạn có thể yêu cầu agent tự sinh bản này bằng prompt:
Generate a PR summary using the template below, based on:
- All commit messages in this branch (attached)
- The full diff of this branch against main (attached)
If a section has nothing relevant, write "None." — do not invent content
to fill a section. Be specific: use exact function/class/type names, not
generic descriptions. Mark any signature or public API change under
"Interface Changes" and classify it as Breaking or Non-Breaking.
<paste template>
Sau khi có bản PR summary này, bạn có thể dùng nó thay cho toàn bộ diff trong các session sau (ví dụ khi có người khác tiếp tục PR, hoặc khi agent cần "nhớ lại" một PR đã merge từ lâu để hiểu vì sao code hiện tại trông như vậy) — giảm từ có thể vài nghìn token diff xuống vài trăm token summary, mà vẫn giữ được lý do và tác động chính.
Mẹo:
- Luôn tách rõ Breaking/Non-Breaking trong Interface Changes — đây là phần nhiều team bỏ qua nhất nhưng lại gây hậu quả lớn nhất khi agent hoặc người khác tiếp tục build trên PR mà không biết một signature đã đổi.
- Yêu cầu agent viết "None." thay vì bịa nội dung cho phần không áp dụng — nguyên tắc chống hallucination quan trọng khi dùng template có nhiều mục cố định.
- Lưu file PR summary này ngay trong repo (ví dụ docs/changelog/PR-4821.md) nếu PR đủ lớn/quan trọng — nó trở thành tài liệu tra cứu lâu dài, không chỉ là context tạm cho một session agent.
Tóm tắt Test Output: Từ 10.000 Dòng CI Log đến một Bản Tóm tắt Trạng thái Test
Log của một lần chạy CI/CD trên một test suite lớn dễ dàng đạt vài nghìn đến hàng chục nghìn dòng — đặc biệt với framework verbose (Jest với --verbose, pytest với nhiều plugin, hay log của test song song trên nhiều worker). Đưa nguyên log này vào context vừa tốn token khủng khiếp, vừa chứa cực nhiều thông tin trùng lặp: hàng trăm dòng "PASS" giống nhau, stack trace lặp lại cho cùng một lỗi ở nhiều test case, log debug của framework không liên quan đến kết quả.
Có bốn kỹ thuật nén chính, nên áp dụng theo thứ tự:
1. Tóm tắt số lượng trước, chi tiết sau. Luôn bắt đầu bằng một dòng tổng số: bao nhiêu pass, fail, skip, thời gian chạy. Đây là thông tin agent cần đọc trước tiên để quyết định có cần đi sâu vào chi tiết không.
2. Chỉ giữ chi tiết đầy đủ (stack trace) cho các test fail. Test pass không cần giữ gì ngoài tên — không ai cần biết cách một test pass. Ngược lại, test fail cần giữ nguyên message lỗi và ít nhất phần stack trace liên quan đến code của bạn (có thể cắt bỏ phần stack trace đi vào internals của framework/library nếu quá dài).
3. Deduplication (loại trùng) lỗi giống nhau. Nếu 40 test case khác nhau đều fail với cùng một lý do gốc (ví dụ tất cả gọi một API đã đổi signature), không cần lặp lại stack trace 40 lần — nhóm chúng lại và chỉ hiện một stack trace đại diện kèm danh sách tên các test case bị ảnh hưởng.
4. Phát hiện flaky test (test không ổn định). Nếu bạn có lịch sử chạy CI, so sánh kết quả run hiện tại với các run gần đây — test nào fail không nhất quán (pass lúc này, fail lúc khác, không do thay đổi code) nên được đánh dấu riêng "flaky" thay vì báo động như một regression thật.
Ví dụ minh họa — log CI thô (rút gọn, thực tế có thể dài gấp 50-100 lần):
PASS src/services/__tests__/order-service.test.ts (42 tests, 1.2s)
PASS src/services/__tests__/inventory-client.test.ts (18 tests, 0.6s)
FAIL src/services/__tests__/payment-flow.test.ts
● processPayment > should charge customer and update status
TypeError: Cannot read properties of undefined (reading 'total')
at OrderService.processPayment (src/services/order-service.ts:41:29)
at Object.<anonymous> (src/services/__tests__/payment-flow.test.ts:23:5)
at ... (18 more internal jest/node frames)
● processPayment > should rollback inventory on payment failure
TypeError: Cannot read properties of undefined (reading 'total')
at OrderService.processPayment (src/services/order-service.ts:41:29)
at Object.<anonymous> (src/services/__tests__/payment-flow.test.ts:45:5)
at ... (18 more internal jest/node frames)
● processPayment > should handle already-paid order gracefully
TypeError: Cannot read properties of undefined (reading 'total')
at OrderService.processPayment (src/services/order-service.ts:41:29)
at Object.<anonymous> (src/services/__tests__/payment-flow.test.ts:61:5)
at ... (18 more internal jest/node frames)
PASS src/services/__tests__/cancel-order.test.ts (9 tests, 0.4s)
Test Suites: 1 failed, 3 passed, 4 total
Tests: 3 failed, 66 passed, 69 total
Time: 3.8s
Bản tóm tắt sau nén — dùng cả 4 kỹ thuật trên:
TEST STATUS BRIEFING
Result: 3 failed / 69 total (66 passed) — 3.8s
Suites: order-service.test.ts OK · inventory-client.test.ts OK ·
payment-flow.test.ts FAIL (3/12) · cancel-order.test.ts OK
FAILURE (1 unique cause, 3 test cases affected):
order-service.ts:41 — TypeError: Cannot read properties of undefined
(reading 'total'). order.total accessed before order is fetched/defined.
Affected tests: "should charge customer and update status",
"should rollback inventory on payment failure",
"should handle already-paid order gracefully"
Likely cause: processPayment() reads order.total before the
`await this.db.orders.findById(orderId)` line resolves, or findById
returns undefined for these fixtures.
FLAKY: none detected (compared against last 5 CI runs on this branch).
Từ khoảng 20 dòng log thô lặp lại 3 lần gần như giống nhau (trong thực tế thường là hàng trăm/hàng nghìn dòng khi có nhiều test case cùng fail vì một nguyên nhân), bản tóm tắt rút xuống còn khoảng 10 dòng nhưng agent vẫn có đủ thông tin: vị trí lỗi chính xác (order-service.ts:41), nguyên nhân khả năng cao nhất, và danh sách đầy đủ test case bị ảnh hưởng — đủ để bắt đầu sửa ngay mà không cần đọc lại log thô.
Mẹo:
- Luôn nhóm lỗi theo "unique cause" trước khi đưa cho agent — agent dễ bị dẫn sai hướng nếu thấy "3 test fail" và nghĩ có 3 vấn đề độc lập cần fix riêng.
- Với pipeline CI có chạy song song nhiều worker, gộp log theo suite trước khi tóm tắt — log interleaved (đan xen) giữa các worker gần như vô nghĩa với agent nếu không được sắp xếp lại theo suite/test case.
- Giữ lại số dòng/số file chính xác nơi lỗi xảy ra (order-service.ts:41) trong mọi bản tóm tắt — đây là thông tin rẻ về token nhưng đắt giá nhất để agent định vị và fix nhanh.
Token Budgeting cho Code Context: Một Mô hình Phân bổ Thực tế
Sau khi có các kỹ thuật nén ở trên, câu hỏi thực dụng tiếp theo là: trong một context window có hạn (ví dụ 200K token cho một model hiện đại, nhưng thực tế session coding thường chỉ nên dùng một phần vì cần chỗ dự trữ cho phản hồi và các lượt tiếp theo), nên phân bổ bao nhiêu token cho mỗi loại thông tin?
Dưới đây là một mô hình phân bổ thực tế cho một session coding agent trung bình, giả định ngân sách khả dụng khoảng 60.000 token cho context "làm việc" (working context, chưa tính system prompt và phần dự trữ cho output):
| Loại context | % ngân sách | Token (~60K) | Lý do |
|---|---|---|---|
| Code skeleton của các file liên quan | 30% | ~18.000 | Cần diện rộng — agent phải thấy toàn bộ "bản đồ" module liên quan để định vị đúng nơi cần sửa, nhưng không cần chi tiết implementation của mọi file. |
| Full source của file đang sửa trực tiếp | 25% | ~15.000 | Nơi agent thực sự viết/sửa code cần độ chính xác tuyệt đối — không nén. |
| Diff/semantic change log của các thay đổi gần đây | 15% | ~9.000 | Đủ để agent hiểu "đã đi tới đâu" trong session, ưu tiên semantic change log cho commit cũ, diff thô cho commit vừa tạo. |
| Test output đã tóm tắt (test status briefing) | 15% | ~9.000 | Đủ chi tiết về lỗi hiện tại để fix, không cần log CI thô. |
| Lịch sử hội thoại đã tóm tắt (conversation history) | 10% | ~6.000 | Theo kỹ thuật tóm tắt hội thoại/incremental summarization đã học — chỉ giữ quyết định và ràng buộc quan trọng, không giữ nguyên văn toàn bộ lượt trao đổi. |
| Dự trữ linh hoạt (buffer) | 5% | ~3.000 | Cho các trường hợp cần tra cứu thêm 1 file phụ, hoặc paste thêm một đoạn log khi agent tự yêu cầu. |
Vài nguyên tắc khi áp dụng bảng này vào thực tế:
- Tỷ lệ này không cố định — nó dịch chuyển theo loại tác vụ. Khi debug một lỗi test cụ thể, tăng phần Test Output lên 25-30% và giảm Code Skeleton xuống 15-20%, vì bạn cần đào sâu vào một điểm lỗi hơn là quét rộng codebase. Khi làm một refactor lớn ảnh hưởng nhiều file, tăng Code Skeleton lên 40%+ và giảm phần Test Output nếu chưa chạy test.
- "Full source của file đang sửa" chỉ nên có 1-3 file cùng lúc. Nếu bạn thấy cần full source của hơn 3-4 file đồng thời, đó là dấu hiệu tác vụ nên được chia nhỏ, hoặc bạn đang thiếu một bước skeleton hóa ở đâu đó.
- Buffer 5% không phải là lãng phí — nó là bảo hiểm chống context window bị "vỡ" đột ngột khi agent cần tra cứu gì đó ngoài dự tính giữa session, tránh việc phải cắt bớt phần quan trọng đã có sẵn để nhường chỗ.
- Đo thực tế bằng token counter, không đoán. Hầu hết SDK (bao gồm Anthropic SDK) có API đếm token trước khi gửi request — dùng nó để kiểm tra bảng phân bổ này có đang đúng thực tế hay không, đặc biệt khi codebase của bạn có style code khác biệt (ví dụ code Java thường verbose hơn code Python cho cùng logic, ảnh hưởng đến tỷ lệ % hợp lý).
Mẹo:
- Coi bảng phân bổ này là điểm khởi đầu, không phải luật cứng — theo dõi qua vài session xem loại context nào bạn hay phải cắt bớt/mở rộng nhất, rồi điều chỉnh lại tỷ lệ cho phù hợp với codebase và loại tác vụ của team bạn.
- Khi ngân sách token bị siết chặt hơn nữa (ví dụ dùng model có context window nhỏ hơn, hoặc cần dự trữ nhiều cho output dài), hy sinh phần Conversation History trước — đây là phần dễ tóm tắt lại từ đầu nhất nếu mất, so với việc mất chi tiết code hoặc test.
- Ghi log số token thực tế đã dùng cho mỗi loại context sau vài session — dữ liệu định lượng này giá trị hơn cảm tính khi thuyết phục team đầu tư vào pipeline compression tự động.
Tổng kết
Code, diff, và test output cần một triết lý nén khác với hội thoại: giữ tuyệt đối chính xác phần cấu trúc/giao diện, cắt bỏ mạnh tay phần triển khai/log lặp lại không liên quan đến tác vụ hiện tại. Code skeleton lược bỏ body hàm nhưng giữ signature, type, docstring — thường giảm 70-90% token mà không mất thông tin cần cho việc gọi hàm đúng. Semantic change log biến git diff thô thành danh sách hành động có ý nghĩa, luôn giữ nguyên tên định danh và đánh dấu rõ thay đổi breaking. Một template PR summary có cấu trúc (Change Intent, Scope, Architectural Changes, Interface Changes, Key Implementation Decisions, Behavioral Changes, Test Coverage) cho phép tóm tắt cả một feature branch lớn thành vài trăm token vẫn đủ để hiểu và tiếp tục làm việc. Test output khổng lồ từ CI nên được nén qua bốn bước: tổng số trước, chi tiết stack trace chỉ cho fail, deduplication theo nguyên nhân gốc, và phát hiện flaky test. Cuối cùng, một mô hình token budget rõ ràng — chia theo skeleton, full source, diff, test output, và lịch sử hội thoại — giúp bạn chủ động quản lý context window thay vì để nó tự tràn và bị cắt ngẫu nhiên ở những chỗ quan trọng.