Harness Academy
Tài liệu gốc 16 9 phútTiếng Việt + English
docs/adr/0006-conversation-compaction.mdXem bản gốc
Ngôn ngữ bài đọc
Bản dịch tiếng Việt

Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.

0006. Nén hội thoại - nén bộ nhớ trong ngữ cảnh + khi thoát hai tầng tương đối với cửa sổ, trên JSONL

Trạng thái: Được chấp nhận — §3 (nguồn token), §3a/§5 (điểm cắt), §7 (/phản hồi nhỏ gọn) và hậu quả về kích thước đuôi được sửa đổi bởi ADR-0018 Ngày: 2026-06-26

Bối cảnh

ADR-0002 §8/§9 đã xây dựng lớp ngữ cảnh M1 — lớp JSONL chỉ nối thêm nhật ký phiên (context/session_log.py) cấp nguồn decode --resume, và ghi lại bộ nhớ khi thoát một câu (memory/extract.py::summarize_session) - và **cố ý nén hoãn lại **, đặt tên summarize_session như "đường may nén chặt M4 phát triển từ." Một phiên dài message_history phát triển theo hướng cửa sổ ngữ cảnh của mô hình (rủi ro về chi phí/độ trễ/cắt ngắn); riêng biệt, duy trì mô hình MEMORY.md được giữ dưới giới hạn của nó bằng cách bỏ các dòng cũ nhất (mất các dữ kiện cũ).

ADR này ghi lại quá trình nén dưới dạng ba cơ chế hợp tác cộng với khả năng chi trả cho giao diện người dùng, dưới dạng một tính năng (nhiệm vụ 041-049, tính năng context-compaction):

  1. Microcompaction — tầng trong bộ nhớ, giá rẻ, không có LLM giúp loại bỏ các phần thân đầu ra công cụ cũ.
  2. Nén nén hoàn toàn — Cấp LLM tóm tắt lịch sử cũ hơn; kiên trì vào nhật ký JSONL.
  3. Nén bộ nhớ — máy nén LLM khi thoát cho MEMORY.md, thay thế lossy thả cũ nhất.
  4. Context Gauge — vòng tròn điền vào chân trang đọc cùng một cài đặt cửa sổ.

điểm tiếp xúc pydantic-ai đã được xác minh dựa trên pydantic-ai 2.0.0 đã cài đặt** (§3), vì vậy chúng không lại bị kiện tụng ở hạ lưu.

Quyết định

  1. Lưu trữ để nén hoàn toàn: a đã đánh máy compaction dòng trên nhật ký JSONL chỉ nối thêm hiện có - KHÔNG SQLite. Sự khác biệt được cố ý, ghi lại từ cách diễn đạt "nhật ký hội thoại (SQLite)" của AGENTS.md và hàng "Datastore: SQLite" của Tech Stack. Đối với một agent một tiến trình, một agent, một agent chỉ bổ sung Nhật ký JSONL với dòng điểm kiểm tra độc lập là đủ và sử dụng lại đường nối ADR-0002 §9. SQLite ở lại tùy chọn hoãn lại; nhiệm vụ 048 sửa AGENTS.md context/ diễn đạt để (JSONL). A compaction dòng mang thông điệp tóm tắt được tuần tự hóa và phần đuôi được giữ lại; khi phát lại load() loại bỏ lịch sử tích lũy tại dòng đó và khởi động lại từ [summary, *tail], sau đó tiếp tục - trước đó các bản sao nguyên văn của phần đuôi được thay thế, không bao giờ được tính hai lần.

  2. Hai trình kích hoạt trong ngữ cảnh, CẢ HAI đều tự động, cộng với một trình kích hoạt thủ công — tầng đầu tiên rẻ nhất. Sau một lượt vẫn tồn tại ở WOULD_STOP, và chỉ khi compaction_enabled: if input_tokens >= full_level → đầy đủ; elif input_tokens >= micro_level → microcompaction; khác không có gì. Manual /compact (chỉ ở chế độ nhàn rỗi) buộc phải nén đầy đủ, bỏ qua các ngưỡng và compaction_enabled. Microcompaction giảm token cho ** lượt tiếp theo **.

  3. Đo lường: dự trữ tương đối theo cửa sổ (thay thế lựa chọn ngưỡng phẳng trước đó, theo ý muốn của người dùng yêu cầu). Một cấp độ sẽ kích hoạt khi input_tokens >= context_window * (1 - reserve), với ** có thể định cấu hình cửa sổ** (compaction_context_window_tokens, mặc định = Cửa sổ nhập liệu của Gemini 2.5 Flash 1_048_576 — "đặt cái này vào cửa sổ nhập của mô hình đang hoạt động của bạn") và phân số dự trữ trên mỗi tầng: compaction_reserve_fraction = 0.20 (bắn hoàn toàn ở mức đầy 80%) và microcompaction_reserve_frag = 0,40 (micro fires earlier, at 60% full). INVARIANT: microcompaction_reserve_section > compaction_reserve_section (dự trữ vi mô nhiều hơn → kích hoạt trước), được xác nhận theo mặc định. Cái này cố tình thay thế quan điểm ngưỡng phẳng/"không có cửa sổ cho mỗi mô hình" trước đó của ADR — nhưng cửa sổ là ** một số có thể định cấu hình duy nhất, vẫn không phải là bảng cho mỗi mô hình . ** Đã xác minh pydantic-ai 2.0.0 API: việc sử dụng là một thuộc tính — run.result.usage.input_tokens (run.result.usage() tăng lên TypeError; 1.x request_tokens tên đã biến mất); TestModel cư trú input_tokens (56 cho một lời nhắc ngắn gọn). pydantic-ai's ModelProfile hiển thị trường cửa sổ ngữ cảnh no (đã được xác minh), do đó cài đặt có thể định cấu hình là hợp đồng duy nhất - không tự động phát hiện. Dự phòng an toàn: input_tokens == 0 (không có người ở) → không có đám cháy cấp nào lượt đó.

    3a. Tính năng nén vi mô CHỈ có trong bộ nhớ — không bao giờ tồn tại. Nó xây dựng lại các tin nhắn cũ hơn giữ lại cái đuôi gần đây, thay thế từng cái ToolReturnPart/RetryPromptPart nội dung có phần giữ chỗ ("[tool output elided by microcompaction]") thông qua dataclasses.replace (không có đột biến tại chỗ). Nó chỉ trống nội dung - không bao giờ xóa một tin nhắn/phần - vì vậy nó không bao giờ có thể mồ côi cặp lệnh gọi công cụ/kết quả (không cần có ranh giới). Nó ** bình thường **. Nó không ghi vào nhật ký JSONL và không di chuyển _persisted_count: nhật ký giữ độ trung thực đầy đủ để phục hồi; tiếp tục phát lại toàn bộ lịch sử và lại microcompact. Đây là sự khác biệt rõ ràng so với việc nén hoàn toàn (mà IS vẫn tồn tại).

  4. Hình dạng tóm tắt thu gọn đầy đủ: khung Markdown cố định. MỘT công cụ tóm tắt MỚI, đầy đủ hơn (context/compaction.py), khác biệt với summarize_session (dành cho việc ghi lại bộ nhớ) nhưng tái sử dụng nó _resolve_model kiểu mẫu và phong cách phiên âm. Skeleton (hội tụ bằng opencode và pi): # Conversation summary → ## Goal → ## Constraints & Preferences → ## Progress (Xong · Đang Tiến trình · Bị chặn) → ## Key Decisions → ## Next Steps → ## Critical Context. Bộ xương đầy trở thành một cái đầu tổng hợp ModelRequest/UserPromptPart, được đóng khung như một bản tóm tắt của phần trước (rút gọn) cuộc trò chuyện.

  5. Những gì còn sót lại sau quá trình nén hoàn toàn: thông báo tóm tắt + phần đuôi nguyên văn gần đây, được gắn vào một lần nén Ranh giới. Lịch sử sau khi nén là [summary_message, *tail]; cái đuôi phù hợp compaction_keep_recent_tokens (mặc định 20_000), cắt quay trở lại ranh giới theo lượt của người dùng để nó không bao giờ tách cặp công cụ-gọi/kết quả. Không có tập tin hydrat hóa lại (được người dùng xác nhận lại: giữ gần đây quay nguyên văn, không đọc lại file). Bản tóm tắt trước, với tư cách là phần tử 0, tạo nên đầy đủ liên tiếp việc nén hợp nhất miễn phí — không có logic hợp nhất.

  6. Tính nhất quán bất biến (nén hoàn toàn). Nén hoàn toàn sẽ giữ nhật ký JSONL (mới compaction dòng), trong bộ nhớ message_history ([summary, *tail]) và _persisted_count (đặt lại về điều đó length) theo bước khóa. Ô tô chạy bên trong đoạn bay đơn của ngã rẽ; /compact chỉ chạy khi rảnh rỗi.

  7. Điều khiển bằng tay. /compact là lệnh gạch chéo dành riêng được phân tích cú pháp trước nhánh kỹ năng, chỉ ở chế độ nhàn rỗi (bận → đường thân thiện), buộc phải nén hoàn toàn với ContextCompacted xác nhận.

  8. Cấp độ thứ hai — Nén bộ nhớ khi thoát cho MEMORY.md, ở giới hạn 200 dòng. Khi tệp đạt tới memory_max_lines (200), compress_memory_file(cwd, *, model_or_settings) tạo nên một cuộc gọi LLM giá rẻ (sử dụng lại _resolve_model) để loại bỏ/hợp nhất các dữ kiện có tín hiệu cao nhất và viết lại theo mũ, được kẹp cứng bằng clip_lines_to_budget. Nối vào extract_on_exit sau viên đạn ghi ngày tháng nối thêm, kiểm soát bởi memory_compression_enabled (mặc định True), hoàn toàn không gây tử vong. ** Thả cũ nhất vẫn là dự phòng được đảm bảo** (mức trần luôn ở bên trong append_session_summary VÀ dự phòng khi cuộc gọi thất bại/trống) — vì vậy giới hạn LUÔN được thực thi, ngay cả với mô hình không/không thành công.

  9. Thước đo ngữ cảnh (Khả năng chi trả giao diện người dùng, cùng một cửa sổ = nguồn thông tin chính xác duy nhất). Vòng tròn điền vào chân trang (tui/render.py::context_gauge) biểu hiện last_input_tokens / compaction_context_window_tokens như một glyph điền vào bánh ○◔◑◕● + tỷ lệ phần trăm, được tô màu xanh lục/vàng/đỏ theo các dòng điền cấp giống (1 - micro_reserve = 60%, 1 - full_reserve = 80%), do đó người dùng xem cách tiếp cận ngữ cảnh sự nén chặt. Nó đọc sạch sẽ AgentTurnHandler.last_input_tokens tài sản (không bao giờ là một attr riêng). Lưu ý đầu cuối: một vòng có khoảng cách thực sự không thể hiển thị được bằng một ký tự; ○◔◑◕● là thiết bị di động xấp xỉ pie-fill.

Sơ đồ

flowchart TB
    subgraph turn["Per-turn cascade (WOULD_STOP) — window-relative"]
        measure{"input_tokens vs window*(1-reserve)"}
        micro["Microcompaction (NO LLM, in-memory only)<br/>fires &gt;= window*(1-micro_reserve) (~60% full)<br/>blank old ToolReturn/RetryPrompt bodies · idempotent<br/>NOT persisted · log keeps full fidelity"]
        full["Full Compaction (LLM, persisted)<br/>fires &gt;= window*(1-full_reserve) (~80% full)<br/>fixed skeleton summary + verbatim tail @ Compaction Boundary"]
        line["append_compaction(summary, tail)<br/>{type: compaction} on JSONL — NO SQLite"]
        reset["reset history=[summary,*tail]<br/>_persisted_count=len · emit ContextCompacted"]
    end

    manual(["User /compact (idle)"]) --> full
    measure -->|&gt;= full level| full
    measure -->|&gt;= micro level| micro
    measure -->|below / disabled / 0 tokens| noop["no-op"]
    micro --> emitm["emit ContextMicrocompacted"]
    full --> line --> reset
    line -.persisted.-> replay["load(): compaction line →<br/>discard, restart [summary,*tail], continue"]
    replay --> resume(["decode --resume → compacted history,<br/>re-microcompact as needed (full-fidelity log)"])

    gauge["TUI footer Context Gauge ○◔◑◕●<br/>last_input_tokens / window · green&lt;60% · yellow 60-80% · red&gt;=80%"]
    measure -.same window.-> gauge

    subgraph exit["On quit — second level"]
        append["append_session_summary()<br/>(drop-oldest clamp = always-on ceiling/fallback)"]
        memcap{"MEMORY.md &gt;= 200 lines (memory_max_lines)?"}
        memcomp["compress_memory_file() — ONE LLM call<br/>dedupe/merge high-signal · rewrite under caps"]
        memfb["fail/blank → drop-oldest fallback (cap enforced)"]
    end
    append --> memcap
    memcap -->|yes &amp; enabled| memcomp
    memcap -->|no| memskip["leave file"]
    memcomp -->|ok| memdone["write back + hard clamp"]
    memcomp -->|fail/blank| memfb

    classDef micro fill:#1565c0,stroke:#0d47a1,color:#ffffff;
    classDef full fill:#6a1b9a,stroke:#4a148c,color:#ffffff;
    classDef store fill:#2e7d32,stroke:#1b5e20,color:#ffffff;
    classDef mem fill:#e65100,stroke:#bf360c,color:#ffffff;
    classDef ui fill:#00838f,stroke:#005662,color:#ffffff;
    classDef plain fill:#eceff1,stroke:#607d8b,color:#102027;
    class micro,emitm micro;
    class full,reset full;
    class line,replay store;
    class append,memcap,memcomp,memfb,memdone,memskip mem;
    class gauge ui;
    class measure,manual,noop,resume plain;

hậu quả

  • Đường may nén M1 được thực hiện từ summarize_session mà không làm phiền nó; nén hoàn toàn và việc tái sử dụng nén bộ nhớ giống nhau _resolve_model mẫu, vì vậy CI vẫn ngoại tuyến.
  • Ngưỡng tương đối của cửa sổ thay thế các số phẳng trước đó theo yêu cầu của người dùng. Trình kích hoạt là bây giờ "cửa sổ đầy đến mức nào" (input_tokens >= window*(1-reserve)), thích ứng với bất kỳ mô hình/ cửa sổ mà người dùng định cấu hình — trong khi vẫn giữ lại một số có thể định cấu hình duy nhất, không phải bảng cho mỗi mô hình. Vì pydantic-ai không hiển thị cửa sổ mô hình nên cài đặt là hợp đồng (không tự động phát hiện sự trôi dạt).
  • Dự trữ theo từng tầng mang lại khả năng phòng thủ đầu tiên với chi phí thấp nhất: vi nén (không có LLM, trong bộ nhớ) hấp thụ áp lực đầy từ ~60%; nén hoàn toàn (một cuộc gọi LLM, vẫn tồn tại) chỉ ở mức ~ 80% đầy đủ hoặc trên /compact.
  • Tính năng nén vi mô chỉ có trong bộ nhớ — nhật ký JSONL vẫn giữ được độ trung thực hoàn toàn; tiếp tục áp dụng lại nó. Nó không bao giờ có thể mồ côi một cặp công cụ (để trống nội dung, không xóa gì). Đánh đổi: chi tiết đầu ra công cụ cũ rời đi cửa sổ trong ngữ cảnh (có thể phục hồi từ nhật ký) - tiết kiệm chi phí dự kiến.
  • Khả năng phục hồi được bảo toàn trên JSONL để nén hoàn toàn; một điểm kiểm tra không đúng định dạng sẽ xuống cấp một cách an toàn lịch sử chưa được nén lại (khoan dung, giống như messages-phát lại dòng).
  • Sự phân kỳ JSONL-over-SQLite là có chủ ý và được ghi lại (§1); nhiệm vụ 048 sửa AGENTS.md.
  • Kích thước đuôi sử dụng kích thước thô chars≈/4 ước tính (chỉ giữ lại ranh giới "cũ" vi mô - không bao giờ trình kích hoạt, có thẩm quyền của nhà cung cấp), vì pydantic-ai chỉ hiển thị mức sử dụng tổng hợp trên mỗi chặng. Bị giới hạn (chỉ ảnh hưởng đến mức độ lưu giữ bối cảnh gần đây). Đã xem xét và chấp nhận.
  • Nén bộ nhớ thay thế loại bỏ lỗi cũ nhất bị mất ở giới hạn 200 dòng bằng nén LLM tín hiệu cao, cũ nhất còn lại là mức dự phòng/trần được đảm bảo - sự thật bền vững tồn tại lâu hơn nhiều so với sự thật thuần túy cho phép cắt ngắn và giới hạn luôn được thực thi ngay cả với mô hình không có/không thành công.
  • Bộ đo bối cảnh liên kết cùng một cửa sổ với một cảnh báo hiển thị để người dùng thấy quá trình nén sắp diễn ra; nó các bậc màu được lấy từ cùng một phân số dự trữ (nguồn sự thật duy nhất). Phong cách Glyph là một xấp xỉ điền vào bánh di động, có thể hoán đổi trong một trợ giúp.
  • Các lần nén đầy đủ liên tiếp không cần mã hợp nhất — phần tóm tắt trước đó sẽ giữ vai trò chính.
  • Không phải mục tiêu (có chủ ý): tập tin tái hydrat hóa (được xác nhận lại), bảng cửa sổ ngữ cảnh cho mỗi mô hình / tự động phát hiện (cửa sổ là một số có thể định cấu hình) và bất kỳ cửa hàng SQLite/bền nào - tất cả đều bị trì hoãn. (Vi tính không còn là mục tiêu nữa; nó là một cấp độ trong phạm vi.)
Mục lục nguồn