多轮对话缓存与 Serving Engine 对比总结
多轮对话缓存与 Serving Engine 对比总结
文档目的
本文档总结本次会话中围绕以下主题的讨论结果:
- 基于 API 的多轮对话系统如何处理上下文、cache 和 token 消耗
- 单轮 KV cache 与多轮 prefix cache / session cache 的区别
opencode在长会话中的上下文管理方式- 开源或半开源 serving engine 在长对话、多轮前缀复用场景下的差异
- 从计算机体系结构和资源占用角度理解这些差异
本文档是一次工程分析记录,不是厂商白皮书。对于闭源 API 平台,文档只保留“官方明确公开的信息”和“基于公开实现可做的合理工程推断”。
一、核心概念区分
1. 单轮 KV cache
单轮 KV cache 是模型在一次推理请求内部使用的缓存。
- 它保存的是 Transformer 每层注意力计算所需的 key/value 张量
- 主要目的是避免在同一次生成过程中,为每个新 token 重复计算已经出现过的前缀
- 它通常只存在于一次请求的生命周期内
- 它属于推理引擎内部状态,不是应用层文本状态
直观理解:
- 用户发出一条请求
- 模型开始生成
- 在这次生成尚未结束前,已有前缀的 attention 结果会被缓存起来
- 继续生成下一个 token 时,模型复用这些张量,而不是把整个前缀重新算一遍
2. 多轮 prefix cache / session cache
多轮 prefix cache / session cache 不是同一个东西,但都属于“跨请求复用前缀”的范畴。
- prefix cache 更强调“相同或高重合前缀”的跨请求复用
- session cache 更强调“某个会话上下文”的持续复用能力
- 它们通常发生在 API 平台层或 serving engine 层,而不是单次推理内部
与单轮 KV cache 的关键差异:
- 单轮 KV cache 是一次请求内部的计算缓存
- 多轮 prefix/session cache 是跨请求的前缀复用机制
- 前者的目标是减少同次生成中的重复计算
- 后者的目标是减少多轮对话中重复 prefill 的计算、延迟,某些平台还会减少计费
3. 历史摘要压缩
历史摘要压缩指的是:
- 不再把整段旧对话原样发送给模型
- 而是先用模型或规则,把旧对话总结成更短的结构化摘要
- 后续对话只保留摘要和最近若干轮消息
这是一种典型的应用层上下文管理策略。
它与 prefix cache 的差别是:
- prefix cache 不改变语义上的输入内容,只是复用计算或缓存命中
- 摘要压缩会改变发给模型的文本本身
- prefix cache 主要省计算、降延迟,部分平台还能省账单
- 摘要压缩既能省计算,也能直接降低上下文占用和累计输入 token
4. 对话状态外置
对话状态外置指的是把不必一直放在 prompt 里的信息移到模型外部。
典型外置内容包括:
- 文件全文
- 检索结果
- 工具输出
- 命令执行日志
- todo 列表
- 代码 diff
- 中间工作记忆
只有在需要时,再把其中一部分注入给模型。
这是长会话系统中真正决定“是否会爆上下文”的关键手段之一。
5. 累计 token 消耗
累计 token 消耗是指:
- 一整个会话从开始到当前为止
- 所有轮次输入、输出、推理、缓存读写等 token 或等价计费量的累加
它回答的是:
- 这次会话总共花了多少钱
- 总共消耗了多少输入/输出量
6. 当前上下文窗口占用
当前上下文窗口占用是指:
- 当前这一轮请求发给模型的内容
- 占模型最大 context window 的比例
它回答的是:
- 这一轮会不会接近模型上限
- 当前请求还剩多少上下文空间
它不等价于累计 token 消耗。
一个会话完全可能:
- 累计已经花了非常多 token
- 但因为旧历史被压缩/外置
- 当前窗口占用仍然很低
二、关于“上一轮 KV cache 能否跨多轮直接复用”
在大多数商用 API 模式下,答案通常是:不能直接复用应用可见的上一轮 KV cache。
原因包括:
- 一次 API 请求结束后,底层推理实例通常就释放了执行态
- 下一次请求不保证被路由到同一块 GPU、同一台机器、同一批次
- 商用 API 很少把“上一轮的原始 KV 状态”作为可持续传递对象暴露给应用
因此,用户在应用层看到的“多轮缓存复用”,通常不是:
- 应用自己拿到了上一轮的 KV 张量并继续推理
而通常是:
- 平台内部对前缀做了匹配和缓存命中
- 或平台提供了 prompt caching / context caching / cached content 之类的能力
- 或应用层自己做了摘要压缩与状态外置
三、多轮系统降低 token 消耗和上下文增长的主要手段
如果一个系统每轮都重新发起 API 请求,但又不想让长会话迅速膨胀,通常会组合使用以下机制:
1. 应用层压缩
- 摘要旧对话
- 保留近期轮次
- 丢弃无用中间结果
2. 应用层状态外置
- 旧文件内容不常驻 prompt
- tool 输出不长期驻留
- 需要时再检索注入
3. 平台层 prompt caching
- 让相同前缀在跨请求时获得更低延迟
- 某些平台能返回 cached tokens 并提供更低成本
4. Serving engine 层 prefix KV 复用
- 对重复前缀减少 prefill 计算
- 降低 TTFT
- 提升吞吐
5. 选择性回放
- 不是把全会话每条消息都重放
- 而是只带最近重要消息、摘要、系统提示和必要状态
四、opencode 的现象说明了什么
用户观察到:
- 在
opencode里连续进行了十多轮问答 - 当前上下文窗口占用仍然不到 30%
- 系统提示是在剩余 20% 时才会压缩消息
这个现象说明:
- 它大概率不是一个“每轮原样全量重放全部历史”的极简实现
- 它至少在某些阶段做了消息过滤、压缩或裁剪
进一步查看仓库代码,可以确认这个判断是对的。
五、opencode 代码层面的结论
1. 它不是永远把全历史原样重放
packages/opencode/src/session/prompt.ts 中,会在进入主循环时读取:
MessageV2.filterCompacted(MessageV2.stream(sessionID))
这说明送入模型之前,会先过滤会话流里已经被 compaction 折叠过的部分。
从实现逻辑看:
- 一旦遇到最近一次成功完成的 compaction 边界
- 更早的历史就不会继续保留在活跃上下文里
所以 opencode 的活跃上下文不是“会话开头到当前的完整日志”,而是“最近一次有效压缩之后的消息片段”。
2. 它有自动 compaction 机制
packages/opencode/src/session/compaction.ts 中有 SessionCompaction.isOverflow()。
逻辑要点:
- 会读取模型 context limit
- 会保留一定的 reserved buffer
- 默认 buffer 最多为
20_000token - 一旦最近一次 assistant 调用的 token 总量接近阈值,就认为需要压缩
也就是说:
- 压缩不是等到完全爆窗才做
- 而是在接近上限前提前触发
3. compaction 不是简单截断,而是生成结构化摘要
packages/opencode/src/session/compaction.ts 中定义了 compaction prompt,目标是生成一个便于后续继续工作的摘要。
摘要模板会明确保留:
- Goal
- Instructions
- Discoveries
- Accomplished
- Relevant files / directories
这表明 opencode 不是粗暴丢弃历史,而是试图把旧对话压成一个“可继续工作的工作记忆”。
4. 它还会 prune 旧 tool 输出
在 SessionCompaction.prune() 中:
- 会向后遍历消息
- 对旧的 tool result 做 token 估算
- 当旧工具输出累计过大时,把更早的工具输出标记为 compacted
被 compacted 的工具结果不会再以原始内容送给模型,而会变成:
[Old tool result content cleared]
这意味着 opencode 不只是压缩“对话文本”,也会压缩“工具侧的长输出”。
5. UI 里的 “Context xx% used” 不是累计 token
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx 里:
- 会取最近一条有输出的 assistant message
- 使用它的
input + output + reasoning + cache.read + cache.write - 再除以该模型的 context limit
因此这个百分比反映的是:
- 最近一次模型调用占用了多少窗口
它并不等于:
- 整个 session 历史累计消耗了多少 token
与此同时,sidebar 里单独把所有 assistant message 的 cost 累加成 spent。
这进一步说明:
- 当前上下文占用
- 累计会话成本
在 opencode 里也是显式区分的两个指标。
6. cache.read/write 更像 provider 返回的缓存计费/命中信息
packages/opencode/src/session/index.ts 和 provider 相关适配代码中,可以看到:
- 会读取
cached_tokens - 把它们归入
tokens.cache.read与tokens.cache.write
这说明:
opencode确实会记录 provider 层的缓存命中或缓存写入量- 但这不表示应用自己持有了可跨轮续跑的原始 KV cache
更合理的理解是:
- provider 对某些前缀做了平台级缓存
- 并把命中/写入统计返回给了应用
六、仅凭现象能否判断“不是每轮全量重放”
如果完全不知道后端实现,只看“十多轮还不到 30%”这个现象,不能百分之百证明。
因为也可能存在以下情况:
- 模型窗口本来很大
- 每轮都很短
- 实际发给模型的工具内容很少
- 某些平台把一部分内容标成 cached tokens
但是对 opencode 这个仓库本身来说,可以明确地说:
- 它不是每轮把全部历史原样重放给模型
- 它有 compaction
- 有 filterCompacted
- 有 prune 旧 tool 输出
因此对这个项目本身,结论是可以下得比较硬的。
七、闭源 API 厂商公开到了什么程度
1. OpenAI
官方公开程度相对较高。
能明确知道的点包括:
- 存在 prompt caching
- 平台会按前缀命中
- 返回 cached token 相关信息
- 文档中明确提到过
key/value tensors
这意味着 OpenAI 至少已经公开到了“缓存对象接近 KV 层”的程度。
2. Anthropic
Anthropic 公开了:
- prompt caching 产品能力
- 断点控制
- 缓存时长
- 计费和适用位置
但公开资料没有像 OpenAI 那样把实现写到 KV tensor 这一层。
因此:
- 可以确认它有跨请求前缀缓存能力
- 但不能根据官方文档直接断言内部具体就是哪一种 KV 组织方式
3. Google Gemini / Vertex AI
Google 公开了:
- implicit caching
- explicit caching
cached_content之类能力
这说明它有显式上下文缓存接口和可观测缓存行为。
但在公开资料里,底层数据结构和缓存组织方式没有像某些开源 serving engine 那样讲透。
八、开源或半开源 serving engine 的主要路线
本次会话重点讨论了几类有代表性的 serving engine:
- SGLang
- TensorRT-LLM
- TGI v3
- LMDeploy
- vLLM
- LightLLM
- llama.cpp
需要特别强调:
- 这些系统大多优化的是“服务端重复 prefill 计算”
- 不一定改变
opencode看到的输入 token 数 - 更不一定改变“语义上发给模型的上下文长度”
也就是说:
- 如果
opencode每轮照样把同样的消息文本发过去 - 用户侧看到的“当前上下文 token 数”未必会变
- 变化更明显的是服务端算力消耗、TTFT 和吞吐
九、如果统一由 opencode 调用各家 API,按长对话前缀复用效果排序
在本次会话中,给出的是一个有前提的工程排序。
前提包括:
opencode每轮都继续发送同样的历史前缀- 后端使用兼容 API
- sticky session 成立
- 同模型、同量化、同硬件级别
- 关注的是长对话、多轮、高前缀重复下的缓存效果
在这个前提下,排序为:
SGLang(开启 HiCache)TensorRT-LLMTGI v3LMDeployvLLMLightLLMllama.cpp
1. 为什么 SGLang 通常排第一
原因不是它“会算得更多”,而是它的缓存体系最像一个真正的分层存储系统:
RadixAttention负责 prefix KV 复用HiCache引入多层缓存L1 GPU / L2 host / L3 distributed storage
这意味着:
- 热前缀可以留在 GPU
- 不够热的可以退到 CPU
- 更老但仍有价值的还能下沉到更低层级
对于长会话、多实例、缓存容易被挤掉的场景,这类分层设计最有优势。
2. 为什么 TensorRT-LLM 排第二
它的强项在于:
- radix search tree
- prioritized LRU
- partial reuse
- host offloading
也就是说,它不只是“有缓存”,而是对“哪些 KV 更值得保留、如何回收、如何复用”有更精细的控制。
在 NVIDIA 生产环境中,这类设计非常有竞争力。
3. 为什么 TGI v3 可以排到前面
TGI 更强调:
- 长 prompt 下前缀复用的实际体感
- 很低的 lookup 开销
- chunking 和长 prompt 优化
虽然公开结构细节不如前两者完整,但在长前缀重用场景中,它公开 benchmark 的表现很强。
4. 为什么 LMDeploy 和 vLLM 在中间
这两类系统更像“块级缓存系统”:
- prefix 或 block 复用能力成熟
- 能明显减少重复 prefill
- 结构相对简单,工程稳定
但从公开能力看:
- 它们不像 SGLang 那样强调多层缓存
- 也不像 TensorRT-LLM 那样强调更复杂的价值保留策略
因此在超长、多轮、多租户条件下,缓存生存能力通常略弱一档。
5. 为什么 LightLLM 更靠后
LightLLM 的公开重点更偏:
- token-level memory 管理
- 降低碎片
- 提升吞吐
它当然也会影响长对话性能,但从“跨请求前缀缓存体系”这个角度,不像前几家那样把它作为主卖点。
6. 为什么 llama.cpp 最后
它更适合:
- 单机
- 本地
- 轻量使用场景
而不是大规模、统一 API、多轮长会话服务。
十、从“省 token”角度应该怎么理解这些排序
这个问题在会话里专门澄清过。
如果“省 token”指的是:
- 真正减少送给模型的文本 token
- 真正减小当前上下文占用
那么最强的方案不是这些 serving engine,而是:
- 应用层摘要压缩
- 状态外置
- 只保留近期窗口
而如果“省 token”指的是:
- 服务端少做重复 prefill 计算
- 平台记账时 cached tokens 更便宜
- TTFT 更短、吞吐更高
那么 prefix cache / prompt cache / serving engine 的排序才有意义。
因此必须区分:
逻辑 token 数平台层 cached token实际 GPU 计算量
它们不是同一个指标。
十一、从计算机体系结构角度再看一次排序
1. 真正涉及的资源类型
长对话、多轮前缀复用,本质上消耗的是以下资源:
- GPU 计算
- GPU 显存
- CPU 内存
- PCIe / NVLink / 网络带宽
- 调度与元数据管理开销
2. 为什么 SGLang 往往在体系结构上最占优
因为它更像一个:
- GPU 显存缓存
- CPU 内存缓存
- 分布式存储缓存
组成的多级缓存体系。
类比计算机体系结构,它更接近:
L1/L2/L3 cache + DRAM + 远端存储
它用更多层级的存储资源,换取更高的长会话缓存命中率。
收益是:
- 减少重复 prefill 计算
- 降低因显存不足造成的缓存失效
代价是:
- 系统复杂度更高
- CPU 内存和网络开销更高
3. 为什么 TensorRT-LLM 也很强
它像一个更“工业化”的缓存控制器:
- 不只是存和取
- 还控制优先级、保留策略、回收策略和 partial reuse
因此它在资源紧张时更容易保住“高价值缓存”。
4. vLLM / LMDeploy 更像高质量页缓存
从体系结构角度,它们可以理解成:
- block/page 粒度的缓存系统
- 配合哈希索引与 LRU 回收
它们很成熟、很稳,但在“超长会话、多层存储、跨实例缓存保活”上通常不如前两名激进。
5. 最终的体系结构结论
对于长对话系统:
- 真正决定上限的,不是谁会算 attention
- 而是谁更像一个好的分层缓存系统
这也是为什么:
SGLang(HiCache)与TensorRT-LLM
在体系结构角度通常会被排在更前面。
十二、最终结论
1. 关于多轮对话缓存
- 单轮 KV cache 与多轮 prefix/session cache 不是一回事
- 多轮缓存大多发生在平台层或 serving engine 层
- 应用通常拿不到可直接续跑的上一轮原始 KV 状态
2. 关于 opencode
opencode不是每轮原样重放全部历史- 它有 compaction、filterCompacted 和 prune 机制
- UI 中的上下文占用百分比只反映最近一次调用的窗口占用
- 不等于整个会话累计 token 消耗
3. 关于 serving engine
如果统一由 opencode 调用各家 API,并且关注的是长对话中的前缀复用效果,那么本次会话中的工程排序为:
SGLang(HiCache)TensorRT-LLMTGI v3LMDeployvLLMLightLLMllama.cpp
4. 关于“最省 token”的本质
若指真正减少发给模型的文本 token:
- 应用层摘要压缩与状态外置最关键
若指减少服务端重复 prefill 计算与提升缓存命中收益:
- serving engine 的 prefix/KV cache 设计才是主角
换句话说:
opencode这类长会话系统的上限,首先取决于应用层上下文管理- 其次才取决于底层 serving engine 如何组织和保留 prefix KV cache