ARTICLE · 1315014
插件怎样组成一个 Agent 应用?读懂 Profile 组合
Agent 循环 → Session 投影 → 工具调度 → Profile 组合
今天讲第四部分
前三篇,我们分别讨论了 Agent 循环、Session 投影和工具调用。
这些部分回答了一个 Agent 如何执行任务、如何保存状态,以及如何操作外部环境。
但还有一个启动前的问题:
这次运行,要加载哪些插件?使用哪些配置?通过浏览器、命令行,还是 SDK 提供服务?
在 DeepSeek Harness 中,这些选择由 Profile 及其配置层共同描述。启动器读取它们,组合插件树,再交给 Cordis 加载。

1. 先分清 Plugin、Bundle 和 Profile
这三个概念处于不同层次。
例如,Agent 循环、工具注册表、Session 存储分别由插件提供。基础 Bundle 将常用能力组合起来,应用 Bundle 再补充 Web 或一次性任务入口。
Profile 负责选择这次运行采用的组合。
这样,应用的组成有了可以读取、修改和检查的配置记录。
2. Web 和 headless 怎样共享能力?
项目提供了几个内置 Profile 模板:
web | dsh-basedsh-web-app |
headless | dsh-basedsh-headless |
sdk | dsh-basedsh-sdk-app |
acp | dsh-basedsh-acp-app |
sdk-minimal | dsh-sdk-minimal |
以 Web 和 headless 为例,它们都从基础包获得共享能力,再加载各自的应用层。
Web 增加浏览器应用需要的服务和界面插件。headless 则增加一次性任务运行器,直接处理任务并输出结果,不加载 Web 服务。
它们的区别会影响实际加载的服务、启动方式和生命周期,并不只是展示形式。
sdk-minimal 则说明,共享基础包并非强制要求:它由一个独立 Bundle 描述完整的最小 SDK 组合。
3. Profile 保存在哪里?
一个 Profile 位于 Harness home 下,大致包含:
profiles/web/package.jsoncordis.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: hmrdisabled: true
修改工具展示模式可以写成:
- id: toolsconfig:mode: native
补丁也可以通过 insert 插入新行。后面的补丁可以继续修改前面刚插入的行。
这使基础包能够提供默认组合,上层再进行调整。
但 ID 写错不会自动创建目标。当前实现会对未匹配的补丁发出警告并跳过,因此,补丁文件被成功读取,不代表每条补丁都已生效。
6. 最容易误解的规则:config 整体替换
不少配置系统会递归合并对象。DeepSeek Harness 的 ID 定位补丁直接替换提供的字段,其中 config 是一个整体。
假设原配置是:config:a: 1b: 2后层提供:config:b: 3补丁组合后的config是:config:b: 3
原来的 a: 1 不会被补丁机制保留下来。插件随后可能为缺失字段应用默认值,也可能因缺少必需字段而拒绝加载,这取决于插件自己的配置定义。
因此,覆盖某一行的配置时,需要写出希望保留的字段。
这一规则让结果更直接,但也意味着维护者需要了解目标插件的完整配置,而不能只关注本次修改的一个值。
7. 配置组合完成,应用就启动成功了吗?
组合得到的是插件配置树。接下来,Loader 还需要:
解析并加载插件模块。 校验配置。 等待所需服务。 执行插件初始化。 检查启动状态。
例如,一个消费者声明需要 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 应用,也为定位“代码存在,但功能没有生效”的问题提供了检查顺序。















































































