扒了Reasonix的源码: 总算知道为什么缓存命中率这么高了
DeepSeek API的缓存命中率低是常见痛点,本文通过详细分析Reasonix项目,揭示提升命中率至99%以上的关键策略与工程实践。

作者在OpenMMV项目中默认接入DeepSeek,但缓存命中率常低至20%-30%,引发高额费用。
DeepSeek的前缀缓存命中部分价格极低,但命中率低导致API花费显著增加。
在L站和B站的AI社区,Reasonix作为针对DeepSeek适配的agent harness,常被提及能实现99%的缓存命中率。
同用DeepSeek,为何命中率差异大?作者深入探究Reasonix的实现。
DeepSeek的官方缓存机制
DeepSeek API的prompt caching为服务端自动机制,无需显式标记,仅需多次请求的prompt前缀完全一致,命中部分即按缓存价计费。
命中规则从prompt首token开始逐位比较,任何token不同则后续全部miss。
要提高命中率,需保持prompt前缀部分不变,可变部分放置于末尾。
记忆约束
Reasonix在其项目记忆文件REASONIX.md中设有一条约束:
翻译:
这是Reasonix针对DeepSeek缓存命中的工程约束。
再看boot阶段,internal/boot/boot.go的Build()函数,前缀组装顺序设计如下:
复制代码// boot.go
sysPrompt, err := cfg.ResolveSystemPromptForRoot(root)
// output style 折叠进 base prompt,只组装一次,进入 cache-stable prefix
if st, ok := outputstyle.Resolve(cfg.Agent.OutputStyle, outputstyle.Dirs()); ok {
sysPrompt = outputstyle.Apply(sysPrompt, st)
}
sysPrompt += "nn" + config.LanguagePolicy// 持久记忆在这里折叠进系统提示,一次
mem := memory.Load(memory.Options{CWD: root, UserDir: config.MemoryUserDir()})
sysPrompt = memory.Compose(sysPrompt, mem)// skill 只放索引(名字+描述),正文按需加载,不进 prefix
skills := skillStore.List()
if !tokenEconomy {
sysPrompt = skill.ApplyIndex(sysPrompt, skills)
}
base prompt放在最前面,正常情况下保持不变。即使记忆跨session变化,base prompt仍然是合法的缓存前缀。
skill只将名字和描述塞入前缀,正文不进prefix,使用时才加载。
该sysPrompt组装后传给agent.NewSession,成为Session.Messages[0],之后在session中不再变动。
工具schema同样在启动时注册进tool.Registry,session内不变。plan模式等功能切换也不改变工具列表。
复制代码// agent.go
// planMode, when true, refuses any tool call whose ReadOnly() is false.
// The cache-friendly bits — system prompt, tools schema, message history —
// are left untouched, so the toggle costs nothing in cache hits.进入plan模式不会从schema中删除写工具,而是在执行时拦截,让模型看到错误后自行适应。
这是值得关注的细节。
xml拼接
若用户在会话过程中更改计划模式、临时添加记忆等状态变化,模型必须知晓。
不能直接写入system prompt,否则前缀变化会导致缓存命中失效。
Reasonix的解决方案是,所有中途变化的元素作为XML块拼接到当前user消息的最前面。
源码实现在internal/control/input.go的Compose()中:
复制代码// input.go —— Compose
func (c *Controller) Compose(text string) string {
plan := c.planMode
notes := c.pendingMemory
goal, goalStatus, goalResearchMode := c.goals.snapshot() // active goal 块,拼在 user turn 前面
if strings.TrimSpace(goal) != "" && goalStatus == GoalStatusRunning {
text = activeGoalBlock(goal, goalResearchMode) + "nn" + text
}
//plan mode 标记,拼在 user turn 前面
if plan {
text = PlanModeMarker + "nn" + text
}
//推理语言块,瞬时 user-turn 上下文
text = agent.WithReasoningLanguage(text, reasoningLanguage) //中途加的记忆,搭 turn
if len(notes) > 0 {
var b strings.Builder
b.WriteString("n")
b.WriteString("The following project-memory changes were just made and apply from now on:n")
for _, n := range notes {
b.WriteString("- " + n + "n")
}
b.WriteString("nn")
text = b.String() + text
}
//后台任务完成通知,搭 turn
if c.jobs != nil {
if note := c.jobs.DrainCompletedNoteForSession(c.parentSessionID()); note != "" {
text = "n" + note + "nnn" + text
}
}
return text
}
这些active-goal、PlanModeMarker、reasoning-language、memory-update、background-jobs块全部拼在最新user消息之前,处于prompt尾部。
前缀部分稳定,能命中缓存;尾部部分变化。
注释中明确了策略。
Reasonix将会话状态与缓存前缀隔离,最新状态变化拼入xml,不破坏前缀。
协议层防cache miss
除了harness层设计,在provider适配层,Reasonix还针对DeepSeek做了几个避免缓存失效和请求失败的处理。
DeepSeek的thinking模式有一个常见cache miss场景:当历史中某条带工具调用的AI消息重新发送时,必须一同发送该消息的原始思维链reasoning_content,否则DeepSeek返回400。
Reasonix在buildRequest中专门处理:
复制代码// openai.go —— buildRequest
// DeepSeek thinking mode 400s a tool_calls turn whose reasoning_content was
// dropped on a cache-miss replay, so round it back — but only on the turn
// that carries the tool calls.
if c.deepseek && m.Role == provider.RoleAssistant && len(m.ToolCalls) > 0 {
cm.ReasoningContent = m.ReasoningContent
}
仅在带tool_calls的那一轮回传推理内容,既避免400,又不将所有reasoning塞回,防止多余花费。
这些边角逻辑正是缓存命中策略的细节优化之处。
工程纪律
命中率要做到99%点几,光代码写好不够,还需保证后续改动不退化,Reasonix为此设置几个工程规矩。
第一,缓存诊断。 agent的run loop中每一轮取前缀快照与上一轮对比,结果挂到Usage事件上。诊断信息包含PrefixChanged、PrefixChangeReasons,告知miss原因,如system变化、工具变化或日志压缩重写。用户名能看到为何本轮miss,因为看不见就管不住。采用会话级累计命中率而非单轮比率,压缩时不重置累计值,因此压缩不会导致显示命中率暴跌。
第二,CI中的cache-guard。 包含两个脚本。一个跑TestReleaseCacheHitGuard测试,是发版前缓存命中率不能退化的检测。另一个在PR改动缓存敏感文件(如internal/boot、internal/tool、internal/provider)时,强制要求PR body写明Cache-impact:和Cache-guard:两行,敷衍词如n/a、none、todo会被拒绝,从而杜绝劣质PR。
第三,写进项目记忆的PR规则。 REASONIX.md将这套规范固化为对所有贡献者的约束。每个PR必须显式回答该改动是否影响DeepSeek缓存命中率。这种组织级工程纪律是Reasonix能长期维持99%点几命中率的护城河之一。
独立session历史
Reasonix支持双模型(planner计划和executor执行),易导致缓存崩坏。若在同一会话中切换模型,system、tools、历史格式全变,缓存清零。Reasonix让planner和executor各自的session独立,不共享历史。planner用自己的session产出计划,计划作为结构化文本交给executor,executor在独立session执行,两个session各自缓存前缀稳定增长。
请求结构
综合以上层次,一次发给DeepSeek的请求prompt结构大致如下:
上半段为缓存命中区,逐字节稳定,整个session不变,包含base prompt、output style、language、memory、skill索引、所有工具schema以及append-only增长的历史。
下半段为缓存未命中区,每轮新增,仅为尾部一小段:active-goal、reasoning-language、memory-update、background-jobs块、plan mode标记以及用户实际输入文本。DeepSeek从头匹配,前缀与上一轮相同则大面积命中,仅尾部新拼部分为cache miss。
通过系统化的工程纪律与巧妙设计,包括静态前缀隔离、XML动态拼接、协议层适配及独立会话历史,缓存命中率可达近100%,这不仅是技术问题更是工程组织问题。