为什么先谈判断顺序
Agent 做插件时最贵的错误,通常不是写错一行代码,而是:
- 在不该做插件的地方做了插件;
- 先写实现,后补契约;
- 把 CLI 的命令原样翻译成 Tool;
- 写操作没有确认和回滚;
- 只测“顺利路径”,不测错误和版本差异;
- 代码能跑,却把不该公开的内容一起公开了。
这篇文章来自 GetNote Toolbox 的真实实践[3],但重点不是“我们做了什么”,而是一套可以迁移到其他 Agent 任务上的判断顺序:
先判断该不该做,再画契约;先只读,后写入;先验证,后发布。
1. 先判断:该不该做插件
不是所有 CLI 都值得包成插件,也不是所有工作流都值得做成 Tool。
| 场景 | 更合适的选择 |
|---|---|
| 高频、参数结构化、需要安全护栏 | Tool |
| 低频、复杂、需要领域判断 | CLI 或 Skill |
| 一次性任务、固定脚本 | 直接跑脚本 |
| 只读诊断、环境管理 | CLI 或 Skill |
| 需要跨应用连接外部系统 | 先评估 MCP 等协议,不要为了“统一”而重写 |
一个简单的判断是:这个能力变成 Tool 之后,替模型承担了什么?
如果答案只是“把命令换了一个名字”,它不值得成为 Tool。真正值得的,是下面至少一项:
- 结构化参数和校验;
- 安全边界,例如确认、超时、输出上限;
- 结果归一,例如 ID、limit、错误语义;
- 版本兼容;
- 可发现性和路由。
让 Agent 先做的第一件事:
先不要写代码。
请输出一份能力审计:
1. 这个 CLI 有哪些命令、输入、输出和副作用?
2. 哪些是高频、结构化、需要护栏的?
3. 哪些应该留给 CLI 或 Skill?
4. 哪些能力互相是逆操作?
5. 哪些地方需要版本兼容?
2. 先画契约,再写代码
Tool 不是函数名的集合,而是模型和外部能力之间的契约。[1][2]
每个 Tool 至少要写清:
- 名字和适用场景;
- 参数类型、必填/可选、范围;
- 返回什么高信号信息;
- 可能有哪些错误;
- 有没有副作用;
- 失败时模型应该怎么下一步。
以 GetNote Toolbox 为例,我们最终得到 15 个工具,覆盖搜索、保存、读取、更新、删除、标签、知识库和配额。15 不是目标,它是按工作流和契约边界推出来的结果。
几个具体原则:
- Tool 名带领域前缀,例如
getnote_search,不要用全局的search; - 同一个领域的动作可以合并成一个带
action的工具; - 互逆操作要成对出现,例如 add / remove;
- 低频、复杂、变化快的能力先不要做;
- 不提供通用
run_command或任意 HTTP 透传。
让 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:
- 未登录:提示
getnote auth login; - 版本不兼容:说明最低版本和升级方式;
- 参数错误:指出哪个参数、允许范围;
- 超时:给出超时值和调整方法;
- 输出超限:说明上限和缩小请求的方法;
- 业务失败:保留 CLI 的 message 和上下文。
模型看到的 ID、数量和错误,必须能直接用于下一步行动。
4. 工具面不是 CLI 的 1:1 映射
一个常见的邪路是:CLI 有 50 个命令,就做 50 个 Tool。
工具越多,模型的选择空间越大,误选和上下文成本也越高。更合理的是按工作流组织工具面:
- 高频读写优先;
- 安全敏感操作单独设计;
- 互逆操作成对;
- 低频能力先留给 CLI 或 Skill;
- 相近动作合并,而不是拆成一堆近义工具。
GetNote Toolbox 做到 15 个工具后就停下来了。不是不能再加,而是新增一个工具必须有明确收益。
让 Agent 先做的第三件事:
请先画出工具面,不要写代码:
- 哪些操作合并?
- 哪些是互逆操作?
- 哪些暂时不做?
- 每新增一个 Tool,模型少做了哪一步判断?
5. 写操作先设计失败
读操作出错,最多是结果不对;写操作出错,可能真的改坏用户的数据。
写操作设计要回答:
- 最坏会发生什么?
- 是否需要显式确认?
- 能不能先预览影响面?
- 失败后怎么回滚?
- 有没有幂等或可重试的设计?
- 返回信息是否足以让模型判断“真的成功了”?
GetNote Toolbox 的选择:
- 删除笔记必须显式
confirm: true; - 标签删除必须用标签 ID,不能用标签名猜;
- 更新标签是覆盖式语义,必须在描述里说明;
- 知识库 remove 只解除关联,不删除笔记;
- 涉及写入时,先建测试数据,结束后清理。
让 Agent 先做的第四件事:
在执行任何写操作前,先输出:
1. 会修改哪些对象;
2. 最坏影响是什么;
3. 如何确认;
4. 如何回滚;
5. 如果没有确认,停止执行。
6. 认证、权限与信任边界
GetNote Toolbox 不读取、不缓存、不回显 API Key,只调用已经登录的本机 CLI。
这是一个有意的设计:
- 插件不保存凭证;
- 认证由 CLI 统一管理;
- 用户可以用
getnote auth status诊断; - 密钥不进入模型上下文。
但“不碰密钥”不等于“没有信任边界”:
- 子进程会继承 Host 环境变量;[6]
npx兜底可能下载最新 CLI,带来版本漂移和供应链风险;- 插件调用的本地 CLI 本身必须是受信任的;
- 权限、沙箱和确认仍然要独立设计。
在意稳定性时,应该用 binaryPath 固定本地 CLI 版本,而不是默默依赖 latest。
7. 测试分两层
假 CLI 保证契约
单元测试不碰真实账号,用 fake CLI 覆盖:
- 19 位 ID;
- 嵌套响应;
- 旧版 flag 回退;
- 标签 add/remove 后回读;
- 知识库 add/remove;
- 删除确认。
这层测试快、稳定、可重复,也不会泄露真实笔记。
真实 CLI 保证环境
再用真实 CLI 做只读冒烟测试,确认:
- CLI 已登录;
- 真实输出形态;
- 工具调用能跑通;
- 错误提示可操作。
真实输出可能包含笔记片段,所以不能直接贴进 issue、聊天或提交。测试数据应尽量合成;必须使用真实账号时,先建测试笔记,结束后清理,报告本地生成并加入 gitignore。
让 Agent 先做的第五件事:
不要只说“完成了”。
请给出:
- 测了哪些路径;
- 哪些是假 CLI,哪些是真实 CLI;
- 失败路径覆盖了什么;
- 证据在哪里;
- 哪些还没有验证。
8. 版本兼容要可诊断
CLI 版本差异不是边缘情况,而是插件设计的一部分。
真实遇到的差异包括:
- 19 位 ID 有时是 number,有时是 string;
- 旧版忽略
limit; note update和kb remove在新版需要-y,旧版不认识这个 flag;- 保存链接时可能返回嵌套的
data.note。
可以选择兼容,也可以选择只支持新版本,但必须明确:
- 支持哪些版本;
- 推荐哪个最低版本;
- 版本不满足时给什么错误;
- 是否有回退路径;
- 在哪里固定 CLI 路径。
兼容不是“支持越多越好”,而是让失败可解释、可行动。
9. 发布前做反向审查
代码能跑,不等于可以公开。
发布前要问的不是“还缺什么功能”,而是“哪些东西不该带出去”:
- 评测报告里有没有真实笔记 ID、标题、片段?
- 内部草稿有没有本机路径、账号信息、操作计划?
- Git 历史有没有个人邮箱?[8]
- 包名 scope 有没有带出本机用户名?
- npm 包里有没有测试、
.env、内部文档?[7] - 公开 README 有没有“待写/规划中/占位”?
- 当前仓库到底是交付物,还是工作台?
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 做一个插件、工具或自动化之前,先让它回答:
- 这个能力真的需要成为 Tool 吗?
- 它替模型承担了哪项契约、护栏或归一化?
- ID、数量、错误和返回是否可靠?
- 写操作最坏会发生什么,怎么确认和回滚?
- 认证和密钥在哪一层,信任边界是什么?
- 假 CLI 和真实 CLI 分别验证了什么?
- CLI 版本变化时,失败信息是否可操作?
- 公开之前,哪些数据绝对不能带出去?
如果 Agent 回答不了,不要让它继续写代码。
12. 结论
让 Agent 少走弯路的关键,不是给它更多工具,而是给它更清楚的边界:
- 先判断该不该做;
- 先画契约,再写实现;
- 先只读,后写入;
- 先测试,后发布;
- 先审查,后公开。
工具是能力的接口,Skill 是使用的知识,权限是行动的边界。三者都清楚,Agent 才是在工作,而不是在碰运气。
参考
- DeepSeek Harness 插件开发
- DeepSeek Harness GitHub 仓库
- GetNote Toolbox
- Twitter Snowflake(归档仓库)
- MDN: Number.MAX_SAFE_INTEGER
- Node.js: child_process
- npm publish 文档
- Git 文档
链接均为公开来源;产品行为可能随版本变化。