夜雨聆风学习资料网

ARTICLE · 1315014

插件怎样组成一个 Agent 应用?读懂 Profile 组合

插件怎样组成一个 Agent 应用?读懂 Profile 组合

Agent 循环 → Session 投影 → 工具调度 → Profile 组合

今天讲第四部分

前三篇,我们分别讨论了 Agent 循环、Session 投影和工具调用。

这些部分回答了一个 Agent 如何执行任务、如何保存状态,以及如何操作外部环境。

但还有一个启动前的问题:

这次运行,要加载哪些插件?使用哪些配置?通过浏览器、命令行,还是 SDK 提供服务?

在 DeepSeek Harness 中,这些选择由 Profile 及其配置层共同描述。启动器读取它们,组合插件树,再交给 Cordis 加载。


1. 先分清 Plugin、Bundle 和 Profile

这三个概念处于不同层次。

概念
主要职责
Plugin
提供具体服务、事件监听或功能
Bundle
将可复用的插件配置和相关代码作为包分发
Profile
选择有序的 Bundle,并保存自身配置补丁及外部插件依赖

例如,Agent 循环、工具注册表、Session 存储分别由插件提供。基础 Bundle 将常用能力组合起来,应用 Bundle 再补充 Web 或一次性任务入口。

Profile 负责选择这次运行采用的组合。

这样,应用的组成有了可以读取、修改和检查的配置记录。


2. Web 和 headless 怎样共享能力?

项目提供了几个内置 Profile 模板:

Profile
默认 Bundle 组合
webdsh-base
 + dsh-web-app
headlessdsh-base
 + dsh-headless
sdkdsh-base
 + dsh-sdk-app
acpdsh-base
+ dsh-acp-app
sdk-minimal
独立的 dsh-sdk-minimal

以 Web 和 headless 为例,它们都从基础包获得共享能力,再加载各自的应用层。

Web 增加浏览器应用需要的服务和界面插件。headless 则增加一次性任务运行器,直接处理任务并输出结果,不加载 Web 服务。

它们的区别会影响实际加载的服务、启动方式和生命周期,并不只是展示形式。

sdk-minimal 则说明,共享基础包并非强制要求:它由一个独立 Bundle 描述完整的最小 SDK 组合。

3. Profile 保存在哪里?

一个 Profile 位于 Harness home 下,大致包含:

profiles/  web/    package.json    cordis.patch.yml

其中,package.json 中的 dsh.profile.bundles 保存有序 Bundle 列表,也可以记录外部插件依赖。

cordis.patch.yml 保存这个 Profile 自己的补丁。

Bundle 则在自己的包声明中,通过 dsh.bundle.patch 指向所携带的补丁文件。

这让“使用哪些包”和“怎样配置这些包”分别有明确的记录位置。


4. 配置如何一层一层组合?

启动时,系统从空的插件列表开始,依次应用配置层:

Bundle 1  → Bundle 2  → 其余 Bundle,按声明顺序  → Profile 自身补丁  → Harness home 全局补丁  → 启动时指定的补丁

例如,常见 Web 组合可以理解为:

基础包  → Web 应用包  → web Profile 的个人配置  → 全局配置  → 本次启动覆盖

后层可以修改前层已经插入的插件行,但前提是补丁能够匹配目标。

全局补丁的位置也值得留意:它在 Profile 自身补丁之后。因此,排查配置时,只检查 Profile 文件可能还不够。

启动器还可能根据特定开关追加专门的策略补丁,具体优先级需要看对应启动逻辑。


5. 补丁怎样找到要修改的插件?

插件配置行可以声明稳定的 id。后续补丁通过这个 ID 定位目标。

例如,禁用热重载插件可以写成:

- id: hmr  disabled: true

修改工具展示模式可以写成:

- id: tools  config:    mode: native

补丁也可以通过 insert 插入新行。后面的补丁可以继续修改前面刚插入的行。

这使基础包能够提供默认组合,上层再进行调整。

但 ID 写错不会自动创建目标。当前实现会对未匹配的补丁发出警告并跳过,因此,补丁文件被成功读取,不代表每条补丁都已生效。


6. 最容易误解的规则:config 整体替换

不少配置系统会递归合并对象。DeepSeek Harness 的 ID 定位补丁直接替换提供的字段,其中 config 是一个整体。

假设原配置是:config:  a: 1  b: 2后层提供:config:  b: 3补丁组合后的 config 是:config:  b: 3

原来的 a: 1 不会被补丁机制保留下来。插件随后可能为缺失字段应用默认值,也可能因缺少必需字段而拒绝加载,这取决于插件自己的配置定义。

因此,覆盖某一行的配置时,需要写出希望保留的字段。

这一规则让结果更直接,但也意味着维护者需要了解目标插件的完整配置,而不能只关注本次修改的一个值。


7. 配置组合完成,应用就启动成功了吗?

组合得到的是插件配置树。接下来,Loader 还需要:

  1. 解析并加载插件模块。
  2. 校验配置。
  3. 等待所需服务。
  4. 执行插件初始化。
  5. 检查启动状态。

例如,一个消费者声明需要 sessionProjections,就必须存在能够提供该服务的插件。仅仅在列表中写上消费者,并不能补齐依赖。

项目还区分关键启动项与可选项:

  • 已启用的关键项无法激活时,启动会失败并清理应用。
  • 只有可选项失败时,可以保留成功启动的插件,同时报告诊断信息。
  • 缺失或明确禁用的关键项,不会被当作一个已启用却启动失败的实例。

因此,分析启动问题时,需要分开看:最终配置是什么,以及这些配置对应的插件是否真正激活。


8. 为什么热重载需要资源清理?

某些长时间运行的组合支持配置热重载。

配置变化可能导致插件重启或卸载。如果旧插件留下监听器、注册项或后台任务,新实例启动后就可能与旧实例同时工作。

DeepSeek Harness 要求注册通过 ctx.effect()、ctx.on() 等机制绑定到插件生命周期。插件卸载时,相关效果才能撤销。

这把 Profile 组合与前三篇的生命周期管理连接起来:应用可以重新组合,但旧工作也需要正确结束。

支持热重载也不意味着所有更新都具有全局事务回滚能力。后续配置更新与首次启动的检查方式存在区别,不能简单地假设一次更新失败后,所有插件都会恢复到原状。


9. 怎样知道最终加载了什么?

当一个值可能来自多个配置层时,查看组合结果比只看某个文件更直接。

在源码仓库中,可以使用:

pnpm dsh --profile web --dump-config

它输出组合后的插件树。配置导出与启动复用补丁组合逻辑,有助于避免两套规则产生偏差。

排查时,可以依次确认:

  • 目标插件行是否存在?
  • ID 是否匹配?
  • 是否被后层禁用?
  • config
     是否被整体替换?
  • 所需服务是否有提供者?

配置导出解释的是“准备加载什么”。模块能否导入、服务能否就绪、端口能否绑定,还需要实际启动诊断。


10. 把四篇内容连起来

现在,这个系列可以从启动到执行连成一条路径:

Profile 选择 Bundle 与配置层  → Loader 激活插件树  → Agent 循环推进任务  → 工具运行时检查并执行调用  → Session 保存事件  → 投影生成可读取的状态

新增能力时,通常需要同时考虑两个位置:

  • 功能由哪个插件实现。
  • 哪个 Bundle 或 Profile 负责加载它。

前者决定能力本身怎样工作,后者决定它在哪些应用中可用。

阅读 Profile 相关源码时,可以从下面这条路径入手:

读取 Profile 声明  → 解析 Bundle  → 读取各层补丁  → composeEntries()  → Loader 加载与启动检查

这条路径解释了插件代码怎样变成一个实际运行的 Agent 应用,也为定位“代码存在,但功能没有生效”的问题提供了检查顺序。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料