只改一点提示词,影响到底大不大
设上一轮请求已经处理了一段很长的上下文。下一轮中,前面绝大部分没有变化,只在后面增加了一个新的 chunk,例如另一个子 Agent 的输出。这个变化对 KV cache 的影响,通常没有想象中大。
如果新内容确实位于旧上下文的末尾之后,旧上下文仍是新上下文的完整前缀。服务端只要仍持有相应的缓存条目,就可以从已有断点继续 prefill 新增部分。它不要求整段请求与上一轮完全一样。
四种常见情况可以先这样判断:
| 变化方式 | 最长公共前缀 | 通常可复用到哪里 | 直观结论 |
|---|---|---|---|
| 在末尾追加消息、工具结果、子 Agent 输出 | 覆盖全部旧输入 | 旧输入末端之前的最近可用断点 | 影响通常最小 |
| 在靠后位置插入少量内容 | 到插入点之前 | 插入点之前的最近可用断点 | 前面大段可用,尾部重算 |
| 在靠前位置改一个词或一个参数 | 只到早期差异处 | 早期差异之前的最近可用断点 | 少量改动也可能造成大面积 miss |
| 把相同内容重新排序 | 往往在第一个换序位置就分叉 | 换序位置之前 | 内容没少,缓存却可能几乎归零 |
Agent 层结论 “上文有变化”不是一个足够精确的问题。需要问的是:变化是追加,还是改写;首个差异出现在第几个 token;那个位置之前是否恰好有一个已写入的缓存边界。
这里有一个容易忽略的限定词:通常。最长公共前缀只是“理论上兼容的范围”,不是命中的承诺。缓存可能未达到厂商规定的最短长度,可能只按整块写入,可能已经过期或被驱逐,也可能被路由到了没有该条目的推理节点。因此,同一个精确前缀有时会命中,有时仍会 miss。
下面把这个判断写成一个小模型。设旧请求渲染后的 token 序列为 ,新请求为 。两者的最长公共前缀长度为:
设当前系统真正可读取的缓存断点集合为 。一个条目进入这个集合,需要同时满足:已经写入、未过 TTL、未被驱逐、模型与相关配置一致、隔离键一致,并且当前请求被路由到能够访问它的位置。实际可复用边界是:
若不存在满足条件的断点,则 。新请求至少需要重新计算的输入范围约为 ,也就是 个 token;块式系统还会受到块粒度约束。
这两个量把两个经常混在一起的问题分开了: 由上下文内容决定, 由缓存实现和当时的运行状态决定。应用开发者可以直接影响 ,也能通过断点、版本、路由键和调用节奏间接提高 ,但不能把缓存驻留当成永远成立的事实。
KV cache 与 Prompt Cache
讨论缓存时,最常见的歧义来自两个都叫 cache 的机制。它们保存的对象往往有关联,生命周期却不同。
单次生成中的 KV cache
自回归模型生成第 个 token 时,需要让当前 Query 与此前位置的 Key、Value 做注意力计算。若每生成一个 token 都重新计算全部历史位置的 K/V,前面的工作会反复发生。KV cache 在每一层保存历史 token 对应的 Key 和 Value;下一步只计算新 token 的 K/V,再把它们追加到该层缓存。
Hugging Face 的缓存说明给出了这一过程的张量形状:常见实现中,每层 Key/Value 形如 [batch_size, num_heads, seq_len, head_dim],序列维随着解码逐步增长。这里的 num_heads 在分组查询注意力实现中应理解为 KV head 数量,而不一定等于 Query head 数量。
这一机制解决的是同一条推理序列内部的重复计算。请求开始时,模型对输入 prompt 做 prefill;随后进入逐 token decode。请求若在服务端结束,KV cache 是否继续存在、能否被下一次 API 调用使用,并不是单次生成机制本身能够回答的。
跨请求的 Prompt/Prefix Cache
Prompt Cache 或 Prefix Cache 把已经处理过的输入前缀变成可再次查找的对象。它保存的核心载荷通常正是各层 KV 张量,但在其外面还需要一套服务系统:
- 如何给前缀建立身份,例如 token 哈希、父块哈希、模型版本和附加配置;
- 在什么位置写入,是自动选择、显式断点,还是只缓存完整块;
- 缓存放在 GPU、CPU 还是更慢的介质,保留多久,内存不足时驱逐谁;
- 请求怎样路由到持有该条目的机器;
- 不同组织、租户或信任域之间怎样隔离;
- 命中、写入和普通输入分别如何计费与观测。
所以,Prompt Cache 就是 KV Cache 只说对了载荷的一部分。更准确的说法是:Prompt Cache 是围绕可持久复用的前缀 KV 状态建立的索引、存储、路由、生命周期和隔离机制。
以 vLLM 为例,Automatic Prefix Caching 的设计说明使用“父块哈希+当前块精确 token+附加哈希”标识 KV 块,只缓存完整块;LoRA 标识、多模态输入哈希和 cache salt 都可能参与身份计算。内存紧张时,可复用块仍可能按 LRU 被驱逐。由此可见,token 相同 和 这次真的读到了缓存 之间隔着一整层服务系统。
Semantic Cache 不是本文的 Prompt Cache
Semantic Cache 通常对用户问题、检索结果或最终答案做向量化,以语义相似度命中旧结果。它可能直接返回一份旧答案,也可能复用某段应用层产物。两句话的 token 前缀完全不同,只要语义接近,Semantic Cache 仍可能命中;相反,两次请求有极长的精确公共前缀,Semantic Cache 也未必关心。
二者的风险边界也不同。Prompt Cache 一般不跳过本轮 Decode,模型仍会依据完整上下文生成;Semantic Cache 可能跳过推理并返回旧答案,因此要处理时效、权限和错误传播。把两者混称,会让 缓存是否改变输出 缓存是否保存答案 之类的问题失去明确答案。
三个阶段放在一条时间线上
一次 Agent 调用之于推理可以粗略分成三段:
- Prefill:处理本轮完整输入,计算各层隐藏状态并建立输入部分的 K/V。长上下文的 TTFT 往往受这一阶段显著影响。
- Decode:每次生成一个或一小批 token,读取此前 K/V,并把新 K/V 追加进去。Prompt cache 不会把未来输出预先算好。
- 下一次 API 请求:客户端再次提交上下文,服务端按渲染后的前缀寻找可复用状态。找到了,从断点之后继续 prefill;找不到,就重新计算更长的输入。
Agent 层结论 使用 previous_response_id、conversation object 或框架内的 memory,不代表“历史推理状态免费保存”。这些能力处理的是历史传递和会话状态管理。服务端是否命中 prompt cache、历史输入 token 如何计费,要看具体 API。以 OpenAI Responses API 为例,Conversation state 指南明确区分了会话延续与输入计费;因此不能从“代码里没有重发数组”推导出“历史无需再处理或计费”。
为什么一个早期改动会让后面的大段内容失效
要回答这个问题,必须看 K/V 是怎样得到的。
后续 token 的状态带着前文
在第 层,位置 的输入隐藏向量记为 。一组简化的注意力投影为:
因果自注意力只允许位置 读取不晚于自己的位置:
第一层的 主要由当前位置 token 的嵌入和位置表示投影而来;但该层输出已经混合了位置 的信息。进入第二层后, 变了,新的 也随之改变。层层传播之后,位于差异点后面的 K/V 并不是“这个 token 自己的静态编码”,而是依赖此前上下文的中间状态。
如果在位置 插入一个 token,那么对所有 ,至少发生两类变化:
- 可注意的前文集合或其内容发生变化,注意力权重及混合结果随之变化;
- 原 token 的位置向后平移,绝对位置编码或 RoPE 等相对旋转关系发生变化。
因此,旧序列位置 之后的 K/V 一般不能直接搬到新序列中。哪怕插入的是一句“语义上无关”的注释,普通前缀缓存也不会尝试证明它无关。它使用的是精确身份匹配,不是语义等价证明。
这也解释了为什么尾部追加特殊。对旧输入长度 而言,追加发生在 。位置 的 token、位置以及可见前文均未改变,旧 K/V 与新请求的相同前缀兼容。模型只需为新增部分建立状态。
“首个差异 token”比“改了多少字”更直观
假设一个请求长 20,000 token:
- 在第 19,900 个 token 后增加 100 token,理论最长公共前缀仍是 19,900;
- 在第 100 个 token 改 1 个 token,理论最长公共前缀只有 99;
- 改写第 8,000 至 8,100 个 token,理论最长公共前缀是 7,999,与改写区间有多长无关;
- 删除开头一个看似多余的空格,若分词或渲染结果从这里开始不同,公共前缀可能在开头就终止。
这不是说差异点之后每个 token 的数值一定都会产生巨大变化,而是说常规系统没有一个既便宜又通用的依据,能把旧的后缀 KV 当作新上下文的精确计算结果。标准前缀缓存选择保守地从分叉点后重算。
可用断点还会把理论前缀向前取整
理论上公共到第 7,999 个 token,不表示一定能复用 7,999 个。若系统只在 1,024、2,048、4,096 处写过条目,且 4,096 之后的条目已经被驱逐,那么实际边界只能退到 4,096。若系统以 16-token 完整块为单位,即使公共前缀到 7,999,也最多命中到 7,984。
Anthropic 的当前实现很好地揭示了“稳定内容”和“已写入条目”的差别。官方 Prompt Caching 指南 说明,显式 cache_control 只在标记处写入;读取时会从断点有限地向前查找先前真正写过的条目,而不是看到某段内容稳定就临时把它当成已有缓存。当前文档写明每个断点最多回看 20 个 block,并允许最多 4 个断点。
OpenAI 的当前文档采用另一套规则。Prompt Caching 指南 描述隐式或显式断点、每请求最多 4 次写入以及有限数量的读取候选。值得留下一个版本脚注:截至 2026-09-03,该指南写“最近 50 个断点”,同日的 Responses API Reference 写“最近 80 个断点”。
KV cache 的显存为什么会成为系统问题
对传统 decoder-only 模型,用 MHA/GQA 形式做一个量纲正确的近似。设层数为 ,每层 KV head 数为 ,每个 head 维度为 ,缓存长度为 ,每个元素占 bytes,则单序列 KV 内存约为:
系数 2 对应 Key 和 Value。并发批次若不能共享前缀,还要乘以序列数;实际内存还包括块表、对齐和分配器开销。这个近似不应机械套在 MLA、滑动窗口、分块注意力、量化 KV、CPU/NVMe offload 等实现上,但它清楚说明一件事:KV 占用随层数和序列长度线性增长,长上下文乘上高并发后很快成为显存调度问题。
PagedAttention 论文 正是从 KV 内存巨大、动态伸缩且易产生碎片与重复拷贝的问题出发,引入类似虚拟内存分页的管理方式,并支持请求内与请求间的 KV 共享。论文作者在其测试条件下报告了相对当时基线 2–4 倍的吞吐提升。这个数字是系统论文的实验结果,不是开启 prefix caching 就能普遍获得的倍数。
推理系统层解释 前缀复用不仅减少 FLOPs,也在改变 KV 块的生命周期和共享关系。缓存更多前缀可以缩短 prefill,却会占用本可用于增大 batch 的显存。到多 Agent、高并发场景,这会变成“保留谁、驱逐谁、何时预取、怎样调度”的联合优化,而不再只是 prompt 排版问题。
常见的 Agent 上下文改动
Agent 框架很少直接操作 token。我们通常在改消息数组、工具 schema、记忆、附件或采样参数,然后由 SDK 与服务端把它们渲染成模型上下文。
| Agent 侧动作 | 通常可复用到哪里 | 失效原因 | 更稳妥的处理 |
|---|---|---|---|
| 末尾追加用户消息 | 旧请求的末端断点 | 旧前缀未变 | 保持追加式 history |
| 末尾追加 tool result | 调用记录之后的既有断点 | 新结果只是新后缀 | 把结果作为新的 tool/result item 追加 |
| 末尾追加子 Agent 输出 | 旧请求末端或聚合槽位前的断点 | 与普通新增消息相同 | 固定来源标签与序列化格式 |
| 把新结果回填到旧任务旁边 | 插入点之前 | 插入改变后续 token 的前文与位置 | 不追求视觉邻接;在尾部引用原 call ID |
| 改 system/developer prompt 开头 | 改动之前,通常极短 | 早期前缀分叉 | 版本化并批量发布,不逐请求拼动态字段 |
| 增删或重排工具定义 | 工具区之前,可能接近零 | 工具通常位于消息之前 | 保持全集稳定,运行时限制可调用范围 |
| 改 JSON Schema 描述或属性顺序 | 首个渲染差异之前 | schema 参与模型输入 | 使用确定性生成器和 schema 版本号 |
| 改输出格式、reasoning/effort 等 | 依厂商渲染层级而定 | 非文本参数可能渲染进前缀 | 把配置纳入 cache identity 并看官方失效表 |
| 只改空格、标点或换行 | 首个 token 差异之前 | 缓存按精确渲染前缀匹配 | 规范化模板,不做请求时美化 |
| 对旧历史做摘要替换 | 摘要插入点之前 | 后面的历史整体换位或改写 | 阶段性 checkpoint,接受一次冷写 |
| 从头截断最早历史 | 常常为零 | 新输入不再以旧开头开始 | 把它当成新 epoch,重新预热稳定头部 |
| 同样一组结果按完成时间排序 | 第一个次序不同处之前 | 并发完成顺序不稳定 | 按固定 agent ID/任务槽位排序 |
末尾追加:最符合因果模型,也最符合工程直觉
多轮对话的天然形态就是 append-only log:用户消息、assistant 输出、工具调用和工具结果依次出现。只要框架没有在背后重写旧消息,这种结构让上一轮完整上下文成为下一轮的前缀。
举个例子:协调器此前已经拥有 18,000 token 的公共上下文,现在收到一个 600-token 的子 Agent 结果,并把它追加到末尾。若最近的存活断点在 17,920,那么新一轮只需从 17,921 开始 prefill;旧前缀不因末尾多出 600 token 而整体报废。
注意“把输出追加到末尾”和“把输出插到所属任务下面”是两个不同操作。后者在人类阅读界面里更整齐,却会把插入点之后所有内容向后推。在供模型使用的事件流里,最好用稳定的 call_id、agent_id 或引用字段建立关联,而不是靠物理邻接表达归属。
system prompt 的一个小补丁,可能是最大的缓存事件
System prompt 往往处在渲染上下文最前面。若在线请求每次都把当前时间、部署哈希、实验组或用户显示名拼到这里,最长公共前缀会在很早的位置结束。此时下方几万 token 的工具、资料和历史再稳定也无法挽回。
一个常见反例是:
You are SupportAgent.
Current time: 2026-09-03T14:07:31.498+08:00
Trace ID: a3f4...
...后面是 20k token 的固定政策和工具定义...
时间戳每次不同,cache identity 从头部就分叉。改法不是伪造一个稳定时间,而是先问:它是否真的需要进入模型上下文?Trace ID 通常只属于日志元数据;若时间是任务必要条件,也应放在稳定资料之后、本轮动态输入附近,并保持格式固定。
角色指令本身也应有版本边界。support-agent/v17 发布后,同版本内保持字节级稳定;升级到 v18 时接受一次缓存冷写。把配置发布当成一次显式 epoch 切换,比在每个请求里微调措辞更容易观测,也更容易回滚。
工具定义往往比 system prompt 靠前
许多 Agent 开发者把 tools 看成“API 参数”,误以为不在 prompt 中。对模型而言,工具名、描述、参数 schema 和调用约束必须以某种形式进入上下文。Anthropic 明确给出了 tools → system → messages 的缓存层级;工具缓存规则写明修改工具定义会使 tools、system 和 messages 的缓存整体失效,改变 tool_choice、并行工具设置、图片或 thinking 参数则可能从更晚层级失效。
OpenAI 的当前指南也把工具名称、描述、schema、顺序,以及 parallel_tool_calls、text.format、reasoning effort、verbosity、compaction 等列入相关匹配条件。不能据此假定每家内部采用同一渲染模板,但可以得到一个跨厂商的稳妥原则:把所有会影响模型输入或推理配置的字段都视为 cache identity 的一部分,直到官方文档明确排除。
频繁地按本轮任务增删 tools,可能节省了几百个工具描述 token,却让后面的长 system prompt 和资料失去复用。更好的办法通常是保持工具定义集合和顺序稳定,再用服务端支持的选择机制限制本轮可调用项。OpenAI 文档建议通过 tool_choice、allowed_tools 或 deferred tool loading 控制范围;Anthropic 的 tool search/deferred tools 会把后来发现的工具以 tool_reference 形式追加,从而尽量保住前缀。
这仍不是绝对规则。如果工具全集有数万 token、绝大多数请求只用两个工具,始终携带全集可能使常规输入、显存和注意力成本超过缓存收益。正确做法是做实验,而不是把“工具稳定”升级为教条。
语义相同,不代表缓存身份相同
下面两段 JSON 对业务代码可能等价:
{"type":"object","properties":{"city":{"type":"string"},"days":{"type":"integer"}}}
{"properties":{"days":{"type":"integer"},"city":{"type":"string"}},"type":"object"}
若序列化顺序原样进入模型上下文,它们的 token 序列不同。类似问题还包括 Unicode 规范化、换行风格、尾随空格、可选字段默认值、数字格式、Markdown 表格对齐以及无意义的提示词“润色”。
解决方式是确定性渲染:固定键排序策略,固定空白,固定工具顺序,固定模板版本,并对最终请求结构做快照测试。不要只对模板源文件做哈希,因为 provider 端仍可能添加指令、角色标记或特殊 token。应用侧哈希适合发现自身漂移,服务端 usage 字段才是实际命中的证据。
compaction 是一次有计划的缓存换代
上下文不断增长,总会触碰长度、质量或成本边界。把旧历史压成摘要通常是对的,但它会把一大段原始前缀换成新文本。若从头截掉消息,旧序列甚至不再是新序列的前缀。
缓存友好的 compaction 不是永不压缩,而是避免每轮改写:
- 在一段时间内维持追加式 history;
- 达到质量或长度阈值时生成一次经验证的 checkpoint 摘要;
- 建立新的上下文 epoch:稳定头部+摘要+从该点开始的新事件;
- 接受首轮重新 prefill,并在随后的多轮中复用新 epoch;
- 旧 epoch 按保留策略退出,而不是两套上下文来回切换。
如果为了提高 hit rate 而永远保留已经无用的历史,模型每轮仍要读更长输入,KV 占用继续上涨,甚至会降低答案质量。一次必要的 cache miss,可能换来之后更短、更便宜、更可靠的多轮运行。
Agent 层结论 缓存优化的对象不是“自然语言字符串”,而是一个版本化的、确定性渲染的上下文事件流。追加是常态;早期改写、重排和截头是 epoch 变化,应被显式管理和监控。
模型看到的不是一段 prompt,而是渲染后的上下文
应用代码中常见的是这样的结构:instructions、tools、messages、response_format、reasoning。它们经过 SDK 与 provider 模板后,才成为模型实际读取的序列。一个典型但非标准化的逻辑分层可以写成:
[Provider instructions / model-specific markers]
[Tool definitions and tool-runtime configuration]
[System / developer instructions]
[Shared reference corpus]
[Tenant or user stable profile]
[Versioned agent role]
[Conversation history]
[Tool calls and tool results]
[Current request and per-turn dynamic data]
顺序并非跨厂商协议。Anthropic 公开的是 tools → system → messages;OpenAI 文档强调缓存针对 provider 最终渲染的上下文,并列出文本、图像、文档、工具和若干配置。应用框架也可能在消息前自动注入安全说明、技能定义、日期或 memory。换句话说,你在日志里看到的“prompt 没变”,未必等于模型前缀没变。
经常被漏算的变化
- 工具层:schema 的 description、required 数组顺序、工具列表顺序都可能变化。
- 媒体层:即使文本相同,图片、音频或文档输入的内容哈希、预处理方式和占位 token 也可能成为缓存键的一部分。vLLM 就会把多模态输入哈希加入块身份。
- 输出约束:JSON Schema、grammar 或
text.format可能在模型前缀中出现,也可能改变服务端执行路径。 - 推理配置:reasoning effort、thinking 开关、并行工具调用等字段在不同模型上可能被渲染到不同层级。Anthropic 的失效表甚至区分了“只使 messages 失效”和“连 tools/system 一起失效”的模型。
- 框架状态:自动 compaction、头部截断、memory 更新、提示词模板升级,都会改变用户没有直接填写的部分。
会话 ID 管理的是关联,不是推理状态
previous_response_id 或 conversation object 让客户端不必手工拼接所有历史,也能让服务端维持输入输出项的关联。这是状态管理接口。Prompt cache 则是 prefill 复用机制。两者可以协同,但不能互相替代。
心智模型应该是:
- 会话状态回答“这一轮应该继承哪些历史 item”;
- 渲染器回答“这些 item 和参数最终按什么顺序进入模型”;
- prompt cache回答“渲染结果的哪个前缀有可读取的计算状态”;
- 计费器回答“普通输入、缓存写入、缓存读取分别如何计量”。
因此,调试时应同时记录逻辑会话 ID、prompt/template 版本、工具清单摘要、模型与关键配置、缓存路由键,以及服务端返回的 usage 细项。只记录 previous_response_id 无法解释命中变化。
“同一个模型名”也应谨慎理解
模型快照、tokenizer、聊天模板、工具渲染或后端实现发生变化时,旧状态未必可复用。托管服务一般会把这些兼容性条件纳入内部键,不让客户端误读不兼容 KV;自托管系统则需要自己保证模型权重、适配器、位置配置、KV dtype 和模板版本一致。
Cache key 最好采用结构化语义,例如:
model_snapshot / tokenizer_version / prompt_v17 / tools_v9 / tenant_bucket
它不必真的拼成明文,更不应包含敏感原文;重点是让路由键、监控标签和应用版本边界对齐。一个只写 support-agent 的永恒 key,会把多代互不兼容的流量混在一起,让命中率和故障定位都变得含糊。
Cache 友好的 Agent 上下文架构
缓存友好的上下文不是“把所有静态内容塞到开头”这么简单。它要在稳定性、权限、相关性、长度和更新时效之间排序。
按变化频率和共享范围排布
一个实用的前缀骨架是:
- 稳定公共前缀:长期安全策略、输出基本约束、常用且稳定的工具定义;
- 租户或用户稳定资料:权限允许共享且一段时间内不变的知识、偏好或词表;
- 版本化角色指令:某个 Agent 的任务边界与行为约束;
- 追加式历史:消息、工具调用、工具结果和已确认决定;
- 本轮动态输入:当前问题、检索结果、时间、临时权限和 trace 相关信息。
排序要同时考虑共享范围。公共资料虽然很稳定,若含租户私有信息,就不能跨租户共用一个 cache identity。租户资料更新频率低,但权限边界比命中率更重要。常见做法是公共段可跨用户复用,租户段以 tenant salt/key 隔离,用户敏感段再细分;具体能否分层共享取决于服务商提供的隔离能力。
在真正有价值的边界写入
显式断点适合三类位置:昂贵而稳定的公共段末尾、更新频率较低的租户资料末尾、长对话的阶段性 checkpoint。断点不是越多越好。每个系统都有写入成本、候选数量、最短长度或块粒度;而且一个断点只有在后续请求会回来读时才有价值。
设某段前缀含 个 token,未缓存输入单价归一为 1,写入倍率为 ,读取倍率为 。在理想的连续复用情形中, 次请求不缓存的前缀成本是 ,缓存后的前缀成本为:
只有满足 时,纯 token 价格才更低。以常见的 为例,首次写入后只要成功复用一次,纯前缀 token 成本就已低于两次普通输入;但现实还要乘上实际命中概率,并计入不可复用后缀、TTL、并发首写竞争、存储费和运维复杂度。
固定版本、顺序和序列化
需要固定的不是人眼看到的“大意”,而是构成渲染输入的结构:
- prompt 模板用不可变版本发布,线上请求只引用版本;
- tools 按稳定 ID 排序,不按注册时间或字典遍历顺序;
- JSON schema 使用确定性序列化,并为生成器做快照测试;
- 共享资料用内容寻址版本,不在请求时插入“最后更新于……”;
- 子 Agent 输出使用固定 envelope,例如
agent_id、task_id、status、payload; - 同一缓存流量保持模型、reasoning、输出格式和相关工具配置一致。
确定性并不意味着永不更新。它意味着同一版本真的相同,版本变化真的可见。
动态元数据尽量不进上下文
Trace ID、请求 UUID、日志采样标记、机房、重试次数通常只服务于可观测性。它们应留在 API 元数据、日志或 tracing span 中。如果模型需要在工具调用中携带关联 ID,可以让运行时在模型输出后补齐,而不是要求模型重复一个随机值。
时间也要区分用途。“现在是 2026-09-03”可能是回答日程问题的必要事实;“本请求创建于 14:07:31.498”多数时候不是。前者放在动态后缀,后者留在日志。不要为了缓存删掉模型完成任务所需的事实,也不要把系统内部噪声包装成模型上下文。
稳定工具面,动态授权面
若供应商支持,工具定义与本轮授权最好分层:稳定工具 schema 留在可缓存前缀,本轮可调用子集通过 allowed_tools、tool_choice 或等价机制表达。大量长尾工具使用 deferred loading,让工具被发现后追加,而不是每轮重排头部。
这里必须做安全检查:稳定地向模型展示一个工具,不等于稳定地授予调用权限。执行器仍要在服务端按当前用户、租户和操作类型做授权;不能因为 schema 已缓存就把旧权限一起缓存。Prompt cache 保存模型输入的计算状态,不应成为权限决策存储。
把 compaction 视为 checkpoint
一个可操作的策略是为每个会话维护 context_epoch。epoch 内保持追加;触发压缩后,生成、校验并冻结摘要,context_epoch 递增。新的 cache key 和监控标签随之切换。这样,命中率下降能被解释为有计划的换代,而不是神秘回归。
压缩阈值不应只由 token 上限触发。还应看任务是否进入新阶段、旧工具结果是否失效、摘要能否保留证据链、模型质量是否受冗余历史影响。缓存服务于 Agent,而不是 Agent 服务于缓存。
Multi Agent 的分叉与汇合
多 Agent 并不会改变前缀缓存的基本规律,只会让“谁与谁共享哪一段前缀”变得更值得设计。
分叉点以前放公共根,角色差异放在以后
设三个子 Agent 都需要组织安全策略、项目资料和一套基础工具,随后才分别承担检索、代码审查和事实核验。如果每个角色 prompt 都从第一个 token 开始独立编写,即使大意相同,也很难形成精确公共前缀。更合适的布局是:
[组织公共策略 v8]
[项目资料 snapshot-42]
[基础工具 schema v11]
├─ [role: researcher] [task A]
├─ [role: reviewer] [task B]
└─ [role: verifier] [task C]
公共根在分叉点以前逐 token 相同,服务端才可能共享相应 KV 块。角色名、任务说明、临时资料和预算放到分叉点以后。若把 role=researcher 写在第一行,再复制公共资料,三条分支在人眼看来共享很多内容,缓存树却从开头就分开了。
这不是让所有子 Agent 使用同一份权限。公共根只能放真实可共享的信息;私有凭证、用户数据和角色授权仍需在相应隔离域或分支中处理。
每个分支维护自己的追加式链
一个子 Agent 多次被调用时,应尽量延续自己的事件链,而不是每轮由协调器重新生成“当前状态全文”。后者通常会改写摘要、任务清单顺序或时间字段,导致角色分支本身也无法连续复用。
可以把分支状态理解为:
shared_root@42 + role@3 + branch_epoch@7 + append_only_events
当任务目标发生根本变化,或分支需要 compaction,再切换 branch_epoch。这让缓存 miss 与业务状态变化有清楚对应关系。
汇合时确定性更重要
并行子 Agent 的返回顺序具有随机性。若聚合器按完成时间拼接结果,本轮可能是 A、C、B,下轮变成 B、A、C;两个请求的首个结果槽位就不同,后续公共内容全部失去前缀关系。
更稳的办法有两种:
- 等齐一个批次,按预先固定的
agent_id或task_slot排序,再序列化; - 流式汇合时把每个到达结果追加成独立事件,并接受不同运行之间只共享到分叉前;不要在旧历史中回填。
第一种提高跨运行的前缀一致性,但增加等待最慢分支的延迟。第二种更快,却牺牲一部分跨运行命中。选择取决于任务时延,而不是缓存美观度。
聚合器若要让结果与旧任务关联,可以在尾部写 result_for: task-B。把结果插回数千 token 之前的任务定义旁边,只能复用到插入点之前。对模型而言,引用字段已经足够表达关系。
发表评论
未登录评论需提供昵称和联系邮箱,提交后默认进入审核。
目录
作者
ThunGuo
Server R&D Engineer