Affected area
Agent sessions / streaming
Problem statement (what should this solve)
LLM 的 prompt 缓存按字节级前缀匹配。systemPrompt 序列化在所有消息之前,因此它里面任何一个字节发生变化,都会同时作废 system 段以及它后面的全部对话历史——哪怕用户什么都没做。
目前 systemPrompt 里混着若干逐轮漂移的运行时状态,导致长会话中缓存反复失效:
- memory 索引新鲜度尾缀是精确天数(渲染为
- some note [slug|u|3d])。3d 明天就变成 4d——这一天里记忆内容一个字都没改,但整条缓存前缀连同全部历史一起作废。
- memory 索引本身逐轮可变:模型刚写完一条记忆,下一轮索引就跟着变。
- taskList 快照每轮重读:
TaskCreate / TaskUpdate 的工具结果其实已经把变化送达模型了,system 段里再放一份属于重复投递,且每次推进都打断前缀。
- subagent message bus 与 roster 的易变字段(
status / last_task / last_summary)与稳定的身份字段同行渲染,任一子代理状态变化就整段漂移。
- skills 的
/skill-name 显式提及进 system 段:这轮加上、下轮撤掉,一次用户输入连废两次前缀。
影响场景:开着子代理的长会话、带 memory 的日常会话、跨天续用的会话。实测一个 5 轮工具循环的会话里,systemPrompt 出现了 5 个互不相同的版本(即每轮都在漂移)。
此外还缺观测手段:前缀失效时只能看到 cacheRead 掉零,无从判断是 system 段还是 tools 段引起的,导致问题难以定位和回归。
Proposed behavior
先补观测,再按「模型需要多快看到这个变化」把运行时状态分三类处理,而不是用同一种手法套所有目标:
| 类别 |
判据 |
手法 |
实例 |
| A |
变化已由别的通道送达模型 |
按压缩纪元冻结 |
taskList(工具结果就是主通道) |
| B |
变化必须尽快送达,无其他通道 |
增量渲染 + 后置到消息尾部 |
message bus、roster 易变字段 |
| C |
只对当轮有效的一次性内容 |
移出共享 prompt,挂当轮 user 消息尾部 |
skills 显式提及 |
方向选错是功能倒退而不是少省 token:把 B 类做成冻结会让子代理消息延迟送达;把 A 类做成增量后置则是过度设计。
新增内容一律挂到最后一条 user 消息尾部,而不是新开 system 块——Anthropic 每请求最多 4 个 cache_control 断点,OAuth 路径下 pi-ai 已全部用满(identity 块 / systemPrompt / 最后一个 tool / 最后一条 user 消息的最后一个 content 块)。新增 system 块不会增加断点,只会挤掉已有的;挂 user 消息尾部等于复用第 4 个断点,零额外成本。
memory 新鲜度改为四档分桶(d0/w/m/old)而非直接删除标记——system prompt 里没有日期锚点,相对标记是模型判断记忆新旧的唯一依据。
Estimated change scope
crates/agent-gui/src/lib/debug/prefixCacheShape.ts (新增,观测)
crates/agent-gui/src/lib/memory/prompts/turnInjection.ts (新增)
crates/agent-gui/src/lib/chat/context/contextTailBlock.ts (新增)
crates/agent-gui/src/lib/chat/skills/mentionInjection.ts (新增)
crates/agent-gui/src/lib/subagents/{bus,roster,index}.ts
crates/agent-gui/src/pages/chat/turns/runAgentConversationTurn.ts
crates/agent-gui/src/pages/chat/runtime/{conversationContextBuilders,useSendChatTurn,useManualCompaction}.ts
crates/agent-ui/src/lib/skills/index.ts
Alternatives considered
- 直接删掉 memory 新鲜度标记:最大化稳定性,但 system prompt 里没有日期锚点,会用可测量的缓存收益换取未测量的召回劣化。故选择分桶折中。
- 给尾部块做 attach/detach 精确剥离(避免落库):前缀稳定性同样完美,但两个剥离点漏一个就静默落库,复杂度高。倾向接受增量块落库这一已知代价。
- 一次性挂载不重放:下一轮消息从会话状态重建会恢复原样,那条消息的字节变了两次,比不挂还糟。已否决。
备注
我已完成实现与实测,可随时提 PR。DeepSeek 实测(5 轮工具循环,改动前后对照,重复两遍):第 2 轮起命中率 79.6% → 98.9%,run 内不同的 systemPrompt 数 5 → 1。整体命中率(含必然 0% 的首轮)64.2% → 79.3%。
需要先请维护者确认这个方向是否可接受,特别是 memory 新鲜度分桶对召回质量的影响目前未测量——这是该提案最主要的未知项。
Pre-submit checklist
Affected area
Agent sessions / streaming
Problem statement (what should this solve)
LLM 的 prompt 缓存按字节级前缀匹配。
systemPrompt序列化在所有消息之前,因此它里面任何一个字节发生变化,都会同时作废 system 段以及它后面的全部对话历史——哪怕用户什么都没做。目前
systemPrompt里混着若干逐轮漂移的运行时状态,导致长会话中缓存反复失效:- some note [slug|u|3d])。3d明天就变成4d——这一天里记忆内容一个字都没改,但整条缓存前缀连同全部历史一起作废。TaskCreate/TaskUpdate的工具结果其实已经把变化送达模型了,system 段里再放一份属于重复投递,且每次推进都打断前缀。status/last_task/last_summary)与稳定的身份字段同行渲染,任一子代理状态变化就整段漂移。/skill-name显式提及进 system 段:这轮加上、下轮撤掉,一次用户输入连废两次前缀。影响场景:开着子代理的长会话、带 memory 的日常会话、跨天续用的会话。实测一个 5 轮工具循环的会话里,
systemPrompt出现了 5 个互不相同的版本(即每轮都在漂移)。此外还缺观测手段:前缀失效时只能看到
cacheRead掉零,无从判断是 system 段还是 tools 段引起的,导致问题难以定位和回归。Proposed behavior
先补观测,再按「模型需要多快看到这个变化」把运行时状态分三类处理,而不是用同一种手法套所有目标:
方向选错是功能倒退而不是少省 token:把 B 类做成冻结会让子代理消息延迟送达;把 A 类做成增量后置则是过度设计。
新增内容一律挂到最后一条 user 消息尾部,而不是新开 system 块——Anthropic 每请求最多 4 个
cache_control断点,OAuth 路径下 pi-ai 已全部用满(identity 块 / systemPrompt / 最后一个 tool / 最后一条 user 消息的最后一个 content 块)。新增 system 块不会增加断点,只会挤掉已有的;挂 user 消息尾部等于复用第 4 个断点,零额外成本。memory 新鲜度改为四档分桶(
d0/w/m/old)而非直接删除标记——system prompt 里没有日期锚点,相对标记是模型判断记忆新旧的唯一依据。Estimated change scope
Alternatives considered
备注
我已完成实现与实测,可随时提 PR。DeepSeek 实测(5 轮工具循环,改动前后对照,重复两遍):第 2 轮起命中率 79.6% → 98.9%,run 内不同的
systemPrompt数 5 → 1。整体命中率(含必然 0% 的首轮)64.2% → 79.3%。需要先请维护者确认这个方向是否可接受,特别是 memory 新鲜度分桶对召回质量的影响目前未测量——这是该提案最主要的未知项。
Pre-submit checklist