第 11 卷
- **waterfall 语义**:监听者的**返回值**传给下一个监听者/最终请求。返回 `{...seed,
reasoningEffort}` 就是"保留原配置 + 覆盖推理档位"。
- **`agent/request` 每步都触发**:`agent-loop` 的 `buildRequest` 在每一步都会走
这个 waterfall——所以动态决策天然按步生效。
4.5 测试
插件测试不能停在“模块能加载”。推荐按三层验证:纯函数单测负责业务规则,waterfall 契约测试负责运行时边界,实机验证负责完整 agent 循环。
第一层:纯函数单测
纯函数测试零运行时依赖、执行快,适合覆盖所有决策分支:
import { describe, expect, it } from 'vitest' import { decideEffort } from '../src/effortdecision.ts' it('downgrades to low for simple tool chains', () => {expect(decideEffort({
recentCalls: [{ name: 'write', argsSize: 40 }],selected: 'high', allowDowngrade: true, allowUpgrade: true,})).toBe('low') }) // ... 更多分支:全新提示保持基线 / 禁用降档 / 超大载
荷升 max / 混合工具升 high
第二层:waterfall 契约测试
纯函数通过不代表插件接线正确。最常见的隐蔽错误是漏掉 `await next()`,它会让上游的 `provider`、`model`、`tools` 等字段静默丢失。契约测试用最小 `Context` 替身捕获监听器,
不需要 API Key,也不启动 dsh:
it('awaits next, preserves the seed, and only overrides reasoning effort', async () =>
| { | const next = async () => ({ | provider: 'deepseek-official', | model: 'deepseek- |
|---|---|---|---|
| reasoner', | reasoningEffort: 'max', | tools: ['read', 'write'], | }) const result = |
await runRegisteredHandler({ agent: { session: { events: [] } } }, next)
| expect(result).toEqual({ | provider: 'deepseek-official', | model: 'deepseek-reasoner', |
|---|---|---|
| reasoningEffort: 'high', | tools: ['read', 'write'], | }) }) |
配套模板的 [`tests/plugin.spec.ts`](../examples/plugin-template/tests/plugin.spec.ts) 还覆盖:
- 只注册一个 `agent/request` 监听器;
- 只读取最近 8 个 `tool/call`,忽略其他事件;
- 下游 waterfall 抛错时原样向上传播,不静默吞错。
运行全部自动测试:
cd examples/plugin-template npm install npm test npm run typecheck
第三层:真实 agent 循环验证
契约测试证明插件边界符合预期,但不能替代真实运行时。挂载插件(第 3 章方法)→ 重启 `dsh web` → 发一个创建文件的任务 → 观察 dsh 进程日志:
[speed-plugin] agent/request: calls=[]=> reasoningEffort=high [speedplugin] agent/request: calls=[{"name":"write",…}] => reasoningEffort=low
第一轮无工具调用 → 保持基线 `high`;检测到 `write` 工具 → 下一轮降为 `low`。**注入链路完整工作。**
如果需要把这一层放进 CI,可参考官方讨论 [#462:无 API Key 验证 waterfall 行为] (https://github.com/deepseek-ai/deepseek-harness/discussions/462):用 mock LLM
产生固定工具调用,经 headless profile 跑真实 agent loop,再用审计快照检查预期 waterfall 和 `tools/result`。讨论中的命令基于特定 rc/master 源码,复用时应以目标版本的
脚本和事件清单为准,不要把固定的事件数量当成跨版本断言。
> 完整可运行代码:见 [`examples/plugin-template/`](../examples/plugin-template/)。
> ** ⚠️ 档位支持因适配器/模型而异(真实踩坑)**:`decideEffort` 返回 `low` 后,如果当前 provider 的适配器能力表不支持 `low`(如 `deepseek-official` 适配器只有
`off`/`high`/`max`),请求会报 `does not support reasoning effort "low"`——**这是适配器缺口,不是插件 bug**(官方 API 实际支持 low,见
api-docs.deepseek.com/guides/thinking_mode/;FAQ Q4 有档位说明)。
> > **按模型能力表适配的写法**:降档目标先查适配器支持,映射到可用档位:
> ```ts > const SUPPORTED = ['off', 'high', 'max'] // 按当前适配器能力表
> const effort = decideEffort({...})// 插件想要的档位 > const final = SUPPORTED.includes(effort) ? effort : (effort === 'low' ? 'high' : effort) //
low 不支持时回退 high > ```
4.6 给新手的三条开发纪律
- **先找扩展点**:要改的行为 90% 有官方钩子(`agent/request`、`settings`、
`conversationEvents`、`slots`)——不要 fork 核心。
- **逻辑抽纯函数**:决策/计算逻辑与 dsh 解耦 → 单测毫秒级、覆盖全分支。
- **边界和实机都要验证**:契约测试证明 `next()` 透传和字段保留,实机日志证明真
实 agent loop 生效。
> 📚 **官方 cookbook 延伸阅读**(2026-08 官方新增,官方仓库 `docs/cookbook/`):
`adding-a-package.md`(如何加包)、`adding-a-tool.md`(如何加工具)、`adding-aconversation-node.md`(加对话节点)、`adding-an-llm-adapter.md`(写 LLM 适配器)、
`adding-a-vendored-package.md`(vendor 包)、`extension-cookbook.md`(扩展点合集)。
本章走的是"最小提速插件"路径,官方 cookbook 覆盖更多扩展点类型,进阶时对照读。
---
动手练习(检验你是否真懂了)
- **理解题**:不看原文,说出这个提速插件的三层架构(纯函数层 / 插件主体层 / 测
试层)各自负责什么
> 自查:参考本章 4.2-4.5 节的文件结构
- **理解题**:解释为什么 `decideEffort` 要设计成纯函数而不是直接在 `apply(ctx)` 里
写逻辑。如果决策逻辑依赖了 `ctx.session`,还能单测吗?
> 自查:参考本章 4.3 节"为什么拆成纯函数"段落
- **动手题**:给 `decideEffort` 写一个新的测试用例:当 `recentCalls` 里有 3 个简单
工具 + 1 个超大参数(`argsSize = 5000`)时,应该返回什么档位?写出测试代码并运行
> 自查:参考本章 4.5 节单元测试示例。**注意**:3 简单 + 1 超大 = 4 个调用,ratio = 0.75 **恰好命中第一分支**(`ratio >= 0.75 && allowDowngrade`)→ 返回 `low`(取决
于 `allowDowngrade`,`allowUpgrade` 分支执行不到)
- **动手题**:在 `apply(ctx)` 里,如果把 `await next()` 改成 `const seed = next()`
(不 await),会发生什么?写出你的推理,然后在实机中验证 > 自查:参考本章 4.4 节"三个关键点"第 1 条
- **动手题**:假设你要写一个类似的插件,但改为根据"当前会话的工具调用总次数"来
决定档位(超过 20 次自动降为 low),写出纯函数签名和核心逻辑 > 自查:参考本章 4.3 节纯函数设计模式,关键是输入输出类型定义
- **思考题**:`agent/request` waterfall 可以有多个监听者。如果提速插件和另一个
插件都监听了 `agent/request`,谁先执行?返回值怎么传递?
> 自查:参考本章 4.4 节"waterfall 语义"段落 + 第 3 章 3.4 节扩展点说明
常见疑问 FAQ
**Q1:`next()` 返回的 seed 里到底包含什么字段?**
seed 是当前 waterfall 链上游累积的请求配置,通常包含 `provider`、`model`、 `reasoningEffort`、`tools` 等字段。你的监听者拿到 seed 后,spread 覆盖想改的字段,其
余原样传递。具体字段以官方 `agent-loop` 包的类型定义为准(rc 阶段可能变动)。
**Q2:为什么 npm 包的类型签名要"放宽"?不能直接用官方类型吗?**
因为 npm 发布的 `@deepseek-ai/dsh-agent` 等包没有 re-export 内部的事件类型增强(`Events` 接口没有被 module augmentation 扩展)。直接用
`ctx.on('agent/request', ...)` 会报类型错误。解决方案是在边界用 `as unknown as` 转换,这是 rc 阶段的临时方案,正式版可能修复。
**Q3:我的插件需要在每个工具调用后都触发决策,`agent/request` 够吗?**
够。`agent/request` 在每次模型请求前触发(包括工具调用后的下一轮请求)。你只需要在监听者里读取最近的工具调用历史(从 `payload.agent.session.events` 里倒序找 `tool/call`
事件),就能做按步决策。
全站分卷索引 · 43 卷
- 1第 1 卷
- 2第 2 卷
- 3第 3 卷
- 4第 4 卷
- 5第 5 卷
- 6第 6 卷
- 7第 7 卷
- 8第 8 卷
- 9第 9 卷
- 10第 10 卷
- 11第 11 卷
- 12第 12 卷
- 13第 13 卷
- 14第 14 卷
- 15第 15 卷
- 16第 16 卷
- 17第 17 卷
- 18第 18 卷
- 19第 19 卷
- 20第 20 卷
- 21第 21 卷
- 22第 22 卷
- 23第 23 卷
- 24第 24 卷
- 25第 25 卷
- 26第 26 卷
- 27第 27 卷
- 28第 28 卷
- 29第 29 卷
- 30第 30 卷
- 31第 31 卷
- 32第 32 卷
- 33第 33 卷
- 34第 34 卷
- 35第 35 卷
- 36第 36 卷
- 37第 37 卷
- 38第 38 卷
- 39第 39 卷
- 40第 40 卷
- 41第 41 卷
- 42第 42 卷
- 43第 43 卷