# 异步上下文压缩插件:当前问题与处理状态总结 这份文档详细梳理了我们在处理 `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` 完全对齐。 至此,带有复杂工具调用的长轮次对话也能安全地进行背景自动压缩,不再有任何截断和强制删减的风险!