# 异步上下文压缩插件:当前问题与处理状态总结
这份文档详细梳理了我们在处理 `async_context_compression`(异步上下文压缩插件)时,遭遇的“幽灵截断”问题的根本原因,以及我们目前的解决进度。
## 1. 根本原因:两种截然不同的“世界观”(数据序列化差异)
在我们之前的排查中,我曾错误地认为:`outlet`(后置处理阶段)拿到的 `body["messages"]` 是由于截断导致的残缺数据。
但根据您提供的本地运行日志,**您是对的,`body['messages']` 确实包含了完整的对话历史**。
那么为什么长度会产生 `inlet 看到 27 条`,而 `outlet 只看到 8 条` 这种巨大的差异?
原因在于,OpenWebUI 的管道在进入大模型前和从大模型返回后,使用了**两种完全不同的消息格式**:
### 视图 A:Inlet 阶段(原生 API 展开视图)
- **特点**:严格遵循 OpenAI 函数调用规范。
- **状态**:每一次工具调用、工具返回,都被视为一条独立的 message。
- **例子**:一个包含了复杂搜索的对话。
- User: 帮我查一下天气(1条)
- Assistant: 发起 tool_call(1条)
- Tool: 返回 JSON 结果(1条)
- ...多次往复...
- **最终总计:27 条。**我们的压缩算法(trim)是基于这个 27 条的坐标系来计算保留多少条的。
### 视图 B:Outlet 阶段(UI HTML 折叠视图)
- **特点**:专为前端渲染优化的紧凑视图。
- **状态**:OpenWebUI 在调用完模型后,为了让前端显示出那个好看的、可折叠的工具调用卡片,强行把中间所有的 Tool 交互过程,用 `... ` 的 HTML 代码包裹起来,塞进了一个 `role: assistant` 的 `content` 字符串里!
- **例子**:同样的对话。
- User: 帮我查一下天气(1条)
- Assistant: `包含了好多次工具调用和结果的代码 今天天气很好...`(1条)
- **最终总计:8 条。**
**💥 灾难发生点:**
原本的插件逻辑假定 `inlet` 和 `outlet` 共享同一个坐标系。
1. 在 `inlet` 时,系统计算出:“我需要把前 10 条消息生成摘要,保留后 17 条”。
2. 系统把“生成前10条摘要”的任务转入后台异步执行。
3. 后台任务在 `outlet` 阶段被触发,此时它拿到的消息数组变成了**视图 B(总共只有 8 条)。**
4. 算法试图在只有 8 条消息的数组里,把“前 10 条消息”砍掉并替换为 1 条摘要。
5. **结果就是:数组索引越界/坐标彻底错乱,触发报错,并且可能将最新的有效消息当成旧消息删掉(过度压缩)。**
---
## 2. 目前已解决的问题 (✅ Done)
为了立刻制止这种因为“坐标系错位”导致的数据破坏,我们已经落实了热修复(Local v1.4.0):
**✅ 添加了“折叠视图”的探针防御:**
- 我写了一个函数 `_is_compact_tool_details_view`。
- 现在,当后台触发生成摘要时,系统会自动扫描 `outlet` 传来的 `messages`。只要发现里面包含 `` 这种带有 HTML 折叠标签的痕迹,就会**立刻终止并跳过**当前的摘要生成任务。
- **收益**:彻底杜绝了因数组错位而引发的任务报错和强制裁切。UI 崩溃与历史丢失问题得到遏制。
---
## 3. 当前已解决的遗留问题 (✅ Done: 逆向展开修复)
之前因为跳过生成而引入的新限制:**包含工具调用的长轮次对话,无法自动生成“历史摘要”** 的问题,现已彻底解决。
### 最终实施的技术方案:
我们通过源码分析发现,OpenWebUI 在进入 `inlet` 时会执行 `convert_output_to_messages` 还原工具调用链。因此,我们在插件的 `outlet` 阶段引入了相同的 **逆向展开 (Deflation/Unfolding)** 机制 `_unfold_messages`。
现在,当后台任务拿到 `outlet` 传来的折叠视图时,不会再选择“跳过”。而是自动提取出潜藏在消息对象体内部的原生 `output` 字段,并**将其重新展开为展开视图**(比如将 8 条假象重新还原为真实的 27 条底层数据),使得它的坐标系与 `inlet` 完全对齐。
至此,带有复杂工具调用的长轮次对话也能安全地进行背景自动压缩,不再有任何截断和强制删减的风险!