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

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

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。

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

相关推荐
ovO1 小时前
DeepSeek Harness 源码解读(七):会话日志为何是唯一真相源
开源·agent
武子康1 小时前
机器人策略 90% 与 92%:为什么两个百分点通常不足以证明更
人工智能·llm·agent
ovO1 小时前
DeepSeek Harness 源码解读(十):六条设计纪律如何约束可替换运行时
开源·agent
阿里云大数据AI技术1 小时前
PAI支持一键部署Qwen3.8-Flash-Next、GLM-5.3等最新开源模型
人工智能·开源·llm
ovO1 小时前
DeepSeek Harness 源码解读(八):文件、命令、审批与沙箱如何协作
开源·agent
阿里云云原生1 小时前
经验自进化:自动挖掘经验资产,消融实验验证真实收益丨AgentLoop 数据飞轮实践(五)
agent
ovO2 小时前
DeepSeek Harness 源码解读(六):Provider、Consumer 与能力接缝
开源·agent
李燚2 小时前
把规则搬回家:三个 BC 的贫血→充血重构实录(第103篇)
golang·agent·ddd·领域驱动设计·eino·deepflux·eino adk
2601_962304913 小时前
2026年健康科普视频怎么制作:一条开源工具链从全手动到半自动的工程复盘
开源·音视频