让chatgpt来给我讲解下deepseek harness的插件体系
最近deepseek发布了harness,试用了一下结合deepseek-4-flash,用它尝试写一个php的现代版phpmyadmin,因为也是一个比较重要且有一点复杂度的应用
试了下除了数据展示有点问题,修了一轮就完成了,整体表现还是不错的
然后看了下它主打的是万物都是插件
这个概念还是比较复杂的,尝试让gpt老师帮我讲解下
DeepSeek Harness 就像一个“由很多小员工临时组成的公司”。
没有一个超级总经理包办所有事情:
- 一个员工负责问大模型;
- 一个员工负责读文件;
- 一个员工负责运行命令;
- 一个员工负责审批危险操作;
- 一个员工负责保存聊天记录;
- 一个员工负责显示网页界面。
这些“员工”就是插件。
需要什么员工,就把什么员工叫来;不需要了,就让它离开,并自动收拾自己留下的东西。
你对 DeepSeek Harness 说:
帮我读取 package.json,看看项目用了哪些依赖。
系统内部大致发生了这些事:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
你输入问题
↓
Agent Loop 插件:决定下一步做什么
↓
LLM 插件:把问题发给 DeepSeek
↓
DeepSeek 返回:我要调用 read_file
↓
权限插件:这个文件允许读吗?
↓
文件工具插件:读取 package.json
↓
日志插件:记录本次工具调用
↓
Agent Loop 插件:把文件内容再次交给 DeepSeek
↓
DeepSeek 生成最终回答
↓
Web UI 插件:把回答显示出来
关键点是:
上面每一个步骤都可以由不同插件负责。
比如你可以:
- 把 DeepSeek 换成其他模型;
- 把本地文件系统换成远程沙箱;
- 给文件读取前增加权限检查;
- 给模型调用增加重试;
- 给工具调用增加日志;
- 换掉整个 Agent Loop。
其他部分不需要跟着重写。
普通软件的结构经常是:
1
2
3
4
固定核心程序
├── 插件位置 A
├── 插件位置 B
└── 插件位置 C
核心程序是老板,插件只能填几个预留位置。
DeepSeek Harness 更像:
1
2
3
4
5
6
7
极小的组装平台
├── Agent Loop 插件
├── 模型插件
├── 工具插件
├── 权限插件
├── 存储插件
└── UI 插件
连“核心 Agent 怎么循环运行”本身也是插件。
所以它不是:
一个 Agent,加上一些插件。
而是:
很多插件组合起来以后,才形成一个 Agent。
假设你写了一个天气工具插件。
它不能一启动就直接工作,因为它要先找到“工具管理器”。
所以它会声明:
1
我需要:tools 服务
代码里大概是:
1
export const inject = ['tools']
意思就是:
只有工具管理器存在时,才启动我。
系统启动时可能出现这种情况:
1
2
3
4
5
6
天气插件:我需要 tools
工具管理器:还没启动
天气插件:那我先等着
工具管理器启动
天气插件:条件满足,现在开始工作
如果工具管理器后来被替换:
1
2
3
4
5
6
7
旧工具管理器卸载
↓
天气插件也暂时卸载
↓
新工具管理器启动
↓
天气插件重新启动,并连接新管理器
这就是 DeepSeek Harness 很特别的地方:
插件依赖不是只在启动时检查一次,而是一直被系统关注。
插件启动时会拿到一个 ctx:
1
2
3
export function apply(ctx) {
}
你可以把 ctx 理解成“公司总机”。
插件不知道其他插件住在哪里,只需要问总机:
1
2
3
4
5
ctx.tools
ctx.llm
ctx.sessions
ctx.fs
ctx.sandbox
比如天气插件说:
1
ctx.tools.register(weatherTool)
意思就是:
总机,请把我的天气能力登记到工具管理器里。
这样天气插件不需要自己找到 Agent Loop,也不需要直接修改模型提示词。工具管理器会负责把天气工具告诉模型。
这是整个系统最值得理解的设计。
假设天气插件启动后做了三件事:
1
2
3
1. 注册 weather 工具
2. 监听工具调用事件
3. 启动一个定时器
如果只是粗暴地删除插件代码,可能留下:
- 一个无法调用的 weather 工具;
- 一个永远存在的事件监听器;
- 一个一直运行的定时器。
DeepSeek Harness 要求插件在创建东西时,同时登记“怎么撤销”。
可以理解成: 于是卸载插件时,系统会自动倒着清理:1
2
3
4
5
6
7
8创建 weather 工具
同时记下:卸载时删除 weather 工具
添加事件监听器
同时记下:卸载时移除监听器
启动定时器
同时记下:卸载时停止定时器 所以热更新才比较可靠。1
2
3停止定时器
移除监听器
删除 weather 工具
修改天气插件代码后,系统可以: 而不是让新旧两套东西同时残留。1
2
3
4
5完整清理旧天气插件
↓
重新加载新代码
↓
重新注册新天气工具
这个“创建时顺便记录撤销方法”的机制,技术上叫effect。
现在再引入 Fiber 就很容易了。
Fiber 可以理解成:
某个插件这一次运行的“工作档案”。
里面记录着:
- 这个插件是谁;
- 它在等哪些服务;
- 它现在有没有启动;
- 它注册过什么东西;
- 卸载时要清理什么;
- 它是否启动失败。
一个插件大致会经历 对应的技术名称是:1
2
3
4
5
6
7
8
9等待条件
↓
正在启动
↓
正常运行
↓
正在清理
↓
已经卸载 所以 Fiber 不是线程,也不是子进程。1
2
3
4
5PENDING
→ LOADING
→ ACTIVE
→ UNLOADING
→ DISPOSED
它只是插件的一份“生命周期档案”。
有些插件提供能力,比如文件系统。
另一些插件不提供新能力,只想在某个流程中插一脚。
例如权限插件想在执行 Bash 前检查命令:
1
2
3
4
5
6
模型请求执行 rm ...
↓
权限插件检查
↓
允许:继续执行
拒绝:直接返回错误
这个过程很像高速公路收费站:
1
2
3
4
5
6
请求
→ 审批站
→ 权限站
→ 超时站
→ 日志站
→ 真正执行
每一站都可以:
- 检查请求;
- 修改请求;
- 记录请求;
- 放行;
- 拒绝继续。
在代码里,放行通常是调用: 不调用1
next()
next(),就表示到此为止。
这就是它所谓的waterfall。本质上就是一条可插拔的中间件链。
这里最容易被名词绕晕。
真正执行工作的代码。
一个 Bundle 可能一次带来很多插件,例如:
1
2
3
4
5
6
Web Bundle
├── Web 服务器插件
├── 浏览器通信插件
├── 聊天界面插件
├── 设置页面插件
└── Web 启动插件
Profile 是公司组织方案
比如:
1
2
3
web Profile
├── 基础 Bundle
└── Web Bundle
而:
1
2
3
headless Profile
├── 基础 Bundle
└── 命令行 Bundle
因此:
1
dsh --profile web
可以理解成:
按照 Web 这套组织方案,把对应部门和员工全部组装起来。
执行:
1
dsh plugin --profile web add some-plugin
大致等于:
1
2
3
4
5
1. 用 pnpm 下载 npm 包
2. 查看这个包有没有声明自己是 DSH Bundle
3. 找到它携带的插件配置
4. 把它加入 web Profile
5. 下次启动时加载其中的插件
大概是这么个逻辑