Forge 如何组织请求,实现提示缓存(Prompt Cache)复用
引言
过去几个月,我们一直在做一个开源项目:Forge,一款以终端为中心的 AI IDE,面向 Vim 和终端的重度用户,让他们不必切换上下文就能用上 AI 辅助编程。Forge 的目标是让软件从想法一路走到上线,全程不离开终端工作区。你可以把它理解成 Vim + Claude Code + 终端 + Git 的结合体,全都跑在你的终端里。
Forge 的核心之一,是它的 AI 编程辅助模块(harness)。它接收用户的自然语言提示,负责调研、设计并构建软件。在第一版里,即便小心翼翼地挑选模型,OpenCode Go 订阅的每周额度也撑不过一天。对一个自筹资金的项目来说,这给预算带来不小的压力,也影响到产品的持续使用。
OpenCode Go 按美元金额计费,而缓存后的输入比未缓存的输入便宜,通常能差一个数量级。于是提示缓存(prompt caching)成了一条很有希望的路子,可以把可用额度撑得更久。
本文会深入讲 Forge 里是怎么实现并测试提示缓存的,最终做到了 99% 以上的缓存命中率。
为什么要用提示缓存
编程类请求往往反复带上之前请求里的指令和上下文。这些共享上下文每次都要重新处理,白白花掉成本和时间。
模型推理时,会先处理输入、生成中间态的键值(KV)状态,然后一个 token 一个 token 地输出。有了提示缓存,服务商就能把提示前缀对应的合格 KV 状态保留下来;之后如果新请求的开头前缀相同,就可以直接复用。
能不能被缓存、能不能被复用,取决于服务商的缓存策略。对编程辅助工具来说,把请求内容排一排顺序——稳定的指令和上下文放前面,请求之间会变的内容放后面——就能提高复用的概率。
只要后续请求的开头前缀相同且符合条件,服务商就能复用已缓存的键值状态。
2026 年,顶级的编程工具缓存命中率平均在 90% 左右。Forge 内部给自己定的目标是 95%。
Forge 如何构造请求
为了保持前缀稳定,Forge 把静态内容放在请求开头,动态内容放在末尾。
Forge 的请求路径分两层。
-
规范消息:Forge 的
ModelRequest对象,与具体提供商无关。 -
提供商专属的网络请求:发起网络请求前,
ModelRequest对象会按各家提供商的 API 格式重新组织。
Forge 先构造一条规范请求,再由提供商适配器做序列化,并加上各自的缓存标记或路由提示。
构造规范消息时,Forge 的步骤如下:
-
先创建系统消息:里面包含系统提示词、工作区的
AGENTS.md,以及自动发现的 Skills。加入系统消息之前,Skills 会按名称排序,会话的临时目录也会插入系统提示词中。 -
把系统消息保留在对话历史里。恢复此前的会话时,Forge 会重放日志,并使用当时存下来的系统消息。这样,即使用不同的系统消息恢复旧会话,也不会让缓存失效。
-
把用户消息、助手回复和工具调用结果追加到消息末尾。
-
每一步模型调用前,如有需要,会先修复并压缩历史。压缩会重写上下文,并开启一个新的缓存周期,因为旧前缀已经不能再描述当前的提示词了。
-
最后创建
ModelRequest。它打包了对话记录、选中的工具和模型、工作区根目录、路由和会话 ID、推理相关设置,以及prompt_cache: true。
规范消息就绪后,Forge 会把它转换成提供商专属的网络请求。
-
Anthropic:在有用的边界位置放置缓存标记。Forge 会标记最后一个系统块、最后一条工具定义,以及当前对话的末尾。随着对话增长,对话标记会移到新的末尾;系统和工具标记则留在各自的位置。
-
OpenAI 兼容 API:把对话按顺序作为消息发送,包含 system、user、assistant 回复以及 tools。Forge 把每个工具转换成 function schema,并加上一个由工作区路径派生出的
prompt_cache_key。
Forge 如何保持前缀稳定
为了保持前缀稳定,Forge 做了这几件事:
-
完整保留历史记录:新的用户轮次和工具结果只是往对话记录后面追加,之前的消息原样不动。
-
变量列表排序:工具名和 Skills 都经过排序,避免 map 或文件系统的偶然顺序改变提示词。
-
内容冻结:图片快照和冻结的工具输出,防止后续文件改动影响旧的对话记录内容。
-
保留系统指令:使用记录在案的 system message,而不是根据「当前恰好存在的指令」重新生成。
-
缓存路由:Forge 把工作区路径哈希成
prompt_cache_key,帮助服务商的缓存算法把同一个工作区的所有会话路由到一起。
我们如何统计缓存命中
每次模型返回后,Forge 会读取服务商上报的缓存输入 token 数并做归一化处理。
-
Anthropic:上报
cache_read_input_tokens。 -
OpenAI 兼容 API:在 token-details 字段中提供
cached_tokens。
Forge 把各次模型调用中的已缓存和未缓存输入 token 相加。由于 Anthropic 单独上报未缓存输入,而 OpenAI 会把已缓存 token 计入总量,因此需要对各服务商的总 prompt token 做归一化。
我们如何评估 Prompt Caching
我们测试了 Forge 的缓存感知请求流程,使用 DeepSeek V4.1 Flash、OpenCode Go 订阅。评估基于工具调用、代码仓库内容和系统指令的快照。我们用了三个不同的用例,覆盖不同场景。
-
对话持续增长:起始系统消息相同。之后每个请求都会新增相同的用户消息、助手回复和工具返回结果。以此衡量对话变长时缓存复用的表现。
-
仅结尾变化:每个请求的开头部分保持一致,只改动最后的用户消息或工具返回结果。以此衡量近期上下文变化时,Forge 能否复用前面的内容。
-
早期指令变化:改动工作区指令中的一行,然后发送相同的后续对话和工具结果。以此衡量当提示词靠前部分发生变化时,复用会在哪里中断。之后重复发送改动后的请求,衡量新前缀的复用情况。
每个用例我们各运行了 10 次评估。
结果与发现
均值、最小值、最大值和标准差均基于每个用例的 10 次缓存感知运行计算得出。按 token 加权的比率等于缓存读取 token 总数除以输入 token 总数。
各用例的 token 总数为 10 次运行的合计。未缓存输入是从总输入中减去缓存读取和缓存写入后的剩余部分。
-
追加消息或仅改动最后一条消息能提升缓存命中率,前两个用例的平均缓存命中率分别为 99.60% 和 99.48%。
-
改动位于消息靠前部分的指令,会让缓存命中率比其他用例下降约 5 个百分点,未缓存输入 token 约 30k,而前两个用例仅约 2.5k。
-
早期变化用例在多次运行之间的波动更大(标准差约 1 个百分点),其他用例约为 0.2–0.25 个百分点,说明缓存对消息靠前部分的变化更敏感。