deepseek-harness 学习手册
动手实验室Lab 05 · 让 agent 现场造一个工具
25 min

让 agent 现场造一个工具

cordis_inspect_list → cordis_define → cordis_run:模型给自己长出一个新能力,然后原样卸掉。

最后核对于 v0.1.1-rc.2 · commit b150a551b8 · 2026-08-22

前四个 Lab 都是你在写插件。这一课换个角色:你只提需求,模型自己写插件、自己挂上去、自己用。

它不是魔法表演。做完这一课你要能回答一个具体问题:为什么这件事在 dsh 里是结构上安全的,而在别的架构里不是。答案早在 M2 就给过了——注册即 effect。

先读这一段再动手

官方对这个工具集的定性是:「沙箱隔离了全局,但它不是安全边界……把这个工具集当成 bash 权限来对待。」

动态代码拿到的 Service 连的是真实运行时。所以:拿一个你不介意弄坏的 profile,别在生产机器上做这一课。

1 把工具集挂上去

不在任何一棵出厂的插件树里——生成式 tool catalog 在它那一行写着「Not in any shipped tree(a deliberate opt-in)」。仓库里有一个现成的 demo 组合:

terminal(在仓库 checkout 里)
$ pnpm run demo:cordis
Cordis Web: http://127.0.0.1:3081

它做的事很朴素:拿 examples/web-cordis/cordis.yml--patch overlay,叠在出厂的 web 组合上,占 3081 端口。也可以走 ACP 那条:pnpm run demo:cordis acp

在 commit b150a55 上,这条命令跑不起来

本站实测,pnpm run demo:cordis 在这个 commit 上会大声失败:

实测输出
Error: dsh: plugin tree failed to load: failed to apply loader entry
include (cordis:include): duplicate loader entry id: cordis-host-runner

原因很清楚:packages/bundle/web-app/cordis.patch.yml 自己已经插了 cordis-host-runner(第 108 行)、cordis-client-runner(179 行)和 ui-cordis(223 行),而 examples/web-cordis/cordis.yml 又插了一遍 cordis-host-runnerweb-app bundle 后来把这三行吸收进去了,demo overlay 没跟上。

这本身就是一次「规则卡 02 · 误配置必须大声失败」的现场演示——重复 id 不会被静默忽略,它在加载时直接把进程停掉。

绕过它不需要改仓库:overlay 是配置层的东西,自己写一份只补 tool-cordis 的就行。

~/cordis-demo.overlay.ymlyaml
# web-app bundle 已经自带 runner 与浏览器 UI 三行,这里只补模型可见的工具
- id: webserver
  config:
    host: 127.0.0.1
    port: 3081

- insert:
    - id: tool-cordis
      name: '@deepseek-ai/dsh-tool-cordis'
terminal
$ node --import tsx apps/cli/src/bin.ts web --patch ~/cordis-demo.overlay.yml

本站的 demo 录屏就是这么跑出来的。核对于 commit b150a55;上游修好之后 pnpm run demo:cordis 应该会重新可用。

光有这四个工具还不够。它们注入的是 ctx.dynamicCordisRunner(由 @deepseek-ai/dsh-cordis-host-runner 提供,那个包才拥有定义注册表和 vm 沙箱)。一个只有工具、没有 runner 的组合,这些工具永远不会激活——这是 inject 依赖声明的直接后果。

2 先只读地问:三个 inspect

给模型一个需求之前,先自己看看它会怎么开局。官方 system prompt 里的推荐工作流,前三步全是只读的:

  1. cordis_inspect_list —— 发现当前 Host 和 Client 有哪些 Inspect Provider,以及它们的只读查询方法
  2. cordis_inspect_query —— 用返回的 platform / provider / method / schema 去查精确的 Service、Event、Builtin、Slot、主题 token 或工具信息
  3. cordis_inspect_self —— 看当前 session 自己的插件、Package、版本指针、源码和诊断
这里藏着一个很值得抄的模式:两级查询

prompt 里写得很明确:「查 Service.listServiceEvent.listEvents 时不带 input,从紧凑签名目录里挑,然后再查那个精确的 service 或 event」——精确查询才返回结构化契约和它引用到的类型。

为什么不一次给全?因为只有被查询到的那一份契约会进入上下文。把一份大目录做成两级查询,比塞进 system prompt 便宜一个数量级(见 M4)。

3 提一个需求,看它 define

试试这种量级的需求(太大它会正确地拒绝走这条路):

给模型的话prompt
给我造一个临时工具,输入一段文本,返回它的字符数、
行数和最长一行的长度。再给它配一个小面板,我能直接粘贴进去看结果。

模型会调 cordis_define。关键在于这一步什么都没跑——它只校验参数和语法、记录源码。README 的原话:「它不申请审批、不执行 apply、不改 currentPackageId

你会看到对话里出现一张卡片,带一个启动控件。返回值里有两个 id:

  • pluginId —— 这个可以随时间被修改的插件。新插件你只提交一个 3–6 个小写字母的语义前缀,最终 id 由 Host 分配。
  • packageId —— 该插件下一个不可变的源码版本。改代码 = 定义一个新 Package,永不覆盖旧版本。

4 run:审批、异步、以及「starting 不等于成功」

cordis_run 才是真正把它挂进当前 context 的那一步。这里有三个状态要分清,prompt 里专门花了三段教模型别搞混:

返回意思模型该做什么
awaiting-approval未授权的 Client Package 产生了一个审批请求告诉用户去 UI 里允许或拒绝。不要等、不要重试、不要声称它在跑。
starting请求进了异步流程,浏览器还在激活starting 不等于成功。等系统通过 steering 上下文报最终结果。
技术失败加载或激活出错cordis_inspect_self 读诊断,修同一个插件——不要悄悄新建一个替身。

还有一条关于授权的设计:单个勾只授权当前这个 Package,双勾授权这个插件的未来版本。而且「一次技术失败之后,授权仍然有效」——失败不该惩罚用户已经做过的信任决定。

以及一句很硬的:「用户拒绝之后,不要再次请求审批。」

5 stop:看见「注册即 effect」

这才是这一课真正的高潮。让模型调 cordis_stop,然后观察:

  • 刚才那个工具从工具表里消失了——下一次模型请求就看不见它的 schema 了
  • 那个面板从浏览器里撤走了
  • 定义还在:插件、每一个不可变 Package、授权、版本指针全部保留,随时能再 run
  • 对一个已经停止的插件再 stop 一次,幂等成功
这里没有任何一行清理代码

动态插件的作者(模型)没有写过 unregisterTool,也没有写过 removePanel。它只是在 apply(ctx) 里注册了东西,而 Cordis 把这些注册当作 effect 记着,处置这棵子树就是把它们逐个撤回来。

顺带说个诚实的限制:这个 ctx façade 不暴露 effect(),所以插件代码没法注册自定义 disposer——on / provide / tools.register 是受支持的清理路径。README 的「已知限制」里明写着。

最后 cordis_undefine 是永久删除:先停、先取消未完成请求,然后删掉每个 Package、授权和版本指针。历史卡片只剩一条「插件已移除」的记录。

6 顺便观察一件事:KV cache

tool-cordis 自己的 README 在 Model Experience 一节里写了这么一句:

「运行或停止一个 prompt / 工具贡献,会改变后续请求的前缀,并可能从第一个变动的贡献开始让复用失效;一个不变的运行集合保持前缀稳定。」

换句话说,你刚才那次 cordis_runcordis_stop各自打掉了一次 KV cache。这不是 bug,是这个能力的固有成本,而且它被逐字写进了包的公开契约里——这正是 M4 那套做法的意义:一个能力的代价,和它的功能一样,是要写出来的。

对比即教学

同样这件事,在别家要几步?

dsh 0 次重启 在活的运行时里定义 → 挂载 → 使用 → 原样撤销,含浏览器 UI。不写盘、不改配置、活不过重启,按 session 隔离。
MCP 路线 1 次重连 模型写一个 server,落盘、启动、重连。更持久,但要跨一次进程边界。
待核对
写文件路线 1 次重启 模型写一个插件 / skill 文件,重启生效。最持久,反馈最慢。
待核对
三条路对应三种生命周期,不是三种能力等级。dsh 自己也这么说:想留下一个实验成果,就让 agent 走正常开发流程实现成一个普通插件——动态插件不能被自动晋升。这一课真正的价值不在「造工具」,在于让你亲眼看见一次干净的撤销
下一站

M10 · 批判视角与常见问答——把这一整套东西的代价和风险摊开算账。

目录

本页

Lab 05 · 让 agent 现场造一个工具