news 2026/8/16 13:16:09

deepseek-harness-minimal-tool-plugin-blog

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
deepseek-harness-minimal-tool-plugin-blog

给 deepseek-harness 写一个工具插件:从开发到真实调用

请以您的实际环境为准,以下仅供参考
最近在接触 deepseek-harness(简称 dsh),顺手给它写了一个最小工具插件,把从开发、测试、发布到最终被模型真实调用的整个过程走了一遍。这里记录一下,包括过程中踩过的坑。

一、dsh 是什么

deepseek-harness 是 DeepSeek 的一个 agent 开发框架,设计上贯彻"一切皆插件":LLM 适配层是插件,工具是插件,执行策略也是插件。宿主是一个 Cordis 微内核,通过依赖注入和事件系统把各部分组装成完整的 agent 运行时。

对想扩展 dsh 的人来说,最小可交付的单元就是一个工具插件:通过ctx.tools.register()向模型暴露一个新工具,工具会自动进入系统提示词组装、参数校验和执行管线。也就是说,写一个工具,模型就能在会话里真正使用它。

这次的目标:从零写一个可运行、可发布、可被模型真实调用的最小工具插件。

二、最小工具插件的结构

一个 dsh 工具插件 = Cordis 插件四要素 + 一个工具定义。

四要素:

要素说明
name插件名,唯一标识
inject依赖注入声明,['tools']表示需要工具注册服务
apply(ctx)插件入口,在这里注册工具(注册是一个 effect,插件销毁时工具自动注销)
Config可选,插件的可配置项

工具定义用@deepseek-ai/dsh-tools提供的defineTool,核心三部分:

  1. parameters:模型可见的参数 schema,类型会自动推导进executeargs
  2. output:结构化返回值声明 + 纯函数render(模型看到结构化 value,界面看到 render 产物)
  3. execute:真正执行的地方,必须尊重exec.signal取消信号

有一个契约必须记住:工具返回 canonical JSON value,不是自然语言文本。不能返回"文件内容是 xxx"这种话让模型去解析,要直接返回结构化数据。

三、写第一个工具 read_file

项目结构保持最小:

my-dsh-tool/ ├── package.json ├── tsconfig.json ├── src/ │ └── index.ts # 插件入口 ├── test/ │ └── index.test.ts # vitest 单测 └── README.md

插件本体只有 31 行:

import{readFile}from'node:fs/promises';importtype{Context}from'@deepseek-ai/cordis';import{defineTool}from'@deepseek-ai/dsh-tools';exportconstname='my-tool';exportconstinject=['tools'];exportfunctionapply(ctx:Context){ctx.tools.register(defineTool({name:'read_file',description:'Read a file from disk.',parameters:{path:{type:'string',required:true,description:'Absolute path'},limit:{type:'number',description:'Max chars to read'},},output:{schema:{type:'string'},render:(_args,value)=>[{type:'text',text:value}],},asyncexecute(args,exec){// args 已按 schema 校验:{ path: string; limit?: number }consttext=awaitreadFile(args.path,{encoding:'utf8',signal:exec.signal,// 尊重取消信号});returnargs.limit?text.slice(0,args.limit):text;},}));}

几个值得注意的细节:

  • inject: ['tools']声明依赖注入,ctx.tools才会可用
  • defineTool的类型推导:parameters写完,executeargs类型就自动有了,不用手写
  • exec.signal:把取消信号透传给readFile,模型中断时工具会快速失败而不是卡住
  • render必须是纯函数:界面卡片展示逻辑,禁止任何 I/O 和随机性(流式和回放会反复执行它)

package.json 的关键字段(type: module必配,scoped 包发布):

{"name":"@your-scope/my-dsh-tool","type":"module","main":"lib/index.js","types":"lib/index.d.ts","files":["lib"],"publishConfig":{"access":"public"},"engines":{"node":"^22.19.0 || >=24.0.0"},"peerDependencies":{"@deepseek-ai/cordis":"^4.0.1","@deepseek-ai/dsh-tools":"0.1.0-rc.6"}}

四、单测:不启动宿主也能验证

工具可以脱离完整 dsh 宿主做测试:构造一个真实的ToolRuntime挂到 CordisContext上,再跑插件的apply

functionmount(){constctx=newContext()ctx.provide('systemPrompt',{tools(){}})newToolRuntime(ctx,{})apply(ctx)returnctx}

然后直接对ctx.tools做断言。我写了 5 个用例:

  1. 模型可见 schema:read_file出现在ctx.tools.schemas()里,参数结构正确
  2. 正常调用:返回结构化 canonical 值(不是 prose)
  3. 参数校验:缺必填参数path会被拒绝,isError: true
  4. 取消路径:已 abort 的 signal 让工具快速失败
  5. render 产物:返回的 content block 与 canonical value 一致

工具的核心契约(参数校验、取消、结构化返回)都被单测固定住,后续 dsh 版本升级时可以用它当兼容性检查。

五、发布到 npm

发布前自检配置时发现三个问题:

  1. files白名单:不写的话会把node_modulestest等无关文件一起打进包
  2. 依赖位置不对:@deepseek-ai/cordis@deepseek-ai/dsh-tools应该放peerDependencies(宿主环境本来就有,避免双实例),而不是dependencies
  3. 缺发布配置:scoped 包默认不公开,必须显式publishConfig.access: "public"

修完这些,又补了descriptionlicenserepositoryengineskeywordsREADME.md,才真正执行发布。

发布时遇到 403,需要 2FA。改用只授权这一个包的 granular access token 配置到.npmrc后成功。

发布不是一次性的——中间因为修 peer 依赖声明,版本从0.1.0升到0.1.10.1.2,每次都执行npm publish并用npm view确认注册表里能看到。

六、挂载到 profile

dsh 的插件分发走 profile 机制:每个 profile(如 web、headless)是一个独立环境,通过cordis.patch.yml声明要加载的插件。

用官方命令挂载(会自动处理依赖安装与路径):

dsh plugin--profile headless add

手动改 patch 文件也可以。追加新 entry 的语法是- insert:(无 id 追加;- id:是定位已存在 entry 用的):

-insert:-id:my-toolname:'@your-scope/my-dsh-tool'

七、真实调用

挂载后做了两层验证。

第一层,headless 一次性会话:直接发指令要求调用工具,从日志确认工具被调度、被真实执行、结果被模型正确引用。

第二层,web 界面:启动dsh web(默认本地端口 3080),向模型发送"用 read_file 读取 package.json 并告诉我 name 字段"。界面上能看到完整过程:

Think 推理 → Tool call · read_file · package.json(工具调用卡片)→ 最终回答 "name": "@your-scope/my-dsh-tool"

回答里的包名和磁盘上 package.json 的name字段完全一致(文中用@your-scope代指真实 scope),说明模型确实通过我们写的工具读了文件,而不是猜的。至此,从开发到真实调用这条路径就走通了。

八、遇到的问题和解决过程

按时间顺序记录,很多在官方文档里没有现成答案。

1. dsh-tools 依赖装不上

>=0.1.0声明装不上。原因:dsh 家族包还在 preview 阶段,真实最新版本号是0.0.1-rc.1(后来跟进到0.1.0-rc.6),不是语义化版本能猜出来的。解决:npm view @deepseek-ai/dsh-tools versions查真实版本号,锁定可用的 rc 版本。

2. TS2307:node:fs/promises 找不到

编译报找不到 Node 内置模块。解决:补装@types/node,并在tsconfig.json"types": ["node"]

3. npm publish 403:2FA 要求

发布被拒。解决:用 granular access token(授权范围只限这一个包)写入.npmrc

4. 发布显示成功但注册表查不到

在某环境里 publish 显示成功,npm view却查不到。原因是 registry 返回 404 后延迟同步出 200,属于环境的伪成功。解决:以真实终端npm publish+npm view <pkg>确认为准。

5. 最大的坑:会话崩溃 Cannot read properties of undefined (reading ‘prepare’)

插件挂载后启动会话直接报UNKNOWN: Cannot read properties of undefined (reading 'prepare'),工具调度失效。

根因是dsh-tools 双实例:手动 npm 安装时 profile 解析到dsh-tools@0.0.1-rc.1,而宿主 dsh 内嵌的是0.1.0-rc.6。两个实例导出的工具调度符号不同,插件注册的工具找不到正确的调度器。

解决分三步:

  1. 改用官方dsh plugin --profile headless add(pnpm 路径)重建 profile,避免手动 npm 安装导致的双实例
  2. 插件 peerDependencies 从0.0.1-rc.1升级到0.1.0-rc.6,与宿主内嵌版本对齐
  3. 测试代码同步更新:ToolRegistry类更名为ToolRuntime

教训:插件框架的依赖必须与宿主内嵌版本严格一致,手动装出双实例是这类"符号不匹配"崩溃的主要来源。

6. pnpm store 路径被沙箱拦截

pnpm 安装时写磁盘被沙箱拦截。解决:--store-dir指向项目内路径,并重定向临时目录环境变量。

7. setx 设置环境变量后新终端读不到

setx只写注册表,不广播到已运行的进程;explorer 不重启,新终端继承的还是旧环境。解决:重启 explorer 进程使环境广播刷新。

8. patch 语法错误:entry “my-tool” not found

- id: my-tool报 entry not found。原因:- id:是定位已存在 entry 的语法,新增 entry 必须用- insert:无 id 追加。

9. API key 明文落盘

.env里存了 API key,明文在磁盘上。解决:迁移到用户级环境变量,从项目目录删除.env文件。

10. 发布配置三个 P0

见第五节:files白名单、依赖放peerDependencies、scoped 包必须publishConfig.access: "public"

九、总结

从 31 行的read_file到模型在界面上真实读出文件内容,这条路径验证了 dsh 插件机制的最小闭环:

defineTool 注册 → schema 进入模型视野 → 模型决策调用 → execute 真实执行 → render 回传 → 模型组织回答

几点心得:

  1. 先写单测再谈发布:工具契约(参数校验、取消、结构化返回)是踩坑高发区,单测能提前发现
  2. 发布前过一遍配置检查:files 白名单、peerDependencies、publishConfig.access
  3. 挂载用官方命令:dsh plugin add会自动处理依赖路径,手动 npm 安装容易搞出双实例
  4. 版本对齐是生命线:peer 版本必须与宿主内嵌版本一致,否则就是莫名的运行时崩溃
  5. 确认结果要复核:npm view确认发布、界面确认调用,不要轻信中间层的"成功"提示

参考:官方仓库 deepseek-harness(GitHub 上的 deepseek-ai/deepseek-harness 项目,文档里有工具编写、插件形态、架构等说明)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/16 13:15:39

AI基础设施实战:从概念到部署,构建企业级AI能力栈

最近在技术圈里&#xff0c;甲骨文&#xff08;Oracle&#xff09;的一则新闻引发了广泛讨论&#xff1a;这家老牌数据库巨头正“举债重金押注 AI 基建”&#xff0c;同时被曝出正在制定新一轮裁员计划。这看似矛盾的操作背后&#xff0c;其实揭示了当前企业级技术市场一个深刻…

作者头像 李华
网站建设 2026/8/16 13:07:43

Linux root执行passwd报Permission denied的深度排查与解决方案

1. 问题现象与核心矛盾解析 “Permission denied”这个提示&#xff0c;对于任何一位Linux用户&#xff0c;尤其是系统管理员来说&#xff0c;都再熟悉不过了。它通常意味着当前用户没有执行某个操作或访问某个文件的权限。但当一个已经登录为 root 的用户&#xff0c;在执行…

作者头像 李华
网站建设 2026/8/16 13:03:00

MyBatis-Plus为何叫好不叫座?从技术选型到工程实践的深度解析

1. 一个现象引发的思考&#xff1a;好用与普及度的背离 最近在几个技术社区和项目群里&#xff0c;发现一个挺有意思的现象。大家讨论持久层框架时&#xff0c;MyBatis-Plus&#xff08;简称MP&#xff09;的口碑普遍不错&#xff0c;很多用过的人都会说一句“真香”&#xff0…

作者头像 李华
网站建设 2026/8/16 13:01:49

DeepSeek-V4-Flash视觉API实战:从零接入到生产级应用指南

最近在折腾一些需要视觉理解能力的自动化任务时&#xff0c;遇到了一个挺有意思的“坑”。手头有个项目&#xff0c;需要程序能看懂截图、分析界面元素&#xff0c;然后自动生成操作指令。一开始&#xff0c;我理所当然地想到了那些耳熟能详的多模态大模型&#xff0c;但要么是…

作者头像 李华
网站建设 2026/8/16 12:59:02

深入解析FlatBuffers:高性能二进制序列化原理与实战

1. 项目概述&#xff1a;从零认识FBB 如果你最近在关注一些开源项目或者技术社区的讨论&#xff0c;可能会频繁地看到一个缩写&#xff1a;FBB。乍一看&#xff0c;它可能像某个新潮的社交平台&#xff0c;或者某个神秘的开发框架。实际上&#xff0c;FBB是一个在特定技术领域内…

作者头像 李华