全部文章
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 会话。