乐于分享
好东西不私藏

我花一下午给 dsh 写了个插件,就为了让它别再磨叽

我花一下午给 dsh 写了个插件,就为了让它别再磨叽

前两篇,一篇聊它是什么,一篇带你 30 秒跑起来。这篇兑现我在第 2 篇结尾挖的坑,亲手写一个插件。

写之前先说清楚,这个插件不是炫技,是我真觉得它慢。

它八成时间都在想,不是在干活

用久了你就会发现,dsh 有个习惯。每次调工具之前,模型都要重新把前因后果想一遍。写个文件,想一遍;读个文件,又从头想一遍。

一个五十步的工具链任务,九成时间就这么被"想"掉了。工具本身执行,反而不到百分之一。

所以最划算的提速,不是换模型,是把简单轮次的思考档位调低。但手动切来切去太蠢了。我就想,能不能写个插件,让它自己判断,简单的时候就少想点。

先把逻辑抽出来,跟框架解耦

动手前想清楚一件事。决策逻辑要独立成纯函数,不碰 dsh 运行时。这样它能单独测,毫秒级跑完,不用起一整个 agent。

插件就三层。

  • 纯函数层,决定这轮该用哪档思考
  • 插件主体层,挂到 dsh 的扩展点上
  • 测试层,覆盖所有分支

骨架长这样。

dsh-speed-plugin/├── package.json├── src/│   ├── effort-decision.ts│   └── index.ts└── tests/    └── effort-decision.spec.ts

package.json 里最关键的是这几点。

{  "name": "dsh-speed-plugin",  "type": "module",  "main": "src/index.ts",  "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" } },  "peerDependencies": {    "@deepseek-ai/cordis": "^4.0.1",    "@deepseek-ai/dsh-agent": "^0.1.0-rc.6"  }}

一句话提醒,依赖版本务必用 ^0.1.0-rc.6 这条线。rc.1 那条线的依赖链是断的,装到一半就 404。

决策函数,就二十来行

这段代码是我真写出来、真跑过的。逻辑很直白,看最近几次工具调用,简单工具占多数,就把思考降到低档。

export type EffortId = 'low' | 'high' | 'max'const SIMPLE_TOOL_RE = /^(fs|bash|terminal|read|write|grep|glob|edit|ls|cat|rm|cp|touch|mkdir|pwd)/iconst HEAVY_ARGS = 800export function decideEffort(input: EffortDecisionInput): EffortId {  const { recentCalls, selected, allowDowngrade, allowUpgrade } = input  if (recentCalls.length === 0) return selected  const ratio = recentCalls.filter((c) =>    SIMPLE_TOOL_RE.test(c.name) && c.argsSize < HEAVY_ARGS,  ).length / recentCalls.length  const heaviest = recentCalls.reduce((m, c) => Math.max(m, c.argsSize), 0)  if (ratio >= 0.75 && allowDowngrade) return 'low'  if (heaviest >= HEAVY_ARGS * 4 && allowUpgrade) return 'max'  if (ratio < 0.75) return allowUpgrade ? 'high' : selected  return selected}

这张图就是它的决策流程。

最近八次调用里,简单工具占七成五以上,降档。出现超大参数,升级。都没有,保持原样。

接线的时候,三个坑等着你

纯函数只是纸面逻辑,真正接入 dsh 靠的是 agent/request 这个扩展点。每次模型请求前都会触发一次,监听者返回的值会传给下一个,这叫 waterfall。

主体就这么短。

export function apply(ctx: Context, config: SpeedPluginConfig = DEFAULT_CONFIG): void {  if (!config.enabled) return  ctx.on('agent/request' as any, async (payload: any, next: () => Promise<any>) => {    const calls = recentToolCalls(payload.agent)    const selected = (payload.config?.reasoningEffort as any) || config.baseline    const nextEffort = decideEffort({ recentCalls: calls, selected, ... })    if (nextEffort !== selected) {      payload.config = { ...(payload.config || {}), reasoningEffort: nextEffort }    }    return next()  })}

这段代码我踩了三个坑,都标出来,你写的时候绕开。

  • next() 是 Promise,必须 await。不 await 直接拿来 spread,拿到的是空对象,provider 和 model 全丢,报错报得你怀疑人生。
  • 扩展点要选对。dsh 九成行为都有官方钩子,先翻文档找钩子,别自己硬造。
  • 逻辑一定抽成纯函数。挂在 ctx 里写,测都没法测。

跑一遍测试,五条全绿

纯函数的好处这时候就出来了。不起 agent,直接 node 跑测试。

=== decideEffort 单元测试 ===  ✓ 空历史 → 保持 high  ✓ 简单工具占比 100% → low  ✓ 超大参数 → max  ✓ 简单占比 50% → high  ✓ 禁用降档 → 保持 high结果:5 通过,0 失败

装进 dsh 之后,它就在每次请求前偷偷帮你把档位调好,你啥都不用管。

顺带说两个性能真相

写插件是引子,真正值钱的认知有两个。

第一,推理档位三选一。日常用 high,简单批量用 low,复杂推理和 debug 才上 max。别贪 max,思考 token 翻倍,钱和时间都烧。

第二,缓存命中才是成本第一变量。实测会话缓存命中率能到九成七,命中之后输入价打折到一两成。所以长任务别频繁开新会话,保持同一个会话往下走,前缀稳定,命中率就高。

写在最后

写这个插件没费多少劲,一下午,二十几行核心代码。但「一切皆插件」这句话,我是真信了。它把改行为的入口做成了标准接口,谁都能伸手进去拧一下。

下一篇是系列收官,也是最硬核的一篇。我把三个 Agent 拉到同一个模型上,真刀真枪比一场,用真实数据聊聊到底该怎么入局。