Quản lý context và summarization
Tổng quan
Giữ các session dài nằm trong cửa sổ context của model mà không phá hủy transcript. Thêm một interface ước lượng token (heuristic bây giờ, tiktoken sau), cắt bớt tool argument trước khi summarization, summarization không làm thay đổi dữ liệu gốc (non-mutating) kích hoạt ở mức 0.85 của cửa sổ và giữ lại 0.10 cuối, offload lịch sử sang /conversation_history/{session_id}.md, thử lại khi bị overflow, và các cache breakpoint của prompt Anthropic. Không có gì ở đây sửa log message đã lưu: việc nén (compaction) chỉ được áp dụng bên trong WrapModelCall, nên lịch sử thô vẫn replay được.
Yêu cầu
Chức năng
- Ngân sách được tính trên **toàn bộ request**: `Estimator.EstimateRequest(req)` đếm các system block (gồm các fragment memory và skill được đóng góp ở phase 11), mọi block của message, và các tool definition đã serialize. Chỉ ước lượng `messages` sẽ đếm thiếu khoản chi phí cố định lớn nhất của request, và đó là lý do một "trigger 0.85" vẫn có thể overflow.
- Capability tùy chọn `TokenCounter`: một provider có thể cài `CountTokens(ctx, req) (int, error)`; adapter Anthropic cài nó trên `Messages.CountTokens`. Khi có, nó được dùng để quyết định trigger (heuristic là phương án dự phòng khi lỗi hoặc timeout), để ngưỡng dựa trên số liệu thật thay vì `bytes/4`.
- Một bảng model profile trong config cung cấp
max_input_tokenscho từng model với một giá trị mặc định có tài liệu. - Cắt bớt tool argument chạy trước: các argument cũ của
write_file/edit_file/tool lớn trong những message cũ hơn cửa sổ giữ lại bị cắt thành một đoạn xem trước, điều này thường tránh được hẳn việc summarization. - Summarization kích hoạt khi số input token ước lượng vượt
TriggerFrac(0.85) của cửa sổ; nó giữ nguyên vănKeepFrac(0.10) message cuối, tóm tắt phần còn lại bằng một lời gọi summarizer riêng, và đặt bản tóm tắt lên đầu như một message user duy nhất. - **Quy tắc ranh giới:** ranh giới giữ lại đã tính được bị dịch *lùi về* message assistant gần nhất mà không còn `tool_use` nào chưa có `tool_result` tương ứng trong phần được giữ. Một ranh giới rơi vào giữa một tool batch sẽ gửi đi một `tool_result` mà `tool_use` của nó đã bị loại (hoặc ngược lại), và provider sẽ trả 400 vĩnh viễn cho session đó.
- Một validator sau compaction chạy trên phần đã nén trước khi gửi: mọi `tool_result` phải có `tool_use` đứng trước nó trong phần đó và mọi `tool_use` phải có `tool_result`. Validation thất bại sẽ hủy việc compaction, ghi log mức error, và quay về cắt bớt cơ học (mechanical truncation) — không bao giờ gửi một request không hợp lệ.
- Các message bị loại được nối vào
/conversation_history/{session_id}.mdqua filesystem backend, và bản tóm tắt nhúng path đó kèm chỉ dẫn dùngread_file. - Prompt summarization phải có một điều khoản bảo toàn định danh (id, path, tên file, kết quả tool call, câu hỏi còn mở, kế hoạch hiện tại).
provider.ErrContextOverflow(hoặc stop reasonmodel_context_window_exceeded) kích hoạt summarization và thử lại đúng một lần cùng lượt (turn) đó. Một lần overflow thứ hai không làm run thất bại: nó hạ cấp xuống cắt bớt cơ học (bỏ nguyên các turn cũ nhất, giữ nguyên tính toàn vẹn của tool batch, cho đến khi request vừa) rồi thử lại thêm một lần. Làm run thất bại là phương án cuối cùng, chỉ khi việc cắt bớt không thể làm vừa nổi cả cửa sổ giữ lại.- Các cache breakpoint của prompt do `PromptCacheMiddleware` đặt **trong lúc ghép system block tại thời điểm chain**: nó ghi lại chỉ số của system block ổn định cuối cùng và đặt `CacheControl` ở đó, và đặt breakpoint thứ hai lên message tóm tắt mở đầu lịch sử đã nén. Vì memory (phase 11) đóng góp block muộn hơn trong chain, các block của nó rơi vào chỉ số cao hơn breakpoint do cách thiết kế, không phải do quy ước. Middleware bỏ qua hoàn toàn việc đặt breakpoint khi provider đã resolve báo `Capabilities.PromptCaching == false`.
- Một tool
compact_conversationcho phép model tự nén theo yêu cầu.
Phi chức năng
- Compaction được ghi trong
State.Privatevà chỉ áp dụng lênModelRequest.Messagesgửi đi;State.Messagesvà bảngmessageskhông bao giờ bị ghi lại. - Các lời gọi summarizer dùng cùng provider nhưng một model cấu hình được (rẻ hơn), và được tính riêng trong usage.
- Summarization là idempotent theo từng turn: một
WrapModelCallthứ hai trong cùng turn dùng lại event compaction đã có. - Test có tính xác định: estimator tiêm vào được, và fake provider kịch bản hóa cả phản hồi của summarizer lẫn lỗi overflow.
Kiến trúc
// internal/agent/tokens.go (extended)
type Estimator interface {
Estimate(text string) int
EstimateRequest(req *provider.Request) int // system blocks + messages + tool defs. Red team: F9
}
// Optional provider capability; used for the trigger decision when available. Red team: F9
type TokenCounter interface {
CountTokens(ctx context.Context, req provider.Request) (int, error)
}
type ModelProfile struct {
MaxInputTokens int
TriggerFrac float64 // 0.85
KeepFrac float64 // 0.10
SummarizerModel string
}
// internal/agent/mwsummarize.go
type SummarizationMiddleware struct {
Est Estimator
Profile ModelProfile
Backend fs.Backend
Provider provider.Provider
} // ModelWrapper + ToolProvider (compact_conversation)
type compaction struct {
AtTurn int
Summary string
KeptFrom int // index into State.Messages
OffloadPath string
Tokens struct{ Before, After int }
}
// internal/agent/mwtruncate.go
type ToolArgTruncationMiddleware struct {
Est Estimator
MaxArgTokens int
KeepRecent int // messages exempt from truncation
} // ModelWrapper, runs inside Summarization so summarization sees truncated args
// internal/agent/mwcache.go
type PromptCacheMiddleware struct{} // ModelWrapper: sets CacheControl breakpointsKhông có bảng mới; sessions có thêm các bộ đếm.
-- migrations/00008_session_context_counters.sql
alter table sessions add column last_input_tokens int not null default 0;
alter table sessions add column compactions int not null default 0;
-- Red team: F9 — no `summary` column: nothing reads it. The authoritative summary is the
-- compaction record in runs.state_json plus /conversation_history/{session_id}.md.Instrumentation
- Span event
gen_ai.conversation.compactedtrên span turn vớihango.tokens.before,hango.tokens.after,hango.messages.evicted,hango.compaction.reason(threshold|overflow|tool),hango.fs.pathcủa file offload lịch sử; attributegen_ai.conversation.compacted=truecũng được đặt trên spanchatkế tiếp để một turn đã nén lọc được. - Lời gọi provider của chính summarizer là một span CLIENT
chat {summarizer model}riêng vớihango.call.purpose=summarize, nên chi phí summarizer không bao giờ bị giấu trong generation chính. - Span event
provider.context_overflow_retrykhi một lỗi overflow kích hoạt compaction và thử lại. - Metric: histogram
hango.context.input_tokens(attributegen_ai.request.model), counterhango.context.compactions(attributereason), histogramhango.context.evicted_messages, counterhango.context.tool_args_truncated,gen_ai.client.token.usagecó thêmhango.call.purposeđể tách được token của summarizer, histogramhango.cache.read_ratiosuy ra từ token cache-read so với token input. - Trường log: một dòng info cho mỗi lần compaction với session id, số token trước/sau, số message bị loại và path offload.
Các file liên quan
Tạo mới
internal/agent/mwsummarize.go,internal/agent/mwsummarize_test.go.internal/agent/mwtruncate.go,internal/agent/mwtruncate_test.go.internal/agent/mwcache.go,internal/agent/mwcache_test.go.internal/agent/summarize_prompt.go— prompt summarization kèm điều khoản bảo toàn.internal/agent/profile.go— bảng model profile nạp từ config với một giá trị dự phòng có tài liệu (trigger 170k / giữ 6 message khi một model chưa biết).migrations/00008_session_context_counters.sql.docs/learning/10-context-management.md.
Sửa đổi
internal/agent/tokens.go— thêmEstimateRequest, capabilityTokenCountertùy chọn, và tài liệu cho interfaceEstimator.internal/agent/mwstack.go— điền slot Summarization (sau Filesystem/offload, sau Subagent) với việc cắt bớt lồng bên trong nó, và slot PromptCache trước slot Memory.internal/agent/loop.go— khi gặpErrContextOverflow, đánh dấu lý do compaction và thử lại turn một lần.internal/store/pg/session.go— cập nhậtlast_input_tokensvàcompactionstại ranh giới turn.internal/config/config.go—HANGO_MODEL_PROFILES(JSON),HANGO_SUMMARIZER_MODEL,HANGO_CONTEXT_TRIGGER_FRAC,HANGO_CONTEXT_KEEP_FRAC,HANGO_TOOL_ARG_MAX_TOKENS.docs/system-architecture.md,docs/learning/README.md.
Các bước triển khai
- Mở rộng
internal/agent/tokens.govớiEstimateRequest(system block + mọi message block + tool definition đã serialize, cộng các overhead cố định cho mỗi message và mỗi tool block) và thêm capabilityTokenCountertùy chọn cùng bản cài đặtMessages.CountTokenscủa Anthropic, với timeout có giới hạn và heuristic dự phòng. - Viết
internal/agent/profile.go: parseHANGO_MODEL_PROFILESdạng{"model": {"max_input_tokens": n, ...}}, dự phòng 170000 cho trigger và giữ 6 message với các model chưa biết, và chỉ log một lần cho mỗi model chưa biết. - Viết
internal/agent/mwtruncate.go: mộtModelWrappersao chép slice message của request và cắt JSON argument của tool-use cùng text của tool-result trong những message cũ hơnKeepRecentthành một đoạn xem trước kèm ghi chú kích thước. Không bao giờ đụng vàoState.Messages. - Viết
internal/agent/summarize_prompt.go: chỉ thị summarizer giữ nguyên văn các định danh, path file, kết quả tool, quyết định, kế hoạch hiện tại và câu hỏi còn mở, và viết bản tóm tắt như một bản briefing cho một agent mới. - Viết
internal/agent/mwsummarize.go:- Trong
WrapModelCall, ước lượng toàn bộ request gửi đi (EstimateRequest, hoặcCountTokenskhi provider hỗ trợ). Nếu dưới trigger thì gọinext. - Nếu không, tính ranh giới giữ lại, dịch lùi nó về một message assistant không có
tool_usechưa khớp, nối các message bị loại dưới dạng markdown vào/conversation_history/{session_id}.mdqua backend, gọi model summarizer với phần bị loại, dựng[]provider.Message{summaryUserMessage} + kept, chạy validator tool_use/tool_result, lưucompactionvào private state có kiểu, đặt message của request thành phần đã nén, rồi gọinext. - Ghi span event compaction và metric; các bộ đếm session do loop ghi tại ranh giới turn, không phải ở đây.
- Phơi ra
compact_conversationnhư mộtToolProviderép nén ở lần gọi model kế tiếp với lý dotool.
- Trong
- Xử lý overflow: trong
loop.go, khi một lời gọi model trảErrContextOverflowhoặc stop reason làContextExceeded, đặt cờ ép compaction với lý dooverflowvà thử lại turn một lần. Ở lần overflow thứ hai, hạ cấp xuống cắt bớt cơ học (bỏ nguyên các turn cũ nhất, không bao giờ tách một tool batch) với lý dotruncatevà thử lại thêm một lần; chỉ lần thất bại thứ ba mới làm run thất bại. - Viết
internal/agent/mwcache.go: trong lúc ghép system block tại thời điểm chain, ghi lại chỉ số của system block ổn định cuối cùng và gắnCacheControlở đó, cộng một breakpoint trên message tóm tắt mở đầu lịch sử đã nén. Bỏ qua hoàn toàn khiCapabilities.PromptCachinglà false. Chú thích nói rõ rằng bất cứ thứ gì đóng góp prompt block muộn hơn trong chain (memory ở phase 11) đều rơi sau chỉ số breakpoint do cách thiết kế. - Chèn truncation, summarization và prompt cache vào
DefaultStackở các slot đã ghi trong tài liệu; cập nhật test cố định thứ tự. - Test: tính đơn điệu của estimator và việc
EstimateRequesttăng khi tool definition hoặc system block tăng; truncation chỉ cắt các message cũ và giữ nguyên các message gần đây; summarization kích hoạt ở ngưỡng, tạo ra một request đã nén, để nguyênState.Messagesvà DB (khẳng định bằng cách nạp lại lịch sử), và ghi file offload; bản tóm tắt chứa path offload; một ranh giới giữ lại tính được nằm giữa một tool batch được dịch lùi để phần đã nén không cótool_usehaytool_resultmồ côi, khẳng định bằng validator trên một fixture có ranh giới 0.10 rơi giữa mộttool_usevàtool_resultcủa nó ; validator thất bại thì quay về truncation thay vì gửi đi; overflow → compaction → overflow lần hai → cắt bớt cơ học → thành công (fake provider);compact_conversationép compaction; test vị trí cache-control khẳng định chỉ số breakpoint trong các system block đã ghép và trên message tóm tắt, cộng một khẳng định no-op khiPromptCachinglà false. - Cập nhật docs và viết learning doc.
Dàn ý tài liệu học
docs/learning/10-context-management.md phải có:
- Khái niệm: cửa sổ context là một ngân sách, và ba cơ chế độc lập (offload theo từng kết quả, cắt bớt argument, summarization) cùng lý do không cơ chế nào đứng một mình là đủ.
- Các lựa chọn thiết kế: compaction không làm thay đổi dữ liệu gốc bên trong
WrapModelCall, offload lịch sử cộng một path trong bản tóm tắt để agent khôi phục được chi tiết, điều khoản bảo toàn định danh, model summarizer riêng, thử lại khi overflow, cache breakpoint đặt trước slot memory. - Đánh đổi: mặc định 0.85/0.10; summarization so với sliding window so với retrieval; ước lượng heuristic so với tiktoken (và vì sao interface có ngay từ bây giờ); một lần thử lại so với nén lặp lại.
- Cơ chế prompt caching: điều gì làm vô hiệu một prefix, vì sao thứ tự trong middleware stack là yếu tố then chốt, và cách đọc token cache-read so với cache-write.
- So sánh với goclaw:
loop_compact.gotóm tắt khoảng 70% đầu và giữ khoảng 30%/KeepLastMessagesvới một prompt “MUST PRESERVE” cứng, ghi đè lên blob session đã lưu; interfaceTokenCountervới một bản cài đặt tiktoken cộng một phương án dự phòng thuần Go. - So sánh với deepagents:
create_summarization_middlewarevới trigger 0.85 / giữ 0.10 điều khiển bằng profile, cắt tool-arg trước summarization, phần bị loại được nối vào/conversation_history/{session_id}.md, thử lại khiContextOverflowError, compaction được ghi trong một_summarization_eventprivate và áp dụng bên trongwrap_model_callđểstate["messages"]vẫn replay được, cùng toolcompact_conversationmà model gọi được. - Vì sao hango sao chép deepagents ở đây gần như y nguyên (không làm thay đổi dữ liệu gốc là lựa chọn đúng cho một server phải replay được các run) và đi khác với kiểu compaction ghi đè của goclaw.
- Cách debug tầng này: lọc Tempo các span có
gen_ai.conversation.compacted=trueđể xem turn nào đã nén cùng số token trước/sau;hango.context.input_tokensvàhango.context.compactionstheo reason cho thấy một session có đang thrashing không; spanchatcủa chính summarizer (vớihango.call.purpose=summarize) phơi chi phí summarizer;hango.cache.read_ratiosụp về không nghĩa là một cache breakpoint bị một middleware phía sau làm vô hiệu;/conversation_history/{session_id}.mdlà ground truth về những gì đã bị loại. - Tính toàn vẹn của tool batch như một bất biến cứng của mọi phẫu thuật context, và vì sao một validator rẻ hơn việc debug một session bị 400 vĩnh viễn.
- Bài tập: đẩy một session vượt trigger và đọc event compaction; dựng một lịch sử có ranh giới giữ lại rơi giữa tool batch và xem việc dịch lùi; thêm một middleware sau cache breakpoint và xem tỉ lệ cache-read tụt xuống.
- Ngân sách thời gian: 11h cài đặt + 2h learning doc.
Tiêu chí thành công
-
go build ./... && go vet ./... && go test ./...đều qua. - Test summarization: một session vượt trigger tạo ra một request gửi đi đã nén trong khi
MessageStore.Historyvẫn trả về toàn bộ transcript nguyên vẹn (integration test). - Transcript bị loại tồn tại tại
/conversation_history/{session_id}.mdvà path của nó xuất hiện trong text bản tóm tắt (test). - Chỉ riêng việc cắt tool-arg đã giữ một session ở ngưỡng biên dưới trigger mà không cần summarization (test).
- Overflow kích hoạt compaction và một lần thử lại; overflow lần hai hạ cấp xuống cắt bớt cơ học và thành công; chỉ lần thất bại thứ ba mới làm run thất bại (test).
- Một compaction có ranh giới giữ lại thô rơi giữa một tool batch sẽ được dịch lùi, và validator sau compaction xác nhận mọi
tool_resultđều cótool_usecủa nó (test trên fixture đối kháng). - Các cache-control breakpoint rơi đúng chỉ số mong đợi trong các system block đã ghép và trên message tóm tắt; với
Capabilities.PromptCaching == falsekhông có breakpoint nào được đặt. -
EstimateRequesttính cả system block và tool definition (test cho thấy ước lượng tăng khi chỉ danh sách tool tăng); với provider cóTokenCounter, trigger dùng giá trị đã đếm và quay về heuristic khi việc đếm lỗi. - Các bộ đếm session được ghi đúng một lần mỗi turn (test đếm số câu
UPDATE sessionsqua một run hai turn có một lời gọi model bị thử lại). -
compact_conversationép compaction ở lần gọi kế tiếp (test). - Trong Grafana, một turn đã nén hiện span event
gen_ai.conversation.compactedvới số token trước/sau, vàhango.context.compactionstăng với đúngreason; lời gọi summarizer xuất hiện như một spanchatriêng.
Đánh giá rủi ro
| Rủi ro | Khả năng x Tác động | Dấu hiệu | Phản ứng định sẵn |
|---|---|---|---|
| Bản tóm tắt làm mất một định danh mà agent cần | Cao x Cao | Agent hỏi lại những điều đã biết, hoặc không tìm thấy file mình đã ghi | Điều khoản bảo toàn trong prompt cộng path offload lịch sử trong bản tóm tắt để khôi phục chi tiết bằng read_file; file offload là lưới an toàn |
| Compaction làm thay đổi lịch sử đã lưu | Trung bình x Nghiêm trọng | Các run replay bị lệch | Compaction nằm trong State.Private và chỉ ghi lại request gửi đi; integration test nạp lại lịch sử là cổng chặn |
| Sai số ước lượng làm nén muộn và overflow cứng | Cao x Trung bình | ErrContextOverflow trên production | EstimateRequest bao phủ toàn bộ request, TokenCounter tùy chọn dùng số đếm thật, và đường overflow hạ cấp xuống cắt bớt cơ học trước khi làm run thất bại |
Compaction tách một cặp tool_use/tool_result và làm session bị 400 vĩnh viễn | Trung bình x Nghiêm trọng | Provider trả 400 ở mọi turn sau đó của session ấy | Ranh giới được dịch lùi về một message assistant sạch, validator sau compaction, và một fixture đối kháng có ranh giới rơi giữa một tool batch |
| Chính lời gọi summarizer bị overflow | Trung bình x Trung bình | Summarizer trả overflow | Phần bị loại được chia nhỏ theo cửa sổ của chính summarizer; thất bại thì quay về một bản tóm tắt cơ học bị cắt, liệt kê role của message và tên tool |
| Cache breakpoint bị một middleware phía sau làm vô hiệu | Trung bình x Trung bình | hango.cache.read_ratio gần bằng không | Thứ tự stack ghim PromptCache trước Memory; test thứ tự cộng metric tỉ lệ cache phát hiện hồi quy |
| Compaction thrashing ở mọi turn | Trung bình x Trung bình | hango.context.compactions tăng theo từng turn | Phần giữ lại đảm bảo tiến triển; một guard từ chối nén hai lần trong một turn và log ở mức warn |
Rollback
goose down một bước sẽ bỏ các bộ đếm session. Gỡ các slot Summarization, truncation và PromptCache khỏi DefaultStack khôi phục hành vi của phase 9; vì compaction không bao giờ làm thay đổi lịch sử đã lưu nên không cần sửa dữ liệu.