← 返回目录

第 11 卷

第 11 / 43 卷作者:Un-named日期:

  1. **waterfall 语义**:监听者的**返回值**传给下一个监听者/最终请求。返回 `{...seed,

reasoningEffort}` 就是"保留原配置 + 覆盖推理档位"。

  1. **`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 给新手的三条开发纪律

  1. **先找扩展点**:要改的行为 90% 有官方钩子(`agent/request`、`settings`、

`conversationEvents`、`slots`)——不要 fork 核心。

  1. **逻辑抽纯函数**:决策/计算逻辑与 dsh 解耦 → 单测毫秒级、覆盖全分支。
  2. **边界和实机都要验证**:契约测试证明 `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 覆盖更多扩展点类型,进阶时对照读。

---

动手练习(检验你是否真懂了)

  1. **理解题**:不看原文,说出这个提速插件的三层架构(纯函数层 / 插件主体层 / 测

试层)各自负责什么

> 自查:参考本章 4.2-4.5 节的文件结构

  1. **理解题**:解释为什么 `decideEffort` 要设计成纯函数而不是直接在 `apply(ctx)` 里

写逻辑。如果决策逻辑依赖了 `ctx.session`,还能单测吗?

> 自查:参考本章 4.3 节"为什么拆成纯函数"段落

  1. **动手题**:给 `decideEffort` 写一个新的测试用例:当 `recentCalls` 里有 3 个简单

工具 + 1 个超大参数(`argsSize = 5000`)时,应该返回什么档位?写出测试代码并运行

> 自查:参考本章 4.5 节单元测试示例。**注意**:3 简单 + 1 超大 = 4 个调用,ratio = 0.75 **恰好命中第一分支**(`ratio >= 0.75 && allowDowngrade`)→ 返回 `low`(取决

于 `allowDowngrade`,`allowUpgrade` 分支执行不到)

  1. **动手题**:在 `apply(ctx)` 里,如果把 `await next()` 改成 `const seed = next()`

(不 await),会发生什么?写出你的推理,然后在实机中验证 > 自查:参考本章 4.4 节"三个关键点"第 1 条

  1. **动手题**:假设你要写一个类似的插件,但改为根据"当前会话的工具调用总次数"来

决定档位(超过 20 次自动降为 low),写出纯函数签名和核心逻辑 > 自查:参考本章 4.3 节纯函数设计模式,关键是输入输出类型定义

  1. **思考题**:`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第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷
  5. 5第 5 卷
  6. 6第 6 卷
  7. 7第 7 卷
  8. 8第 8 卷
  9. 9第 9 卷
  10. 10第 10 卷
  11. 11第 11 卷
  12. 12第 12 卷
  13. 13第 13 卷
  14. 14第 14 卷
  15. 15第 15 卷
  16. 16第 16 卷
  17. 17第 17 卷
  18. 18第 18 卷
  19. 19第 19 卷
  20. 20第 20 卷
  21. 21第 21 卷
  22. 22第 22 卷
  23. 23第 23 卷
  24. 24第 24 卷
  25. 25第 25 卷
  26. 26第 26 卷
  27. 27第 27 卷
  28. 28第 28 卷
  29. 29第 29 卷
  30. 30第 30 卷
  31. 31第 31 卷
  32. 32第 32 卷
  33. 33第 33 卷
  34. 34第 34 卷
  35. 35第 35 卷
  36. 36第 36 卷
  37. 37第 37 卷
  38. 38第 38 卷
  39. 39第 39 卷
  40. 40第 40 卷
  41. 41第 41 卷
  42. 42第 42 卷
  43. 43第 43 卷