【LLM】LLM 上下文与 KV Cache:从基本原理到 Hermes Agent 实践
多轮对话的连续性依赖上下文管理,推理效率则依赖计算结果的复用。两者共同影响 Agent 的运行成本,但属于不同层面的机制。理解这一边界,有助于解释模型的“记忆”、前缀缓存的命中条件,以及 Harness 在长任务中的工程取舍。
本文以普通因果 Transformer 的推理过程为基础,并结合 Hermes Agent 官方文档介绍上下文管理实践。
多轮对话与上下文管理
短期记忆来自请求中的历史上下文
普通 LLM 推理通常不会更新模型权重,也不会在两次独立请求之间自动保留上一次对话的内部状态。所谓对话中的“短期记忆”,主要通过将此前的消息重新放入本轮上下文实现。模型能够引用先前的信息,是因为这些信息再次出现在当前请求中。
在普通无状态 API 中,多轮请求通常按以下方式构造:
1 | 第 1 轮输入:S + U1 |
其中,S 是系统提示词,U 是用户消息,A 是模型回复。上一轮的输入与输出,都会成为下一轮的历史输入。Agent 调用工具后,工具调用记录和工具结果也会进入后续上下文。
因此,这种“记忆”并不是模型在请求结束后自行记住了对话,而是外部系统保存历史,再为下一次推理提供相关信息。服务端会话或响应链可以代替客户端管理历史、减少重复传输,但模型仍需使用恢复的上下文或计算状态。
历史累积带来上下文增长
如果每轮都携带全部历史,请求长度就会持续增长。对 Agent 而言,文件内容、终端日志和工具结果也可能占用大量 token,使上下文快速膨胀:
- 窗口压力:历史占用越来越多的上下文空间,最终可能超过模型的上下文窗口。
- 重复计算:若无法复用已有计算结果,每轮都需要重新处理大量相同前缀。
- 成本增长:旧消息在后续轮次仍属于输入,反复发送会持续产生输入费用。
KV Cache 用于减少其中的重复计算:相同历史前缀再次出现时,推理服务可以复用已经计算的注意力状态。它并不会缩短输入或扩大上下文窗口,因此历史裁剪、摘要与检索仍然是必要的上下文管理手段。
历史、上下文与缓存的职责划分
一个典型 Agent 系统中,模型根据上下文生成内容,推理服务执行计算并管理 KV,Harness 组织消息、调用工具和保存运行状态。三类信息各有用途:
- 对话历史:持久保存用户消息、模型回复与工具记录。
- 模型上下文:本轮实际提供给模型的信息,受上下文窗口限制。
- KV Cache:已计算的注意力状态,用于减少重复计算。
例如,一次调试任务的磁盘历史可以保留完整报错和多轮尝试,而本轮上下文只包含“已定位到配置问题”的摘要、最近的工具结果和当前指令。原始记录仍然存在,但并未全部交给模型。
外部记忆可以由历史存储、摘要和检索共同实现,它与临时推理缓存具有不同的生命周期。会话保存一周后仍可恢复信息,但无法仅凭会话存在推断 KV 缓存仍然有效。
KV Cache 与前缀复用原理
从“苹果”理解 Q、K、V
自回归模型逐个生成 token。处理每个位置时,模型通过注意力机制,从当前可见的内容中提取相关信息。以“水果店里摆着新鲜的苹果”为例,可以把处理“苹果”的过程理解为:回看前文,寻找有助于理解这个词的线索。
注意力计算中,Query、Key 和 Value 分别承担三个角色:
- Query(Q)像当前的查找需求:读到“苹果”时,需要寻找有助于理解它的相关信息。
- Key(K)像各条线索的检索标签:“水果店”“新鲜”等内容各自提供可供匹配的标签,帮助模型判断哪些位置值得更多关注。
- Value(V)像线索携带的具体信息:“水果店”提供售卖水果的场景,“新鲜”提供与食物有关的描述。这些信息按相关程度汇总,参与当前位置的理解。
Q 表示当前要找什么,K 帮助判断哪些内容相关,V 提供从这些内容取出的信息。这只是便于理解的类比,模型并不会真的为每个词写出问题或标签。如果“苹果”被切分为多个 token,上述过程分别发生在各个 token 的位置。
从线索匹配到 Attention 结果
这三个角色通过以下公式联系起来:
1 | Attention(Q, K, V) = softmax(QKᵀ / √dk) × V |
公式描述的是一次“匹配线索,再汇总信息”的过程:
- QKᵀ 为线索打分:将当前位置的 Q 与各个可见位置的 K 逐一匹配,得到相关性分数。“水果店”“新鲜”等线索可能获得较高分数。转置符号 ᵀ 用于配合批量匹配的计算。
- 除以 √dk 调整分数尺度:dk 表示 Q、K 中包含的数值个数。除以它的平方根,是为了避免分数尺度过大,使后续的关注分配更稳定。
- softmax 将分数转为关注比例:各个位置获得非负权重,所有权重之和为 1;相关性较高的位置通常获得更多关注。
- 乘 V 按关注比例汇总信息:各个位置的 V 按权重混合,得到本次注意力计算的结果。
Attention(Q, K, V) 就是汇总后的信息表示,供模型后续处理。在“苹果”的例子中,它可能带入水果场景与新鲜程度等信息。这个结果还不是最终答案,也不是预测出的下一个词。
Q、K、V 来自模型在各层对当前位置的内部表示所做的转换,常简写为:
1 | Q = H × Wq |
H 是当前位置在该层的内部表示,Wq、Wk、Wv 是模型训练得到的转换参数。实际计算还包含位置处理、多头注意力等机制,但不影响这里的基本分工。
KV 随前文变化,不随后文变化
注意力结果会参与后续层的处理,因此较高层的 K、V 通常已经受到前文影响。把句子换成“这家科技公司叫苹果”,前文提供的线索就变成了公司语境;即使“苹果”对应相同 token,其完整的多层 KV 也通常不同。
在模型参数和计算配置固定的普通因果注意力模型中,可以将这种依赖概括为:
1 | 每个 token 的 KV = f(这个 token, 它前面的所有内容, 它的位置) |
它随前文变化,不随后文变化。这里表示计算可能依赖这些信息,并不意味着每个前文 token 的影响都相同。
例如,在“水果店里摆着新鲜的苹果”后面追加“,旁边还有香蕉”,不会改变已有“苹果”的 KV,因为因果注意力使前面的 token 看不到后文。但修改“苹果”之前的内容,或改变它的位置,通常就需要重新计算相关 KV。
这也解释了 KV 与 Token Embedding 的区别:Embedding 通常是从词表查找得到的初始表示,完整的多层 KV 则依赖前文和位置,不能为词表中的每个 token 预先保存一份通用 KV。
从单次生成缓存到跨请求前缀复用
处理新位置时,模型生成该位置自己的 Q,再用它匹配已有位置的 K,并汇总对应的 V。由于新增后文不改变已有 KV,前文的 K、V 可以保留下来,供后续位置继续使用。
KV Cache 保存的正是这些已经算好的 K、V。此前位置的 Q 已经完成了自己的查找,后续位置会使用新的 Q,因此这种缓存主要保留 K、V。
这一机制先用于同一次请求的连续生成,再扩展到不同请求之间:
- 单次生成中的 KV Cache:生成后续 token 时,直接使用前文已有的 KV,避免反复计算前文状态。
- 跨请求的前缀缓存:如果推理服务保留了此前请求的 KV,新的请求又使用相同前缀,就有机会跳过这部分前缀的重复计算。
例如,第一轮输入为 S + U1,第二轮输入为 S + U1 + A1 + U2。第二轮开头的 S + U1 未变,因此有机会复用其缓存;第一轮回复 A1 是否也能直接复用,则取决于服务是否保存了生成部分的缓存。
跨请求前缀缓存主要减少重复的 prefill,也就是输入处理阶段的计算。它不缓存最终答案:新输入仍需处理,新输出仍需生成,新的 Q 仍需对已有 K、V 执行注意力计算。因此,缓存可以降低重复开销,却不会让长上下文的计算成本完全消失。
缓存命中条件与生命周期
前缀匹配与失效
普通前缀缓存匹配从开头连续一致的 token 序列。以下用字母表示连续片段,实际匹配粒度由服务实现决定:
- 追加内容:ABCDEFG → ABCDEFGH,通常可复用 ABCDEFG。
- 修改历史:ABCDEFG → ABXDEFG,通常只能复用 AB。
- 裁剪历史:ABCDEFG → EFG,通常无法复用原序列中的缓存。
- 保留系统提示词:SABCDEFG → SEFG,通常仍可复用 S。
中间修改后,即使后面的文字保持一致,也通常无法直接复用旧 KV。原来的 D 是在 ABC 的前文下计算的,新 D 则处于 ABX 之后。话题在语义上独立,不代表注意力计算被隔离。
裁剪历史也会改变依赖关系。EFG 从原序列尾部移到开头后,其前文和位置均发生变化。不过,新请求 EFG 建立缓存后,下一次追加为 EFGH,又可能复用这一新前缀。
相同文本只是必要条件的一部分。模型、工具定义、消息序列化、位置处理及其他影响计算的配置也需兼容;缓存还必须已经建立、未被淘汰,并能被当前请求访问。不同 session 可以共享前缀,但受租户隔离和部署策略限制。vLLM 的前缀缓存以块为单位组织,块标识包含前缀依赖,因此实际复用边界未必精确落在任意 token 处。
缓存生命周期独立于会话生命周期。命中率由输入稳定性、请求间隔、缓存容量、并发负载和路由共同决定。长期保存历史只能保证信息可恢复,无法保证历史计算结果可复用。
自部署推理框架:直接管理 KV 资源
在自部署的 vLLM、SGLang 中,缓存读取是推理引擎对已有 KV 状态的复用。部署者可以配置缓存容量、淘汰策略及存储层级,也需要处理多实例路由与缓存共享。框架本身不按“缓存读取 token”收费,收益体现为减少重复 prefill、缩短延迟和提高吞吐;实际成本来自 GPU、内存、存储及数据传输。
- vLLM:Automatic Prefix Caching 以块为单位保存和匹配 KV,块标识包含此前前缀的依赖。匹配到的块可直接复用,未匹配部分继续计算。容量不足时回收可淘汰块,其缓存管理包含 LRU 机制。
- SGLang:RadixAttention 使用基数树组织 token 前缀及对应 KV,使具有共同前缀的请求共享计算状态。HiCache 进一步将缓存扩展到 GPU 显存、主机内存和外部存储;命中较低存储层时还需搬运 KV,因此不同层级的命中收益并不相同。
这类框架的缓存通常服务于某个推理实例或配置好的共享范围。仅部署多个副本,不会自动使所有副本拥有同一份缓存。例如,前一轮在实例 A 建立缓存,后一轮被分配到实例 B;若两者未配置缓存共享,即使输入前缀一致,B 也可能需要重新计算。
商用 API:通过接口与计费规则使用缓存
商用 API 由服务商管理底层 KV、存储和调度。调用方通常不能直接读取或搬运 KV,而是保持输入前缀稳定,使用平台提供的缓存选项,并从返回的用量统计确认命中。缓存读取既表示计算复用,也是一类折扣输入用量;其计费边界由服务商定义。
- OpenAI:支持的模型默认启用前缀缓存,部分模型还提供显式控制与保留选项。命中输入享有折扣,写入计费随模型而异。
- Anthropic:通过 cache_control 启用自动缓存或设置显式断点,支持五分钟与一小时 TTL。缓存读取享有折扣,写入可能有溢价。
- 智谱:自动识别可复用的上下文,无需手动设置断点。命中 token 按折扣价计费,可通过 cached_tokens 查看用量。
- DeepSeek:默认启用硬盘缓存,后续请求需匹配已落盘的前缀单元。命中与未命中输入分别统计、采用不同价格。
- Kimi(Moonshot):自动复用符合条件的重复前缀,命中输入享有折扣。Kimi K3 还提供五分钟与一小时 TTL,并单列缓存写入用量。
- Qwen(阿里云百炼):支持自动识别前缀的隐式缓存,以及通过 cache_control 标记的显式缓存。支持范围随模型而异,显式缓存有效期为五分钟,命中后续期。
- OpenRouter:缓存能力与价格取决于上游 provider,并通过粘性路由提高连续请求的复用机会。其完整响应缓存与 KV 前缀缓存是不同机制。
两类系统遵循相同的前缀依赖原理,但可复用的边界与保留规则不同。自部署侧关注缓存资源和调度效率,API 侧关注平台支持的缓存控制、实际命中用量和任务账单。
上下文长度与推理成本
云 API 常将 token 用量区分为以下类别:
- 非缓存输入:本轮未从缓存读取的普通输入,按基础输入价格计费。
- 缓存读取:本轮复用已缓存前缀的输入,通常享有折扣。
- 缓存写入:本轮新建立缓存的输入部分,部分平台单独计费,可能有溢价。
- 输出:本轮新生成的 token,价格通常高于基础输入。
这些是计费分类,不能直接等同于底层每项计算操作。部分服务不单列缓存写入费;显式写入计费模式下,一段 token 按写入价收费后,通常不再重复收取基础输入费。Claude 的账单区分缓存创建、读取及普通输入,并按 TTL 等条件采用不同价格。
一般而言,缓存读取的价格最低,普通输入按基础价格收费;显式缓存写入可能高于普通输入,较长的保留时间也可能对应更高的写入价格。输出 token 的价格通常高于普通输入。具体倍率随平台和模型而变化,部分平台不单独收取缓存写入费。
上一轮回复已经支付生成费,进入下一轮后仍按输入规则计费:命中缓存时按缓存读取价收费,未命中时按普通输入或缓存写入规则收费。生成时计算过 KV,并不保证服务会在下一轮继续保存和复用这些状态。
缓存降低单位输入成本,却不会阻止历史总量增长。持续携带大量无用信息仍会消耗上下文窗口、缓存空间和注意力计算。压缩历史会破坏变化位置之后的旧前缀,但减少后续每轮的输入量。
因此,压缩是否划算,需要比较一次摘要与缓存重建的开销,以及后续多轮缩短上下文的累计收益;同时验证摘要是否保留约束、决策和任务状态。优化目标应是完整任务的成本、耗时与成功率。
Hermes Agent 的上下文管理实践
Hermes 的上下文管理围绕三个核心思想展开:保持前缀稳定、复用不断增长的历史,以及在上下文过长时进行压缩。
按稳定性组装系统提示词
Hermes 将系统提示词按 stable、context、volatile 分层排列。以下是简化示意,展示内容的组织顺序,而非实际请求的完整格式:
1 | [stable:身份与通用指导] |
身份和通用指导位于前部,项目上下文居中,较易变化的信息位于后部。同一项目的多个会话因此有机会共享较长的前缀。
会话内仍需尽量保持已组装的提示词稳定。例如,技能列表发生变化时,可以追加一条“新增可用技能……”的提示,而不立即改写系统提示词中的旧列表。这样,新信息进入上下文,同时保留已有前缀。
随历史增长移动缓存断点
在 Claude 路径中,Hermes 使用 system_and_3 策略,在系统提示词和最后三条非系统消息处设置断点。随着消息追加,后三个断点向后移动:
1 | 请求一:S★ + M1 + M2★ + M3★ + M4★ |
S 表示系统提示词,M 表示非系统消息,★ 表示缓存断点。请求二仍保留请求一截至 M4 的前缀,因此有机会读取已有缓存,再处理新追加的 M5。断点标记的是截至该位置的完整前缀,并不会将某条消息变成独立缓存。
用结构化摘要替换中间历史
历史过长时,Hermes 保留头部与近期消息,将中间历史替换为摘要,并保持工具调用与结果的配对。原始记录可通过会话检索按需恢复。
1 | 压缩前:系统提示词 + 起始消息 + 大量中间历史 + 近期消息 |
例如,一段项目调试历史可以压缩为:
1 | 目标:为登录接口增加限流。 |
摘要保留继续执行任务所需的状态,减少重复携带的日志与中间讨论。由于摘要改变了前文,后面的近期消息即使原样保留,也通常需要重新计算 KV。
这套设计在日常运行中维持前缀稳定,在压缩时接受缓存重建,以缩短后续上下文。缓存复用与信息保留共同服务于任务的连续执行。
实战建议
缓存优化首先要保持可复用前缀稳定,同时接受必要的信息更新与历史压缩。以下几类情况容易导致未命中,可以在 Harness 设计和运行排查中逐项检查。
动态内容进入前缀
时间戳、随机 ID、每轮变化的检索结果或记忆注入,如果放在历史之前,可能使变化位置之后的旧缓存无法复用。例如:
1 | 请求一:系统提示词 + 时间 T1 + 历史 + 问题一 |
即使历史完全相同,时间字段已经改变了它的前文。
- 将通用指导、工具定义和稳定资料放在前部,将当前问题、检索结果和运行状态放在后部。
- 会话启动时生成的快照尽量保持稳定;需要更新的信息通过新增消息提供,避免反复改写旧消息。
- 保持工具顺序和请求序列化稳定,避免无意义的顺序变化破坏前缀。
历史压缩与摘要替换
Hermes 的 compaction 或其他 Agent 的历史总结,会改变模型可见的消息序列。摘要之后的内容即使原样保留,也通常需要重算 KV。不过,摘要之前未变的部分仍可能命中,因此并不必然整段失效;新输入也可能按缓存写入价而非普通输入价计费。
- 在明确的上下文预算或任务阶段边界执行压缩,避免每轮都重新总结历史。
- 摘要保留目标、约束、关键决策、已完成工作和下一步,减少后续重新检索与返工。
- 压缩后继续追加消息,建立并复用新的稳定前缀。
- 比较后续多轮的累计成本,而非只看压缩后的首轮。该轮需要缓存重建,但上下文已经缩短,未必比压缩前更贵。
模型、Provider 与执行分支变化
不同模型的 KV 通常不能互用。Fallback 切换模型、子 Agent 使用不同模型,或聚合平台切换上游 provider,都可能需要建立新缓存。
独立的子 Agent 对话也不保证共享缓存,但“对话独立”不等于“完全不能共享”:若模型、缓存访问范围和前缀均相同,仍可能复用共同的系统提示词或工具定义。
- 在连续执行阶段尽量保持模型与 provider 稳定;需要切换时,将缓存重建纳入成本预期。
- 按实际能力选择子 Agent 模型,并让同类任务使用稳定的公共提示词与工具定义。
- 使用平台支持的缓存路由参数或粘性会话机制,但不要将其视为命中保证。
- 将主 Agent、子 Agent 和 fallback 的用量分别记录,避免将新分支的冷启动误判为原会话缓存失效。
缓存过期与容量淘汰
中断后恢复会话时,缓存可能已过期,也可能因资源压力被淘汰。保留时间取决于平台、模型与配置;一次用餐间隔是否导致失效,不能仅凭“同一个 session”判断。
- 根据真实请求间隔选择平台支持的 TTL;需要人工介入的长任务可评估更长的保留选项及写入成本。
- 自部署时同时观察缓存占用、淘汰和实例路由。缓存存在于实例 A,不代表实例 B 可以直接使用。
- 长时间暂停前保存清晰的任务状态,恢复时只引入必要历史,控制可能的冷启动成本。
- 仅在预计收益覆盖刷新费用时考虑缓存保温,避免为很少复用的历史持续发送请求。
平台缓存条件未满足
前缀相同仍可能因最小长度不足、缺少显式标记、超出查找范围,或对应前缀尚未写入缓存而未命中。
- 核对当前接入路径的缓存规则,不把某个平台的断点、TTL 或计费方式直接套用到其他平台。
- 对需要显式缓存的接口,在稳定资料末尾设置断点,并通过返回的缓存创建与读取用量验证效果。
- 不要为提高命中比例而加入无用内容。最终评估任务总成本、延迟与完成质量,而不是单独追求缓存命中率。
参考资料
- vLLM 自动前缀缓存设计文档: https://docs.vllm.ai/en/latest/design/prefix_caching/
- Claude 提示缓存(cache_control 与 TTL、计费说明): https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- Hermes Agent 开发者文档:Prompt Assembly(系统提示词分层与缓存边界): https://hermes-agent.nousresearch.com/docs/developer-guide/prompt-assembly/
- Hermes Agent 开发者文档:Context Compression and Caching(system_and_3 断点与压缩策略): https://hermes-agent.nousresearch.com/docs/developer-guide/context-compression-and-caching/
- SGLang Radix Cache 源码(基数树组织的前缀缓存): https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/mem_cache/radix_cache.py
- SGLang HiCache 最佳实践(多级存储缓存): https://docs.sglang.ai/advanced_features/hicache_best_practices.html
- OpenAI Prompt Caching(自动前缀缓存与用量字段说明): https://developers.openai.com/api/docs/guides/prompt-caching
- 智谱开放平台:上下文缓存(自动识别与 cached_tokens 用量): https://docs.bigmodel.cn/cn/guide/capabilities/cache
- 智谱开放平台:对话补全 API 参考: https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E5%AF%B9%E8%AF%9D%E8%A1%A5%E5%85%A8
- DeepSeek 上下文硬盘缓存(命中与未命中分开计价): https://api-docs.deepseek.com/zh-cn/guides/kv_cache/
- DeepSeek 模型与价格: https://api-docs.deepseek.com/zh-cn/quick_start/pricing/
- Kimi Context Caching 最佳实践(缓存命中折扣与 TTL 选项): https://platform.kimi.ai/docs/guide/context-caching
- 阿里云百炼:上下文缓存(隐式与显式缓存): https://help.aliyun.com/zh/model-studio/context-cache
- OpenRouter Prompt Caching(上游依赖与粘性路由): https://openrouter.ai/docs/guides/best-practices/prompt-caching
- OpenRouter Response Caching(响应缓存,与 KV 前缀缓存为不同机制): https://openrouter.ai/docs/guides/features/response-caching