欢迎光临
我们一直在努力

deepseek harness 完成一个简单的tool

请添加图片描述
请添加图片描述

开发一个工具
本教程会在 Web UI 中添加一个 greet 工具。请先完成第一个插件,并保留其中的 scratch-plugin 目录。

创建工具插件
将 scratch-plugin/src/my-plugin.ts 替换为:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}

inject 让 Cordis 等待工具注册表就绪。defineTool 根据 parameters 推导并校验 args;execute 返回 output.schema 声明的规范值,output.render 再将该值转换为面向模型的内容。

运行并调用工具
如果开发命令未在运行,请重新启动:

pnpm dsh web –patch ./scratch-plugin/cordis.yml
打开 http://127.0.0.1:3080,然后输入:Use the greet tool to greet Ada. 模型可以调用 greet,并收到 Hello, Ada! 这一工具结果。

一、这段代码在做什么

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}

拆开看:

  • import type { Context } —— 只导入类型,运行时被擦除,不给插件增加运行时依赖。
  • name / inject / apply —— 这是 Cordis 函数插件的固定三个导出,框架靠它们识别并加载这个插件。
  • ctx.tools.register(…) —— 往 tools 服务里注册一个工具,返回的“注销器”会被框架接管,插件卸载时自动注销。

二、Cordis (这行代码背后的框架约定)

Cordis 是 Harness 底层的插件框架(vendored,见 [cordis-primer.md](file:///d:/harness/3/deepseek-harness/docs/cordis-primer.md)),五个核心思想正好对应这段代码:

  • 插件是一个实现 Service 的对象/函数。函数形式必须有 apply(ctx),可选 inject。
  • Context 是服务的仓库。插件通过 ctx.tools 这种稳定的 key 拿服务,而不是直接 import 具体实现。
  • inject 声明服务依赖。inject: ['tools'] 意思是:这个插件要等 tools 服务存在后才加载(见 [03-services.md](file:///d:/harness/3/deepseek-harness/docs/cordis-tutorial/03-services.md))。
  • 事件通信——这里没直接用,但 tools/result 这类事件是工具插件之间解耦的方式。
  • 注册是可逆的 effect。见 [02-lifecycle-and-effects.md](file:///d:/harness/3/deepseek-harness/docs/cordis-tutorial/02-lifecycle-and-effects.md)。
  • 几个关键点落到这段代码:

    • 加载时序与 inject:inject: ['tools'] 让插件在 tools 服务出现前一直处于 PENDING 状态,不会报错、也不执行 apply。一旦 @deepseek-ai/dsh-tools 提供者挂载,apply 才运行,此时 ctx.tools 保证可用。所以 cordis.yml 里的顺序无关紧要,依赖关系决定启动顺序。

    • 注册即 effect:ctx.tools.register(…) 返回的 disposer 被自动挂到插件生命周期上。插件被卸载(热重载 / 配置改动 / 服务消失)时,工具会被自动注销。你不用手动 removeTool。

    • apply 里 ctx.tools 的类型来源:ctx.tools 能通过类型检查,是因为 @deepseek-ai/dsh-tools 用了 TypeScript 声明合并(declare module '@deepseek-ai/cordis')往 Context 接口上加了 tools 属性。注意这段代码没写 import type {} from '@deepseek-ai/dsh-tools',但它确实 import 了 defineTool(这个包本身就带了声明合并),所以 ctx.tools 已类型化。

    三、defineTool (工具 DSL)

    defineTool 来自 @deepseek-ai/dsh-tools,它把参数 spec 转成模型可见的 JSON Schema,并推断 args 类型。详细契约见 [adding-a-tool.md](file:///d:/harness/3/deepseek-harness/docs/cookbook/adding-a-tool.md) 和 [tool.md](file:///d:/harness/3/deepseek-harness/docs/user/develop/basic/tool.md)。

    三个字段各管一件事:

    • parameters → 给模型看的入参 schema,同时决定 execute(args) 里 args 的类型。这里 name: { type: 'string', required: true } 让 args.name 被推断为 string,且 defineTool 会在 execute 运行前校验模型传入的 arguments(类型、必填等)。

    • output.schema → 声明工具的“规范返回值”(canonical value),是程序化的、可持久化的 JSON。这里声明为 string。

    • output.render → 把 value 转成给模型看的 Native 内容块(content blocks)。这里把字符串包成 [{ type: 'text', text: value }]。

    • execute(args) → 真正干活的地方,返回的就是 output.schema 声明的规范值。这里返回 Hello, ${args.name}!。

    一个关键心智模型:execute 返回的是给程序/日志用的规范值,render 才是给模型看的文本,两者分开。这样 PTC 模式(await tools.greet(…))能拿到干净的返回值,而模型能拿到渲染后的文本。

    四、profile (这个插件如何被装进一个应用)

    “profile”是 Harness 的**应用组合(插件树)**概念,定义在 [profile.ts](file:///d:/harness/3/deepseek-harness/packages/boot/app-boot/src/profile.ts#L1-L23) 的模块注释里:

    • 一个 profile 是 $DSH_HOME/profiles/<name> 下的一个目录,包含:
      • package.json —— 声明 dsh.profile.bundles(一个有序的 bundle 列表)和 patchReload。
      • cordis.patch.yml —— 用户自己的 patch 层,在所有 bundle 层之后应用。
    • bundle 是一个 npm 包,其 manifest 里声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }。组合时按 bundles 顺序把每个 bundle 的 patch 层叠加到空列表上,再叠 profile 自己的 patch,最后叠 launcher 层(–patch 文件和 flag 派生的 patch)。

    内置 profile 模板(见 [profile.ts](file:///d:/harness/3/deepseek-harness/packages/boot/app-boot/src/profile.ts#L104-L126)):web、headless、acp、sdk 等,每个由一组 bundle 组成。例如 headless = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless']。

    • [base 层 cordis.patch.yml](file:///d:/harness/3/deepseek-harness/packages/bundle/base/cordis.patch.yml) 是共享核心,其中第 460-461 行就 mount 了 tools 服务:- id: tools → @deepseek-ai/dsh-tools。所以你的 inject: ['tools'] 能在这里被满足。
    • [headless 层](file:///d:/harness/3/deepseek-harness/packages/bundle/headless/cordis.patch.yml) 在 base 之上又插入自己的 patch。

    这个 greet-tool 插件怎么挂进去:通过 –patch overlay。教程里用的是 scratch-plugin/cordis.yml:

    pnpm dsh web –patch ./scratch-plugin/cordis.yml

    这个 overlay 会 insert 你的插件条目(路径要是绝对路径),叠加到 web profile 的插件树上,于是 greet 工具就出现在模型可调用的工具目录里。patch 文件只贡献配置,不改变 loader 解析模块路径的 profile 目录(见 [index.md](file:///d:/harness/3/deepseek-harness/docs/user/develop/basic/index.md))。

    五、把它们串起来的一句话

    defineTool 定义了工具契约 → ctx.tools.register 把它作为一个可逆 effect 注册进 tools 服务 → inject: ['tools'] 保证注册时服务已就绪 → profile 的 bundle/patch 层负责把 dsh-tools(服务提供者)和你的插件(服务消费者)组装进同一棵插件树,最终让模型能调用 greet。

    赞(0)
    未经允许不得转载:171主机测评 » deepseek harness 完成一个简单的tool
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址