DeepSeek Harness 源码解读(九):它适合什么场景,应该从哪里扩展
DeepSeek Harness 最值得比较的并不是工具数量或界面功能,而是"谁拥有运行控制权":主循环是不可替换的中心,还是插件树中的一个默认实现?本文沿 AgentRegistry、agent-loop、Profile、Bundle 和公开扩展点回答两个实际问题:它适合什么项目,以及需求来了以后应该把代码放在哪里。
项目地址:https://github.com/deepseek-ai/deepseek-harness
源码基线:仓库版本 0.1.0-rc.5。重点入口是 docs/architecture.md、packages/core/agent、packages/core/agent-loop、packages/boot/app-boot、packages/bundle 与各能力 Service Definition。
一、本章要回答的问题
读完前八篇,运行时的内部机制已经比较清楚,但真正落地时仍有三类选择:一个普通模型调用是否需要完整 Harness;一个新需求应该修改循环、监听事件,还是增加 Provider;团队怎样从直接使用产品逐步走到开发插件,而不是一开始就理解全部包。
这三个问题不能靠"插件化更先进"回答。插件化会增加概念、配置和诊断成本。只有当可替换能力、长期会话、多入口或安全执行真的成为需求时,这些成本才有回报。
二、核心结论:没有特权实现,不等于没有核心协议
docs/architecture.md 对当前结构的描述很直接:模型适配器、工具注册表、会话日志和 Agent Loop 都是插件,仓库不存在只能通过修改中心模块才能扩展的"特权核心补丁"。
这里的"无特权核心"容易被误解。DeepSeek Harness 当然有核心接口、事件语义和默认实现;packages/core/agent、packages/core/session、packages/core/tools 都属于运行脊柱。真正没有的是一个不可替换、同时垄断消息、工具、状态和控制流的具体实现。
默认驱动器 packages/core/agent-loop 通过一行 effect 注册到 ctx.agents:
ts
ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()')
packages/core/agent/src/index.ts 中的 AgentRegistry.create() 和 resume() 面向 AgentFactory 编程,而不是直接构造 AgentLoop。因此,调用方依赖的是 Agent 服务接口;默认循环只是提供该工厂的插件。
这并不意味着日常扩展应该频繁替换主循环。仓库明确要求新行为优先进入已有服务和事件;只有当 Turn、Step 或持久事件语义本身改变时,才需要替换驱动器,并同步更新架构说明。无特权实现带来"可以替换"的能力,不代表"随手替换"的低成本。
三、四类架构的差别在控制权
下面的比较不是产品排名,而是四种常见工程形态。每一种都可以是正确选择。
| 形态 | 主要组合单位 | 谁控制执行 | 状态通常在哪里 | 更适合 |
|---|---|---|---|---|
| 单次 SDK 脚本 | 函数与 API 调用 | 业务代码 | 调用栈或业务数据库 | 一次问答、固定工具、短任务 |
| 图或工作流引擎 | 节点与边 | 图调度器 | 图状态或检查点 | 流程稳定、分支可枚举、需要显式编排 |
| 固定内核的 Agent 应用 | 内核与 Hook | 中心循环 | 内核拥有的消息与运行状态 | 产品边界清晰、扩展点有限但稳定 |
| DeepSeek Harness | Cordis 插件、服务与事件 | 可替换驱动器,默认是 agent-loop |
追加 Session 事件及其投影 | 多入口、长会话、能力替换和策略叠加 |
DeepSeek Harness 的优势不是"什么都能做",而是同一项能力有稳定的消费接口,Provider 可以在装配层替换,生命周期由 Cordis effect 回收,模型历史又由 Session 日志重建。它付出的代价也很明确:开发者必须理解 Context、依赖注入、事件派发模式、Profile 层叠和持久事件语义。
因此,如果项目只是把一段提示词发给固定模型并返回文本,直接使用 SDK 更短、更透明。如果业务天然是一张有限状态图,图引擎通常更直观。只有当"换模型、换执行环境、换交互入口、恢复会话、按 Agent 隔离能力"开始同时出现时,Harness 的组合方式才真正减少长期成本。
四、运行时不是写死的:Profile、Bundle 与 Patch
DeepSeek Harness 的替换能力先发生在启动阶段。packages/boot/app-boot/src/profile.ts 负责读取 Profile,Bundle 提供可分发的 patch,用户配置和命令行 --patch 再覆盖前面的插件行。packages/bundle/base/cordis.patch.yml 列出的不是一组静态 import,而是当前产品要装载的服务、Provider、工具和策略。
装配顺序可以简化为:
text
Bundle 层 → Profile 自身 patch → 用户主目录 patch → 命令行 --patch
后层可以按插件行的 id 替换前层配置,也可以插入新插件。要查看某台机器最终会启动什么,权威入口不是猜包依赖,而是展开有效配置:
sh
dsh --profile web --dump-config
这条命令非常重要。源码目录告诉你"仓库拥有什么",有效配置才告诉你"这次运行装了什么"。同一个仓库可以由 web Profile 形成浏览器产品,也可以由 headless Profile 形成一次性执行入口;差异主要来自装配,而不是复制一套 Agent 核心。
五、需求来了,先判断属于哪种变化

这张图从左向右读:先判断需求改变的是运行组合、可替换能力、过程策略、单 Agent 范围,还是驱动语义,再选择对应入口。所有入口最后仍汇入同一棵 Cordis 插件树,因此都拥有一致的依赖等待和卸载语义。
5.1 只改变"装什么":使用 Profile 或 Patch
替换模型 Provider、关闭某个工具、为 Web 增加一个可选插件,通常不需要改源码。先对目标插件行做 patch,验证组合后再决定是否发布为 Bundle。配置是产品装配,不应被硬编码进 Agent Loop。
5.2 增加可替换能力:完成三个角色
一个完整能力由 Service Definition、Service Provider 和 Consumer 构成。例如 Shell 的接口在 packages/shell/shell,本地实现位于 packages/shell/bash-local,模型工具位于 packages/shell/tool-bash。Consumer 依赖 ctx.shell,而不是本地执行器类,所以执行世界可以被其他 Provider 替换。
如果新增能力只有工具而没有稳定服务接口,它更像一个单用途工具;如果只有抽象接口而没有 Provider 和 Consumer,则还不能证明接口足以支撑真实调用。三种角色同时存在,才形成可替换能力。
5.3 观察或改变一次运行:选择正确事件域
需要持久化的事实进入 Session 事件;只在当前运行中观察或拦截的行为进入 agent/*、tools/*、fs/* 等事件;纯粹提供操作的能力进入服务。三者不要混用:把持久事实只放在内存 listener 中,恢复后会消失;把临时控制状态写进日志,又会污染可重放历史。
5.4 只影响某个 Agent:使用作用域 Context 或 Preset
agent.ctx 是该 Agent 的作用域 Context。把工具、策略或服务注册到这里,影响范围会随 Agent 生命周期收缩。packages/preset/agent-presets 则把一组插件配置挂到一个持久 preset,再让目标 Agent 的 scope 加入该组合。它适合"同一进程中不同 Agent 使用不同工具集",而不是复制整个运行时。
5.5 真的要改循环:实现 AgentFactory
只有需求改变 Turn/Step 驱动方式时,才考虑替换 agent-loop。新实现需要满足 packages/core/agent 声明的 Agent 和 Factory 语义,并继续产出其他插件依赖的会话事件与 live 事件。能注册成功只是第一步;能让日志重建、工具执行、取消和恢复仍然成立,才是可用的替代驱动器。
六、扩展点速查:想做什么,代码放哪里
| 目标 | 首选入口 | 当前源码位置 |
|---|---|---|
| 增加模型 Provider | ctx.llm.registerAdapter() |
packages/llm/llm/src/index.ts |
| 增加模型可见工具 | ctx.tools.register() |
packages/core/tools/src/index.ts |
| 增加 Shell 后端 | 实现并注册 ctx.shell |
packages/shell/shell/src/index.ts |
| 增加持久终端 | 提供 ctx.terminals 后端并装载工具 Consumer |
packages/terminal/terminal/src/index.ts |
| 增加人类斜杠命令 | ctx.commands.register() |
packages/interaction/commands/src/index.ts |
| 增加后台任务 | 提供 ctx.jobs 并复用 job_* 工具 |
packages/jobs/jobs/src/index.ts |
| 拦截请求、工具或回合 | 对应 agent/*、tools/* 事件 |
docs/architecture.md 的事件与扩展表 |
| 给下一次模型请求补上下文 | agent.inject() |
packages/core/agent-loop/src/agent.ts |
| 增加可恢复会话事实 | 扩展 SessionEventMap 并实现投影 |
packages/core/session/src/types.ts |
| 给单个 Agent 一套能力 | agent.ctx 或 Agent Preset |
packages/preset/agent-presets/src/index.ts |
这张表最重要的不是 API 名称,而是职责方向:模型 Provider 不应该顺手管理会话;工具不应该绕过能力服务直接创建本地进程;临时事件监听器不应该成为持久状态的唯一拥有者。
七、三条上手路线
7.1 先作为产品使用
只想体验完整产品,可以直接启动发布版本:
sh
npx @deepseek-ai/dsh web
它会启动 Web UI。这个阶段重点观察会话、工具、权限和模型设置怎样协作,不必先读全部源码。
7.2 再作为装配系统修改
从源码运行后,先展开 Profile,再用一个临时 patch 增加或替换插件:
sh
pnpm dsh --profile web --dump-config
pnpm dsh web --patch ./scratch-plugin/cordis.yml
这样能把"插件代码有问题"和"插件根本没有被装载"分开。先确认有效配置,再跟踪服务与事件,比从入口文件盲目跳转更快。
7.3 最后才写插件
最小插件只需要 apply(ctx),需要其他服务时声明 inject。例如一个工具插件的骨架是:
ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'Name to greet.' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
docs/user/develop/basic/ 已经提供从第一个插件到工具和配置的连续教程。实际开发时,优先复制教程中的最小结构,再替换业务逻辑;不要从大型内置插件反向裁剪。
八、适用与不适用场景
DeepSeek Harness 更适合以下项目:
- 同一 Agent 核心需要服务 Web、Headless、ACP 或其他入口;
- 会话必须恢复、分叉、审计,模型请求也必须可重建;
- 本地与远端执行、不同模型 Provider 或权限策略需要按部署替换;
- 多个团队分别维护工具、策略、界面和基础能力,需要清晰的生命周期所有权;
- 产品会持续增加插件,不能让每个扩展都修改主循环。
以下场景通常没有必要承担这套复杂度:
- 一次模型请求加少量纯函数工具;
- 控制流稳定且天然适合显式工作流图;
- 不需要恢复会话,也没有多入口或能力替换要求;
- 团队暂时无法承担插件协议、配置层叠和运行时诊断的维护成本。
选择框架时,不要只比较"有没有某个工具"。更应该问:状态由谁拥有,执行由谁调度,替换一个 Provider 会影响多少消费者,卸载插件能否回收注册,进程重启后模型看到的历史能否从持久事实重建。
九、设计亮点、约束与代价
"连主循环也是插件"让 DeepSeek Harness 的扩展上限很高,但真正让这句话成立的是下面的约束:服务接口稳定,注册可撤销,事件模式明确,模型可见内容进入日志,配置错误尽早失败。缺少这些约束,无特权只会变成没有负责人。
它的主要代价是学习曲线和组合诊断。一个行为可能由 Profile、作用域、服务 Provider、事件 listener 和持久投影共同决定。仓库用 --dump-config、生成的事件目录、能力图和 package-owned invariant 降低这项成本,但无法完全消除它。
还要注意当前版本处于开发者预览阶段。README 明确提示未来会有破坏兼容性的变更。适合在真实项目中评估和扩展,不等于已经承诺稳定的插件 ABI 或磁盘格式。
十、小结与下一篇衔接
DeepSeek Harness 与其他形态的核心差别,不是工具更多,而是运行控制权被拆到可装配的插件、服务和事件中。日常需求应优先落到 Profile、Provider、Consumer、事件或 Agent scope;只有驱动语义改变时才替换 AgentFactory。
下一篇将收束整个系列:不再继续罗列包,而是提炼六条真正约束实现的设计纪律,解释为什么这些硬约束反而让插件拥有更大的自由。