You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
composeSystemPrompt()(packages/agent-runtime/src/runtime.ts:2486-2510)把 projectInstructionsPrompt(this.projectInstructions) 放进 context 段(:2500-2505),也就是请求前缀;project-instructions-prompt.ts:3-12 会把每个规则文件的全文拼进 # Project instructions 段。而紧邻的 :2496-2499 注释写着:「The resumable-delegation list deliberately stays out: it changes on every settlement, and a system-prompt change rewrites the request prefix provider prompt caches key on (ADR 0337).」——原则已经写在旁边,只是规则全文(用户随时会编辑的文件)仍然留在前缀里。所以会话中途改 AGENTS.md,下一轮从规则段起整段失效,与你的测量一致。
修法可以照抄那条先例:把规则全文移出 system prompt,改为会话内冻结 + 变更时用尾部消息送达(或按需读取),system 段只保留一句稳定指引。projectMemoryPrompt(:2488)同理,它也是可变文件。
Activity
vastsa commentedon Oct 27, 2026
感谢你提供这么详细的复现步骤、网关数据和请求体对比,定位信息很有帮助。欢迎直接提交 PR,我会尽快审阅;如果实现前想先讨论方案,也可以继续在这里交流。
huangyoudashi commentedon Oct 27, 2026
补充分支:新会话的头部缓存永远无法跨会话共享(0.21.0 与 0.22.0-beta.1 均存在)——系统提示前部嵌入了会话 scratch 路径
现象与定位
对比任意两个会话的 developer 系统提示(各 43,720 字节,结构相同):首个分歧固定出现在字节 3,200 附近,分歧内容就是嵌在头部里的会话 scratch 目录路径(
...\.pi-desktop\scratch\<sessionId>,前 3.2k 字符内出现两次明文)。把两个头部的会话 UUID 归一替换后,其余 4 万字节逐字节一致(末尾仅差一个生成时间戳元数据字段)——即除会话 UUID 外头部完全相同,却被这个 UUID 提前截断了所有跨会话缓存。后果
每个新会话的首次请求只能命中前 512 token,其后整个头部(约 1 万 token,含技能列表、规则全文)全部按未缓存输入计价。实测连续 4 个同模型主会话(间隔 1–2 分钟,缓存必然新鲜)首调全部为
input≈10,14x / cache_read=512:0.21.0 同样存在:两个 0.21 会话的头部在**同一位置(字节 3,200)**嵌着各自的 UUID,非新版回归。另有旁证:无近期同模型会话时首调 cache_read=0(连 512 都没有)。
对照
同一任务、同一模型(deepseek-v4.1-flash)在 OpenCode 下:首调
input=146 / cache_read=16,640(其头部跨会话字节稳定,直接命中缓存),主线命中率 96.9% vs PI 74.6%。差距主体就是这笔每会话 ~1 万 token 的头部冷启动——且会话越短、命中率被拉得越难看。建议
会话级易变内容(scratch 路径)不要放在系统提示前部。可选做法:挪到系统提示末尾或以尾部消息送达;或删除头部中的明文路径、只保留环境变量说明(
PI_SCRATCH_DIR/$env:PI_SCRATCH_DIR本就已注入运行环境,两处明文属重复信息)。与主楼两条(规则全文进前缀、跨轮重建)同族——易变内容改写靠前的前缀段;本条触发频率最高:每开一个新会话付一次。
muzimu217 commentedon Oct 27, 2026
线 1 已核实,属实;而且同一个函数里已经有先例注释,说明这个原则团队是知道的。
composeSystemPrompt()(packages/agent-runtime/src/runtime.ts:2486-2510)把projectInstructionsPrompt(this.projectInstructions)放进context段(:2500-2505),也就是请求前缀;project-instructions-prompt.ts:3-12会把每个规则文件的全文拼进# Project instructions段。而紧邻的:2496-2499注释写着:「The resumable-delegation list deliberately stays out: it changes on every settlement, and a system-prompt change rewrites the request prefix provider prompt caches key on (ADR 0337).」——原则已经写在旁边,只是规则全文(用户随时会编辑的文件)仍然留在前缀里。所以会话中途改 AGENTS.md,下一轮从规则段起整段失效,与你的测量一致。修法可以照抄那条先例:把规则全文移出 system prompt,改为会话内冻结 + 变更时用尾部消息送达(或按需读取),system 段只保留一句稳定指引。
projectMemoryPrompt(:2488)同理,它也是可变文件。线 2 我确认了结构上的不对称,但还没把剥离点钉死:跨轮首条请求走
buildSessionContext()(packages/agent-runtime/src/session-context.ts:94-104)从持久化条目重建,而轮内请求用的是 agent 的实时消息;sessionEntryToContextMessages(:53-92)对type: "message"是原样透传,所以 reasoning 消失要么是因为持久化条目里本来就没有 thinking 块,要么发生在 provider 载荷转换(requiresCompletionsReasoningReplay那层)。需要再跟一步确认,我下一轮补结论。不过「跨轮重建 vs 轮内实时」这个不对称本身就是前缀缓存失效的结构性来源:只要两条路径的序列化不完全一致,每个 turn 边界都会重算一次历史。这条值得单独提 issue 跟踪。
muzimu217 commentedon Oct 27, 2026
线 2 的剥离点补上了,结论是:重建路径无法复现带签名的 thinking,所以任何一次从 transcript 重建都会丢掉 reasoning,与哪个 provider 无关地偏向了 DeepSeek 之外的模型。
证据链:
crates/host-core/src/sessions.rs:212的pub thinking: Option<String>是唯一相关字段,UiMessage里没有thinkingSignature,TS 侧的UiMessage类型也没有。runtime.ts:3109-3119(historyToEntries的 assistant 分支)把m.thinking还原成{ type: "thinking", thinking },只有 DeepSeek Completions 才额外补thinkingSignature: "reasoning_content";:3110-3111的注释直接写明原因:「Completions DeepSeek replay needs thinkingSignature so convertMessages maps the block to reasoning_content instead of dropping it」。convertMessages阶段被丢弃;而轮内请求用的是 provider 原样返回的带签名块,不会被丢。这正好解释了「轮内 104 条 reasoning,跨轮首条请求 0 条」。顺带一个结论:这不是「某次裁剪」造成的偶发现象,而是重建路径的结构性缺陷——只要会话从 transcript 重建一次(重启、切会话后重开、或任何走
historyToEntries的路径),该会话的缓存前缀就必然作废,且无法通过调阈值规避。修法二选一,或两者都做:
UiMessage增加 thinking 的签名/redacted 载荷并原样回放,让重建结果与轮内请求逐字节一致。缺点是签名是 provider 私有的,跨 provider 切换时仍需降级。如果只想先止血,还可以在重建时把 thinking 块统一丢弃(而不是只丢无签名的),让轮内与跨轮的序列化形状至少一致——但那样每次跨轮仍会少一段前缀,缓存只能保住 thinking 之前的部分,不如前两条彻底。