全部文章
product2026/08/12

BibGenie 如何壓縮上下文

從一次 Agent 請求如何增長開始,介紹 BibGenie 如何用結構化 checkpoint、recent tail 與樹形持久化延續長時間科研對話。

當你讓 AI 回答一個簡單問題時,上下文視窗通常不是問題。

但真實的科研工作並不是一次問答:使用者可能先選中一段論文文字,再讓 Agent 檢索 Zotero 文獻庫、閱讀多份 PDF、比較不同研究結論、檢視圖表,最後基於幾十輪對話整理綜述。在這個過程中,使用者訊息、模型回覆、工具呼叫、文獻元資料、PDF 內容和圖片都會持續進入上下文。

無論模型支援 32K、128K 還是更大的上下文視窗,它最終都會被填滿。更大的視窗只能推遲問題,不能消除問題。

為此,我們為 BibGenie 實現了 Agent Context Compaction:當上下文接近模型容量時,系統會把較早的研究過程整理成一個結構化 checkpoint,同時保留最近對話的原文。模型不再讀取全部歷史,卻仍然知道使用者的研究目標、已經確認的證據、涉及的 Zotero 文獻、尚未完成的工作,以及下一步應該做什麼。

本文將從一次 Agent 請求如何增長開始,介紹 BibGenie 如何把上下文壓縮落到科研工作流、Zotero 資料和可分支的持久化對話中。

一次 Agent 請求為什麼會越來越大

對話介面看起來像是在逐條追加訊息,但模型並不會天然“記住”上一輪。每次請求都需要重新帶上完成當前任務所需的上下文,通常包括:

[system prompt][tools][conversation history][new user message]

如果模型決定呼叫工具,一輪對話內部還會繼續產生請求。Agent 執行工具,再把工具呼叫和結果連同此前歷史一起發回模型:

request 1:
[system][tools][user]

request 2:
[system][tools][user][assistant: tool call][tool result]

request 3:
[system][tools][user][assistant: tool call][tool result][assistant: tool call][tool result]

因此,佔用上下文的不只是聊天氣泡。一次 PDF 閱讀可能帶回大段正文,一次 Zotero 檢索可能包含許多條目,多輪工具呼叫也會在同一個 turn 中不斷擴大輸入。隨著研究繼續,下一次請求最終會達到模型的 context window;再多一條訊息,provider 就可能直接拒絕請求。

面對這個上限,系統只有兩個基本選擇:開始一段沒有歷史的新對話,或者把已有上下文轉換為更小、但仍足以繼續工作的表示。Compact 做的是後者。

上下文管理不是“刪除最舊的訊息”

最直觀的處理方式,是只保留最近 N 條訊息,或者在超出限制後從頭刪除。但對研究型 Agent 來說,這兩種方案都不可靠。

首先,訊息數量和 token 數量沒有穩定關係。一句“繼續”可能只有幾個 token,一次 PDF 讀取或工具結果卻可能包含數千甚至數萬 token。按訊息數量裁剪,無法判斷模型真正承受了多少上下文。

其次,舊訊息不等於低價值訊息。使用者的研究目標、納入標準、指定文獻、引用格式和已經做出的關鍵決定,往往出現在對話早期。直接刪除後,Agent 可能仍記得最近一句話,卻忘了整個任務為什麼開始。

最後,Agent 對話不是純文字列表。一次完整研究過程還可能包含:

  • 工具呼叫及其返回結果;
  • Zotero item key、附件和定位資訊;
  • 使用者對錯誤結論的糾正;
  • 已失敗且不應重複的嘗試;
  • 尚未完成的任務和等待驗證的假設;
  • 圖片等多模態內容。

因此,Compact 不能只是截斷。它必須完成一次有控制的資訊重組:丟棄可替代的過程細節,保留繼續工作所必需的狀態。

BibGenie 對 Compact 的定義是:

將較早的模型上下文轉換為結構化研究檢查點,同時保留完整對話歷史和最近對話原文。

這裡有一個重要區別:被壓縮的是下一次傳送給模型的上下文,而不是使用者的歷史記錄。

從完整歷史到“Checkpoint + Recent Tail”

假設一次對話已經產生了三輪對話:

U1 → A1 → U2 → A2 → U3 → A3

Compact 後,BibGenie 不會刪除或改寫這些節點,而是在當前對話末尾追加一個內部 checkpoint C1:

U1 → A1 → U2 → A2 → U3 → A3 → C1
                              ↑
                    durable session leaf

C1 儲存較早上下文的摘要,同時透過 firstKeptMessageId 指向第一條需要繼續保留原文的訊息。假設 U3 是保留邊界,那麼下一次請求真正傳送給模型的是:

C1.summary → U3 → A3

使用者繼續提問後,上下文則變成:

C1.summary → U3 → A3 → U4

這形成了兩個互不衝突的檢視:

  • Durable history:資料庫和介面仍然保留完整的 U1 → A1 → … → C1 → U4;
  • Model projection:模型只接收最新 checkpoint、最近原文和 checkpoint 之後的新訊息。
持久歷史與模型上下文投影:IndexedDB 保留完整會話,模型只接收 checkpoint 摘要和最近訊息

這種設計的核心不是“壓縮一段字串”,而是把可審計的完整歷史和高密度的模型工作記憶分離開來。

Compact 什麼時候觸發

BibGenie 提供了三個觸發入口,並讓它們共享同一套模型解析、容量估算和閾值邏輯。

1. 使用者手動觸發

使用者可以主動點選 Compact。這適合已經完成一個研究階段、希望在進入下一階段前整理上下文的場景。

手動 Compact 不受自動閾值限制,行為更接近使用者明確發出的“現在整理一次上下文”指令:

  • 對於首次壓縮,只要短對話至少已有兩輪,系統就可以摺疊第一輪、保留第二輪原文;
  • 如果已有 checkpoint,且它之後又產生了新訊息,手動 Compact 可以基於 previous summary 生成一個更新後的 checkpoint,即使保留邊界暫時不需要繼續前移;當邊界前移時,被移出 recent tail 的新增歷史才會一起進入摘要;
  • 如果對話只有一個無法摺疊的 turn,或者最新 checkpoint 已經是當前 branch leaf,本次操作會被視為成功的 no-op,不向使用者顯示一個沒有實際幫助的錯誤。

這讓手動入口可以提前建立階段性研究檢查點,同時避免在確實沒有內容可處理時向使用者報告無意義的錯誤。

2. 一輪回答完成後自動檢查

當 assistant 的最終結果成功持久化後,BibGenie 會檢查當前上下文佔用。如果已經接近安全邊界,系統會主動執行 Compact。

這一步把壓縮成本從下一次傳送路徑中提前移走。使用者下一次提問時,通常不需要先等待摘要生成。

自動檢查只會在對話處於穩定狀態時執行。如果工具呼叫、工具結果或使用者審批仍未結束,Compact 會跳過,避免把未完成的工作流固化進 checkpoint。

3. 傳送前 Preflight

Turn 後檢查可以最佳化延遲,但真正保證正確性的,是傳送前 preflight。

在使用者訊息進入 AI SDK 狀態之前,BibGenie 會把這條待傳送訊息臨時加入模型投影,再估算下一次請求的總佔用。這樣,即使使用者一次貼上了大量文字、加入了圖片,或者剛剛切換到 context window 更小的模型,系統也能在呼叫 provider 之前發現風險。

傳送訊息前的預檢壓縮流程

如果 Compact 後仍然超出模型容量,BibGenie 不會把一個註定失敗的請求傳送給 provider,而是保留使用者輸入並提示縮短內容。

用模型容量而不是訊息數量做決策

不同模型擁有不同的 context window 和最大輸出能力,因此 BibGenie 不使用全域性固定閾值。

其基本關係可以概括為:

安全輸入上限 = Context Window - Reserve Tokens

其中 Reserve Tokens 用於為下一輪模型生成保留空間。它會根據當前模型的 context window 動態計算:大視窗模型可以保留更充足的輸出預算,小視窗模型則自動縮小 reserve,而不是套用一個全域性固定值。

Recent tail 的預算同樣隨模型容量變化。這意味著 Compact 既不會為了節省 token 把最近對話全部抹平,也不會在小模型上保留一段根本放不下的 recent tail。

保留邊界只會選擇完整的 user turn,不會從一段 assistant 回覆或工具結果中間切開。如果某個 assistant/tool turn 本身已經超過 recent-tail 預算,系統會選擇它後方最近的 user 邊界,避免為了等待更早的 user message,反而把整個超大 turn 一起保留下來。

優先使用 provider 的真實 usage

跨 provider 的精確 tokenizer 會顯著增加複雜度,而且模型實際計費和上下文計算方式並不完全統一。因此,BibGenie 採用“真實 usage 優先、增量估算兜底”的策略:

  1. 找到最新 checkpoint 之後,最近一條帶有效 usage 的 assistant 訊息;
  2. 把 provider 返回的 token usage 作為基線;
  3. 對該請求之後新增、尚未進入 usage 的訊息做字元估算;
  4. 如果沒有可靠 usage,則估算當前完整的模型投影。

這裡統計的是下一次 provider 請求可能佔用的上下文,而不是使用者累計消費過的 token。計費資料中的 totalUsage 不會被誤當成 context occupancy。

文字使用保守的字元近似;reasoning 在可能被 provider 重放時也計入估算;圖片則按照固定的上下文成本計入 preflight,避免多模態對話被當成只有一個檔名。對於當前外掛的輸入能力,使用者側 file part 主要來自貼上圖片和 Zotero 截圖。

估算不追求偽精確。它的目標是穩定回答一個工程問題:下一輪請求是否仍處於安全範圍內?

摘要不是聊天總結,而是 Agent 狀態快照

普通的“對話摘要”通常只描述發生了什麼,但 Agent checkpoint 還必須讓模型能夠繼續工作。

摘要透過一次獨立的模型請求生成,而不是讓正常 Agent 在原對話裡“順便總結”。這次請求使用專門的 summarization prompt,不載入常規工具,也不要求模型繼續回答使用者。待壓縮的歷史會先被序列化為帶角色和邊界的資料,再作為不可信資料交給摘要模型。這樣既能避免把歷史中的指令誤當成新任務,也允許摘要過程與正常對話使用清晰、可控的輸入。

BibGenie 要求摘要以固定結構儲存以下資訊:

  • Research Goal:當前研究目標;
  • User Requirements and Constraints:使用者要求、偏好和限制;
  • Research Progress:已完成、進行中和被阻塞的工作;
  • Findings and Evidence:已有發現及對應證據;
  • Key Zotero Resources:關鍵條目、附件和 Zotero key;
  • Decisions:已經做出的選擇;
  • Next Steps:下一步行動;
  • Critical Context:繼續任務不可缺少的其他資訊。

Prompt 還要求模型區分三類資訊:使用者陳述、工具返回事實和模型推斷。這可以減少“模型曾經猜測過一次,壓縮後卻被寫成既定事實”的風險。

面向科研上下文的序列化

在生成摘要前,BibGenie 會先把 UIMessage 轉換成適合壓縮的文字表示:

  • 普通文字保留原文和角色標籤;
  • reasoning 不進入長期 checkpoint;
  • 工具輸入保留,過長的工具結果會被截斷;
  • 工具錯誤和拒絕狀態會被明確標記;
  • 圖片和附件保留檔名與 MIME 型別;
  • Zotero 資料保留後續可恢復工作的關鍵欄位。

針對不同 Zotero 資源,系統會保留不同資訊。例如:

  • 文獻條目:item key、標題、引用文字;
  • PDF 頁面:附件名、當前頁碼、總頁數;
  • EPUB:章節索引、href 和 CFI;
  • 標註:annotation key、文字、評論、頁碼和父條目 key;
  • Collection 和 Tag:穩定標識及展示資訊。

這樣做既能避免把龐大的原始物件完整塞進摘要請求,又不會丟掉下一輪工具呼叫真正需要的 Zotero 定位資訊。

連續 Compact 如何避免摘要堆疊

一段長對話可能發生多次 Compact。BibGenie 不會把多個舊 checkpoint 全部傳送給模型,而是只使用最新 checkpoint。

第二次壓縮時,系統會將:

previous summary + 上次壓縮後新增的早期訊息

合併為新的 summary。隨後模型投影只保留最新 checkpoint:

Summary 1 + New History → Summary 2
Summary 2 + New History → Summary 3

因此,上下文不會因為 checkpoint 數量增加而再次線性膨脹。

連續 Compact 還遵守一個單調邊界原則:新的保留邊界不能退到上一次 checkpoint 的邊界之前。已經被 summary 替代的早期歷史不會在後續壓縮中重新展開並混入 recent tail。

手動和自動入口在這裡有意採用不同策略:

  • 自動 Compact 只有在保留邊界能夠真正前移、可以釋放更多上下文時才執行,避免後臺重複生成沒有容量收益的摘要;
  • 手動 Compact 可以在 checkpoint 後出現新訊息時複用原邊界,基於 previous summary 生成新的 checkpoint;如果邊界能夠前移,被移出 recent tail 的新增歷史也會併入摘要。

這種區分讓自動機制專注於容量收益,同時保留使用者主動整理研究階段的能力。

摘要輸出預算取自 reserve,並受模型自身最大輸出長度約束。這個預算只是為複雜研究任務提供安全上限,Prompt 仍然明確要求輸出簡潔。

為什麼 Compact 必須理解樹形 Session

BibGenie 的對話不是簡單陣列,而是一棵可分支的訊息樹。

使用者可以 Retry、編輯最後一條訊息,也可以從歷史 assistant 回覆建立 fork。同一批早期節點可能被多個 session 共享,而每個 session 透過自己的 durable leaf 表示當前分支。

這意味著 Compact 不能重寫舊節點,也不能把保留訊息複製到 checkpoint 後面。否則可能出現:

  • 一個分支的壓縮影響其他分支;
  • 共享節點的 parent 關係被修改;
  • Retry 後仍錯誤繼承已經失效的 checkpoint;
  • 同一段最近訊息在資料庫中被重複儲存;
  • UI 歷史與模型實際看到的上下文不一致。

BibGenie 採用 append-only checkpoint:

分支會話中的追加式 checkpoint

C1.parentId = A3 表示 checkpoint 在時間和樹結構上發生於 A3 之後;C1.firstKeptMessageId = U3 則表示模型投影從 U3 開始繼續保留原文。這是兩個不同維度,不能互相替代。

如果從 A2 建立 fork,新分支不會繼承 C1;如果從 C1 之後的穩定 assistant 建立 fork,則 checkpoint 會自然存在於它的 ancestor chain 中。整個過程不需要複製或修改共享節點。

把 Checkpoint 當作一次併發安全的狀態提交

摘要生成可能需要數秒。在這段時間裡,如果對話分支已經被編輯、重試或其他操作改變,那麼基於舊歷史生成的 summary 就不能提交。

BibGenie 把 Compact 視為一次帶版本條件的狀態提交。開始生成摘要時,系統記錄當前分支的兩個基線:

  • leafId:當前 durable branch 的末端;
  • branchRevision:只在 durable 內容真正變化時遞增的分支版本。

摘要完成後,專用事務會用這兩個值執行 compare-and-swap(CAS)校驗,同時確認工具工作流已穩定、保留邊界仍屬於當前分支。只有基線沒有變化,系統才會追加 checkpoint、移動 session leaf 並遞增版本。任何一項不一致都意味著摘要已經過期,提交必須被拒絕。

Checkpoint 的原子提交流程

如果摘要期間 branch 已經變化,BibGenie 會重新載入資料庫中的真實分支,而不是把舊摘要強行覆蓋到新對話上。使用者在事務完成前取消時,提交會回滾;事務成功後,記憶體狀態再同步到已經提交的 checkpoint。這樣,資料庫與介面最終會收斂到同一個分支狀態。

與 Retry、Edit、Fork 等既有能力協同

一個可靠的 Compact 功能不能只保證“摘要能生成”,還必須保持原有對話操作的語義。

Retry

Checkpoint 是內部訊息,Retry 尋找的是最後一條真實 assistant 回覆。如果被重試的 turn 後面存在 checkpoint,它會隨舊尾部一起失效,新回覆可以在之後再次觸發正常的閾值檢查。

Edit

編輯最後一條真實 user message 時,該訊息之後的 assistant 和 checkpoint 都會一起退出當前 branch。使用者重新傳送後,會再次經過傳送前 preflight。

Fork

Fork 只能選擇穩定的真實 assistant,不能選擇內部 checkpoint。新分支是否繼承某個 checkpoint,完全由目標節點的 ancestor chain 決定。

損壞歷史保護

載入對話時,BibGenie 會檢查缺失 parent、parent cycle 和非法 durable message。如果訊息拓撲損壞,介面只展示能夠安全讀取的部分,並把對話切換為只讀,而不是在載入或 Compact 時靜默修改原始資料庫。

Compact 期間的使用者體驗

上下文壓縮是基礎設施,但它不應該表現為一次神秘的介面卡頓。

當手動或後臺 Compact 開始時,聊天區會顯示 Compacting earlier context…,並允許使用者取消。此時輸入框仍然可以編輯,使用者可以繼續組織下一條問題。

只有當使用者明確點選傳送後,這條提交內容才會被凍結並等待正在執行的 Compact。完成後,BibGenie 會自動繼續傳送;如果失敗或取消,則不會偷偷提交訊息,原草稿仍然保留。

Compact 執行期間,Retry、Edit、Fork、模型切換和再次 Compact 會暫時禁用,因為這些操作都會改變摘要所依賴的 branch。普通輸入不改變 durable history,因此不必過早鎖住編輯器。

最終,使用者能夠感知到的是:

  • 長時間研究不會因為上下文逐漸增長而突然中斷;
  • 不需要新建對話並重新解釋研究背景;
  • 切換到小視窗模型時會在請求前重新評估容量;
  • 關鍵文獻、Zotero key、結論和未完成任務能夠延續;
  • 最近對話仍保留原文,不會突然失去指代和語氣;
  • 外掛重啟後,checkpoint 仍然屬於原來的對話分支;
  • 使用者始終可以檢視 Compact 之前的完整歷史。

Compact 也有代價:Prompt Cache 會重新開始

上下文變短並不意味著第一次請求一定更便宜。許多模型服務會快取請求中未變化的字首;在普通的追加式對話裡,system prompt、工具定義和大部分歷史保持不變,因此下一輪可以複用已有的 prompt cache。

Compact 會主動改寫模型上下文的字首:

壓縮前:
[system][tools][older history][recent tail]
<----------- reusable cached prefix ----------->

壓縮後第一次請求:
[system][tools][summary][recent tail][new user message]
                ^
                從這裡開始已是新的 token 序列

Recent tail 的文字雖然沒有改變,但它前面從 older history 變成了 summary,原來的快取字首無法繼續覆蓋這部分。壓縮後的第一次請求需要重新計算;後續請求則可以圍繞新字首再次建立快取。

這也是 BibGenie 不持續刪除舊工具結果、也不在每輪結束後無條件重寫上下文的原因。壓縮是一種有意進行的 cache reset,應該在上下文壓力或使用者明確需要整理階段狀態時發生,而不是把每個舊 token 都當成需要立刻清除的負擔。

換句話說,目標不是製造最短的 prompt,而是在四件事之間取得平衡:

  • 模型是否還能容納下一輪輸入和輸出;
  • 繼續任務所需的資訊是否完整;
  • 已有 prompt cache 能否繼續複用;
  • 請求的延遲和費用是否合理。

設計取捨:追求可驗證,而不是偽精確

BibGenie 的實現借鑑了成熟 Agent 系統的核心 compaction 思路,並針對 Zotero、多模態輸入和樹形持久化做了適配。整個設計刻意避免把問題無限複雜化:

  • 不依賴一個假裝對所有 provider 都精確的統一 tokenizer;
  • 不物理刪除或重新掛接 Compact 之前的訊息;
  • 不複製 recent tail;
  • 不在 provider overflow 後自動 Compact 並重試原請求;
  • 不允許普通訊息持久化路徑建立或修改 checkpoint;
  • 不把計費用的累計 token 與上下文佔用混為一談。

這些取捨讓系統的關鍵性質更容易驗證:什麼時候觸發、摘要基於哪條 branch、模型看到哪些訊息、資料庫提交了什麼,以及失敗後會停留在哪個狀態。

Compact 本質上仍然是一種有失真壓縮。它的目標並不是讓模型逐字記住所有歷史,而是在有限 context window 中最大限度保留任務連續性。原始歷史負責可追溯,checkpoint 負責繼續工作,recent tail 負責保留區域性細節——三者各自承擔清晰的職責。

從聊天機器人到長期研究夥伴

普通聊天系統主要關心“這一輪應該如何回答”。研究型 Agent 還必須回答另一個問題:下一輪開始時,它應該帶著什麼狀態繼續工作?

BibGenie 的 Context Compaction 不只是一次摘要呼叫。它包括模型容量預檢、研究上下文序列化、結構化 checkpoint、樹形 session 投影、併發版本校驗、事務提交,以及與 Retry、Edit、Fork 等行為的一致性協作。

當一次研究跨越幾十輪對話、多份文獻和大量工具呼叫時,真正重要的並不是記住每一個 token,而是始終知道:

  • 使用者最終想完成什麼;
  • 哪些事實已經得到證據支援;
  • 哪些結論仍然只是推斷;
  • 哪些工作已經完成;
  • 下一步應該做什麼。

這正是 Compact 為 BibGenie 提供的能力:讓有限的上下文視窗,承載一個可以長期推進、持續恢復並且保持研究脈絡的 Agent 對話。