从 dsh 源码看「一切皆插件」与 Spring IoC

摘要

dsh 很火,但更值得研究的是它的实现方式。顺着工具注册链路往下看,会发现它和 Java 开发者熟悉的 Spring IoC 有异曲同工之处:两者都把能力的装配交给容器,让使用方不再直接依赖具体实现。

一切皆插件是如何实现的

dsh 底层使用了 Cordis 框架。定位到 packages/core 就能看到,Session、Agent、Tools、System Prompt 等核心能力,都以 Cordis 服务或插件的形式挂载到共享上下文中。

也就是说,dsh 没有把所有能力硬编码进 Agent Loop,而是让各个插件通过注册机制,把自己的能力贡献到运行时。Agent Loop 不需要自己创建每一种工具,也不需要知道工具由哪个插件实现;它只面向容器已经装配好的能力工作。


1. 插件怎么登记:register()

先看工具注册入口,packages/core/tools/src/index.ts 里的 ToolRegistry.register()

重点看最后的返回值:

ts 复制代码
return this.layers.effect(
  this.ctx,
  layer => layer.tools.insert(name, definition),
  { label: 'tools.register()' },
)

把这段代码抽象一下,它主要完成三件事:

  1. 校验这份工具"说明书"是否合法,例如输出结构、超时参数以及保留名称。
  2. 找到当前作用域对应的工具层,把工具定义插入进去。
  3. 返回一个撤销函数。插件卸载时调用它,就能移除这次注册留下的工具定义。

为什么要返回撤销函数?因为"一切皆插件"不只是能把零件装上去,还要能把它干净地拆下来。注册与撤销是一对操作:插件挂载时贡献能力,插件卸载时收回能力,避免已经失效的工具继续留在运行时。


2. tools 真正保存的是什么:Map<string, V>

继续点击 layer.tools,可以看到它的类型:

ts 复制代码
readonly tools: NamedEntries<ToolDefinition>

NamedEntries 位于 packages/core/scope/src/store.ts,核心实现可以简化为:

ts 复制代码
private data = new Map<string, V>()

insert(name: string, value: V): () => void {
  const data = this.data
  if (data.has(name)) throw this.duplicateError(name)
  data.set(name, value)

  let active = true
  return () => {
    if (!active) return
    active = false
    data.delete(name)
  }
}

本质上,它就是一张 Map:

  • key:工具名 ,例如 bash
  • value:工具定义,里面包含工具名称、参数、执行逻辑和输出描述。

从这套实现看,选 Map 有三个原因:

  • 可以按名字直接查找:核心需求就是"根据工具名找到工具定义"。
  • 保留插入顺序:JavaScript 的 Map 会维护插入顺序,使工具投影后的排列保持稳定。
  • 同一层内禁止重名:如果一张登记表里出现两个同名工具,模型发起调用后就无法得到唯一解释。

还有一个容易忽略的细节:这里保存的是工具能力定义,不是插件对象本身。

插件负责把能力注册进来,并在自身生命周期结束时撤销注册;真正进入登记表的,是工具名和 ToolDefinition。因此,使用方不需要知道这个工具由哪个插件提供,只需要按照名字查找并调用。

这正是解耦发生的地方。


3. 完整流水线:模型怎么使用这张登记表

把工具从注册到执行的过程串起来,大致是这样一条链路:

这条链路上有两个关键分工。

第一,模型只看"菜单",不看"后厨"schemas() 会把可见工具投影为模型需要的字段,模型知道工具叫什么、能做什么、需要哪些参数,但看不到工具内部的执行函数和展示逻辑。

第二,真正执行时,仍然要回到登记表查找定义 。模型生成 tool_call 后,Agent Loop 负责调度,ToolRegistry 再根据名字解析对应工具,进入工具执行流水线,最后调用真正的 execute(...)

所以,schema 解决的是"让模型知道有哪些工具",Map 解决的是"模型选中之后,运行时去哪里找到它"。


4. 本质上,它装的是"工具能力"

一句话概括:

dsh 装进去的是工具能力,大模型根据 schema 决定调用哪一个。

这张 Map 从头到尾只承担一个核心职责:维护"名字 → 能力"的映射,把"谁实现能力"和"谁使用能力"拆开。

不过,真正重要的并不是"用了 Map",而是谁掌握装配权。工具的使用方不再亲自创建、寻找和拼装具体实现,这些工作被交给了容器和注册机制。


5. 对比 Spring:共同点是把装配权交给容器

对于 Java 开发者,这套设计很容易让人联想到 Spring IoC。两者最值得抓住的共同点是:

对象不由自己装配,而由容器注入。

在 Spring 中,一个 Bean 不需要在内部到处 new 自己依赖的对象。它声明依赖,容器负责查找合适的 Bean、创建实例并完成注入。

在 dsh 中,Agent Loop 同样不需要直接依赖每个具体工具插件。插件把 ToolDefinition 注册到 Cordis 管理的作用域工具层,运行时再从容器已经汇集的能力中生成 schema、查找工具并执行。

所以,两者的共同点不只是"底层都有 Map"。Map 只是保存登记结果的数据结构,把装配权从对象自身转移给容器,才是 IoC 的核心。

Spring 的 DefaultListableBeanFactory 负责管理 BeanDefinition;DefaultSingletonBeanRegistry 则管理已经创建的共享单例。概念上可以简化为两类登记:

java 复制代码
// Bean 名称 → Bean 定义
Map<String, BeanDefinition> beanDefinitionMap;

// Bean 名称 → 已创建的共享单例
Map<String, Object> singletonObjects;

这两套登记机制都通过"名字 → 对象或定义"的映射,把能力提供者与使用者隔开。不过,它们的实现目标并不相同。

5.1 一张核心工具表 vs 定义与实例分开管理

dsh 的 ToolDefinition 自带执行逻辑,注册完成后就具备可调用条件。Spring 则区分 BeanDefinition 与 Bean 实例:定义描述 Bean 应该如何创建,实例则由容器按作用域和初始化策略进行管理。

这里不能简单理解成"Spring 一定等到 getBean() 才创建"。在常见的 ApplicationContext 启动流程中,非懒加载单例通常会被预实例化;懒加载 Bean 或其他作用域,才可能在实际获取时创建。

5.2 Agent 作用域分层 vs Spring 容器和作用域体系

dsh 的工具注册存在全局层和 Agent 作用域层。Agent 层可以看到自身作用域下的工具,并按规则与全局工具组合;同一层内的重名会被拒绝,而作用域工具可以遮蔽全局工具。

Spring 也支持父子容器以及 singleton、prototype、request、session 等作用域,但它解决的是 Bean 查找与实例生命周期问题,不能简单概括为"单层"。两者相似的是分层查找思路,具体语义并不相同。

5.3 可逆注册 vs 以容器生命周期为主

dsh 把注册视为一种可撤销的 effect。插件卸载时,对应工具能够从当前层中移除,这是热插拔设计的重要基础。

Spring 的典型使用方式则是先注册 Bean 定义,再由容器统一创建和管理 Bean。Bean 定义能否覆盖也不是固定的"重名必报错",而取决于注册表和应用的覆盖策略;在常规业务开发中,Bean 的创建和销毁仍主要跟随容器生命周期。

所以,二者不是同一套实现,却共享一个非常接近的 IoC 思路:调用方只声明自己需要什么,能力的查找、组合和生命周期管理交给容器。


一句话收尾

这张 Map,是 dsh 把"一切皆插件"落到工具系统中的关键账本:插件向里面登记能力,模型根据 schema 点名,运行时再按名字取出真实定义并执行。

它和 Spring 的 Bean 注册机制有相似的 IoC 思想,但 dsh 更强调 Agent 作用域和可逆注册;Spring 则拥有更完整的 Bean 定义、实例创建和作用域管理体系。

但 Map 只是表面,容器装配才是核心。理解这一点之后,再回头看 dsh 的工具系统,就不再是一堆零散插件,而是一条清晰的链路:注册、投影、选择、查找、执行、撤销。

而这条链路与 Spring IoC 的相似之处,最终都落在同一句话上:

对象不由自己装配,而由容器注入。


相关推荐
这就是佬们吗1 小时前
AI Agent 的四根支柱:LLM、工具、记忆与规划是如何协同的
大数据·数据库·人工智能
Black_Rock_br1 小时前
本地AI工具更新,#DSH Desktop桌面端# 正式发布迭代
人工智能·python·开源·运维开发·开源软件
Henry-SAP1 小时前
小米自研AI芯片亮相
人工智能·云原生·sap·erp
林澈在路上1 小时前
AI做歌用哪个最好 2026国产AI音乐工具评测
大数据·人工智能·aigc·音视频·音频
保持清醒的坐标系1 小时前
腾讯开源 teamai-cli:让全队 AI 工具用同一套"员工手册"
后端·github
AI服务老曹1 小时前
抽烟识别算法接入 AI 视频分析平台的流程与误报优化指南
人工智能·算法·音视频
LINgZone21 小时前
Maven多模块依赖管理与私有JAR包策略
java·maven·jar
静开1 小时前
Skills和MCP 到底什么区别
前端·人工智能
阳明山水1 小时前
销量预测2025—2026:技术演进、实证发现与系统架构的系统性综述
人工智能·深度学习·算法·机器学习·架构