Chuyển đến nội dung
Quản lý context và summarization

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_tokens cho 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ăn KeepFrac (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}.md qua filesystem backend, và bản tóm tắt nhúng path đó kèm chỉ dẫn dùng read_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 reason model_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_conversation cho phép model tự nén theo yêu cầu.

Phi chức năng

  • Compaction được ghi trong State.Private và chỉ áp dụng lên ModelRequest.Messages gửi đi; State.Messages và bảng messages khô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 WrapModelCall thứ 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 breakpoints

Khô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.
Cả hai bộ đếm được ghi **đúng một lần mỗi turn, tại ranh giới turn**, trong cùng một transaction ngắn lưu message của turn — không bao giờ ghi bên trong `WrapModelCall`, vì như vậy sẽ đặt một lần ghi database lên đường gọi model và đếm trùng các turn bị thử lại.

Instrumentation

  • Span event gen_ai.conversation.compacted trên span turn với hango.tokens.before, hango.tokens.after, hango.messages.evicted, hango.compaction.reason (threshold|overflow|tool), hango.fs.path của file offload lịch sử; attribute gen_ai.conversation.compacted=true cũng được đặt trên span chat kế 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ới hango.call.purpose=summarize, nên chi phí summarizer không bao giờ bị giấu trong generation chính.
  • Span event provider.context_overflow_retry khi một lỗi overflow kích hoạt compaction và thử lại.
  • Metric: histogram hango.context.input_tokens (attribute gen_ai.request.model), counter hango.context.compactions (attribute reason), histogram hango.context.evicted_messages, counter hango.context.tool_args_truncated, gen_ai.client.token.usage có thêm hango.call.purpose để tách được token của summarizer, histogram hango.cache.read_ratio suy 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êm EstimateRequest, capability TokenCounter tùy chọn, và tài liệu cho interface Estimator.
  • 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ặp ErrContextOverflow, đánh dấu lý do compaction và thử lại turn một lần.
  • internal/store/pg/session.go — cập nhật last_input_tokens và compactions tạ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

  1. Mở rộng internal/agent/tokens.go với EstimateRequest (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 capability TokenCounter tùy chọn cùng bản cài đặt Messages.CountTokens của Anthropic, với timeout có giới hạn và heuristic dự phòng.
  2. Viết internal/agent/profile.go: parse HANGO_MODEL_PROFILES dạ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.
  3. Viết internal/agent/mwtruncate.go: một ModelWrapper sao 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ơn KeepRecent thành một đoạn xem trước kèm ghi chú kích thước. Không bao giờ đụng vào State.Messages.
  4. 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.
  5. Viết internal/agent/mwsummarize.go:
    • Trong WrapModelCall, ước lượng toàn bộ request gửi đi (EstimateRequest, hoặc CountTokens khi provider hỗ trợ). Nếu dưới trigger thì gọi next.
    • 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_use chưa khớp, nối các message bị loại dưới dạng markdown vào /conversation_history/{session_id}.md qua 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ưu compaction vào private state có kiểu, đặt message của request thành phần đã nén, rồi gọi next.
    • 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_conversation như một ToolProvider ép nén ở lần gọi model kế tiếp với lý do tool.
  6. Xử lý overflow: trong loop.go, khi một lời gọi model trả ErrContextOverflow hoặc stop reason là ContextExceeded, đặt cờ ép compaction với lý do overflow và 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ý do truncate và 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.
  7. 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ắn CacheControl ở đó, 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 khi Capabilities.PromptCaching là 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ế.
  8. 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ự.
  9. Test: tính đơn điệu của estimator và việc EstimateRequest tă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ên State.Messages và 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_use hay tool_result mồ côi, khẳng định bằng validator trên một fixture có ranh giới 0.10 rơi giữa một tool_use và tool_result củ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 khi PromptCaching là false.
  10. 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.go tóm tắt khoảng 70% đầu và giữ khoảng 30%/KeepLastMessages với một prompt “MUST PRESERVE” cứng, ghi đè lên blob session đã lưu; interface TokenCounter vớ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_middleware vớ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 khi ContextOverflowError, compaction được ghi trong một _summarization_event private và áp dụng bên trong wrap_model_call để state["messages"] vẫn replay được, cùng tool compact_conversation mà 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_tokens và hango.context.compactions theo reason cho thấy một session có đang thrashing không; span chat của chính summarizer (với hango.call.purpose=summarize) phơi chi phí summarizer; hango.cache.read_ratio sụ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}.md là 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.History vẫ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}.md và 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_use củ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 == false không có breakpoint nào được đặt.
  • EstimateRequest tí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 sessions qua 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.compacted với số token trước/sau, và hango.context.compactions tăng với đúng reason; lời gọi summarizer xuất hiện như một span chat riêng.

Đánh giá rủi ro

Rủi roKhả năng x Tác độngDấu hiệuPhản ứng định sẵn
Bản tóm tắt làm mất một định danh mà agent cầnCao x CaoAgent 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ưuTrung bình x Nghiêm trọngCác run replay bị lệchCompaction 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ứngCao x Trung bìnhErrContextOverflow trên productionEstimateRequest 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ễnTrung bình x Nghiêm trọngProvider trả 400 ở mọi turn sau đó của session ấyRanh 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ị overflowTrung bình x Trung bìnhSummarizer trả overflowPhầ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ệuTrung bình x Trung bìnhhango.cache.read_ratio gần bằng khôngThứ 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 turnTrung bình x Trung bìnhhango.context.compactions tăng theo từng turnPhầ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.