目录

  1. 写在前面:本文讨论的到底是哪一种 Memory
  2. 一、问题不是上下文不够大,而是上下文没有事实源
  3. 二、先建立对象地图:History、Context、Snapshot、Journal 与 Memory
    1. 1. Message、turn 与 Canonical Transcript
    2. 2. Prompt Context 与 Context Envelope
    3. 3. Frozen Context Snapshot 与 Frozen Generation Journal
    4. 4. Conversation Checkpoint 与 Long-term Memory
    5. 5. Provider State 也不是 TXChatDemo Memory
  4. 三、Context V2:把 UI Projection 与 Prompt Truth 分离
  5. 四、Checkpoint 为什么采用结构化摘要
  6. 五、一次自动压缩究竟如何发生
    1. 1. 持久化并冻结
    2. 2. 校验身份与指令所有权
    3. 3. 使用最终 Provider adapter 规划 token
    4. 4. 同时冻结主路径与回退路径
    5. 5. 原子准入与一次父结算
    6. 6. 先压缩,再回答
    7. 7. 返回的只是 Candidate
    8. 8. Canonical Result 与结算先收敛
    9. 9. 最后通过 CAS 激活
    10. 用户实际场景:协作撰写一篇移动端故障复盘文章
      1. 0. 用户已经和 TXChatDemo 讨论了什么
      2. 1. 当前用户请求先持久化,再冻结 Generation
      3. 2. Execution Plane 判断是否需要压缩
      4. 3. 为什么必须按连续分界切开 exact turns
      5. 4. Compaction 主路径和 fallback 在执行前同时冻结
      6. 5A. Compactor 成功后,先产生了什么,Answer 模型最终看到了什么
      7. 5B. Compactor 失败时,文章还能不能继续写
      8. 5C. Demo示例
      9. 6. Answer 成功之后,CP-42 为什么还不能立即生效
      10. 7. 用这个写作场景回看整条链路
  7. 六、Candidate 为什么不能直接写成 Active Memory
  8. 七、失败为什么不能破坏会话
    1. 压缩也不天然意味着更便宜
  9. 八、与 Codex、Claude、LangGraph 对比:相似词汇下的不同所有权
    1. 1. Codex 与 OpenAI Responses API 要分开看
    2. 2. Claude 把 Context Editing、Compaction、Memory Tool 分成不同能力
    3. 3. LangGraph 提供原语,应用定义业务语义
    4. 4. 放在同一张表里看
  10. 九、哪些原则值得复用
    1. 1. 完整历史与 Prompt 投影必须分离
    2. 2. 压缩只覆盖连续旧前缀
    3. 3. 近期轮次保留原文,并以 turn 为原子
    4. 4. 摘要是低权限数据,不是系统指令
    5. 5. Candidate 不等于 Active Memory
    6. 6. 主路径与 fallback 必须同时冻结
    7. 7. Token policy 应由最终 Provider adapter 决定
  11. 术语速查:第一次阅读时可以回到这里
  12. 十、测试重点不是摘要像不像,而是不变量会不会破
  13. 结语
  14. 参考资料

写在前面:本文讨论的到底是哪一种 Memory

“LLM Memory”经常被用来指代几件完全不同的事情:把聊天记录全部塞进上下文、把旧消息总结成一段话、从向量库检索用户资料、让 Agent 把工作进度写入文件,甚至只是把模型上下文窗口换得更大。

本文讨论的是其中一个更窄、也更容易被低估的场景:单个会话不断变长后,怎样在保留近期原文的同时,把较早的连续历史压缩成可继续使用的会话 Checkpoint

这不是跨会话用户画像,也不是向量检索,更不是让模型永久记住用户。TXChatDemo 首版只处理单会话短期记忆(thread-level short-term memory):完整会话仍然存在本地,模型每次只看到为当前 Generation 组装并冻结的有限上下文。

还需要先说明一条发布边界。本文基于 TXChatDemo 当前代码、版本化契约和架构文档总结设计与实现;“代码和契约存在”“某个环境开启能力”“真实生产流量完成质量验证”是三件不同的事。本文是工程实践分享,不是生产能力公告。文中的比例和预算只用于解释策略形状,不是行业标准,也不代表某次部署的实时配置。

一、问题不是上下文不够大,而是上下文没有事实源

TXChatDemo 早期采用过一种常见方案:通过 contextMsgNumber 保留最近 N 条可参与 Prompt 的消息。它实现简单,短会话里也足够有效,但随着历史加载、失败重试和长对话出现,几个结构性问题逐渐暴露出来。

第一,消息条数不是 token budget。Token budget 是本次模型输入、输出可使用的 token 预算。同样是 20 条消息,可能只是 20 句简短问答,也可能包含长代码、Markdown 表格和几万字分析。按条数截断无法证明请求一定适配模型窗口,也无法稳定预测成本与延迟。

第二,UI 状态不应该决定模型知道什么。旧链路中,页面首次加载一段历史,用户上拉后再把更早消息插入会话数组头部;后续请求如果直接从这个数组取 Prompt history,那么“用户有没有翻过历史”就可能改变模型下一轮看到的上下文。两个语义相同的会话,仅因为 UI 分页状态不同,就可能生成不同请求。

第三,截断没有覆盖语义。保留最近 N 条只能说明“这些消息还在”,却不能说明更早内容为什么消失、哪些事实已经被摘要、摘要覆盖到了哪一轮,以及历史被编辑后旧摘要是否仍然可信。

第四,重试很容易漂移。一次网络超时之后,如果重新从当前 UI 数组组装 Prompt,期间历史加载、模型设置或消息状态发生变化,同一个 generation ID 可能对应两份不同语义输入。幂等本应表示同一操作重复提交仍保持同一语义结果;如果请求编号相同、内容却已经变化,幂等就只剩下表面形式。

旧耦合可以简化成下面这张图:

flowchart LR
    DB["本地消息记录"] --> Page["UI 分页查询"]
    Page --> Visible["当前已加载的消息数组"]
    Visible --> Prompt["截取最近 N 条"]
    Scroll["用户上拉历史"] --> Visible
    Prompt --> LLM["LLM 请求"]

    classDef risk fill:#fff1f0,stroke:#cf1322,color:#5c0011;
    class Visible,Prompt risk;

真正的问题不是 N 取 10、20 还是 50,而是 Prompt Context 没有独立的事实源。只要 UI projection(由持久数据派生出的界面视图)同时承担显示、分页和 Prompt 真相,任何压缩算法都只是补丁。

因此这次演进最重要的结论是:

Memory 压缩首先是状态一致性问题,其次才是摘要生成问题。

二、先建立对象地图:History、Context、Snapshot、Journal 与 Memory

很多陌生感来自一个单词被用来指代不同对象。先用一条主线把它们放到正确位置:

1
2
3
4
5
6
7
8
9
10
11
Canonical Transcript(完整事实)
↓ 按 source cutoff 投影
Prompt Context(本轮准备让模型看到的内容)
↓ 编码并持久冻结
Frozen Context Snapshot(本次 Generation 的不可变语义输入)
↓ 进入持久执行
Generation Operation / Journal(执行与恢复状态)
↓ 可选 Compaction
Checkpoint Candidate(尚未生效的摘要候选)
↓ verify + stage + CAS
Active Checkpoint(下一轮可读取的会话内压缩投影)

1. Message、turn 与 Canonical Transcript

  • Message 是一条带 role 和正文的消息。TXChatDemo Context V2 的公开会话数据只接受 userassistant,App 不能在普通历史中伪造 systemdeveloper role。
  • turn 是 Context 的业务原子。一个完整 turn 至少有 user message,也可以带一个已完成、可参与 Prompt 的 assistant message。源码中的 ChatCanonicalTurn 正是这个结构。
  • Canonical Transcript 是按 turn ordinal 排序的本地对话语义事实源。“Canonical”表示后续投影应从它推导,不表示所有候选 Answer 都自动成为事实;只有被选择并提交的 assistant candidate 才进入可用投影。
  • Message History / UI projection 是用户界面看到的历史。它可以分页、缓存和重建,但不能反过来决定 Prompt。源码把 Canonical 存储与 materialized Message store 分开,并通过持久 journal 关闭两次写入之间的崩溃窗口。

turn ordinal 是会话 lineage 内单调递增的轮次序号,例如 81、82、83。它不是数据库自增主键,也不是消息时间戳;它的作用是表达覆盖边界和连续性。

2. Prompt Context 与 Context Envelope

  • Prompt Context 是“本轮模型最终会看到的语义材料”这一逻辑概念,通常包含权威 Agent 指令、可空 Checkpoint、recent exact turns 和 current input。
  • Context Envelope 是 App 把自己拥有的会话数据编码成版本化跨端数据契约(wire contract)的具体结构。源码类型是 ChatContextEnvelope,包含 conversation_idepochlineage_id、requested mode、coverage、source ordinal/digest、Checkpoint、exact turns 和 current input。

两者不能画等号,因为 Gateway 还会在 Envelope 外根据已认证的 Agent ID 注入权威指令,Execution Plane 也会根据最终 Provider 模板完成 request rendering。换句话说:Envelope 是 Prompt Context 的 App-owned 部分,不是完整 Provider request。

同样,账号 owner(当前账号主体)与 backend environment(后端环境)属于已认证 Operation scope,并不会都作为普通字段塞进 Context Envelope;它们通过 Operation identity、request fingerprint 和 receipt binding(签名字段绑定)共同约束。

3. Frozen Context Snapshot 与 Frozen Generation Journal

这两个词最容易被误认为同义词:

对象 回答的问题 项目中的具体实现
Frozen Context Snapshot “这一次 Generation 的语义请求到底是什么?” 编码后的 Context/semantic request、context snapshot digest 和 request fingerprint
Generation Journal “这次 Generation 执行到哪一步,重启后怎样恢复?” ChatGenerationOperationRecord 中的 operation 状态、cursor、result、billing、checkpoint ingestion 等字段
Frozen request blob “丢失 create 响应后,重放哪一份原始请求字节?” ChatGenerationFrozenRequestStore 保存的受保护请求数据

因此,“持久化并冻结”不是只把一个数组写进数据库。项目代码先生成 semantic request,再将完整 frozen request blob 与 operation journal 身份一起保存;只有本地保存成功,网络 create 才能继续。已有 generation 恢复时从 frozen request store 读取原字节,不重新调用 Assembler。

这里的 semantic request 是会改变模型回答语义的请求部分,例如 Agent ID、模型设置、生成参数和 Context;短期认证 header、网络重试次数不属于它。Blob 是按整体保存的二进制数据;journal 是用于崩溃恢复的持久状态记录。

Generation Operation 是一次持久、可恢复的生成业务操作;Generation Attempt 更适合指某次具体网络或 Provider 尝试。重连、SSE 恢复和相同 operation 的安全重放不应生成新 Snapshot;用户明确 Retry/Regenerate 才创建新的 generation identity 和新 Snapshot。

4. Conversation Checkpoint 与 Long-term Memory

  • Conversation Checkpoint 是对当前会话某个连续旧前缀的结构化摘要。例如覆盖 turn 1~80,不代表 turn 1~80 被删除,而代表本轮 Prompt 可以用摘要代替逐条展开。
  • Checkpoint Candidate 是模型和服务端生成、但尚未获得本地记忆写权限的候选。
  • Active Checkpoint 是通过 receipt、digest、coverage、本地状态和 CAS 校验后,下一次 Assembler 才允许读取的唯一当前版本。
  • Long-term Memory 是跨 conversation/session 的用户或应用知识,需要独立的写入授权、存储、召回和删除策略。TXChatDemo v1 的 Conversation Checkpoint 不实现这一层。

5. Provider State 也不是 TXChatDemo Memory

某些 Provider API 可以通过 response ID、thread ID 或不透明 compaction item 延续上下文,这里统称 Provider State。它由 Provider 的接口与保留策略定义。TXChatDemo 的 Canonical Transcript、Checkpoint 激活和账务不能仅靠 Provider State 推导,否则换 Provider、结果过期或状态不可读时,本地业务语义就失去事实源。

尤其要注意:Checkpoint 不是删掉旧消息后的替代品。完整 Transcript 仍然保留;Checkpoint 只是在有限上下文窗口中的压缩投影。即使 Checkpoint 损坏,也不应该破坏原始会话记录。

三、Context V2:把 UI Projection 与 Prompt Truth 分离

新方案引入独立的 Canonical Transcript。UI cell 仍然是用户看到的 materialized projection——也就是为显示生成的可重建视图——但 ChatGenerationContextAssembler 不再读取页面当前加载了多少消息,而是从持久事实中组装:

1
2
3
4
权威 Agent 指令
+ Active Checkpoint(可空)
+ Checkpoint 之后的连续完整轮次
+ 当前用户输入

当前用户消息必须先持久化为 canonical turn,然后才能创建 generation。Assembler 根据 sourceTurnOrdinal 建立明确 cutoff。随后 semantic request 被编码为 frozen request,受保护 blob 与 operation journal 一起落盘。认证刷新、网络重放、SSE 重连和 App 重启恢复同一个 generation 时,都复用相同 frozen request;用户主动 Retry 或 Regenerate 才会创建新的 generation 和新快照。

这里几个字段的含义如下:

字段或术语 含义 为什么需要
sourceTurnOrdinal 本次 Generation 当前 user turn 的 ordinal,也是语义 cutoff 防止恢复时读取 cutoff 之后的新消息
currentInput 当前 user turn 的 message ID 与正文 始终单独保留,不能静默截断
exactTurns source turn 之前、按完整 turn 保存的原文 给模型保留近期逐字细节
checkpoint 可空的 active verified Checkpoint 压缩连续旧前缀;Memory 关闭或 lineage 降级时为空
coverage contiguousrecent_only 表示这份输入能否声称从 lineage base 连续覆盖到当前
mode App 请求的 automaticrecent_exact_onlydegraded_recent_only 记录用户策略和 App 组装结果,不等于最终执行分支
preferenceRevision Memory 设置版本 防止旧 generation 绕过用户后来的设置变化
sourcePrefixDigest Canonical 投影从 lineage base 累积到 source turn 的 SHA-256 链值 检测历史内容或选中 Answer 发生语义变化
epoch clear/delete 等语义重置后递增的会话世代 让重置前对象整体失效
lineageID 当前连续历史谱系的标识 防止旧 Checkpoint 跨谱系复用
lineageBaseTurnOrdinal 当前 lineage 可以合法追溯的起点边界 约束 summary 引用和 coverage 下界

epochlineage 都用于“断开旧世界”,但粒度不同:epoch 表达会话经历了一次语义重置;lineage 表达当前可连续追踪的历史链。删除历史消息时,代码会推进 epoch、生成新的 lineage,并清理旧 frozen request 与 Checkpoint,使迟到结果不能复活旧上下文。

flowchart LR
    UI["UI Message Projection"]
    Transcript["Encrypted Canonical Transcript"]
    Active["Verified Active Checkpoint"]
    Assembler["Deterministic Context Assembler"]
    Frozen["Frozen Context Snapshot"]
    Operation["Durable Generation Operation"]
    Candidate["Checkpoint Candidate"]

    Transcript --> UI
    Transcript --> Assembler
    Active --> Assembler
    Assembler --> Frozen
    Frozen --> Operation
    Operation --> Candidate
    Candidate -. "verify + stage + CAS" .-> Active

    classDef truth fill:#e6f4ff,stroke:#1677ff,color:#002766;
    classDef projection fill:#f6ffed,stroke:#52c41a,color:#135200;
    class Transcript,Frozen truth;
    class UI,Active projection;

组装逻辑可以抽象为下面的概念伪代码;它用于解释分支,不是 Swift API 的逐字抄录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function assembleContext(scope, sourceTurn, policy):
state = transcript.loadState(scope)
preference = transcript.loadMemoryPreference(scope)
current = transcript.requireUserTurn(sourceTurn)

if preference.allowsAutomaticMemory and lineage.isHealthy:
checkpoint = transcript.loadVerifiedActiveCheckpoint(scope)
coverageEnd = checkpoint?.coveredThrough ?? state.lineageBaseTurn
exactTurns = transcript.loadContiguousTurns(
after: coverageEnd,
through: sourceTurn - 1
)
else:
checkpoint = null
exactTurns = transcript.loadBoundedRecentExactTurns(before: sourceTurn)

if exactTurns are not contiguous or envelope exceeds hard limits:
lineage.markDegraded()
checkpoint = null
exactTurns = transcript.loadBoundedRecentExactTurns(before: sourceTurn)

return freeze(checkpoint, exactTurns, current)

这里有两个关键点。

其一,连续性比“尽量多带一些历史”更重要。Automatic 模式下,如果 Checkpoint 覆盖到第 80 轮,那么 exact turns 必须从第 81 轮连续到当前输入之前。中间缺一轮,就不能继续声称新 Checkpoint 覆盖了完整前缀。

其二,最近原文以完整 turn 为单位保留。一个 turn 包括 user message 和对应的有效 assistant message。预算不足时从最旧 turn 开始丢弃,不把一问一答切成语义残片,也不静默裁剪当前用户输入。

四、Checkpoint 为什么采用结构化摘要

常见 rolling summary 是一整段自然语言。它很灵活,却不容易验证:模型是否遗漏了未解决问题?某条用户偏好来自哪一轮?一句话究竟是事实、决策,还是模型自己的推测?

TXChatDemo 的 Checkpoint 把摘要拆成五类:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"schema_version": "checkpoint-v1",
"conversation_goal": [
{
"text": "完成一篇 LLM Memory 压缩实践文章",
"source_turn_ordinals": [12]
}
],
"confirmed_facts": [],
"decisions_and_constraints": [],
"user_preferences": [],
"unresolved_items": []
}

这五类字段在项目源码中分别对应:

字段 保存什么 不应该保存什么
conversation_goal 当前会话仍在追求的目标 模型自己新增的目标
confirmed_facts 对话中已经确认、后续仍有用的事实 未确认推测或外部知识猜测
decisions_and_constraints 已做出的决定、边界和禁止项 把普通讨论误写成最终决定
user_preferences 用户明确表达且与本会话相关的偏好 未经授权扩展成跨会话画像
unresolved_items 尚未解决的问题和后续动作 已完成事项或摘要模型的新任务

每个 ChatContextSummaryItem 不是一段裸字符串,而是 text + source_turn_ordinalssource_turn_ordinals 表示这条摘要声称依据哪些 turn,它提供的是可追踪性,不是事实证明:模型仍可能误读原文,但系统至少能拒绝引用范围之外的内容。

项目校验器对 summary 做了可执行约束:字段集合必须精确匹配;item 文本不能为空并有大小上限;引用 ordinal 不能重复,必须大于 lineage base 且不超过 coverage;summary 整体必须非空并满足大小上限。这些限制来自 ChatContextCheckpointReceiptVerifier.validateSummaryObject,不是文章凭经验发明的规则。

由此建立至少三个可检查条件:

  1. 引用的 turn 必须属于本次提供给 Compactor 的连续来源范围;
  2. coveredThroughTurnOrdinal 必须与被压缩前缀的实际终点一致;
  3. source digest 与本地 Canonical Transcript 不一致时,旧 Checkpoint 不得继续生效。

这里需要区分三个经常混淆的 digest:

Digest 对什么计算 解决什么问题
sourcePrefixDigest / cumulative source digest Canonical turn 投影的连续前缀摘要链 历史内容、选中 candidate 或顺序变化后,旧 coverage 是否还对应同一事实源
summaryDigest 规范化后的结构化 summary JSON receipt 签过的摘要是否被替换或损坏
request fingerprint / snapshot digest 本次冻结 semantic request 或 Context Snapshot 同 generation 重放的请求字节是否发生变化

Digest 只证明“内容是否与已绑定版本一致”,不证明内容本身为真。攻击者如果能同时替换正文和一个未受信任的 digest,单独的 hash 没有意义,所以 Checkpoint 还需要 receipt:Gateway 使用受信任签名密钥签发的紧凑凭证,把 generation、owner reference、environment、conversation、epoch、lineage、coverage、source digest、summary digest 和策略版本绑定在一起。App 使用缓存的公开验证钥校验签名和全部 claims;任一 binding 不匹配,candidate 都不能 staged。

Checkpoint 还被明确视为低权限、不可信的 conversation data。它进入 Answer Prompt 时会处于数据边界内,不能成为 Agent/system instruction。原因很现实:用户历史中可能包含“忽略之前规则”之类文本,摘要模型也可能把它保留下来。如果把摘要提升为 system prompt,就等于通过 Memory 通道完成权限升级。

Lineage 可以理解为“这份摘要属于哪一条连续历史谱系”。如果用户清空会话、删除覆盖范围内的消息,或者历史连续性被破坏,旧摘要即使 JSON 完美、签名也曾有效,也不再属于当前 lineage。

因此,结构化摘要并不能消除幻觉,但它把“摘要好不好”拆成两类问题:语义质量仍需评测;schema、大小、范围、引用、digest、签名和 lineage 则可以确定性校验。系统不能证明模型总结得完全正确,却能证明一份来源越界、已被篡改或属于旧会话状态的摘要绝不会获得写权限。

五、一次自动压缩究竟如何发生

TXChatDemo 把 Context 规划、商业准入和 Provider 执行分成不同边界。为了便于公开分享,下面把私有服务统一称为 Gateway/Control Plane、Execution Plane 和 Billing Store。

这里的 owner 指“对某类状态拥有最终解释权的组件”,不是代码作者。例如 App 是本地对话语义的 owner,Execution Plane 是权威 token plan 和实际执行分支的 owner,Billing Store 是 reservation/settlement 的 owner。

flowchart TB
    subgraph App["TXChatDemo App"]
        Transcript["Canonical Transcript"]
        CheckpointStore["Protected Checkpoint Store"]
        Journal["Frozen Generation Journal"]
        Assembler["Context Assembler"]
    end

    subgraph Gateway["Gateway / Control Plane"]
        Authority["Identity + Agent Authority"]
        Policy["Policy + Pricing"]
        Admission["Admission + Settlement"]
    end

    subgraph Execution["Execution Plane"]
        Planner["Provider-aware Token Planner"]
        Compactor["Compactor"]
        Runner["Durable Operation Runner"]
    end

    Billing["Billing Store"]
    Provider["Answer Provider"]

    Transcript --> Assembler
    CheckpointStore --> Assembler
    Assembler --> Journal
    Journal --> Authority
    Authority --> Planner
    Policy --> Admission
    Planner --> Admission
    Admission <--> Billing
    Admission --> Runner
    Runner --> Compactor
    Runner --> Provider
    Runner --> Admission
    Admission --> Journal
    Journal --> CheckpointStore

存储所有权也刻意不对称:Checkpoint 正文只在 App 的受保护本地存储中长期保存;Gateway 与 Billing Store 只保留状态、版本、digest 和数字 usage;Execution Plane 只在 operation 生命周期内保存加密 candidate。服务端在执行压缩时仍会短暂处理会话内容,因此这不是“服务端从未见过正文”的零知识设计,但它避免把完整 Checkpoint 正文变成新的长期云端历史副本。Prompt、Answer、Checkpoint 正文、receipt 和原始用户标识也不得进入普通日志或指标。

完整过程可以分成九步。

1. 持久化并冻结

App 先提交当前 user turn,再从 Canonical Transcript 组装 Context Envelope。Envelope 本身包含 conversation ID、epoch、lineage、source cutoff、source digest、可空 Checkpoint、连续 exact turns 和 current input;账号与环境 scope 由 Envelope 外的已认证 Operation identity 绑定。

这里的 source cutoff 就是 source_turn_ordinal:本轮只允许观察到哪个 current user turn。冻结表示完整 semantic request 编码成确定字节,计算 fingerprint,并与 Generation journal 一起持久化;它不是把一个 Swift 数组留在内存里。项目的 savePrepared 会先保存 frozen request,失败则不发送 create,从而避免出现“服务端可能已经执行,但 App 没有可恢复请求”的窗口。

2. 校验身份与指令所有权

Gateway 校验用户、环境、会话和 Checkpoint receipt,并根据 Agent ID 注入 Agent authority,即由服务端拥有的高权限行为指令。App 不能在公共 Context 中自行声明 systemdeveloper role,避免客户端把普通会话数据升级为高权限提示词。

这里的 scope 是 owner、backend environment 与 conversation 的组合边界;同一个 conversation ID 在不同账号或环境下不是同一个 scope。owner reference 是服务端为已认证 owner/environment 生成的不可直接还原标识,用于 receipt 和策略 binding,不等于原始用户 ID。

3. 使用最终 Provider adapter 规划 token

App 可以做保守估算,用于提前展示首次压缩的用户告知,但它不知道服务端指令、Provider reserve 和最终 tokenizer。是否越过水位、请求是否适配模型窗口、Answer output 需要预留多少空间,都由 Execution Plane 的最终 Provider adapter 权威计算。

低于高水位且没有结构预警时,Automatic 模式直接回答。超过水位或达到消息数/字节结构预警时,才进入 Compaction 规划。类似“输入上限的一定比例触发、压缩后回到更低软目标、保留一段 recent exact”这样的参数属于版本化 bootstrap policy,不应被写死成行业常量。

相关术语可以这样理解:

术语 含义
Provider adapter 把统一语义请求渲染成特定 Provider 请求,并使用匹配该请求形态的 tokenizer/count API
Effective Input Cap 结合模型窗口、产品限制和输出预留后,本次允许使用的有效输入上限
High Watermark 进入压缩评估的软水位;越过它不等于已经超过模型硬上限
Post-compaction Target 压缩后希望把 Answer 输入控制到的较低目标,为继续对话留出空间
Recent-exact Budget 给近期连续原文保留的预算,不是固定 turn 数
Structural Preemption 即使 token 估计尚可,消息数或序列化字节数已接近结构上限,提前进入规划或拒绝
Output Reserve 为模型回答预留的输出空间,不能全部让历史输入占满

App 的 ChatContextConservativeTokenEstimator 只负责安全的本地组装边界;Execution Plane 的 authoritative counter 才是准入、最终窗口适配和 Credits projection 的依据。两者版本会分别记录,不能用客户端估算值冒充 Provider 计数。

4. 同时冻结主路径与回退路径

Automatic plan 不是只生成一个 Compaction 请求,而是同时冻结:

  • Primary:context_compaction + answer
  • Fallback:只使用有界 recent exact turns 的 answer

Primary 是优先执行的计划;在这里包含 context_compactionanswer 两个内部 component。Fallback 是同一 Generation 中预先批准的备用 Answer 分支。recent exact 指保持原文、按完整 turn 组成的最新连续后缀;它不是向量检索结果,也不是按“重要性”抽样出的若干消息。

这一点决定了失败是否可控。如果 Compactor 失败后才临时重新查询历史、重新估算 token 或临时选择另一模型,fallback 已经不是同一个 generation 的预定分支,而是一次新的、未审计决策。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function planAndExecute(frozenContext, authority, policy):
counts = providerAdapter.count(frozenContext + authority)

if counts.belowHighWatermark and not structurallyPreempted:
return executeDirectAnswer()

primary = freeze(compactionRequest, compactedAnswerTemplate)
fallback = freeze(recentExactAnswerRequest)
reservation = admit(primary, fallback)

result = executeCompactor(primary.compactionRequest)
if result is invalid or checkpoint signing is unavailable:
markCompactionAsSystemAbsorbed()
return execute(fallback)

return execute(primary.answerTemplate.with(result.checkpoint))

5. 原子准入与一次父结算

Compaction 和 Answer 是一个持久 Generation Operation 的两个内部 component,而不是两个彼此独立的用户操作。Billing Store 在原子准入阶段选择主计划或回退计划,并建立一个父 reservation;终态只进行一次 settlement。

  • Admission(准入):在 Provider 执行前,验证身份、计划、能力、预算与幂等状态,决定本次 operation 是否允许开始。
  • Reservation(预留):为已选择计划冻结可用 Credits 上界,不等于已经扣除最终费用。
  • Component:同一 Generation 内可单独记录 usage 的执行阶段,例如 context_compactionanswer
  • Settlement(结算):根据实际使用的 component、Provider usage 和结果可见性,幂等地核销或释放父 reservation。
  • Fail closed:当身份、计数、价格或持久状态不确定时拒绝推进,而不是猜一个“可能正确”的值继续执行。

只有“余额明确不足”之类已定义业务条件才能选择较低成本分支。数据库状态不明、pricing 不可用或 token projection 不可信时必须 fail closed,不能把系统不确定性伪装成“自动降级成功”。

6. 先压缩,再回答

Compactor 输入只包含上一个 Checkpoint 和它之后需要被覆盖的连续 exact turns。新 Checkpoint 通过结构校验后,被放入预先冻结的 Answer 模板;近期原文和当前输入仍然保留。这样 Answer 同时获得“较早历史的压缩状态”和“近期对话的逐字细节”。

Compactor 是负责生成结构化 Checkpoint 的模型调用或执行组件;它不拥有 active memory 写权限。Reassembly 是把验证通过的 candidate 插入冻结 Answer 模板后重新渲染并再次计数的步骤。摘要可能比预期更长,所以“Compactor 成功”之后仍可能因为 reassembly 超过硬上限而切换 fallback。

7. 返回的只是 Candidate

Compactor 的原始 summary 先经过结构校验,再由 Gateway 建立绑定 scope、lineage、coverage、source digest、summary digest 和策略版本的 receipt;签名或绑定失败时,这份 summary 不得进入 Primary Answer。即使 signed candidate 中的规范化 summary 已参与 Answer 且 Answer 已经生成,新 Checkpoint 仍然没有资格立刻成为 active memory。

candidate 表示“服务端提出了一份可供验证的副作用”,不是“服务端替 App 改好了本地记忆”。receipt 证明受信任 Gateway 接受并绑定了这份 candidate;它也不能替代 App 对本地 Canonical Transcript digest、当前 preference 和 active version 的校验。

8. Canonical Result 与结算先收敛

App 必须先把可见 Answer 持久化到 Canonical Transcript,服务端结算也必须到达允许激活的终态。页面是否仍然打开不是条件:页面离开只是 detach presentation,不应该让持久 generation、结果提交或 Memory side effect 消失。

Canonical Result 是可重复读取、不可变的 Generation 终态结果,不是 SSE 中某个临时增量。SSE candidate 可以用于低延迟提示,但项目代码要求只有 immutable result 确认过的 Checkpoint source 才能进入受保护 Checkpoint storage。Canonical projection 则是 App 把选中的 Answer 提交进本地 Message 与 Canonical Transcript 后形成的会话投影;Result 存在和本地投影完成是两个阶段。

9. 最后通过 CAS 激活

App 验证 receipt、summary digest、source digest 和 coverage,把 candidate 存为 staged。只有当前 transcript revision 在提交事务期间没有变化、冻结的 memory preference revision 仍匹配、epoch/lineage 仍有效、Canonical Result 已提交、billing 已 settled、本地 Answer 仍被 selected,才执行 compare-and-swap,把 staged Checkpoint 提升为 active。

CAS(compare-and-swap) 可以直译为“比较后再替换”:只有数据库里当前值仍等于调用方刚刚读取和预期的值,才允许原子更新。项目的 repository 会在同一事务中重新检查 transcript/preference revision、epoch、lineage、staged 状态、coverage 单调前进和 source digest,然后才替换 activeCheckpointID。CAS 防的是读取校验之后、真正写入之前又有并发更新,而 source digest、epoch 和 lineage 共同防止更早的语义变化被忽略。

sequenceDiagram
    participant App as TXChatDemo App
    participant Gateway as Gateway / Control Plane
    participant Exec as Execution Plane
    participant Billing as Billing Store
    participant Compact as Compactor
    participant Answer as Answer Provider

    App->>App: persist current turn + freeze context
    App->>Gateway: immutable context envelope
    Gateway->>Exec: plan with authority and provider template
    Exec-->>Gateway: primary + frozen fallback + token projection
    Gateway->>Billing: atomic admission / parent reservation
    Billing-->>Gateway: selected plan
    Gateway->>Exec: admit selected immutable plan
    Exec->>Compact: optional compaction
    alt compaction valid and signed
        Compact-->>Exec: structured checkpoint
        Exec->>Answer: checkpoint + recent exact + current input
    else invalid, unavailable, or uncertain
        Exec->>Answer: frozen recent-exact fallback
    end
    Answer-->>Exec: terminal answer and usage
    Exec-->>Gateway: terminal components
    Gateway->>Billing: settle parent reservation once
    Gateway-->>App: canonical result + candidate + billing state
    App->>App: verify → stage → CAS activate

用户实际场景:协作撰写一篇移动端故障复盘文章

抽象术语放进真实任务里更容易理解。下面假设一位移动端工程师正在和 TXChatDemo 协作撰写一篇故障复盘文章:对话先梳理复现条件、调查范围和修复约束,随后逐步补充日志结论、纠正根因判断并核对验证数据。为方便演示,token、水位和摘要大小均为教学用近似值,不代表通用常量,也不表示 TXChatDemo 当前生产配置。

先用一张图建立全局视角。后面的九个小节会依次展开图中的冻结、规划、压缩、回退、回答和激活步骤:

flowchart TB
    subgraph Source["① 冻结时的会话状态"]
        CP30["Active CP-30<br/>覆盖 turn 1...30"]
        Exact["Exact turn 31...48<br/>近期原文"]
        Current["turn 49 current input<br/>重写根因与修复验证"]
    end

    Freeze["冻结 Generation G-49-A<br/>固定语义输入"]
    Plan["Provider-aware token plan<br/>同时冻结 Primary 与 Fallback"]

    subgraph Execute["② 两条预定执行路径"]
        Compact["Compactor 输入<br/>CP-30 + turn 31...42"]
        Validate{"candidate 工程校验通过?"}
        CP42["CP-42 candidate<br/>覆盖 turn 1...42"]
        Primary["Primary Answer<br/>CP-42 + turn 43...48 + turn 49"]
        Fallback["recent-exact fallback<br/>turn 43...48 + turn 49"]
    end

    Answer["Answer Provider<br/>生成可见回答"]
    Result["Canonical Result<br/>回答先持久化"]

    subgraph Commit["③ Checkpoint 副作用提交"]
        HasCandidate{"Result 携带<br/>CP-42 candidate?"}
        Activate["App 复核 → staged → CAS"]
        Active42["CP-42 成为 active<br/>供下一轮使用"]
        Keep30["CP-30 保持 active<br/>本轮仍有回答"]
    end

    CP30 --> Freeze
    Exact --> Freeze
    Current --> Freeze
    Freeze --> Plan
    Plan --> Compact
    Plan --> Fallback
    Compact --> Validate
    Validate -- "是" --> CP42
    Validate -- "否或结果不明" --> Fallback
    CP42 --> Primary
    Primary --> Answer
    Fallback --> Answer
    Answer --> Result
    Result --> HasCandidate
    HasCandidate -- "是" --> Activate
    HasCandidate -- "否" --> Keep30
    Activate --> Active42

    classDef source fill:#e6f4ff,stroke:#1677ff,color:#002766;
    classDef frozen fill:#f9f0ff,stroke:#722ed1,color:#22075e;
    classDef primary fill:#f6ffed,stroke:#52c41a,color:#135200;
    classDef fallback fill:#fffbe6,stroke:#d4b106,color:#613400;
    classDef terminal fill:#f5f5f5,stroke:#595959,color:#262626;
    class CP30,Exact,Current source;
    class Freeze,Plan frozen;
    class CP42,Primary,Active42 primary;
    class Fallback,Keep30 fallback;
    class Answer,Result terminal;

这张图有两个阅读重点:Compaction 失败只会让本轮切换到预先冻结的 recent-exact fallback,不会阻塞 Answer;而 CP-42 candidate 即使参与了本轮回答,也要等 Canonical Result 落地并通过 App 复核、暂存和 CAS 后,才能成为下一轮可读的 active Checkpoint。

0. 用户已经和 TXChatDemo 讨论了什么

假设会话已完成 48 个 turn。完整的 1~48 轮都保存在 Canonical Transcript;当前 active Checkpoint CP-30 覆盖连续旧前缀 1~30,turn 31~48 仍以 exact turns 保存:

状态 内容 是否保留原文 在本例中的作用
Canonical Transcript turn 1~48 保留全部讨论、纠正过程和回答
Active Checkpoint CP-30 turn 1~30 的结构化摘要 摘要代替 Prompt 中的旧原文 保存故障边界、调查范围和修复约束
Exact Turns turn 31~48 保存近期调查结论和逐字修正
Transcript Revision r48 不适用 表示组装前观察到的会话版本
Memory Preference Revision m7 不适用 表示用户仍允许自动会话压缩

对话中的关键信息分布如下:

1
2
3
4
5
6
7
8
9
turn 3:故障只在设备长时间断网后回到前台时出现,短时网络抖动无法复现。
turn 9:复盘范围限定为“连接恢复成功但消息列表停住”,不扩展到登录和推送链路。
turn 17:证据只能引用脱敏日志、状态迁移和聚合指标,不披露账号或设备标识。
turn 26:修复必须同时覆盖前台恢复、后台重连和重复回调三个入口。

turn 41:已经确认恢复流程中存在 cursor 与本地 revision 竞争,仍有两项指标待核对。
turn 45:用户纠正:主因不是网络超时,而是旧 cursor 被恢复任务再次提交。
turn 47:用户给出已脱敏的验证结论:聚焦测试通过,长断网场景不再卡住。
turn 48:双方确认接下来重写“根因”和“修复验证”两节。

CP-30 应保留 turn 3、9、17、26 中仍有效的复现边界、调查范围、证据约束和修复要求;turn 41、45、47、48 尚未被它覆盖,必须继续保留原文。尤其是 turn 45 的“不是网络超时”属于刚刚发生的事实纠正,如果只保留一份过旧摘要,模型很容易沿用已经被否定的根因。

这里仍然没有删除 turn 1~30。CP-30 只是下一次 Prompt 对旧前缀的压缩表示,完整原文继续存在 Canonical Transcript。

1. 当前用户请求先持久化,再冻结 Generation

用户现在发送:

1
2
turn 49 user:请根据刚刚确认的结论,重写“根因”和“修复验证”两节;
明确主因不是网络超时,不要泄露账号或设备标识,最后给出可发布的结论。

App 先把 turn 49 提交到 Canonical Transcript,使 transcript revision 从 r48 推进到 r49,然后组装本次 Context Envelope:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Context Envelope 内:
conversation_id:当前复盘文章会话
epoch / lineage:E3 / L9
source_turn_ordinal:49
source_prefix_digest:Digest(Canonical projection through turn 49)
preference_revision:m7
checkpoint:CP-30,覆盖 turn 1...30
exact_turns:turn 31...48
current_input:turn 49 user message

Envelope 外、但绑定同一 Operation:
generation_id:G-49-A
owner / environment:来自已认证 scope
request fingerprint:完整 frozen request 的摘要

App 本地并发状态:
transcript revision:组装时为 r49,但它不是 Envelope wire 字段

冻结之后,即使用户又发送 turn 50 补充了另一份日志,G-49-A 也不能偷偷把它加入本轮输入。turn 50 应属于下一次 Generation;否则同一个 generation ID 在重试前后就可能代表不同请求。

2. Execution Plane 判断是否需要压缩

Gateway 校验 scope、epoch、lineage 和策略版本,并注入权威 Agent 指令。Execution Plane 使用最终 Answer Provider adapter 对实际请求模板计数。假设本次策略得到以下示意预算:

1
2
3
4
Effective Input Cap:16,000 tokens
High Watermark:12,000 tokens
Post-compaction Target:约 9,000 tokens
Recent-exact Target:约 4,000 tokens

直接回答路径的测量结果是:

Direct 请求组成 示意 token 数
权威 Agent 指令 800
CP-30 1,300
turn 31~48 原文 10,250
turn 49 当前输入及协议开销 650
合计 13,000

13,000 尚未超过 16,000 的硬上限,却已经越过 12,000 的高水位,因此 Planner 选择提前压缩。这样做不是为了追求最低 token 数,而是给 Answer 输出、Provider 模板差异和会话继续增长留出余量。

3. 为什么必须按连续分界切开 exact turns

Planner 不能只挑出 turn 41、45、47 这些“看起来重要”的轮次,再丢掉中间内容。那样会让 Checkpoint 的 coverage 声称与真实来源脱节。它只能找到一个分界点:分界点之前的连续前缀进入 Compactor,分界点之后的连续后缀逐字保留。

使用最终 Provider counter 从最新 turn 向前扩展 recent-exact 后缀,假设计数结果为:

候选 recent-exact fallback 计数结果 决策
权威指令 + turn 45~48 + turn 49 2,850 可容纳,继续向前扩展
权威指令 + turn 43~48 + turn 49 3,760 可容纳
权威指令 + turn 42~48 + turn 49 4,150 超过 recent-exact 目标

最终得到:

1
2
3
4
5
旧 Checkpoint:CP-30,代表 turn 1...30
本次 Compacted Prefix:turn 31...42
Protected Recent Exact:turn 43...48
Current Input:turn 49
预期新 Candidate:CP-42,连续覆盖 turn 1...42

这个分界很有业务含义:turn 41 的调查状态会随 turn 31~42 一起进入新摘要;turn 45 对根因的明确纠正、turn 47 的验证结论和 turn 48 的写作动作仍保留原文。它们不会因为“摘要看起来已经足够”而被改写成近似说法。

4. Compaction 主路径和 fallback 在执行前同时冻结

真正调用 Compactor 之前,系统已经准备好两条不可变分支:

1
2
3
4
5
6
7
G-49-A
├── Primary:automatic_compaction
│ ├── compact:CP-30 + turn 31...42 → CP-42 candidate
│ └── answer:CP-42 + turn 43...48 + turn 49

└── Frozen Fallback:recent_exact
└── answer:turn 43...48 + turn 49

Primary 可以同时获得两类信息:

  • CP-42 读取故障的复现边界、调查范围、证据约束以及已确认到哪一步;
  • 从 turn 43~48 原文读取“主因不是网络超时”这一最新纠正,以及刚确认的验证结论。

Fallback 则主动放弃较早 Checkpoint,只保证使用已经冻结的近期原文回答。它的上下文较少,但不会在 Compactor 失败后临时查询历史、切换模型或重新选择 turn。

两条 Answer 路径使用同一个 Answer 模型,并在 admission 前一起进入 token projection、reservation 和 billing projection。Fallback 因而是同一个 G-49-A 的预定分支,不是失败后新发起的一次未审计 Generation。

5A. Compactor 成功后,先产生了什么,Answer 模型最终看到了什么

这里的“模型”需要先拆成两个角色,否则很容易误以为同一个模型既总结历史、又回答用户:

角色 本例输入 输出 明确不负责什么
Compactor CP-30 + turn 31~42 结构化 Checkpoint summary 不回答 turn 49,不生成可信签名,也不能激活本地 Memory
Answer Provider 权威 Agent 指令、已校验的低权限 summary、turn 43~48 原文和 turn 49 用户最终看到的文章内容 不决定 candidate 是否能 staged 或 active

Compactor 的原始模型输出对应下面 JSON 中的 summary 对象。Execution Plane 会先规范化和校验它,再由 Gateway/Control Plane 添加 Checkpoint 身份、digest 和签名 receipt,形成交给 App 的完整 candidate。因此,下面展示的是 App-facing signed candidate,不是 Compactor 模型能够独立生成的原始结果。UUID、digest 和签名继续使用公开占位符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"checkpoint_id": "<uuid-v4>",
"schema_version": "checkpoint-v1",
"summary": {
"schema_version": "checkpoint-v1",
"conversation_goal": [
{
"text": "复盘长时间断网恢复后消息列表停住的问题,并将调查范围限定在恢复链路",
"source_turn_ordinals": [9]
}
],
"confirmed_facts": [
{
"text": "故障只在设备长时间断网后回到前台时出现,短时网络抖动无法复现",
"source_turn_ordinals": [3]
},
{
"text": "恢复流程存在 cursor 与本地 revision 的竞争,两项指标仍待核对",
"source_turn_ordinals": [41]
}
],
"decisions_and_constraints": [
{
"text": "证据只能引用脱敏日志、状态迁移和聚合指标,不披露账号或设备标识",
"source_turn_ordinals": [17]
},
{
"text": "修复必须同时覆盖前台恢复、后台重连和重复回调三个入口",
"source_turn_ordinals": [26]
}
],
"user_preferences": [],
"unresolved_items": [
{
"text": "核对尚未确认的两项指标后再形成最终结论",
"source_turn_ordinals": [41]
}
]
},
"summary_digest": "<sha256-of-normalized-summary>",
"source_digest": "<cumulative-source-digest-through-turn-42>",
"covered_through_turn_ordinal": 42,
"receipt": "<signed-compact-receipt>",
"receipt_key_id": "<verification-key-id>"
}

先看 summary 内部。项目代码把每一条记忆表示为 ChatContextSummaryItemtext 是压缩后的陈述,source_turn_ordinals 是这条陈述来自哪些 Canonical turn 的位置。五个数组不是五种权限,而是让后续模型和校验器更容易区分信息用途:

JSON 字段 中文含义 本例内容 容易误解的边界
conversation_goal 当前会话仍在推进的目标 复盘长时间断网恢复后消息列表停住的问题 它说明“正在完成什么任务”,不证明任何故障事实
confirmed_facts 已在本次对话中确认、后续可以继续使用的事实 长断网后回前台才能复现;cursor 与 revision 存在竞争 confirmed 只表示会话内已确认,不等于系统做了外部事实认证,也不保证 Compactor 不会总结错误
decisions_and_constraints 已作出的决定以及后续输出必须遵守的约束 证据必须脱敏;修复覆盖三个恢复入口 它仍是低权限会话数据,不能覆盖 Agent/system 指令
user_preferences 用户明确表达、希望后续延续的内容或表达偏好 本例为空 它不是 App 的 Memory 开关、计费同意或账号设置;来源中没有偏好时不能为了填字段而编造
unresolved_items 仍未解决、需要后续追踪的问题 两项聚合指标仍待核对 它表示待办或不确定项,不能混入 confirmed_facts 当作结论

以这一项为例:

1
2
3
4
{
"text": "恢复流程存在 cursor 与本地 revision 的竞争,两项指标仍待核对",
"source_turn_ordinals": [41]
}

它表达的是“这条压缩陈述来源于 turn 41”。source_turn_ordinals 不是原文副本,也不是脚注式的真实性证明;它只是可校验的来源定位。服务端可以据此拒绝引用 Compactor 从未见过的 turn,App 也能检查引用是否落在当前 lineage base 与 covered_through_turn_ordinal 之间。按照当前 checkpoint-v1 verifier,每条 item 必须有 1~32 个互不重复的来源 ordinal,text 不能为空且最多 4,096 UTF-8 bytes;五类 item 合计不能超过 256 条,规范化 summary 不能超过 64 KiB。这些是当前项目契约限制,不是通用 LLM Memory 常量。

本例没有需要长期保留的用户表达偏好,因此 user_preferences 保持空数组。结构化 schema 要求五个分类字段都存在,不代表每个分类都必须包含内容;但五类合计又不能全部为空,避免产生“格式合法但没有任何记忆”的空 Checkpoint。

再看 summary 外层。这里放的是一致性和可信来源元数据,而不是供 Answer 模型理解业务含义的文章素材:

JSON 字段 含义与作用
checkpoint_id candidate 的 UUIDv4 身份。它用于幂等存储和状态迁移,本身不表示新旧顺序或可信度
外层 schema_version App-facing candidate 的结构版本;内层 summary.schema_version 让 summary 本身也能被独立规范化和校验
summary Compactor 产生并经规范化校验的五类低权限会话记忆,也是 Answer 路径真正需要读取的业务内容
summary_digest 对规范化 summary 计算的 SHA-256。它用于发现摘要内容被改动,但不能证明摘要语义正确
source_digest Canonical Transcript 连续来源前缀的累积 digest。本例绑定 turn 1~42,用来发现原消息或选中 Answer 已经改变
covered_through_turn_ordinal candidate 声称连续覆盖到哪个 turn。本例为 42,表示它可以在 Prompt 中替代 lineage base 之后到 turn 42 的旧前缀,而不是只覆盖被显式引用的几个 turn
receipt Gateway/Control Plane 签发的紧凑 Ed25519 receipt,把 checkpoint、generation、scope、epoch、lineage、coverage、两个 digest 和策略版本绑定起来
receipt_key_id 指示 App 应使用哪把已缓存的公开验证钥验签;它不是私钥,也不单独代表 receipt 有效

因此,三个经常混淆的字段分别回答三个不同问题:

1
2
3
source_turn_ordinals:某一条摘要陈述“引用了哪些 turn?”
covered_through_turn_ordinal:整个 Checkpoint“连续替代历史到哪里?”
source_digest:被替代的连续来源前缀“现在是否仍与生成时一致?”

这份 JSON 即使语句通顺,也必须先通过工程校验:

  1. covered_through_turn_ordinal 必须恰好是本次连续压缩前缀的终点 42;
  2. source_turn_ordinals 只能引用旧 CP-30 已绑定的来源,或本次明确提供的 turn 31~42;
  3. source_digest 必须与本地 Canonical Transcript 的 turn 1~42 连续前缀一致;
  4. schema、字段大小、摘要 digest、receipt 和策略绑定都必须有效。

校验通过后,Execution Plane 不会把 receiptreceipt_key_id 等控制元数据当作 Prompt 事实交给 Answer Provider,而是把规范化的 summary 包在明确标记为“不可信会话数据”的 user-role 数据块中,插入冻结的 Answer 模板,再使用最终 Provider adapter 重新计数:

Primary Answer 请求组成 示意 token 数
权威 Agent 指令 800
CP-42 规范化 summary 数据块 1,550
turn 43~48 原文 2,350
turn 49 当前输入及协议开销 650
合计 5,350

最终 Answer Provider 看到的是:

1
2
3
4
权威 Agent 指令
→ 低权限结构化历史摘要 CP-42
→ turn 43...48 原文
→ turn 49 当前写作要求

这里的“低权限”非常重要:summary 在 Provider 请求中是带边界标记的 user-role conversation data,而不是 Agent/system instruction。于是 Answer Provider 既能读取故障出现的边界、调查范围、证据脱敏要求和修复覆盖面,也能逐字看到近期更正:主因不是网络超时。Checkpoint 提供稳定的旧事实与约束,recent exact 提供不能被摘要模糊的最新纠正。

5B. Compactor 失败时,文章还能不能继续写

如果 Compactor 返回非法 JSON、输出截断、coverage 不等于 42、引用越界、receipt 签名失败,或者 reassembly 后最终 token 超过已准入上限,系统会放弃 candidate,直接执行预先冻结的 fallback:

1
2
3
4
权威 Agent 指令
+ exact turn 43...48
+ current input turn 49
→ Answer Provider

这个 fallback 仍然能看到 turn 45 的根因纠正、turn 47 的验证结论和 turn 49 的当前要求,因此本轮大概率仍可完成“重写根因与验证”这一核心任务。代价是它暂时看不到 CP-30 中较早的复现边界、调查排除范围和证据约束,回答质量可能下降;这是可用性与历史完整度之间被明确记录的取舍,而不是假装没有损失。

如果 Compactor 运行期间用户又提交了 turn 50,fallback 仍不能把 turn 50 加进来。它必须继续使用 G-49-A 已冻结的 turn 43~49,否则 fallback 就从预定分支变成了一次新的上下文决策。

5C. Demo示例

6. Answer 成功之后,CP-42 为什么还不能立即生效

在调用 Answer Provider 之前,CP-42 已经完成结构校验和 Gateway receipt 签发,否则系统会直接使用 fallback。Answer Provider 返回文章段落后,这份 signed candidate 仍然只是“随 Generation 返回、等待 App 提交的副作用”;App 必须从 immutable Canonical Result 确认它,再验证 receipt 和本地来源绑定并保存为 staged

激活前还要确认:

1
2
3
4
5
6
激活事务观察到的 Transcript Revision 是否仍符合预期?
Memory Preference Revision 仍是 m7 吗?
Epoch / Lineage 仍是 E3 / L9 吗?
G-49-A 的 Answer 已成为 Canonical Result 吗?
Billing 是否到达允许提交副作用的状态?
当前 active Checkpoint 是否仍是预期的 CP-30?

全部成立,CAS 才能把 CP-42staged 提升为 active。下一轮可以从下面的投影继续:

1
2
3
Active Checkpoint:CP-42,覆盖 turn 1...42
Exact Turns:turn 43...49
Current Input:下一条 user message

如果等待期间另一条 Generation 已把 active coverage 推进到 turn 44,或者用户关闭了自动记忆,使 preference revision 从 m7 变成 m8,本次 CAS 就会失败。文章回答仍可正常保存,但过期的 CP-42 不能倒退 coverage,也不能绕过用户的新设置。

7. 用这个写作场景回看整条链路

这个案例真正说明的是:

  • 完整的 turn 1~49 始终保存在 Canonical Transcript;压缩的是本轮 Prompt 投影,不是故障调查记录。
  • CP-42 保存较稳定的文章目标、受众和边界;turn 43~48 保留近期纠错与验证结论,两者承担不同信息密度。
  • Checkpoint 只能覆盖连续旧前缀,recent exact 只能保留连续最新后缀,不能为了“挑重点”在历史中挖洞。
  • Primary 与 fallback 在调用 Compactor 前同时冻结;失败后不重新查询历史,也不把 turn 50 混入 G-49-A
  • Candidate 内容合格只代表可以暂存;只有 result、billing、revision、preference 和 CAS 条件共同成立,才能成为下一轮 active memory。
  • Answer 与 Memory 推进可以分别成功或失败:本轮文章可以写出来,同时拒绝一次不安全、过期或无法验证的 Checkpoint 更新。

从用户视角看,这只是一次“继续帮我写文章”;从系统视角看,它同时涉及事实源、冻结输入、两条执行分支、Provider 计数、摘要验证和并发激活。Memory 压缩之所以是工程问题,正是因为这些状态不能靠“摘要看起来不错”来替代。

六、Candidate 为什么不能直接写成 Active Memory

Memory 压缩最危险的误区,是把“模型成功生成摘要”当成“记忆已经成功更新”。两者之间至少还隔着以下问题:

  • 摘要是否对应这次 generation,而不是过期响应;
  • 它覆盖的历史是否仍与本地 Transcript 一致;
  • 用户是否在生成期间关闭了自动记忆;
  • 当前 Answer 是否真的被持久化并被选为会话的 canonical 结果;
  • billing 是否进入允许提交 Memory side effect 的终态;
  • 是否有另一个并发 generation 已经推进了 active Checkpoint。

因此 Checkpoint 有独立状态机:

状态 含义 能否被下一轮 Assembler 使用
candidate_available immutable result 中发现了候选,但尚未完成本地验证
staged 签名、binding、schema、summary/source digest 和 coverage 已通过,正文进入受保护存储
active result、billing、本地选择与 CAS 条件全部成立
superseded 更高 coverage 的新 Checkpoint 已原子取代它
invalid / rejected 内容、签名、本地完整性或绑定失败
conflict SSE 与 immutable result 或重复 candidate 之间出现不可调和分歧

这里的 side effect 指 Answer 之外附带发生的状态变化,例如写入 Checkpoint。它与 Answer 分开提交:Answer 可以成功成为 Canonical Result,而 Checkpoint 因 Keychain、受保护存储或签名验证暂时不可用进入受控恢复;明确无效时则永久拒绝。

stateDiagram-v2
    [*] --> None
    None --> Staged: candidate 验证通过
    Staged --> Active: result + billing + revision CAS 成功
    Staged --> Invalid: digest / coverage / receipt 失败
    Staged --> Invalid: scope、epoch 或 lineage 过期
    Active --> Superseded: 更新且覆盖范围单调前进
    Active --> Invalid: 本地完整性校验失败
    Active --> None: clear / delete / lineage reset
    Superseded --> [*]
    Invalid --> [*]

激活逻辑可概括为下面的条件伪代码;实际 Swift 实现把 operation eligibility 与 repository 事务校验分在两层:

1
2
3
4
5
6
7
8
9
10
11
function activateCheckpointIfEligible(candidate, operation, expected):
require candidate.status == staged
require operation.canonicalResult == committed
require operation.billing.permitsCheckpointActivation
require operation.localProjection == selected
require currentPreference == expected.preferenceRevision
require currentTranscript == expected.transcriptRevision
require candidate.scopeAndLineageMatchCurrentState
require digest(transcript.prefix(candidate.coverage)) == candidate.sourceDigest

compareAndSwap(activeCheckpoint, candidate)

CAS 不是为了炫技。它保护的是一个非常具体的竞态:App 读取当前 active/revision 并完成校验后、事务真正写入前,另一个线程可能已经更新了状态。与此同时,Compaction 执行期间还可能发生新的 generation、设置变化或历史重置;这些更大范围的变化由 preference revision、epoch、lineage、coverage 和 source digest 一起阻断。只检查“candidate 创建时间更晚”并不能证明它仍适合当前会话。

七、失败为什么不能破坏会话

长对话压缩不是主回答能力本身,而是一个 correctness-bearing、non-blocking side effect:它会影响下一轮模型看到的历史,所以关系到正确性;但它不是本轮 Answer 的必要前提,失败时不能污染 Transcript,也不应该让普通聊天失去可用性。

以下情况都不能把 Compactor 输出送进 Answer Prompt:

  • 空输出或非 JSON;
  • 因长度限制产生的截断 JSON;
  • schema 或字段上限不合法;
  • item 引用了输入范围之外的 turn;
  • summary/source digest 不匹配;
  • token counter 不可用;
  • receipt 签名或 key 不可用;
  • Provider 返回结果是否执行成功无法确定。

这些分支统一使用准入前已经冻结的 recent-exact fallback。系统不会在同一个 operation 里更换 Compactor、重新猜测历史或把半份摘要“尽量用上”。System absorbed 表示某个 Compaction component 已发生或留下 usage,但结果没有进入用户 Answer,费用由系统承担;如果最终没有可见 Answer,用户的实际 Compaction 成本为零。

当用户关闭 Memory 时,系统不再生成或读取 Checkpoint,但不是把整个历史无限发送,而是只发送有界的近期完整轮次。当本地连续性损坏或请求触发结构硬限制时,lineage 进入 degraded_recent_only;此时继续保留完整本地历史,却不再签发声称覆盖旧前缀的新 Checkpoint。项目代码通过关闭后再重新开启 automatic memory 建立新的 lineage ID,并把当前末尾设为新的 lineage base;它不会拿旧摘要跨过缺口继续使用。

三个 requested mode 的区别是:

App mode 产生原因 Checkpoint 行为 对外执行可能看到什么
automatic 用户允许自动 Memory,且本地 lineage 健康 可以读取 active Checkpoint,也可以请求新 Compaction direct、automatic compaction 或准入前 recent exact
recent_exact_only 用户明确关闭自动 Memory 不读、不生成 Checkpoint recent-exact Answer only
degraded_recent_only 用户仍偏好 automatic,但本地连续性或硬限制不允许安全覆盖 不读旧 Checkpoint,不推进 coverage recent-exact Answer only

这三个值是 App 的 requested state。Execution Plane 还会记录 selected state,表示权威规划选中了 direct、automatic compaction 还是 recent exact;终态再记录 effective state,表示实际执行了哪条分支。automatic 请求最终得到 recent_exact_fallback 并不矛盾:请求意图、准入计划和实际结果本来就是三层状态。

这套失败设计的本质是:保留回答能力,但收紧记忆写权限。

压缩也不天然意味着更便宜

如果会话只再继续一两轮,额外调用一次 Compactor 可能比重复携带原文更贵;如果会话持续很久,一个稳定 Checkpoint 才可能通过后续多轮复用摊薄成本。摘要还会增加首轮延迟,并带来事实遗漏、约束弱化和来源漂移风险。因此触发策略不能只看“窗口还能不能装下”,还要同时考虑高水位、结构上限、预期 Answer output、Compactor 健康度、用户授权和商业预算。

同样,不能只比较一次请求的 prompt tokens。完整成本至少包括 Compaction 输入与输出、Answer 输入与输出、失败分支、重复使用次数,以及因摘要质量下降导致的重试成本。TXChatDemo 将 Compaction 与 Answer 放进同一个父 reservation,不是为了宣称压缩一定省钱,而是为了让“用了哪条分支、哪些 component 被实际使用、最终应向谁结算”可以在一个 generation 生命周期内对账。

八、与 Codex、Claude、LangGraph 对比:相似词汇下的不同所有权

横向对比时,最容易犯的错误是看到产品都使用了 “memory”“compact”“checkpoint”,就假设它们解决的是同一层问题。下面只比较公开文档可以证明的能力;未公开的产品内部实现不做推断。

1. Codex 与 OpenAI Responses API 要分开看

OpenAI Responses API Compaction 的公开文档说明了两种 API 形态:在 /responses 请求中配置 context_management.compact_threshold,渲染后的 token 数越过阈值时由服务端自动压缩;或者显式调用 stateless /responses/compact,把完整 context window 交给接口并获得下一窗口。两种形态都可能返回 encrypted compaction item;它是不透明续接状态,不面向人类解释。standalone endpoint 返回的 compacted window 还可能保留其他 item,官方要求把整个返回窗口原样用于下一次 Responses 调用,而不是只摘出其中一个摘要字段。

Codex Config Reference 则公开了 model_auto_compact_token_limit、阈值统计范围 total | body_after_prefix、实验性 compaction prompt 文件,以及 PreCompact / PostCompact hooks 等配置面。这能证明 Codex 允许配置触发阈值、定制部分压缩行为并观察生命周期事件,但不能仅凭这些字段断言 Codex 内部一定直接复用 Responses API,也不能推断其未公开的 coverage、持久化、失败回退或计费状态机

TXChatDemo 与这种产品化 compaction 的主要差异,不是“谁会总结”,而是表示和所有权不同:OpenAI API 公开的是 Provider-owned opaque continuation item;TXChatDemo 选择 App-owned structured Checkpoint,并由自己的跨端协议管理本地完整 Transcript、来源覆盖、receipt、billing 与 CAS 激活。前者集成面更小,后者能按业务 turn 审计“哪段历史以什么状态成为 active memory”,代价是协议复杂度显著增加。

2. Claude 把 Context Editing、Compaction、Memory Tool 分成不同能力

Anthropic 的 Context Editing 可以在服务端按策略清理较旧的 tool results 或 thinking blocks,同时客户端保留完整未修改历史;官方文档把这种选择性 clearing 与摘要式 server-side compaction 区分开。Context Editing 改的是到达 Claude 前的 Provider prompt,客户端不必把自己的完整历史同步成删减后的版本。

Memory Tool 则是另一层:Claude 请求文件操作,应用在自己控制的存储中执行,使信息可以跨 conversation 持久化。官方文档也建议长运行 Agent 可以组合 compaction 与 memory:前者控制 active context,后者保存必须跨摘要继续存在的信息。

这与 TXChatDemo 的概念边界高度一致:会话内 Checkpoint 不等于跨会话 Long-term Memory。不过 TXChatDemo 当前关注普通聊天 turn 的连续覆盖和生成结算;Claude Context Editing 更突出 Agent 工具调用产生的大体积中间结果管理,两者的输入形态和失败责任并不相同。

3. LangGraph 提供原语,应用定义业务语义

LangGraph Memory 明确区分 thread-level short-term memory 与跨 session 的 long-term store。长会话管理可以选择 trim、delete、summarize;checkpointer 则负责保存和恢复 graph state。

这类框架适合快速组合持久化、节点和摘要策略,但它不会替业务自动决定:哪一份摘要具有记忆写权限、摘要覆盖如何绑定业务消息、付费 Compaction 失败时谁承担成本、用户看到的 Answer 与 active memory 应以什么顺序提交。这些仍然需要产品自己的 domain protocol。

4. 放在同一张表里看

维度 TXChatDemo Context V2 Codex / OpenAI 公开能力 Claude 公开能力 LangGraph
主要场景 消费级聊天的会话内连续记忆 长运行对话与 coding harness 上下文 长对话、Agent 工具结果和跨会话文件记忆 可组装 Agent graph 状态与存储
触发权 服务端 Provider-aware plan + 业务门禁 Responses 阈值;Codex 暴露自动压缩配置 服务端阈值/策略或 SDK 控制 应用在 graph/node 中定义
压缩表示 结构化、可解释 Checkpoint Responses compaction item 不透明 摘要或选择性 clearing;Memory Tool 为文件 消息摘要、graph checkpoint、store
完整事实源 App 本地 Canonical Transcript Responses 的 chaining/store 方式由调用方选择;不透明 item 不是人类可审计 Transcript Context Editing 下客户端保留完整历史;Memory Tool 存储由应用控制 Thread state 由 checkpointer 持久化,跨会话数据由 store 持久化
可审计性 coverage、source digest、receipt、状态机 公开 item 主要作为不透明续接状态 可观察 clearing/usage;memory 文件由应用控制 状态和 checkpoint 可查看,语义由应用定义
失败回退 预先冻结 recent-exact Answer 由平台能力和调用方式决定 由策略/API/应用组合决定 由 graph 逻辑决定
跨会话 Memory v1 不支持 不由 compaction 本身直接等同推出 Memory Tool 支持应用侧跨会话存储 Long-term store 支持
计费耦合 Compaction + Answer 一个父 reservation、一次 settlement 使用平台 token/请求计费语义 使用平台 token/请求计费语义 框架不替应用定义商业账本
激活/恢复语义 candidate 经验证、结算和 CAS 后激活;旧 active 可继续使用 Responses 返回下一 context window/item;Codex 未公开同构业务状态机 Compaction/context editing 由 API 管理;Memory 文件写入由应用 handler 执行 graph checkpoint/store 写入和恢复由应用配置

这张表并不是优劣排名。产品化 harness 追求开箱即用和较低集成成本;Agent 框架追求可组合性;TXChatDemo 作为带持久聊天、账号和 Credits 的应用,需要把用户可见结果、上下文、账本与恢复收敛到同一业务生命周期。问题不同,合理的复杂度也不同。

九、哪些原则值得复用

回看整个演进,我认为有七条原则比具体阈值或模型选择更有迁移价值。

1. 完整历史与 Prompt 投影必须分离

UI 展示、历史分页、完整业务记录和本轮 Prompt 是四个不同对象。Prompt 必须从稳定事实源按明确 cutoff 组装,不能从“当前页面恰好加载的数组”推断。

2. 压缩只覆盖连续旧前缀

Checkpoint 必须说明覆盖到哪里;它之后到当前输入之前的 turn 必须连续。缺口不是质量小问题,而是 lineage 不再可信。

3. 近期轮次保留原文,并以 turn 为原子

摘要适合保存目标、事实和约束,但不适合替代所有局部细节。近期原文可以纠正摘要歧义;完整 turn 则避免把 user/assistant 关系切碎。

4. 摘要是低权限数据,不是系统指令

任何来自用户历史或摘要模型的文本都必须处于数据权限。Memory 不应该成为绕过 Agent authority 的隐蔽 prompt-injection 通道。

5. Candidate 不等于 Active Memory

生成成功、结构合法、结果可见、账本终态和记忆激活是不同阶段。把它们合并成一个布尔值,会让并发、重启和迟到响应变得不可解释。

6. 主路径与 fallback 必须同时冻结

真正确定性的降级不是“失败后想一个替代方案”,而是在准入前把两条分支的语义输入、token projection 和成本上界都固定下来。

7. Token policy 应由最终 Provider adapter 决定

客户端估算可以服务 UX,但不能成为准入、窗口适配和计费真相。模型窗口、输出 reserve、tokenizer 和 Provider request rendering 都可能改变,权威计数必须靠近最终调用边界。

术语速查:第一次阅读时可以回到这里

前文已经在首次出现时解释了核心概念,下面按“历史—请求—执行—记忆—账务”重新归类,方便查阅。

术语 一句话解释 不要与什么混淆
Message 一条 user 或 assistant 消息 turn;一个 turn 可以包含两条 message
turn Context 的完整轮次原子,有 user,可带已完成 assistant 数据库 row 或 UI cell
turn ordinal lineage 内用于排序、coverage 和引用的单调轮次号 时间戳、message ID
Canonical Transcript App 本地的完整对话语义事实源 UI 当前加载数组、Prompt Context
UI/materialized projection 从持久事实生成、供界面展示的可重建视图 Canonical truth
Prompt Context 某一次 Provider 调用最终看到的语义输入 完整历史
Context Envelope App-owned、版本化的 Context wire payload 加入 Agent authority 后的完整 Provider request
current input 本次 Generation 的当前 user message exact turns 中的历史 user message
exact turns 保持原文的完整历史轮次 summary、检索片段
recent exact 在预算内保留的最新连续 exact-turn 后缀 随机抽样的“重要消息”
source cutoff 本次允许观察到的最后 source turn Provider 生成结束时间
Frozen Context Snapshot 本次 Generation 不可变的语义请求事实 Generation Journal
frozen request 可重放的完整请求字节 blob 重新从数据库组装的新请求
semantic request 会改变回答语义的 Agent、模型参数与 Context 请求内容 临时认证 header、重试次数
idempotency / 幂等 同一业务操作重复提交仍指向同一语义输入与结果 只复用同一个请求 ID
token budget 输入、输出和安全预留共同占用的 token 预算 消息条数或字符数
wire contract 跨 App/服务传输时约定的字段、类型和版本 内存对象或 UI model
binding 通过签名或严格比较把多个字段绑定为同一可信对象 只检查某一个 ID 相等
immutable 创建后不允许原地修改;变化需要新对象或新版本 普通只读变量
blob 按整体保存和读取的二进制数据 数据库中的每个结构化状态字段
fingerprint 对 frozen semantic request 计算的摘要标识 身份认证签名、source digest
Generation Operation 一次有稳定 generation ID、可恢复的持久生成操作 一次网络连接或页面生命周期
Generation Journal 持久记录 operation、cursor、result、billing、side effect 状态 只保存 Prompt 的 Snapshot
scope owner + backend environment + conversation 的权限边界 仅 conversation ID
epoch clear/delete 等语义重置后推进的会话世代 普通 transcript revision
lineage 一条可以连续声明 coverage 的历史谱系 模型上下文窗口
lineage base 当前 lineage 合法 coverage 与引用的下界 active Checkpoint coverage
transcript revision App 本地 transcript 状态的乐观并发版本 source turn ordinal
preference revision 用户 Memory 模式与 consent 的并发版本 Context policy version
coverage Checkpoint 或 Envelope 声明的历史覆盖形状 摘要质量评分
cumulative source digest Canonical 投影连续前缀的摘要链值 summary digest
Checkpoint 对会话连续旧前缀的结构化摘要投影 完整 Transcript、跨会话 Memory
candidate 尚未获得本地记忆写权限的新 Checkpoint active memory
receipt Gateway 对 candidate 身份、范围、digest 和策略 binding 的签名凭证 摘要正文、付款收据
staged candidate 已通过本地验证并暂存,但不可供下一轮读取 active
active 当前唯一可被 Assembler 使用的 Checkpoint 最新创建的 candidate
superseded 被 coverage 更高的新 active Checkpoint 取代 invalid
CAS 在事务中确认当前状态仍符合预期后才原子替换 无条件覆盖、按时间取最新
Agent authority Gateway 根据 Agent ID 注入的高权限行为指令 用户历史或 Checkpoint 文本
Provider adapter 负责 Provider request rendering、计数与调用适配的边界 App 保守 token estimator
Compactor 生成结构化 Checkpoint candidate 的执行组件 active-memory owner
Primary admission 优先选择的 Compaction + Answer 计划 最终一定成功的路径
fallback 同 Generation 内预先冻结的备用 Answer 请求 失败后临时重组的新 Generation
reassembly 将已验证 candidate 插入 Answer 模板并重新计数 激活 Checkpoint
requested mode App Envelope 表达的策略意图 服务端最终执行结果
selected mode 权威 Planner 和 admission 选中的执行计划 effective mode
effective mode terminal result 表达的实际执行分支 用户设置本身
admission Provider 执行前对身份、计划、能力和预算的原子准入 settlement
reservation 为计划预留 Credits 上界 最终扣费
component 同一 Generation 内单独审计 usage 的阶段 独立用户订单
settlement 按实际结果幂等核销或释放父 reservation Provider streaming 完成
Canonical Result 可重复读取、不可变的 Generation 终态结果 SSE 临时事件、UI 已展示文字
side effect Answer 之外附带的持久状态变化,例如 Checkpoint 本轮 Answer 本身
fail closed 关键状态不确定时拒绝推进 猜测性降级
degraded recent-only 连续 coverage 不可信时仅保留有界近期原文的安全模式 Transcript 已损坏或被删除
Long-term Memory 跨 conversation/session 的独立持久记忆 会话内 Checkpoint
Provider State Provider API 管理的 thread/response/opaque continuation 状态 TXChatDemo 本地业务事实源

十、测试重点不是摘要像不像,而是不变量会不会破

仅准备几段长对话,检查摘要“看起来不错”,远远不足以验证一套 Memory 系统。更值得测试的是:

  • UI 上拉历史不会改变同一 source cutoff 的冻结输入;
  • 缺少显式 nullable Checkpoint、未知字段或非法 digest 在调用 Provider 前失败;
  • exact turns 出现 ordinal 缺口时进入 degraded recent-only;
  • 同一 generation 重放保持 byte-equivalent,Retry/Regenerate 创建新快照;
  • 空 JSON、截断、引用越界和签名失败都不会激活 Checkpoint;
  • Canonical Result 已提交但 Checkpoint side effect 暂时失败时可以恢复;
  • 激活读写窗口内 transcript revision 并发改变,或冻结的 preference revision 已过期,会令 CAS/eligibility 失败;更早的历史语义变化由 digest、epoch 和 lineage 拦截;
  • fallback Answer 可见时,未使用 Compaction 不向用户结算;
  • 页面释放、App 重启和网络重连不会把 durable operation 误当成取消;
  • clear/delete/account/environment 切换会让旧 scope 的 Checkpoint 失效。

质量评测当然仍然需要,而且要覆盖长对话、事实保留、约束遵循、跨语言和摘要漂移。但在此之前,系统必须先证明:即使摘要模型失败、网络失败、账本延迟或 App 重启,它也不会伪造一份已经生效的记忆。

结语

contextMsgNumber 到 Context V2,表面上看是把“最近 N 条”升级成“旧摘要 + 近期原文”;真正发生的变化却更深:

  • Prompt 从 UI 数组变成了由持久事实组装的冻结快照;
  • Summary 从一段文本变成了带 coverage 和来源的 Checkpoint;
  • Compaction 从临时模型调用变成了 Generation Operation 的受控内部阶段;
  • 回退从失败后的临时决策变成了准入前冻结的 recent-exact 分支;
  • Memory 更新从“拿到摘要就覆盖”变成了 verify、stage、settle 和 CAS activate。

因此,LLM Memory 压缩不应只问“用什么 Prompt 总结效果更好”,还应该继续追问:完整事实在哪里?本轮输入何时冻结?摘要覆盖了哪段连续历史?谁有权让它生效?失败后是否仍能确定性回答?迟到结果会不会倒退记忆?费用和可见结果如何一起收敛?

当这些问题都有明确答案时,Memory 才从一个 Prompt 技巧,变成了一项可以上线、恢复、审计和持续演进的系统能力。

参考资料