最近把一个专为 DeepSeek 优化的 Agent 框架开源了,叫 Reasonix:
npm install -g reasonix && reasonix chat这篇文章聊聊为什么值得给 DeepSeek 做一个专属框架,以及具体怎么做的。
LangChain、LlamaIndex 这些通用框架有个共同缺陷:它们把 DeepSeek 当成"base URL 不一样的 OpenAI"。能用,但 DeepSeek 独有的几个特性完全没被利用:
reasoning_content:暴露了模型的推理链Reasonix 的三个 Pillar 就围绕这三点设计。
DeepSeek 缓存的触发条件是 请求的 byte prefix 与上次完全一致。通用框架每轮都在:
结果:实测命中率常常 <20%。
把每次请求的上下文拆成三区,各自有严格不变性:
┌─────────────────────────────────────┐
│ IMMUTABLE PREFIX │ ← 整个会话永不变
│ system + tool_specs + few_shots │ 这是缓存的靶子
├─────────────────────────────────────┤
│ APPEND-ONLY LOG │ ← 只能追加
│ [user₁][assistant₁][tool₁]... │ 旧 turn 作为新 turn 的 prefix
├─────────────────────────────────────┤
│ VOLATILE SCRATCH │ ← 每轮重置
│ R1 思考、临时 plan state │ 永不上传
└─────────────────────────────────────┘
Prefix 一启动就 hash 冻结。Log 的 append() 方法禁止任何 mutate。Scratch 每轮 reset()。
5 轮中文多轮对话(deepseek-chat):
| 指标 | 数值 |
|---|---|
| 缓存命中率 | 85.2% |
| 总成本 | $0.000923 |
| 如果跑 Claude Sonnet 4.6 | $0.015174 |
| 节省 | 93.9% |
同样的对话上 tool-use(计算器 tool),2 轮:命中率 94.9%,省 95.8%。
数字不是预估,是实打实跑 DeepSeek API 拿到的。
deepseek-reasoner 会在 reasoning_content 里输出很长的思考链。通用框架:
里面的规划信号完全没利用。
R1 思考结束后,再发一次 V3 请求(便宜,~$0.0001)在 JSON 模式下提取:
interface TypedPlanState {
subgoals: string[]; // R1 识别出的子目标
hypotheses: string[]; // R1 探讨的假设
uncertainties: string[]; // R1 标记的不确定点
rejectedPaths: string[]; // R1 考虑后放弃的路径
}
实际效果(问一道"3 个盒子标签全错,怎么只摸一个水果确定全部内容"逻辑题):
‹ subgoals (3): 列出所有可能的标签与内容组合 · 确定从哪个盒子摸水果 · 验证唯一性
‹ hypotheses (3): 从"苹果"标签盒摸 · 从"橘子"标签盒摸 · 从"混合"标签盒摸
‹ uncertainties (2): 摸到水果是否能唯一确定 · 混合盒摸到的概率
‹ rejected (2): 从"苹果"盒摸(信息量不足) · 从"橘子"盒摸(对称问题)
这不是幻觉——真实对应 R1 思考链里的实际内容。
Opt-in,默认关。开启方式:CLI --harvest 或 TUI 内 /harvest on。
DeepSeek 的 function calling 有几个已知 bug,通用框架不处理就直接崩:
Reasonix 的 Repair 层四个模块对应修复:
// 1. Auto-flatten 深 schema
ToolRegistry.register({
name: "updateProfile",
parameters: {
type: "object",
properties: {
user: { type: "object", properties: {
profile: { type: "object", properties: {
name: { type: "string" },
age: { type: "integer" },
}},
}},
},
},
fn: ({ user }) => updateInDB(user),
});
// → 自动转成 {"user.profile.name": "...", "user.profile.age": ...} 发给模型
// → dispatch 时自动嵌套回 {user: {profile: {...}}} 再调用
// 2. Scavenge 从 <think> 捞回工具调用
// 3. Truncation 恢复(闭合括号、补 null、去尾逗号)
// 4. Call-storm 滑动窗口熔断
全部默认开启,用户不用配置。
DeepSeek 便宜到什么程度?3 个并行 R1 样本还是比单次 Claude 便宜。于是把研究论文里的"self-consistency sampling"变成了日常可用:
# 一键开
reasonix chat --branch 3
# 或 TUI 内: /preset max
TUI 里 3 路采样并行完成后显示:
🔀 branched 3 samples → picked #1 #0 T=0.0 u=2 #1 T=0.5 u=0 #2 T=1.0 u=3
u= 是 Pillar 2 harvest 出来的 uncertainties.length。默认选择器挑 u 最少的那路。
实测:R1 中等难度题,3 路分支能稳定提升正确率 ~10-15pp,成本约单次 Claude 的 1/5。
| Reasonix 默认 | 通用框架 | |
|---|---|---|
| Prefix 稳定 → 缓存命中 | ✅ 85-95% | ❌ 每轮 prefix 漂移 |
| 深 schema 自动拍平 | ✅ | ❌ 丢参数 |
| 429/503 指数退避重试 | ✅ | ❌ 自己接 callback |
| Tool call 从 <think> 捞 | ✅ | ❌ |
| Call-storm 熔断 | ✅ | ❌ |
| 实时成本/命中率面板 | ✅ TUI | ❌ |
| 零配置开箱 | ✅ npx reasonix chat | ❌ |
即便完全不开 harvest / branching,默认配置相对"LangChain + DeepSeek"已经天然省 40%+(光 Pillar 1 贡献)。
npm install -g reasonix
reasonix chat
第一次跑会弹输入框让你粘 DeepSeek API key(https://platform.deepseek.com/api_keys 免费额度),存到 ~/.reasonix/config.json。之后一条命令即用。
TUI 里所有功能用 slash 命令切换,不用记 CLI 参数:
/preset fast 默认配置(deepseek-chat,最便宜)
/preset smart reasoner + harvest(~10x 成本)
/preset max reasoner + harvest + branch3(~30x 成本,质量最高)
/model deepseek-reasoner 只切模型
/harvest on 只开 harvest
/branch 3 只开 3 路采样
/sessions 列出所有会话
/forget 删除当前会话
/help 完整列表
会话默认持久化——下次进来自动恢复上次的上下文。用过 Claude Code 的熟悉这个模式。
MIT 开源,目前 v0.0.6 pre-alpha。
这个项目是 故意窄 的:
如果你有在 DeepSeek 上跑 agent 的工作负载,欢迎试试 → https://github.com/esengine/reasonix
issue 和 PR 都欢迎。尤其是 Pillar 2 的 branching 选择策略目前只看 uncertainties.length,想听听大家对改进方向的意见——比如能不能用 tool call 成功率、回答长度比、cross-sample 相似度等信号一起判断。
Pillar 1 — Cache-First Loop
问题
DeepSeek 缓存的触发条件是 请求的 byte prefix 与上次完全一致。通用框架每轮都在:
重排历史顺序
注入新的 timestamp
重构 system prompt
工具列表每次序列化结果不同
结果:实测命中率常常 <20%。
解决
把每次请求的上下文拆成三区,各自有严格不变性:
┌─────────────────────────────────────┐
│ IMMUTABLE PREFIX │ ← 整个会话永不变
│ system + tool_specs + few_shots │ 这是缓存的靶子
├─────────────────────────────────────┤
│ APPEND-ONLY LOG │ ← 只能追加
│ [user₁][assistant₁][tool₁]... │ 旧 turn 作为新 turn 的 prefix
├─────────────────────────────────────┤
│ VOLATILE SCRATCH │ ← 每轮重置
│ R1 思考、临时 plan state │ 永不上传
└─────────────────────────────────────┘
Prefix 一启动就 hash 冻结。Log 的 append() 方法禁止任何 mutate。Scratch 每轮 reset()。用中文详细理解介绍原理
下面我来详细拆解 Pillar 1 — Cache-First Loop(缓存优先循环) 的工作原理,把上面那段描述翻译成更通俗、更深入的解释。
DeepSeek API 提供了一种上下文缓存(context caching)能力。它的触发条件非常“死板”:
> 只要两次请求的 字节前缀 完全一致,前面相同的那一段 token 就不会重复计算,只收取极低的缓存读取费。
举个例子:
如果你第一次发送的请求是 "你是一个助手。用户说:你好",模型处理完后,这段内容会被缓存。
第二次请求时,如果你发送 "你是一个助手。用户说:你好。请继续",那么前缀 "你是一个助手。用户说:你好。" 与第一次完全一样,这部分就能命中缓存,不需要重新推理,成本极低。
但如果第二次你发送的是 "你是一个助手。用户说:嗨",前缀 "你是一个助手。用户说:嗨" 和之前不同,缓存就失效了,整段都得重新计算。
这个“完全一致”是字节级别的,非常苛刻。
典型的 AI 编程 Agent(比如一些通用实现)在每一轮对话中,发给 API 的请求都会不断变化,这些变化导致前缀不再稳定,例如:
为了优化上下文窗口,有些框架会把之前的对话打乱,把系统认为更重要的消息放到前面。哪怕只是调换了 user 和 assistant 的顺序,整个前缀就完全不同了。
很多框架会在 system prompt 或历史消息中插入当前时间,比如 "当前时间:2026-05-20 14:23:15"。每一轮时间都在变,即使只差一秒,前缀就变了。
每次调用都可能根据当前工作目录、项目文件列表、可用工具集等信息重新生成 system prompt,而这些信息每一轮都可能不同(比如新增文件、工具返回值变化)。哪怕只是多了一个空格,缓存都会失效。
Agent 需要向模型描述它能调用哪些工具(tool_specs)。很多框架在序列化工具列表时,顺序不稳定,或者参数描述中包含了动态数据(比如当前环境信息)。这就导致每次发给 API 的“工具列表”部分字节都不完全一样。
后果:实测中,这些框架的上下文缓存命中率常常 低于 20%,几乎完全享受不到 DeepSeek 的缓存红利,成本居高不下。
DeepSeek-Reasonix 为了解决上述问题,把每次发给 API 的上下文强制拆分为三个区域,并对每个区域施加严格的不可变性约束。核心思想是:
> 让请求中能够保持稳定的部分绝对不变,必须变化的部分以严格追加的方式增长,易变的部分完全不上传。
下面是三个区域的具体设计。
内容:system 提示词、工具规范(tool_specs)、少样本示例(few_shots)等,这些在整个会话生命周期内不会发生任何改变。
约束:
为什么它是“靶子”?
DeepSeek 缓存是在服务端按请求前缀建立的。只要请求的最开头(比如前几千个 token)始终是这同一段内容,服务端就一定能识别出缓存。即便后面的内容在变化,只要前面这个巨大的“锚点”不变,缓存命中率就有了基本保证。
内容:对话历史,也就是 [用户消息₁][助手回复₁][工具调用结果₁][用户消息₂]... 这样的序列。
约束:
append() 方法,除此之外没有任何 mutate 接口。旧有的 [turn] 永远以相同的形式保留在原地。工作原理与缓存的关系:
[不可变前缀] + [本轮用户问题]
[不可变前缀] + [第一轮完整历史] + [第二轮用户问题]
[不可变前缀] + [第一轮完整历史] 这一整段,和第一轮请求中 [不可变前缀] + [第一轮用户问题...] 有足够长的相同前缀,能够命中缓存。更重要的是:第三轮、第四轮……每一轮新请求的前缀,都完整包含了前一轮请求的全部内容(不变),因此缓存命中长度会随着对话增长而自然延伸,几乎每一轮都能复用前面所有的上下文,只对新追加的部分进行推理。
对比通用框架:通用框架经常为了节省 token 而重排历史,或者把较早的消息摘要化,这直接破坏了前缀的连续性,导致缓存断裂。
内容:Agent 内部推理时用到的临时数据,比如 DeepSeek-R1 的思考链(thinking traces)、当前任务的规划状态、中间假设等。
约束:
reset() 清空,重新构建。为什么要这样做?
R1 的“思考”过程可能包含大量探索性的、不确定的文字,如果把它也放进请求上下文,一方面会浪费 token,另一方面由于思考过程每一轮都完全不同,会严重破坏前缀稳定性。将这些内容放在本地暂存区,就剥离了最大的不稳定因素,彻底保证了上传内容的结构刚性。
通过这种设计,DeepSeek-Reasonix 向 API 发送的每一次请求,其有效负载可以抽象为:
``
[永远不变的 Prefix] + [只追加不修改的历史 Log] + [本轮新输入]
`
而服务端看到的请求,其前缀始终是 [Prefix] + [Log 的已存在部分]`。只要对话在进行,这个前缀就在单调递增地变长,但从不回退、从不修改,因此:
每一轮请求中,绝大多数字节都是“曾经在之前某轮请求中完整出现过”的前缀,所以缓存命中率极高。根据项目声称的测量结果,实际命中率可以达到 85%–95%,这意味着绝大部分输入 token 都只计缓存读取费,成本可以比没有缓存时降低一个数量级。
Cache-First Loop 不是一种普通的缓存优化技巧,而是一套完整的上下文架构设计哲学:它通过不可变性、追加性、隔离性这三个原则,将 Agent 的动态性与 API 缓存机制的静态要求解耦,从而将 DeepSeek 的理论成本优势真正转化为工程上可落地的极致性价比。