我花一下午给 dsh 写了个插件,就为了让它别再磨叽前两篇,一篇聊它是什么,一篇带你 30 秒跑起来。这篇兑现我在第 2 篇结尾挖的坑,亲手写一个插件。
写之前先说清楚,这个插件不是炫技,是我真觉得它慢。
它八成时间都在想,不是在干活
用久了你就会发现,dsh 有个习惯。每次调工具之前,模型都要重新把前因后果想一遍。写个文件,想一遍;读个文件,又从头想一遍。
一个五十步的工具链任务,九成时间就这么被"想"掉了。工具本身执行,反而不到百分之一。
所以最划算的提速,不是换模型,是把简单轮次的思考档位调低。但手动切来切去太蠢了。我就想,能不能写个插件,让它自己判断,简单的时候就少想点。
先把逻辑抽出来,跟框架解耦
动手前想清楚一件事。决策逻辑要独立成纯函数,不碰 dsh 运行时。这样它能单独测,毫秒级跑完,不用起一整个 agent。
插件就三层。
骨架长这样。
dsh-speed-plugin/├── package.json├── src/│ ├── effort-decision.ts│ └── index.ts└── tests/ └── effort-decision.spec.tspackage.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 拉到同一个模型上,真刀真枪比一场,用真实数据聊聊到底该怎么入局。