DSH入门

认知篇

从 CLI 到 DSH 插件:一套让 Agent 少走弯路的判断顺序

这不是 API 清单,而是一套在让 Agent 设计、实现、测试和发布插件时,减少返工和错误的判断顺序。

为什么先谈判断顺序

Agent 做插件时最贵的错误,通常不是写错一行代码,而是:

这篇文章来自 GetNote Toolbox 的真实实践[3],但重点不是“我们做了什么”,而是一套可以迁移到其他 Agent 任务上的判断顺序:

先判断该不该做,再画契约;先只读,后写入;先验证,后发布。

1. 先判断:该不该做插件

不是所有 CLI 都值得包成插件,也不是所有工作流都值得做成 Tool。

场景 更合适的选择
高频、参数结构化、需要安全护栏 Tool
低频、复杂、需要领域判断 CLI 或 Skill
一次性任务、固定脚本 直接跑脚本
只读诊断、环境管理 CLI 或 Skill
需要跨应用连接外部系统 先评估 MCP 等协议,不要为了“统一”而重写

一个简单的判断是:这个能力变成 Tool 之后,替模型承担了什么?

如果答案只是“把命令换了一个名字”,它不值得成为 Tool。真正值得的,是下面至少一项:

让 Agent 先做的第一件事:

先不要写代码。
请输出一份能力审计:
1. 这个 CLI 有哪些命令、输入、输出和副作用?
2. 哪些是高频、结构化、需要护栏的?
3. 哪些应该留给 CLI 或 Skill?
4. 哪些能力互相是逆操作?
5. 哪些地方需要版本兼容?

2. 先画契约,再写代码

Tool 不是函数名的集合,而是模型和外部能力之间的契约。[1][2]

每个 Tool 至少要写清:

以 GetNote Toolbox 为例,我们最终得到 15 个工具,覆盖搜索、保存、读取、更新、删除、标签、知识库和配额。15 不是目标,它是按工作流和契约边界推出来的结果。

几个具体原则:

让 Agent 先做的第二件事:

先不要实现。
请为每个候选 Tool 写出契约:
- 参数 schema
- 返回字段
- 错误类型
- 副作用
- 权限/确认要求
- 验证方式

3. ID、limit 和错误:模型看到的必须可靠

雪花 ID 为什么不能直接当数字

GetNote 的笔记 ID 是 19 位雪花 ID。[4]雪花 ID 是一种由多台机器生成、按时间大致有序的 64 位整数:

0 | 41 位毫秒时间戳 | 10 位机器 ID | 12 位序列号

时间戳让 ID 大致递增,机器位和序列号让同一毫秒内也能生成不同 ID。64 位整数在十进制下通常落在 19 位。

JavaScript 的 Number 是 IEEE 754 双精度浮点数,只有 53 位有效精度。它能精确表示的最大安全整数是:

2^53 - 1 = 9,007,199,254,740,991

19 位 ID 已经超出这个范围。[5]在 1.9 × 10^18 附近,相邻可表示整数的间隔约为 256;雪花 ID 的低位又恰好携带机器和序列信息。

所以:

JSON.parse('{"note_id":1900000000000000001}')
// { note_id: 1900000000000000000 }

这不是简单的“截断”,而是十进制整数转换成 binary64 时的精度丢失和舍入。更危险的是,reviver 拿到的已经是舍入后的 Number,再转 BigInt 也救不回来。

正确处理是:API 和工具契约统一把 ID 当字符串。

{ "note_id": "1900000000000000001" }

如果必须解析数字 JSON,可以使用无损解析器,或在 JSON.parse 之前把超出安全范围的整数字面量先转成字符串。GetNote 插件里的兼容层做的就是后者;它只是补丁,不是通用方案。

limit 和错误也要归一

旧版 CLI 可能忽略 --limit,插件如果原样返回,模型会误以为限制生效了。更可靠的做法是解析后自行截断,并让返回条数与请求一致。

错误也应可操作,而不是只给一个 exit code 1:

模型看到的 ID、数量和错误,必须能直接用于下一步行动。

4. 工具面不是 CLI 的 1:1 映射

一个常见的邪路是:CLI 有 50 个命令,就做 50 个 Tool。

工具越多,模型的选择空间越大,误选和上下文成本也越高。更合理的是按工作流组织工具面:

GetNote Toolbox 做到 15 个工具后就停下来了。不是不能再加,而是新增一个工具必须有明确收益。

让 Agent 先做的第三件事:

请先画出工具面,不要写代码:
- 哪些操作合并?
- 哪些是互逆操作?
- 哪些暂时不做?
- 每新增一个 Tool,模型少做了哪一步判断?

5. 写操作先设计失败

读操作出错,最多是结果不对;写操作出错,可能真的改坏用户的数据。

写操作设计要回答:

GetNote Toolbox 的选择:

让 Agent 先做的第四件事:

在执行任何写操作前,先输出:
1. 会修改哪些对象;
2. 最坏影响是什么;
3. 如何确认;
4. 如何回滚;
5. 如果没有确认,停止执行。

6. 认证、权限与信任边界

GetNote Toolbox 不读取、不缓存、不回显 API Key,只调用已经登录的本机 CLI。

这是一个有意的设计:

但“不碰密钥”不等于“没有信任边界”:

在意稳定性时,应该用 binaryPath 固定本地 CLI 版本,而不是默默依赖 latest。

7. 测试分两层

假 CLI 保证契约

单元测试不碰真实账号,用 fake CLI 覆盖:

这层测试快、稳定、可重复,也不会泄露真实笔记。

真实 CLI 保证环境

再用真实 CLI 做只读冒烟测试,确认:

真实输出可能包含笔记片段,所以不能直接贴进 issue、聊天或提交。测试数据应尽量合成;必须使用真实账号时,先建测试笔记,结束后清理,报告本地生成并加入 gitignore。

让 Agent 先做的第五件事:

不要只说“完成了”。
请给出:
- 测了哪些路径;
- 哪些是假 CLI,哪些是真实 CLI;
- 失败路径覆盖了什么;
- 证据在哪里;
- 哪些还没有验证。

8. 版本兼容要可诊断

CLI 版本差异不是边缘情况,而是插件设计的一部分。

真实遇到的差异包括:

可以选择兼容,也可以选择只支持新版本,但必须明确:

兼容不是“支持越多越好”,而是让失败可解释、可行动。

9. 发布前做反向审查

代码能跑,不等于可以公开。

发布前要问的不是“还缺什么功能”,而是“哪些东西不该带出去”:

GetNote Toolbox 公开前逐项处理了这些问题:删除评测结果和内部草稿,使用新的 Git 历史,替换本机 scope,移除本机环境描述,只保留受审源码、测试、许可证和公开文档。

让 Agent 先做的第六件事:

在公开发布前,请做一次反向审查:
1. 扫描密钥、Token、邮箱、绝对路径、`.env`;
2. 检查评测结果和真实笔记数据;
3. 检查 package 内容,而不是只看工作区;
4. 检查 README/站点有没有内部规划;
5. 给出“可以公开 / 不可以公开”的结论和证据。

10. 一张决策表

岔路 更稳的选择 代价
该不该做插件 先做能力审计 多花一点前期时间
包装 CLI vs 直连 API 优先复用已有 CLI 的认证 要适配 CLI 版本
1:1 映射 vs 工作流工具面 按工作流设计 需要删减和判断
工具越多越好 vs 最小够用 够用就好 可能暂时少一些低频能力
直接返回 JSON vs 归一化 只返回高信号字段 适配成本
删除随便做 vs 必须确认 显式确认 多一步操作
插件管密钥 vs 交给 CLI 交给 CLI 依赖 CLI 登录
只测单测 vs 双层测试 假 CLI + 真实验收 测试成本更高
只支持最新版 vs 兼容旧版 明确支持矩阵 代码分支更多
先公开 vs 先审查 审查后公开 发布更慢

11. 给 Agent 的执行清单

在让 Agent 做一个插件、工具或自动化之前,先让它回答:

  1. 这个能力真的需要成为 Tool 吗?
  2. 它替模型承担了哪项契约、护栏或归一化?
  3. ID、数量、错误和返回是否可靠?
  4. 写操作最坏会发生什么,怎么确认和回滚?
  5. 认证和密钥在哪一层,信任边界是什么?
  6. 假 CLI 和真实 CLI 分别验证了什么?
  7. CLI 版本变化时,失败信息是否可操作?
  8. 公开之前,哪些数据绝对不能带出去?

如果 Agent 回答不了,不要让它继续写代码。

12. 结论

让 Agent 少走弯路的关键,不是给它更多工具,而是给它更清楚的边界:

工具是能力的接口,Skill 是使用的知识,权限是行动的边界。三者都清楚,Agent 才是在工作,而不是在碰运气。

参考

  1. DeepSeek Harness 插件开发
  2. DeepSeek Harness GitHub 仓库
  3. GetNote Toolbox
  4. Twitter Snowflake(归档仓库)
  5. MDN: Number.MAX_SAFE_INTEGER
  6. Node.js: child_process
  7. npm publish 文档
  8. Git 文档

链接均为公开来源;产品行为可能随版本变化。

↑