perf: prevent context bloat and improve cache hit rate#389
Conversation
- Replace full memory tags with lightweight pointers to prevent history bloat while preserving traces. - Add session-level deduplication for system prompts to maximize prefix cache hit rate.
|
Thank you for submitting this PR and participating in Tencent Rhino-bird Open-source Training Program! |
|
Triage note: #375 is now the canonical scoped runtime mitigation for #120, and #439 has already been asked to re-scope marker-based history compaction as a possible follow-up. This PR overlaps on lightweight memory markers and system-message dedupe. The current diff is only one file but changes runtime history/prompt handling, so it should not be merged as the primary #120 fix. Recommendation: re-scope against #375 as a focused follow-up if marker-based compaction is preferred over existing default cleanup. Include targeted tests that show the incremental behavior and avoid broad provider-cache claims in the PR body. |
• 将全量记忆标签替换为轻量级指针,防止历史记录膨胀,同时保留追溯痕迹。
• 增加 Session 级的系统提示词去重逻辑,最大化前缀缓存(Prefix Cache)的命中率。
Description | 描述
本 PR 引入了两个核心优化,旨在彻底解决由于记忆叠加导致的上下文膨胀问题,以及由此引发的大模型前缀缓存(Prefix Cache)失效和打穿 Token 上限的恶性 Bug。
轻量级记忆指针 (Lightweight Memory Pointers)
我们在 before_message_write 钩子中,不再简单粗暴地将记忆替换为空字符串,而是将其替换为极短的追踪标签
。这既防止了历史记录因为数千字的记忆片段被不断固化而呈指数级膨胀,又在历史对话中保留了“这里曾发生过记忆召回”的痕迹,方便后期调试与回溯。
Session 级系统提示去重 (System Prompt Deduplication)
在多轮对话中,重复注入的系统消息(如 )会打乱文本序列并浪费 Token。本 PR 在 before_prompt_build 钩子阶段,自动扫描 event.messages 数组,利用 Set 对 role: 'system' 的消息进行内容级去重过滤,保证大模型收到的前缀永远是最标准且无冗余的。
Related Issue | 关联 Issue
Fix #120
Change Type | 修改类型
[✓] Bug fix | Bug 修复
[ ] New feature | 新功能
[ ] Documentation update | 文档更新
[✓] Code optimization | 代码优化
Self-test Checklist | 自测清单
[✓] Verified locally | 本地验证通过
[✓] No existing features affected | 无影响现有功能
Additional Notes | 其他说明
本地实验与监控数据对比 (基于 DeepSeek API + 代理打点监控):
为了验证此 PR 的有效性,我们在本地编写了 Node.js 代理劫持流量,对大模型实际的 Cache 命中率和 Token 消耗进行了对照实验:
❌ 优化前(Bug 状态 / 记忆被固化至历史):
• 上下文爆炸:由于每轮长达 1500+ Token 的 被当作用户真实输入写入历史,多轮对话后上下文呈现 O(N²) 级膨胀。
• 缓存雪崩:动态变化的记忆片段打断了 LLM 的前缀匹配。
• 实测数据 1: [单次请求] Prompt: 17229 | 缓存命中: 0 (0.00%)
• 实测数据 2: [单次请求] Prompt: 8892 | 缓存命中: 0 (0.00%)
• 现象:稍长的对话就会导致历史命中率断崖式暴跌至 0%,模型被迫重新计算上万 Token,导致严重延迟甚至触发限流。
✅ 优化后(应用本 PR 的指针压缩 + 去重):
• 彻底遏制膨胀:历史记录中只保留轻量指针。
• 实测数据:连续两轮长请求的 Prompt 总量从 18482 仅微增至 18786 (单轮真实增量仅为 +304 Tokens,彻底告别每轮上千 Token 的垃圾冗余)。
• 命中率极速飙升:历史前缀序列保持绝对纯净和稳定。
• 长文本实测 1: [单次请求] Prompt: 23687 | 缓存命中: 19328 (81.60%)
• 长文本实测 2: [单次请求] Prompt: 22877 | 缓存命中: 18816 (82.25%)
• 短文本实测: [单次请求] Prompt: 5639 | 缓存命中: 5248 (93.07%)
• 最终结果:全局(超 37 万 Token 测试规模)累计命中率稳定在 75% ~ 85% 的高水位。
flowchart TD %% 优化前的状态 subgraph Before["❌ 优化前:记忆固化导致缓存雪崩 (Cache Miss)"] direction TB B_Sys["System Prompt (系统设定)"]:::hit --> B_T1["Turn 1 历史\n(冻结了 1500+ 字的动态记忆 A)"]:::hit B_T1 --> B_T2["Turn 2 历史\n(冻结了 1500+ 字的动态记忆 B)"]:::miss B_T2 -. "🚨 前缀发生突变!\n大模型发现记忆 B 与前缀不符\n匹配在此断裂" .-> B_T3["Turn 3 历史\n(无限膨胀)"]:::miss B_T3 -. "🔥 彻底阻断!\n后续几万 Token 全部需要重新计算" .-> B_Curr["当前轮问题"]:::miss end %% 优化后的状态 subgraph After["✅ 优化后本 PR:轻量指针 + 去重 (Cache Hit = 85%+)"] direction TB A_Sys["System Prompt\n(经过 Session 级去重,绝对纯净)"]:::hit --> A_T1["Turn 1 历史\n<memory-omitted />"]:::hit A_T1 --> A_T2["Turn 2 历史\n<memory-omitted />"]:::hit A_T2 --> A_T3["Turn 3 历史\n<memory-omitted />"]:::hit A_T3 -. "✅ 历史记录短小精悍且无突变\n前缀完美匹配!" .-> A_Curr["当前轮问题\n(仅在底部注入当轮最新记忆)"]:::dynamic end %% 样式定义 classDef hit fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000 classDef miss fill:#ffebee,stroke:#f44336,stroke-width:2px,stroke-dasharray:5 5,color:#000 classDef dynamic fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#000结论:
本 PR 在不牺牲记忆追溯能力的前提下,完美修复了上下文爆炸问题,大幅降低了 API 计费成本与推理延迟。