读完 DeepSeek-Reasonix 之后,一个非常强烈的感受是:这个项目真正精妙的设计,并非仅仅依赖于“采用了 DeepSeek 的 KV Cache”,而是它成功地将整个 Agent 的运行循环,塑造成了 DeepSeek prefix cache 最乐意且最高效处理的模样。
不少人在看到超过 99% 的 cache hit 率时,下意识地以为,是不是实现了一套神秘的缓存 API。事实并非如此。DeepSeek 的 prefix cache 是默认开启的;Reasonix 所做的核心工作是:确保在每一轮请求中,其前缀部分在字节级别上能够保持几乎绝对的稳定。
因此,这个惊人的命中率并非源自某个单点技巧,而是一套严谨的工程纪律:系统提示词保持固定,工具列表不随意变动,历史消息不重新排序,临时推理结果不污染上下文,工具执行结果按既定顺序追加,长上下文仅在受控的边界进行折叠与压缩。
换句话说:
先说清楚:这里的 KV Cache 指的是什么
文章中提及的“KV cache 命中 99.5%+”,更精确的表述应当是 DeepSeek API usage 中返回的 prompt cache hit token ratio,而非模型内部 GPU 显存中 KV cache 的压缩率。
DeepSeek 的上下文缓存功能是默认开启的。当后续请求与之前的请求存在重叠的前缀时,这部分重叠内容就会被视为 cache hit,并在 API 返回的 usage 数据中体现为:
prompt_cache_hit_tokensprompt_cache_miss_tokens
这意味着,实现 cache hit 的关键因素不在于“语义上的相似度”,而在于请求的前缀是否能在字节序列上被直接复用。
DeepSeek 官方文档所展示的典型模式大致如下:
第一次请求:A + B
第二次请求:A + B + C
在第二次请求中,A + B 这部分内容就有机会命中缓存。
但是,如果你在每一轮请求中,都让 A 发生哪怕一丁点的字节变化,那么后续即使添加了 B + C + D 等内容,旧缓存也无法复用,因为前缀链条已经被打断了。
这正是普通 Agent 难以充分利用 DeepSeek Cache 的根本原因所在。
真实数据:为什么这个项目让人眼前一亮
Reasonix 的仓库里展示了一位真实用户单日内的运行数据:
| 类型 | Tokens 数量 |
|---|---|
| 输入 — cache hit | 435,033,856 |
| 输入 — cache miss | 767,616 |
| 输出 | 179,763 |
| 全天总计 | 435,981,235 |
由此计算出的输入侧 cache hit ratio 为:
435,033,856 / (435,033,856 + 767,616) = 99.82%
这个数字确实令人印象深刻。
如果我们按照项目中 v4-flash 的预估价格来核算,这一天的总成本大约为 $1.38;然而,如果完全没有缓存可供命中,相同输入量下的成本将急剧攀升至约 **$61.06。
因此,节省成本的关键并不仅仅在于“模型本身很便宜”,更核心的因素是:在长会话场景下,几乎绝大部分的输入 token 都转变成了 cache hit。
这也是 Reasonix 这个项目最值得深入研究的地方:它并非简单地调用了一个便宜的模型,而是围绕模型的计费结构,重新设计和优化了 Agent 的运行时机制。
Reasonix 的哲学:DeepSeek 原生,而不是通用 LLM SDK
Reasonix 的架构文档开篇就明确阐述:
它并非一个通用的 Agent 框架,其设计目标不是“兼容所有模型”。它的每一个抽象层都必须服务于 DeepSeek 特定的行为模式或经济效益。项目的北极星指标也同样明确:打造一个便宜到可以一直保持开启状态的 coding agent。
这句话的分量非常重。
许多 Agent 框架追求的是“多模型兼容性”:能够同时接入 OpenAI、Claude、DeepSeek、Gemini 等不同模型。但这种通用的抽象层通常会带来一个问题:框架内部会将消息、工具调用、历史记录、系统提示词等,抽象成一种“通用的形状”,然后在每次发起请求前,再将其序列化成目标模型所需的特定格式。
这个过程极其容易产生微小却致命的偏移:
- 工具定义的顺序发生了变化
- 序列化 schema 的方式产生了差异
- 系统提示词中无意插入了新的动态时间戳
- 历史消息被压缩或非必要重写
- 工具结果按完成时间而非模型声明的顺序写入
- 临时的规划状态被错误地塞回了上下文
对于普通的 API 调用而言,这些细微的变化可能影响不大。
但对 DeepSeek 的 prefix cache 来说,这些变化可能是灾难性的,会直接导致缓存失效。
Reasonix 的哲学并非“我也支持 DeepSeek”,而是:
我为 DeepSeek 量身定制我的运行时环境。
为什么普通 Agent 命中率低?
一个普通 coding agent 的 prompt,往往是通过类似下面的方式临时拼凑出来的:
系统提示词 + 工具定义 + 用户目标 + 历史对话 + 工具调用 + 工具结果 + 临时规划 + 模型思考 + 摘要 + 时间戳 + 当前状态
如果每一轮都重新拼接这些内容,其中任何一部分发生变化,DeepSeek 都难以将新旧前缀识别为相同的序列。
特别是,coding agent 的上下文通常非常长:工具 schema 庞大、历史工具结果繁多、文件内容丰富,且经常涉及错误重试。只要前缀部分存在一丁点的字节差异,其后附的数十万 token 都可能从 cache hit 变为 cache miss。
许多框架会执行一些看似合理,但对 prefix cache 却很不友好的操作:
每一轮都重新生成系统提示词
每一轮都重新对工具列表进行排序
将工具结果按其返回的时间写入历史
对旧消息进行摘要后,替换掉中间的历史部分
将隐藏的规划状态重新插入到消息列表中
将失败的 tool call 改写成更“干净”的消息格式
所有这些动作,都会破坏一个关键的缓存性质:
第 N+1 轮请求 = 第 N 轮请求 + 新增内容
Reasonix 的核心,就是要在最大程度上守住这个性质。
第一根柱子:Cache-First Loop
Reasonix 将上下文清晰地划分为三个区域:
IMMUTABLE PREFIX: system + tool_specs + few_shots
APPEND-ONLY LOG: assistant₁ / tool₁ / assistant₂ / tool₂ ...
VOLATILE SCRATCH: R1 thought / transient plan state
这三个区域各自解决一类核心问题:
- 最前面的系统提示词、工具定义和 few-shot 示例,必须保持固定不变
- 中间的历史消息只能以追加方式增长,不能重排,也不能随意改写
- 临时性的推理和规划状态,绝不能污染下一轮的上下文
这便是它的 Cache-First Loop 设计。
1. ImmutablePrefix:把最昂贵、最容易漂移的部分钉死
ImmutablePrefix 负责管理三类内容:
- system prompt
- tool specs
- few shots
源码中有一条非常重要的注释:每一次调用 addTool 都会导致一次 cache miss,因为 DeepSeek 的 prefix cache 是与完整的工具列表绑定的。
这充分说明了作者明确知道一件事:tools 参数也是 prompt 前缀的重要组成部分。
很多人只关注消息列表 (messages),却忽略了 tools 参数。如果工具的 schema 在每一轮中都被重新生成、顺序不稳定,或者 MCP 工具的接入与移除不受控,那么 DeepSeek 看到的就不再是同一个前缀。
Reasonix 做了几项非常工程化的设计:
第一,ImmutablePrefix 只允许通过受控的方法来替换 system 提示词或增减工具定义。
第二,每一次 system 或 tools 的变化,都会导致其指纹 (fingerprint) 失效。
第三,它基于 system、tools、few shots 计算出一个 SHA-256 指纹,并提供 verifyFingerprint() 方法来检查是否发生了偏移。
第四,获取工具列表时返回的是对象的克隆 (clone),而非原始引用,这降低了外部代码意外修改工具定义的风险。
这并非代码洁癖,而是为了达成一个明确目标:让前缀部分成为不可变的事实。
2. AppendOnlyLog:历史只增长,不重排,不就地编辑
第二个关键类是 AppendOnlyLog。
它的核心规则极为简单:在常规操作下,只允许执行追加 (append) 操作。
这对 prefix cache 来说至关重要。DeepSeek 最乐于处理的请求形态是:
第 N 轮请求:A + B + C
第 N+1 轮请求:A + B + C + D
第 N+2 轮请求:A + B + C + D + E
这样,从第二轮开始,绝大部分的旧 token 都能命中缓存。
而普通 Agent 很容易将历史变得面目全非:
第 N 轮请求:A + B + C
第 N+1 轮请求:A + B' + D
第 N+2 轮请求:A' + B'' + D + E
虽然看起来内容差不多,但对于缓存而言,已经不再是同一个前缀了。
Reasonix 的 log 设计,其目的就是让历史记录尽可能地保持“上一轮是下一轮前缀”的理想形态。
它并非完全禁止改写历史,而是将改写历史操作限制为少数、显式且可控的事件。在常规的循环 (loop) 中,始终坚持 append-only 原则。
3. VolatileScratch:临时推理不要污染下一轮缓存
第三个区域是 VolatileScratch。
这里存放的是:
- R1 thought(R1 模型的思考过程)
- transient plan state(临时的规划状态)
- 临时 notes(临时的笔记或记录)
这些内容在每一轮结束后都会被重置,并且它们不会直接被发送给上游模型。
这背后的设计思想是:模型在每一轮中产生的隐藏推理、临时规划、修复状态,很多仅在本轮有用。如果将这些内容作为普通消息塞回历史,会引发两个问题:
第一,它们体积庞大,会持续膨胀未来的 prompt。
第二,它们的内容不稳定,会导致前缀不断变化,破坏缓存。
因此,Reasonix 明确区分了“需要保留的事实”和“当轮的临时思考”。
临时思考进入 scratch 区域;真正需要进入历史记录的内容,必须通过工具调用、修复(repair)、摘要(summary)等机制,将其转化为稳定、可复用的 log 内容。
这也是它所谓的“R1 Thought Harvest”的含义所在:并非简单地将整个思维链塞进上下文,而是从 DeepSeek/R1 可能随意放置的信息中,提取出有价值的、结构化的意图,再以稳定的形态纳入工具调用的流程。
第二根柱子:Tool-Call Repair 不是附属功能,而是缓存稳定性的保护层
表面上看,Tool-Call Repair 旨在解决 DeepSeek 工具调用不够稳定这个问题。
但深入阅读源码后会发现,它还有一个更深层次的作用:
保护缓存的连续性。
Reasonix 的修复 (repair) 流水线是:
schema flatten → sca venge → truncation repair → storm breaker
1. Flatten:复杂工具 schema 先摊平,调用后再还原
DeepSeek 在面对复杂嵌套的 schema 时,可能会丢失某些参数。Reasonix 的处理方式是:
- 如果 schema 层级超过 2 层
- 或者叶子节点参数超过 10 个
就将 schema 展平为 dot path 格式。
例如,原始的嵌套结构可能是:
{"user": {"profile": {"name": "..."}}}
展平后变成:
{"user.profile.name": "..."}
模型可以按照扁平的字段填写参数,在实际调度执行前,再 re-nest 回原来的嵌套对象。
这个操作对缓存有何影响?影响很大。
如果工具调用经常失败,模型就会反复进行重试、解释和修正,导致日志中塞入大量失败消息;这些消息不仅增加了 token 消耗,也制造了不稳定的尾部,干扰缓存。
Flatten 操作提高了单次工具调用的成功率,间接使得 append-only log 变得更干净、更稳定。
2. Sca venge:从 reasoning/content 里捞回模型本来想调用的工具
DeepSeek/R1 有时会将其预期的工具调用 JSON 放入 reasoning_content 字段,但却未能准确放入标准的 tool_calls 字段。
Reasonix 不会直接放弃这次工具调用,而是会主动扫描 reasoning 和 content 两个通道,从中提取出 DSML invoke blocks 或 raw JSON 对象,并将其转化为真正的 ToolCall。
这种设计非常具有 DeepSeek 原生的味道。
它并非假设所有模型都完美遵循 OpenAI 的 tool call 格式,而是承认 DeepSeek/R1 有其独特的输出习惯,并在循环中去主动适应和吸收这些偏差。
这也是它能够稳定运行的原因之一:当模型输出出现偏移时,运行时可以将其拉回正轨;而不是让偏差扩散,导致更多的重试和上下文污染。
3. Truncation 与 Storm:让错误不要扩散成上下文污染
Repair pipeline 还会尝试修复被截断的 JSON 参数。
如果无法修复,它不会默默地使用 {} 去执行,而是保留原始参数,让工具层返回明确的 invalid JSON 错误。
这是一种“宁可明确地失败,也不要错误地成功”的态度。
Storm breaker 则会过滤掉重复的工具调用,避免相同的 (tool, args) 组合在滑动窗口内反复出现。
对于 coding agent 来说,这能有效防止一种常见的循环问题:
读取同一个文件 → 未能理解 → 再次读取同一个文件 → 仍然不理解 → 继续读取
这种循环不仅浪费时间和 token,也会迅速撑爆上下文窗口。
因此,Tool-Call Repair 的价值不仅仅是“让工具调用更准确”,更重要的是“让日志记录更稳定”。一个稳定的 Agent,才更有可能实现稳定的缓存命中率。
第三根柱子:并行工具调用也要保持确定性
很多 Agent 为了追求速度,会将多个工具调用并发执行。
但问题在于,如果并发执行的结果按照各自的完成时间写回历史记录,那么每一轮的日志顺序就变得不确定了:网络的快慢、文件的大小、机器的负载等因素,都会改变消息的最终顺序。
对 prefix cache 而言,这同样会导致前缀漂移,破坏缓存。
Reasonix 的调度 (dispatch) 设计得非常克制:
- 每个工具都可以声明
parallelSafe属性 - 默认情况下,工具不被视为 parallel safe
- 只有连续且被标记为 parallel-safe 的调用才会被组成一个 chunk
- 非 parallel-safe 的调用则成为串行化的 barrier(屏障)
- 在 chunk 内部,工具可以并行执行
- 但工具结果在写回历史记录时,仍然按照模型最初声明的调用顺序进行 append
这一点处理得非常巧妙:
追求执行速度,但不牺牲结果的确定性。
这就是 Reasonix 工程哲学的一个缩影:并非为了缓存而牺牲所有体验,而是在速度和确定性之间,做出边界清晰、设计严谨的权衡。
长上下文怎么办?Auto-compact 不是反缓存,而是“受控破坏”
你可能会问:如果 append-only log 持续增长,最终必然会超出上下文长度限制。那么,它如何兼顾长会话和缓存命中率呢?
答案是:Reasonix 会进行压缩 (compact),但这种压缩是受控的、可预测的。
它并非每一轮都压缩,而是在上下文压力达到一定阈值之后,才执行一次折叠 (fold) 操作。
fold 的大致过程如下:
- 估算当前上下文的 token 数量
- 判断是否超过了预设的阈值
- 根据 token 预算,保留最近的部分(tail)
- 尽量在 user message 的边界处进行切割
- 将较旧的头部(head)内容总结成一个 synthetic assistant message
- 将这个总结接在 tail 之前
- 之后,上下文继续保持稳定
这看起来似乎违反了 append-only 原则,但它是“少数、显式、可解释”的违反。
普通框架的问题是:
每一轮都产生轻微漂移 → 持续造成缓存 miss
Reasonix 的策略则是:
平时完全保持稳定 → 偶尔进行一次明确的 fold → fold 后重新趋于稳定
从长期来看,第二种策略更容易实现极高的累计命中率。
而且,它的 summary instruction 设计得非常保守:要求保留用户的原始目标、负面约束、已做的决策、查看或修改过的文件、仍然相关的工具结果以及未完成的待办事项 (open todos),避免进行逐轮次、流水账式的摘要。
这说明其目的并非为了节省 token 而粗暴地进行摘要,而是要将“未来仍然需要命中的语义状态”压缩成稳定、可复用的文本。
为什么能到 99%+?核心是“大部分 token 都是旧前缀”
高命中率的数学原因其实很简单。
假设一个 coding session 已经运行了很长时间,当前的请求包含 500,000 个 input tokens。
其中:
- 498,000 tokens 来自稳定的 system、tools、few shots、历史 log 以及折叠后的摘要
- 2,000 tokens 是当前用户的新输入或新的工具结果
只要前面的 498,000 tokens 与上一轮的请求前缀完全一致,那么本轮输入侧的命中率就是:
498,000 / 500,000 = 99.6%
因此,Reasonix 的工作核心并非“让新 token 命中”。新 token 本就不可命中。
它的核心任务是:
确保旧 token 不发生变化。
这也解释了为什么真实案例能够达到 99.82% 的惊人命中率。当一天内累计输入达到 4.35 亿个 hit tokens,而 miss tokens 仅有 76.8 万个时,这充分说明,绝大多数的请求都在重复利用极长且稳定的旧前缀。
仓库中提供的 τ-bench-lite 测试也提供了一个较小规模的对照数据:
| metric | baseline | reasonix | delta |
|---|---|---|---|
| pass rate | 100% | 100% | +0pp |
| cache hit | 32.8% | 90.2% | +57.4pp |
| mean cost / task | $0.000992 | $0.000593 | ×0.60 |
这个 benchmark 的意义并非证明所有场景都能达到 99% 的命中率,而是说明:
即使在较短的任务中,通过稳定的前缀设计,也能显著提升缓存命中率。
它到底“原生”在哪里?
可以把 Reasonix 的 DeepSeek 原生性总结为四个层次。
第一层:经济原生
它并没有简单地将 DeepSeek 视为一个便宜的 OpenAI 替代品,而是将 DeepSeek 独特的缓存计费差异(cache hit 和 cache miss 的不同价格)作为整个产品设计的基石。
项目自己的 benchmark 直言不讳:
同一个 API,使用不同的客户端,其缓存命中率可能完全不同。
第二层:协议原生
它显式地读取 DeepSeek API usage 返回中的 cache hit/miss tokens 数据,并围绕这个核心指标来构建 UI 展示和成本统计功能。
其成本函数也直接使用了 DeepSeek 的定价公式:
cache hit tokens × hit price + cache miss tokens × miss price + completion tokens × output price
这并非一个附属的统计指标,而是产品体验的核心组成部分。
第三层:行为原生
它承认 DeepSeek/R1 在工具调用方面存在自己独特的偏差:
- tool call 可能出现在 reasoning 或 content 内部
- 复杂的 schema 可能导致参数丢失
- JSON 可能被截断
- 相同的工具可能被重复调用
因此,它专门设计并实现了 flatten、sca venge、truncation repair、storm breaker 等处理机制。
这不是通用 SDK 的适配思路,而是针对特定模型行为模式的深度适配思路。
第四层:上下文原生
它清楚地认识到,DeepSeek prefix cache 的核心需求是稳定的前缀。因此,它将 prefix、log、scratch、dispatch、compaction 等所有模块,都设计成服务于“保持稳定字节序列”这一目标的组件。
这四层结合起来,才真正称得上是“DeepSeek 原生”。
对我们做 Agent 产品有什么启发?
最值得借鉴的并非具体的代码实现,而是以下这五条设计原则。
1. 把 prompt 构造从“函数”升级成“状态机”
通常的做法是每轮调用一个函数:
buildPrompt(state)
Reasonix 的做法更像是一个状态机:
session start: 固定 prefix
每一轮: append log
必要时: 受控 fold
Prompt 不再是每次重新生成的动态字符串,而是一个拥有不变量 (invariant) 的运行时结构。
2. 工具定义要有稳定身份
工具 schema 的顺序、内容、序列化方式都应保持稳定。
MCP 工具的热插拔、动态工具注册、权限变化等操作,都应被视为会导致 cache miss 的重大事件,而非普通的小修小改。
3. 并发结果不能按完成时间写历史
并发执行可以提升效率,但 history append 必须严格按照模型最初声明的顺序进行。
否则,你可能自认为在优化延迟,实际上却是在制造前缀的不确定性 (prefix nondeterminism),破坏缓存。
4. 隐藏状态不要随便进入消息历史
模型的思考过程、临时规划、修复笔记、调试信息等,都需要明确分类:
- 哪些仅仅是当轮的 scratch(临时空间)
- 哪些是未来需要保留的有效事实
将所有信息一股脑地塞进上下文,是导致低命中率和高成本的共同来源。
5. Compaction 要少做、晚做、可解释地做
摘要 (summary) 并非简单地“压缩聊天记录”,而是一次针对前缀的重写 (prefix rewrite)。
既然它会破坏缓存的连续性,就必须在阈值设定、边界切割、保留内容选择上非常谨慎。
Reasonix 的 fold 之所以可以被接受,是因为它在平时能保持不乱写,只有在达到上下文压力点时才会执行,并且完成后会继续维持稳定状态。
也要看清边界
Reasonix 的 99.82% 命中率是一个真实的单日案例,但这并非在所有情况下都能保证。
DeepSeek 的缓存机制是 best-effort 的,并不保证 100% 命中,且缓存也可能被自动清理。
以下情况都可能导致 cache miss:
- 开启新的 session
- 刚启动运行的前几轮
- system prompt 发生变化
- tool list 发生变化
- MCP 工具热插拔
- 执行 compact 操作的那一轮
- 服务端缓存被自动清理
另外,99%+ 的命中率通常出现在 长会话、长前缀、少漂移 的条件下。
在短对话场景中,本来就没有多少旧 token 可以复用,因此命中率不可能很高。
τ-bench-lite 中 Reasonix 的命中率为 90.2%,而真实长会话中达到 99.82%,这两个数字并不矛盾:会话越长、前缀越稳定,旧 token 的占比就越大,命中率自然也就越接近 100%。
结论
Reasonix 的设计哲学可以用一句话来概括:
将 Agent 的运行时设计成 DeepSeek prefix cache 最乐于处理的模样。
这个理想形态包含几个硬性约束:
system / tools / few-shots 必须固定
历史记录只允许追加
临时思考不得污染上下文
工具调用失败在本地修复
支持并发执行,但结果按声明顺序落盘
长上下文仅在阈值处进行受控折叠
cache hit/miss 被持续计量和可视化
因此,它能够达到 99.5% 甚至 99.82% 的命中率,并非依靠某个神秘的 KV cache 技巧,而是因为它将“字节级别的稳定性”提升为了架构设计的基本原则。
DeepSeek 提供了强大的缓存能力,而 Reasonix 则确保其输入长得像可以被高效缓存的输入。
这才是这个项目最值得学习和借鉴的精髓所在。
