LLM 101 FIELD NOTE

Agent Context Economics 101|04|Sessions Are Trees:会话、分支与缓存并不是一回事

Session ID 只是产品状态和路由线索;真正决定缓存复用的是 token prefix。分支、回退、fork 与 resume 因此会产生不同结果。

Session ID 不是缓存本身

继续同一个 coding agent 任务:“定位失败测试、修改实现、运行完整测试并交付结果”。Agent 已经读过失败测试和实现,提出一种修改,运行局部测试后发现仍然失败。产品允许用户回到修改前,尝试另一种修复;也允许把当前进度 fork 出去,让两个执行器并行验证不同方案。

界面上,这些操作都围绕“会话”发生,工程上却必须始终分开三种身份:

  • 产品 / 会话身份:session ID、持久化文件、节点、父节点、当前 head 与 active branch,回答“用户在操作哪份工作记录”。
  • 内容身份:实际渲染给模型的 system、tools、project rules、history 与新输入所形成的精确 token prefix,回答“两次请求从开头连续相同到哪里”。
  • 基础设施位置与可用性:请求落到哪个 Provider、区域、worker 或缓存层,相关 prefix block 是否仍在 TTL 内、是否被驱逐,回答“那段可复用计算现在还能不能被找到”。

Session ID 属于第一层,也可能被第三层当作路由线索,但它既不是内容本身,也不是天然的缓存 key,更不是 cache hit 的证明。同一个 session ID 切换了 active branch,模型重建出的历史会在分叉点后不同;反过来,一个新 session ID 若携带与旧请求逐字节、逐 token 相同的模型可见历史,在 Provider 规则、路由与缓存生命周期允许时,内容仍可能复用。会话相同不保证前缀相同,会话不同也不保证前缀不同。

三层可以任意组合:同一产品会话可能生成不同内容,并先后落到不同 worker;两个产品会话也可能生成同一内容,恰好找到同一组可复用 block。把它们压成一个 session_cache_status = warm 会丢失关键事实:warm 是哪段前缀、在哪个位置、由哪次 usage 证明、到什么时候仍可能有效?产品状态适合恢复工作,内容 digest 适合比较请求,基础设施指标适合解释命中;三者不应互相代填。

线性聊天背后为什么常常是一棵树

聊天界面通常只展示一列消息,因为用户一次只能沿一条路径继续。但这只是当前 active path 的投影:底层可以把每条消息、工具调用、测试结果、压缩事件都保存成不可变节点,用 node_id + parent_id 连接。当前 head 指向哪一个叶子,界面就从该叶子沿 parent 回溯并渲染哪条线性记录;其他路径仍留在树里,供比较、恢复或重新继续。

截至 2026-07-26Pi Sessions 文档就是一个具体实现例子:会话以 JSONL 持久化为树,每个 entry 有 idparentId,当前所在位置是 active leaf;/tree 可以在同一 session file 内选择旧节点并继续,从而创建新分支。Pi 说明“线性 UI 可以建立在追加式树日志之上”,但它不是所有 Agent 产品必须采用的通用架构。其他产品可能复制 transcript、保存 checkpoint,或只保留当前路径。

把贯穿任务画成一棵树:root 包含稳定的 system、工具表、项目规则、用户目标,以及定位失败测试时读到的共同事实。之后形成三个分支:A 修改边界条件但局部测试失败;B 改为修正状态更新并通过局部测试;C 从共同点直接补回归测试,再观察原实现失败。图中的虚线是 common prefix boundary;root 到边界之前是一段连续相同的模型输入,A、B、C 从首个差异起成为三个不同 token 序列。

共享会话主干与三个分支可复用的 token 前缀

树模型有两个好处:产品不必为了显示另一条路线而删除旧事实,审计也能知道某个测试结论来自哪个分支。但“节点共享 parent”只证明产品数据有共同祖先,不能直接证明两次 API 请求共享相同 token prefix。Context Builder 可能换了模型、工具表、项目规则,或在回放时压缩、裁剪、重新序列化历史;这些都会让模型看到的共同边界早于产品树的分叉点。

树也让“最终答案”与“探索事实”分离。B 分支通过局部测试,不代表 A、C 的记录应被抹掉;运行完整测试并交付时,产品可以把 B 设为 head,把 A 的失败和 C 的回归证据保留为旁支。模型下一轮通常只需看到 B 的 active path,以及产品明确选择追加的跨分支摘要,而不是自动把三条互相冲突的 transcript 混成一条历史。若产品生成这种摘要,它就是新的模型可见内容,也应记录自己的节点、版本和 prefix 边界。

Rewind、branch、fork、resume 分别改变什么

这四个词首先是产品操作,不是推理缓存指令;Claude Code、Pi 与其他客户端的具体行为会随产品和版本变化。以下定义用于设计会话系统,实际接入仍要按日期核对产品文档。

  • rewind:把 head 或 active path 移回较早节点,再从那里继续。旧后缀可以保留为非活跃分支,也可以被产品隐藏或截断。它不会凭一个按钮让模型“自动忘记”;真正决定下一轮上下文的是产品随后如何重建请求。若重建结果正好是先前前缀,并且缓存仍可用,才有机会复用。
  • branch:从某个节点产生另一条后继路径。它可以仍属于同一 session ID,也可以由产品创建新 ID。同一 ID 下切换 active branch 时,分叉点后的历史不同;因此路由看到“同一个会话”,模型看到的却可能是另一段序列。
  • fork:把已选前缀复制或引用到另一条产品 lineage,通常得到新的 session ID。一个 warm parent fork 出来的新 ID 可以共享 parent token prefix,但前提是精确前缀、模型与工具等资格一致,而且请求能找到仍驻留的缓存;“新 ID”本身既不会阻止,也不会创造复用。
  • resume:重新加载持久化会话事实并恢复可继续工作的产品状态。它保证的是 durable history 可以用于重建上下文,不保证计算状态仍驻留。若缓存已过期或被驱逐,resume 仍能恢复完整 transcript,下一轮却可能是冷 prefill。

截至 2026-07-26Claude Code 的会话文档中,/branch 会复制截至当前点的 conversation 并切换到拥有新 session ID 的分支,resume 会恢复持久化的 conversation 和部分产品状态;Claude Code Prompt Caching 文档则把 /rewind 描述为回到先前已缓存前缀的产品路径。后一个命中结论依赖 Claude Code 当时的请求布局与持续读热行为,不能外推成“所有 rewind 必命中”。Pi 的 /tree/fork/clone 又采用同 session file 或新 file 的不同组合。产品名相同的动词,不应取代对 head、lineage 和实际 rendered prefix 的记录。

共享主干能复用多少前缀

对于 A、B、C 三个分支,可复用长度不是“共同节点数”,而是实际请求从第一个 token 起连续一致的长度。若三者都使用相同 Provider、模型、effort、system、工具 loadout、项目规则,并以相同渲染规则回放 root,那么从开头到 common prefix boundary 的主干是候选复用区;首个 mismatch 之后,即使三个分支又出现相同的 pnpm test 输出,也不能自动拼回同一段前缀命中。

共同主干还可能因为看似无关的变更缩短:模型或 tokenizer 版本改变、工具 schema 重排、项目规则更新、effort 不同、compact 生成了新摘要、prune 删除了旧消息,都会建立新的 prefix epoch。反之,新建 session ID 后若完整复制同一 active path,并保持模型可见渲染逐 token 一致,Provider 仍可能识别这段内容。这里的“可能”很重要:内容相同只是资格之一,缓存还要仍在、路由可达,并满足 Provider 的长度、隔离和有效期规则。

可以把每个候选分支先做一次“前缀账本”:记录请求总 token、与 parent 的最长共同 token prefix、首个 mismatch 区块,以及 Provider 最终报告的 cache read / write。若 fork 后共同前缀很长而读取为零,先标记为“符合内容复用条件但未观察到读取”,再检查 idle gap、模型与资格配置;不要倒过来修改树,让产品节点看起来更像一次命中。树描述因果路径,usage 描述当次服务结果,它们解决的是不同问题。

因此,产品树与缓存前缀应有明确映射,而不是共用一个模糊的“会话哈希”。每次发请求都应从 active head 重建模型可见路径,为最终 rendered prefix 计算受密钥保护的 digest,并记录首次差异落在哪个区块。digest 只用于比较身份,不能把源码、凭据、用户对话或测试日志原文写入普通诊断日志。

Session affinity 只是寻找缓存的线索

缓存必须驻留在某处,请求也必须找到它。概念上可以区分两条 serving 路径:

会话亲和路由与分布式前缀块的两种概念路径

第一条是 worker / session affinity:路由器把兼容的后续请求尽量送回同一个 warm worker。Session ID、prompt cache key 或其他 affinity hint 在这里帮助缩小搜索范围,但 worker 可能过载、重启或驱逐缓存,调度器也可能优先均衡负载。相同 hint 仍不能让不同 token prefix 变成相同内容。

第二条是 distributed / content-addressed prefix blocks:把可复用前缀按内容标识为 block,让调度和复用不再绑定一台 worker。跨 worker 的索引、搬运、隔离与驱逐会增加系统复杂度。截至 2026-07-26vLLM Automatic Prefix Caching提供了一个实现参考:block hash 组合 parent hash、当前 block 的精确 tokens 与额外隔离字段,并只缓存完整 block。它说明内容寻址和分块可以怎样设计,不代表所有托管 Provider 都采用 vLLM,更不能据此推断某家 API 的内部 block 大小或路由。

对应用方而言,Provider routing、具体 worker、缓存 residency 与 eviction 通常是不可观测的,除非 Provider 明确暴露诊断。不能因为 session ID 未变就宣称命中,也不能只凭 TTFT 下降反推命中;负载、网络和排队同样会影响 TTFT。Provider 若提供 cached tokens、cache read、cache write 等 usage 字段,它们是更强的直接证据;TTFT、idle gap 与请求 digest 用来解释和交叉验证,而不是替代 usage。

并行分支与冷缓存

并行执行 A、B、C 时,共享 root 确实创造了复用机会,但不存在“一个 warm parent 必然温暖所有 children”的保证。三个请求可能同时到达不同 worker;某个 worker 只有部分 block;一个分支等待测试超过 TTL;另一个受并发配额排队;父请求刚建立的写入也可能尚未对其他路径可见。即使三条请求内容开头相同,命中量和 TTFT 也可能不同。

冷启动并发还可能形成 cache stampede:多个分支几乎同时发现没有可用前缀,各自重复 prefill 或写入。是否会去重、合并在途写入或共享新 block,取决于 serving 实现,应用不能在没有 Provider 证据时断言一定重复计费或一定只写一次。更稳妥的设计是限制不必要的 fan-out,在可接受时让共同探索先形成稳定主干,再分派真正独立的替代修复;同时记录每个分支自己的 usage、TTFT、配额等待和结果,不用父会话的“温暖”状态代替实测。

并行是否值得,最终仍要回到任务结果。若两个分支节省了定位时间,却同时占满配额、重复运行完整测试并产生多次冷写入,用户得到的总延迟未必更短;若共享主干稳定、分支问题真正独立、最终只需择优交付,并行才更可能带来净收益。缓存是并行决策中的一项成本信号,不是决定是否分支的唯一目标。

设计会话功能时要记录哪些事实

一份能解释分支成本的最小记录,应把观察到的事实推断分栏保存。

观察事实至少包括:不可变 node_idparent_id;创建 branch、移动 head、rewind、fork、resume 的操作及操作前后 ID;active head 对应的 rendered-prefix keyed digest(不保存 raw secrets);Provider、模型、effort、工具 loadout epoch、项目规则 epoch;每次 compaction / prune 的事件与新摘要版本;请求时间、上次活动时间和 idle gap;使用过的 prompt cache key 或 affinity hint;Provider 原始 cache read / write / cached token 字段;TTFT;分支最终是测试失败、通过、放弃还是成功交付。

推断则单独记录,例如“疑似 TTL 过期”“疑似路由到冷 worker”“首个 mismatch 可能由工具版本变化造成”。每条推断要引用相应事实,并允许状态为 unknown。若 usage 显示零 cache read,可以确认本轮没有按该字段报告读取,却仍不能在无诊断时精确指出是 eviction、routing、资格门槛还是内容变化;若 usage 显示大量读取,则可以确认存在 Provider 报告的复用,也不必虚构它位于哪块 GPU。

落库时还应把会话事件与模型请求分成两张可关联的记录。一次 branch 操作可能尚未触发请求,一次 retry 也可能在同一 head 上产生多个请求;若只按 session ID 聚合,二者都会被抹平。用 operation ID、request ID 和 head node ID 建立关联,便能分别回答“用户选择了哪条路径”“该路径渲染成什么前缀”“每次请求观察到什么缓存结果”。原始 usage 应原样保留并注明字段口径,派生的命中率、疑似 miss 原因和成本估算则带上算法版本,避免日后把旧推断误当成 Provider 事实。

Session tree 让产品能够保存选择,token prefix 决定内容能否共享,基础设施状态决定共享是否兑现。Earendil 的文章只为这种“树—前缀—位置”的提问框架提供编辑灵感,不是 Provider 内部实现的事实权威。把三者分别建模、分别观测,团队才能解释为什么同一个会话会冷、为什么新会话反而可能热,以及一次 fork 到底复制了工作记录,还是也复用了计算。


上一篇:Tool Loadouts:为什么多加一个工具可能更贵

下一篇:The Price of a Miss:TTL、路由与模型切换的隐藏成本

参考资料