# 历史压缩：对话变长以后，模型怎样继续读材料

用户已经与角色聊了很久，但回复模型一次能读取的文本有限。Compact 把较早的一段历史变成摘要，保留最近的原文，再重新计算这次输入是否放得下。它处理的是“这一次把哪些历史交给模型”，不是把所有聊天变成永不丢失、自动检索的长期记忆。

**当前交付形态是应用内的 Compact 运行时、Scenario 配置及检查点存储接线。** 两个公共 npm 包没有把这一整套历史压缩服务直接导出。客户可以使用仓内应用接法，或在源码层移植它并提供模型、计数、历史身份和存储实现。

[配置契约](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L141) · [SDK 公开导出范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/sdk/src/index.ts#L1)

## 先看压缩前后真正拿到什么

举例：前面几十条聊天交代了“莉娅借给用户铜钥匙，约定天黑前归还”；最近四条原文仍在讨论城门。用户现在说：“我回来还钥匙了。”

| 材料 | 压缩前 | 压缩后 |
| --- | --- | --- |
| 角色、规则、本轮输入 | 按既有模板组装 | 继续参与本轮；压缩历史不意味着可以删除这些固定材料 |
| 较早历史 | 多条逐字消息，占用较多窗口 | 例如“用户向莉娅借铜钥匙，承诺天黑前归还；归还尚未在历史中确认” |
| 最近原文 | 最新的 user/assistant 消息 | 按配置保留最近若干完整历史组，继续逐字交给模型 |
| 已保存摘要 | 若曾压缩，会读取可继续衔接的检查点 | 与新候选历史一起参与摘要更新，不能重复把已覆盖的历史加入 |
| 提交给 Actor 的东西 | 一组模型消息 | 重新渲染后的模型消息，包含摘要和保留原文；通过 token 计数后才继续 |

这里的摘要是解释性示例。压缩模型会读取当前用户输入以判断重要性，但“用户说要还钥匙”不能仅因此被叙述成“历史中已经归还”。摘要事实质量由压缩提示词、模型和验收决定；运行时重点校验格式、长度、历史覆盖与版本关系。

## 何时执行：按窗口预算，而不是固定聊天轮数

1. 开启 Compact 的请求先取得压缩模型配置，并读取检查点。即使请求只带较短的近期历史，也先检查能否接上之前的摘要。
2. 计算这次可容纳输入的预算：**模型上下文上限 − 最大输出预留 − 安全余量**。
3. 把实际角色提示词、历史、旧摘要、最终注入内容组装起来，计数；不能只估算历史字符数。
4. 超过配置的触发比例时尝试压缩。若旧摘要的覆盖末端即将离开请求携带的历史窗口，也会尝试提前推进检查点。
5. 选择可压缩的已闭合历史组，排除配置要求保留的最近组，调用压缩模型；再将新摘要和保留原文组装成 Actor 输入，重新计数。
6. 只有实际输入放得下，才采用这次压缩结果；具备持久化条件时还会在 Actor 生成之前保存检查点。

例如模型窗口假设为 8,192 tokens，输出预留 2,048，安全余量 512，则输入预算是 5,632；触发比例设为 0.8 时，普通长度触发线为 4,505。这组数字只演示计算方法，**不是所有场景的默认模型窗口**。窗口取自模型配置，输出预算取自本轮实际配置；调大配置不能扩大模型服务自身支持的上限。

[输入预算](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1326) · [先读取检查点](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1358) · [触发及保留历史](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1394) · [压缩后重新计数](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1455)

## 保存什么、什么时候保存、下一轮拿什么

| 时机 | 产物及保存行为 |
| --- | --- |
| 压缩模型刚完成 | 新摘要暂时只在本次准备过程里，尚未证明最终 Actor 输入放得下 |
| 重新计数通过 | 采用摘要及保留历史。若允许修改检查点、覆盖历史具备持久消息身份、旧版本仍可提交，则 CAS 保存检查点 |
| 检查点内容 | 摘要、覆盖历史的首尾消息 ID 和历史指纹、版本/链路信息，以及模型/模板/计数等审计信息；不是再保存一遍所有聊天正文 |
| 本轮 Actor 写正文 | 使用已经压缩后的输入。检查点在此前已可能保存，不依赖本轮新正文后来是否被用户接受 |
| 下一次请求 | 读取检查点，核对角色/配置投影与历史锚点；可用时读出摘要，再衔接未覆盖的近期原文 |
| 历史删改、重生成或窗口跳跃 | 无法证明旧摘要能与当前历史衔接时，不应继续当成可靠前缀；由代码失效/重建或降级路径处理 |

当前压缩成功而保存失败时，本轮仍可能使用内存中的摘要继续回复；以后请求不保证能读回这次摘要。需要看 `persisted` 或检查点结果，不能把“本轮压缩成功”与“以后一定记住”合成一个状态。

检查点按 **用户＋会话＋Scenario** 读取和保存。应用适配器只在 `chat`、`continue`、`auto_reply` 类型允许修改检查点，其他类型不能借这条路径改写旧摘要；这是组装器收到请求后的权限条件，不表示独立 Auto Reply 建议入口一定会执行 Compact。调用方若直接复用运行时，须明确传入 `checkpointMutationAllowed`。

读回也不是“有摘要就使用”：代码检查格式版本、角色/配置的语义指纹、摘要大小及压缩协议；若覆盖的首尾消息都在当前历史里，还检查覆盖条数与历史内容指纹。至少需要找到旧摘要覆盖的末端消息，才能证明它与后续原文接得上。读失败、配置无法核实或未知格式时，本次不会拿该检查点继续写新版本；已知配置/历史发生变化且允许修改时，先按版本将旧检查点失效，再考虑重建。

[保存前提及覆盖信息](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1462) · [版本提交](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1544) · [本轮返回的投影](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1628)

[请求身份与修改权限](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/integrations/roleplay-axon-adapter.ts#L458) · [读回、校验与失效](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1150)

## 接入和配置：实际字段放在哪里

以下是仓库 `compact-automemory-v0-native.json` 中 Compact 部分的原样配置。它展示当前解析器支持的字段；`compactorModelConfigId` 需在所接入配置中心真实存在。该场景样例并非普通 V2 的统一默认值，也不是打开整个记忆系统的开关。

```json
{
  "compact": {
    "enabled": true,
    "boundaryMode": "selected_turns",
    "compactorModelConfigId": "RP-Harness-Compact-AutoMemory-v0-Native",
    "triggerRatio": 0.8,
    "keepLastCanonicalGroups": 2,
    "checkpointMutationTimeoutMs": 5000,
    "safetyMarginTokens": 512,
    "maxSummaryTokens": 700,
    "reliability": {
      "version": "v1.1",
      "minGenerationTokens": 256,
      "maxCandidateAttempts": 24,
      "maxModelCalls": 2,
      "maxCompactionMs": 120000
    },
    "profile": "configurable-v2",
    "inputProtocol": "dream_text_v0",
    "outputFormat": "plain_text",
    "lengthPolicy": "accept_bounded",
    "executionPolicy": "bounded_recent"
  }
}
```

| 配置 | 控制什么 | 修改后的实际影响 |
| --- | --- | --- |
| `enabled` | 是否启用整条压缩准备链 | 未配置或关闭不等于模型窗口无限，也不会自动有持久摘要 |
| `boundaryMode` | `selected_turns` 从所选消息推导完整轮次；`explicit_groups` 使用显式历史分组 | 决定哪些历史能作为一个整体压缩；身份/边界不正确会影响安全覆盖与持久化 |
| `triggerRatio` | 长度触发比例，必须大于 0 且小于 1 | 越小越早尝试；不是后台每隔多少轮执行 |
| `keepLastCanonicalGroups` | 至少保留多少近期历史组，0–100 | 不是字符数。设为 0 要用 `configurable-v2` |
| `safetyMarginTokens` | 输入预算中额外留的空间 | 增大会减少给输入的预算；不能扩大模型窗口 |
| `maxSummaryTokens` | 摘要最终长度上限 | 摘要还需和其他材料合起来放得下 |
| `compactorModelConfigId` | 摘要使用的模型和提示词配置 | 与 Actor 模型是两个岗位；内容提炼规则由该配置提供 |
| `profile`、`inputProtocol`、`outputFormat`、`lengthPolicy` | 压缩请求和输出的协议 | 当前属于 **Scenario 的 compact 配置**，不能随意塞到 ModelConfig 当运行开关 |
| `executionPolicy: bounded_recent` | 限定近期候选的处理和降级方式 | 需要显式可靠性配置；可能放弃未覆盖历史，不承诺全历史永久保存 |
| `checkpointMutationTimeoutMs` | 检查点失效/写入操作的超时；未配置时已有 2,000 毫秒默认值，可显式设 1,000–10,000 毫秒 | 显式设置后，写入超时会做一次有界读回确认；重建时失效旧检查点与写新检查点分别使用该时限，避免把不确定写入盲目重放 |

`compact.contentPolicy` 已被明确拒绝；代码要求移除旧字段，通过压缩用 PromptManager/Preset 表达内容提炼策略。本轮问题始终进入压缩材料。普通 V2 同时启用 Compact 时还需要 `contextGovernance`，其中 `enforce` 才让后台采用受控的压缩投影；`shadow` 是观测对照。Sumi 配置当前不允许这个 Compact 开关。

[真实配置样例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/config/scenarios/compact-automemory-v0-native.json#L9) · [字段校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L694) · [协议支持范围](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L93) · [旧字段处理](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L135)

<details>
<summary>协议、可靠性预算与失败行为</summary>

- `generic-json-v1` 与 `configurable-v2` 是当前支持的 profile。只有 `configurable-v2` 接受自选输入/输出协议，且必须显式提供 `reliability`。
- 输入协议支持 `harness_json_v1`、`dream_text_v0`、`automemory_recursive_v1`；输出支持 `structured_json`、`plain_text`。
- 结构化摘要包含 `canonical`（可延续事实）、`interaction`（互动关系）、`open_threads`（未完事项）；纯文本投影为 `{format:"plain_text",text:...}`。不是所有输出格式都返回同一业务对象。
- `lengthPolicy` 默认 `reject`；`accept_bounded` 仅允许纯文本。无论采用哪一种，最终还要通过摘要及 Actor 的总预算检查。
- `maxCandidateAttempts` 限制尝试候选，`maxModelCalls` 限制真正发给压缩模型的调用，`maxCompactionMs` 限制候选压缩阶段的时长。它不是整轮请求时限：最终 Actor 输入计数及检查点保存另按请求/存储时限执行。候选次数不等于付费模型次数。
- 可选递归切片 `recursive.segmentContentTokens` 需要 `configurable-v2` 与 `automemory_recursive_v1`；中间摘要不逐片保存成最终检查点，整个结果通过后再提交。
- 模型出错但原输入仍在窗口内时，可以沿用已有输入；否则逐步裁掉可移除的旧完整组，再计数。`bounded_recent` 裁掉未覆盖历史、破坏旧摘要连接时，也会放弃该次旧摘要。仍超窗则返回上下文溢出错误。
- 配了检查点独立超时时，写超时后最多用 2 秒读回，核对版本、链路及完整提交内容；匹配才认定已保存。版本冲突不通过覆盖来“强行成功”。

[输出投影与格式](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-protocol.ts#L4) · [可靠性策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L27) · [保存失败与确认](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1562) · [历史裁剪降级](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/compact-runtime.ts#L1654)
</details>
