DeepSeek Harness 源码解读(九):它适合什么场景,应该从哪里扩展

0 阅读11分钟

DeepSeek Harness 源码解读(九):它适合什么场景,应该从哪里扩展

DeepSeek Harness 最值得比较的并不是工具数量或界面功能,而是“谁拥有运行控制权”:主循环是不可替换的中心,还是插件树中的一个默认实现?本文沿 AgentRegistryagent-loop、Profile、Bundle 和公开扩展点回答两个实际问题:它适合什么项目,以及需求来了以后应该把代码放在哪里。

项目地址:https://github.com/deepseek-ai/deepseek-harness

源码基线:仓库版本 0.1.0-rc.5。重点入口是 docs/architecture.mdpackages/core/agentpackages/core/agent-looppackages/boot/app-bootpackages/bundle 与各能力 Service Definition。

一、本章要回答的问题

读完前八篇,运行时的内部机制已经比较清楚,但真正落地时仍有三类选择:一个普通模型调用是否需要完整 Harness;一个新需求应该修改循环、监听事件,还是增加 Provider;团队怎样从直接使用产品逐步走到开发插件,而不是一开始就理解全部包。

这三个问题不能靠“插件化更先进”回答。插件化会增加概念、配置和诊断成本。只有当可替换能力、长期会话、多入口或安全执行真的成为需求时,这些成本才有回报。

二、核心结论:没有特权实现,不等于没有核心协议

docs/architecture.md 对当前结构的描述很直接:模型适配器、工具注册表、会话日志和 Agent Loop 都是插件,仓库不存在只能通过修改中心模块才能扩展的“特权核心补丁”。

这里的“无特权核心”容易被误解。DeepSeek Harness 当然有核心接口、事件语义和默认实现;packages/core/agentpackages/core/sessionpackages/core/tools 都属于运行脊柱。真正没有的是一个不可替换、同时垄断消息、工具、状态和控制流的具体实现。

默认驱动器 packages/core/agent-loop 通过一行 effect 注册到 ctx.agents

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 HarnessCordis 插件、服务与事件可替换驱动器,默认是 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、工具和策略。

装配顺序可以简化为:

Bundle 层 → Profile 自身 patch → 用户主目录 patch → 命令行 --patch

后层可以按插件行的 id 替换前层配置,也可以插入新插件。要查看某台机器最终会启动什么,权威入口不是猜包依赖,而是展开有效配置:

dsh --profile web --dump-config

这条命令非常重要。源码目录告诉你“仓库拥有什么”,有效配置才告诉你“这次运行装了什么”。同一个仓库可以由 web Profile 形成浏览器产品,也可以由 headless Profile 形成一次性执行入口;差异主要来自装配,而不是复制一套 Agent 核心。

五、需求来了,先判断属于哪种变化

DeepSeek Harness 行为扩展的入口选择

这张图从左向右读:先判断需求改变的是运行组合、可替换能力、过程策略、单 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 事件。能注册成功只是第一步;能让日志重建、工具执行、取消和恢复仍然成立,才是可用的替代驱动器。

六、扩展点速查:想做什么,代码放哪里

目标首选入口当前源码位置
增加模型 Providerctx.llm.registerAdapter()packages/llm/llm/src/index.ts
增加模型可见工具ctx.tools.register()packages/core/tools/src/index.ts
增加 Shell 后端实现并注册 ctx.shellpackages/shell/shell/src/index.ts
增加持久终端提供 ctx.terminals 后端并装载工具 Consumerpackages/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 Presetpackages/preset/agent-presets/src/index.ts

这张表最重要的不是 API 名称,而是职责方向:模型 Provider 不应该顺手管理会话;工具不应该绕过能力服务直接创建本地进程;临时事件监听器不应该成为持久状态的唯一拥有者。

七、三条上手路线

7.1 先作为产品使用

只想体验完整产品,可以直接启动发布版本:

npx @deepseek-ai/dsh web

它会启动 Web UI。这个阶段重点观察会话、工具、权限和模型设置怎样协作,不必先读全部源码。

7.2 再作为装配系统修改

从源码运行后,先展开 Profile,再用一个临时 patch 增加或替换插件:

pnpm dsh --profile web --dump-config
pnpm dsh web --patch ./scratch-plugin/cordis.yml

这样能把“插件代码有问题”和“插件根本没有被装载”分开。先确认有效配置,再跟踪服务与事件,比从入口文件盲目跳转更快。

7.3 最后才写插件

最小插件只需要 apply(ctx),需要其他服务时声明 inject。例如一个工具插件的骨架是:

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。

下一篇将收束整个系列:不再继续罗列包,而是提炼六条真正约束实现的设计纪律,解释为什么这些硬约束反而让插件拥有更大的自由。