位置:首页 > 新手教程 > DeepSeek Harness源码解析:插件化Agent运行时架构搭建原理

DeepSeek Harness源码解析:插件化Agent运行时架构搭建原理

时间:2026-08-20  |  作者:多维游侠  |  阅读:0

本文基于对 deepseek-ai/deepseek-harness 仓库(开发者预览版,MIT 协议)源码与架构文档的逐层阅读写成,从架构与设计两条线拆解这个框架:它为什么长这样、每一层在源码里对应什么、以及这些设计决策背后的取舍。

DeepSeek Harness源码深度解析:一切皆插件的Agent运行时是如何搭建的

一、先弄清定位:Harness 不是模型,是模型的"执行躯壳"

DeepSeek Harness(命令行名 dsh)是 DeepSeek 于 2026 年 8 月开源的 agent harness(智能体执行框架)。官方用一个公式概括它的定位:

Model + Harness = Agent

模型负责"想"。它承担推理、规划、生成。

Harness 负责除此之外的一切。包括读写文件、执行命令、调用工具、管理上下文、审批权限、错误重试、结果闭环。

README 对自身的定义只有一句话:“一切皆插件”(Everything is a Plugin)。整个系统由 Cordis 插件框架驱动,其设计理念来自论文 A Programming Paradigm for Spatiotemporal Composability[3][4]。

这个定位直接决定了它的架构形态。既然 Harness 是"模型之外的所有工程",而这部分又极其发散,那么唯一可持续的组织方式,就是把发散全部推给插件,让核心收敛到最小。

先看一组仓库的量化印象(基于 master 分支快照统计):

指标规模
workspace 包数量约 219 个(packages/ 下 40+ 个功能组)
TypeScript 源码约 2100 个文件、46 万行
测试文件约 650 个 spec/test 文件
构建/包管理pnpm workspace + tsdown,Node ≥ 22.19
vendor 的框架层Cordis 全家桶以源码方式内嵌,重命名至 @deepseek-ai scope

46 万行代码里没有一行是"特权内核"。 这是理解全文的钥匙。

二、全局架构:一棵由配置叠加出来的插件树

2.1 运行时 = 插件树

一个运行中的 dsh 进程,本质上是一棵 Cordis 插件树。

这棵树的形状不是硬编码的,而是由若干配置层(layer)按序叠加出来的:

┌─────────────────────────────────────────────┐
│ --patch 命令行 overlay(最上层,最后应用) │
├─────────────────────────────────────────────┤
│ Harness home 级 cordis.patch.yml │
├─────────────────────────────────────────────┤
│ profile 自己的 cordis.patch.yml │
├─────────────────────────────────────────────┤
│ profile 声明的 bundles(按序) │
│ └─ dsh-base(每个 profile 的第一层) │
│ └─ dsh-web-app / dsh-headless … │
└─────────────────────────────────────────────┘

这里有两个核心概念。源码里都落实为 package.json 上的一个 dsh 字段:

  • Profile(档案):存放在 Harness home($DSH_HOME 或 ~/.dsh)下的一个具名目录。它声明自己叠放哪些 bundle,保存用户安装的树外插件,以及用户自己的 cordis.patch.yml。发行版自带 web 和 headless 两个模板。
  • Bundle(组合包):一个 npm 包,通过 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } 声明自己携带一份 patch 文件。它是"Cordis 配置项 + 挂载代码"的分发格式。

patch 的语义非常克制:按 id 定位某行,整体替换其 config,或插入新行。

它不合并,也不寻址深层字段。这个"整条替换"的约束是刻意的。它保证任何一行配置最多只被"一个 bundle 层 + 用户层"写过,排查配置来源时不会出现三层叠加的鬼故事。

想看自己机器实际启动的树,可以直接执行:

dsh --profile web --dump-config

它打印出的每一个条目,都可以被你写的 patch 替换。包括 agent loop 本身。

2.2 组装机制的源码落点

这套组装逻辑集中在 packages/boot/app-boot(@deepseek-ai/dsh-app-boot)。

它是所有应用入口共享的启动胶水,包括 dsh CLI、ACP demo 等。核心导出包括:

  • boot():创建 root context → 安装 Loader → 挂载 include 树 → 断言所有条目加载并激活成功;
  • composeEntries():用 include 自己的 applyEntryPatches 算法在空条目列表上叠加各 patch 层。保证"dump 出来的配置"和"实际启动的配置"永不分叉;
  • installFailLoud():把启动期或运行期的未处理拒绝统一变成一行带标签的 stderr + exit(1),并给持有终端的插件留出清理窗口,绝不在半成品状态上吊死。

每个profile的基础,正是dsh-base bundle的patch文件(packages/bundle/base/cordis.patch.yml)。

像模型适配器、工具注册表、持久化、沙箱与审批策略、设置、凭据、遥测等,都是以 - insert: 的形式,在此一次性插入。

其注释中明确阐述了一条重要原则:“行顺序不带有任何加载语义”。

激活顺序完全取决于服务的可用性,具体依赖的是下文的 inject。

2.3 Agent preset:给单个会话换能力

profile/bundle 解决的是"进程级"组装。

DSH 还支持会话级组装:agent preset。

apps/cli/config/agent-presets/ 下自带四个 preset:standard、code、minimal、cordis。

其中 standard 是完整编码 agent。每个 preset 本质上都是一份 agent.cordis.yml 组合文件。

preset 文件开头注释写透了一个关键机制:isolate realm(隔离域)。

preset 里的服务行必须放在带 isolate realm 的 group 里。没有它,服务会发布到 root realm 成为进程全局——另一个 preset 发布同名服务就会冲突,host 侧的读取方还会把某个 preset 的实例错配给所有会话。

也就是说,同名服务在不同 realm 下可以有各自的实例。

会话通过 scope 父链join到对应 preset 的 realm。于是,"这个会话用全套工具、那个会话用极简工具"就成了纯配置问题。

这是 Cordis"时空可组合性"论文思想在产品里的直接投影。

三、基石:Cordis 插件框架

Cordis 是 DSH 以 vendor 方式内嵌的底层框架。上游是 cordiverse/cordis,4.0.0-rc.7,本地有硬化修改。

整个 DSH 的插件哲学,可以压缩成五个核心概念。

3.1 五个核心概念

1. 插件是实现 Service 的对象。

最简形态就是一个带 inject 和 apply(ctx) 的对象:

export const name = 'my-plugin'
export const inject = ['tools']          // 声明依赖:等 tools 就绪再启动
export function apply(ctx) {
  ctx.tools.register(defineTool({        // 向共享上下文贡献一个工具
    name: 'greet',
    // ...
    async execute(args) { return 'Hello, ' + args.name + '!' },
  }))
}

2. 上下文(Context)是服务的容器。

一个服务占据一个稳定的 ctx.,如 ctx.tools、ctx.llm、ctx.sessions、ctx.systemPrompt。

其他插件通过 key 查找服务,而非 import 具体实现。这一条是"可替换性"的根源。

3. 用 inject 声明依赖,让加载顺序自己涌现。

插件声明需要什么服务,就等它就绪后再启动。整个系统没有任何手写的"启动编排脚本"。219 个包的加载顺序来自依赖关系图的拓扑序,而不是人肉维护的清单。

4. 类型化事件是插件间通信的主干道。

事件名通过 TypeScript 声明合并(declaration merging)注册。分发有且仅有四种模式,而且每种模式本身就是事件公开契约的一部分:

模式是否 await语义典型用途
emit否监听器按序观察,无返回值状态广播(agent/status)
waterfall否环绕中间件,可改写/短路拦截(agent/pre-step、tools/pre-execute)
parallel是并行扇出多方消费同一事实
serial是按序执行,有返回值终末检查点(agent/turn-stopping)

5. 注册是可逆的副作用。

工具、提示词片段、适配器、监听器,全部通过 ctx.effect() / ctx.on() 安装。

插件卸载时自动撤销。这就是 README 那句"不存在需要打补丁的特权内核"的技术含义。

扩展 dsh 的方式,是把插件挂到其他插件旁边,而不是改别人的代码。

热重载(vendor 了 cordis-plugin-hmr)和用户插件的装卸,也都建立在这种可逆性之上。

3.2 Waterfall:贯穿全系统的拦截原语

四种模式里最重要的是 waterfall。它是 DSH 所有"策略注入点"的统一形态。

语义类似 Koa/洋葱模型:监听器收到 (...args, next),调用 next() 委托给下游并把下游结果包一层返回;不调 next() 直接返回则短路。

架构文档里有一条明确的设计约定:对于单决策事件,短路是设计意图。

拥有决策权的策略监听器,比如审批插件,可以直接返回结果;而只做标注或观察的监听器必须委托。

这条约定让"谁有最终决定权"在协议层面始终清晰。

3.3 Vendor 策略:把框架层变成自己拥有的代码

值得单独说的是 vendor 目录的做法。

DSH 没有通过 npm 依赖 Cordis,而是把 Cordis 及其基础库以源码形式拷入 monorepo,并重命名到 @deepseek-ai scope。

vendor/README.md 给出的理由很直接:harness 要完全拥有自己的框架层。这样才能做到可审计、可打补丁、可锁定版本。

而且这份拷贝不是死快照。

本地修改日志里记录了实质性的硬化,例如 cordis/src/fiber.ts 的生命周期加固:关闭了三个重入式销毁缺口——effect 的 owner 包装在 setup 体运行前注册、卸载开始后拒绝新建 effect、子 fiber 在 internal/plugin 发布前先拿到父级 disposer。

这类修改说明 DeepSeek 团队是在逐行审计并接管框架的并发与销毁语义,而不是简单地"用"一个第三方框架。

四、核心主干:事件溯源的会话 + 可替换的 Agent Loop

packages/core/ 是产品 API 的脊柱。一个轮次恰好流经它的六个包:

agent-loop ──驱动── session(仅追加日志,唯一真源)
│ ▲
├─ system-prompt(组装提示词 + 工具 schema)
├─ llm(流式获取模型响应)
└─ tools(分发工具调用,结果回写日志)

4.1 Session:一切皆为仅追加事件

packages/core/session 实现了一个事件溯源(Event Sourcing)模型。

Session 是一份由类型化 SessionEvent 组成的仅追加日志,是 agent 完整交互历史的唯一真源。

模型看到的历史从不单独存储,而是由 deriveMessages() 从日志投影出来。

回放、fork、恢复、transcript、遥测、持久化,全部派生自同一条事件流。

事件词汇(SessionEventMap)的核心成员如下:

事件含义
turn/start / turn/end轮次边界,turn/end 携带结构化的 TurnEndReason(completed / max-tokens / aborted / error / blocked)
step/start / step/end步骤边界:一次模型请求 + 它引发的工具执行
user/message进入模型的用户消息(人类输入、inject() 注入的上下文、目标续跑,靠 source 区分)
assistant/chunk原始流式 chunk——token 级回放保真度
assistant/message组装完成的助手消息,带 usage 和 sourceEventSeqs 精确指回它的 chunks
tool/call / tool/result工具调用与结果(结果事件用 sourceEventSeqs 指回调用事件)
request/header / request/context请求配置与上下文的锚点(initial / resume / change)

SessionEventMap 可以通过声明合并扩展。compaction 插件就合并进了 compaction/start|summary|end 三个事件类型。

这意味着事件词汇本身也是插件化的。

这套设计里有一条被运行时不变量强制执行的铁律:

模型可见即已记录。

抵达模型请求的一切,都必须能从日志重建,并由一项运行时不变量断言这一点。

packages/core/session/src/invariant.ts 与各包同名的 invariant 文件,配合 ctx.invariants 注册表,把这类架构级承诺变成了运行时会真实抛错的断言,而不是文档里的口号。

新增一种模型可见输入,正确姿势只有一种:

  • 扩展 SessionEventMap
  • 并从日志渲染

任何"绕过日志直接塞给模型"的写法,都会被不变量当场抓获。

另外还有一个细节设计:surface(表面)与日志的分离。

产生消息的事件带 surfaceOp(append / replace)。compaction 摘要落地为一条 surfaceOp: { op: 'replace', start, end } 的 user/message,而 compaction/* 事件本身只进日志不进 surface。

原始历史永久保留在日志里,可审计、可回放。

模型看到的 surface 则是一个可替换的投影。上下文压缩因此不需要"删除"任何历史。

4.2 Agent Loop:一个三态机驱动的轮次/步骤循环

默认的循环实现是 packages/core/agent-loop 的 ReactLoopAgent(packages/core/agent-loop/src/agent.ts)。

这个文件全文不到 500 行,是整个系统最值得精读的single文件。

对外接口(Agent,定义在 packages/core/agent)极简:

  • followup(input):排队到下一轮次并唤醒驱动器。正常用户输入走这里;
  • steer(input):插队到下一步骤并唤醒。中途引导走这里;
  • inject(input):进入下一步骤的收件箱但不唤醒。插件注入上下文走这里;
  • cancel(cause) / whenIdle() / runMaintenance(job)。

三种输入路由区分得如此细,是因为它们对应三种不同的实时性语义:

  • 追问要打断当前节奏
  • steering 要在下一步生效
  • 注入只需最终一致性

内部状态机只有三个相位(Phase 类型):

type Phase =
  | { kind: 'idle'; lastTurn: number }
  | { kind: 'maintenance'; abort: AbortController; ... }   // 维护作业(如压缩)独占期
  | { kind: 'running'; abort: AbortController; turn: number; step: number; ... }

wakeDriver() 是唤醒的唯一入口。

其中的 latch 逻辑处理了一个棘手并发问题:唤醒信号到达时,驱动器正在维护期或已中止怎么办?

答案是锁存 wakeRequested,等驱动器收敛回 idle 时重放。

而被 dispose 的驱动器不锁存,因为拆除永远不该等待一个模型轮次。

轮次主循环(turn() 方法)的骨架,与官方时序图一一对应:

turn/start 落盘
  loop:
    认领 inbox(首轮认 next-turn,之后认 next-step)
    ctx.systemPrompt.assemble() 组装提示词
    agent/pre-step waterfall ── 插件可改写消息或直接拒绝
      拒绝 → 轮次以 blocked 关闭(不消耗模型调用,但日志留痕)
    step/start 落盘,user/message 逐条落盘
    step():
      agent/request waterfall ── 插件可改写请求配置(provider/model/effort…)
      ctx.llm.prepareCall() 解析适配器与精确模型默认值
      request/header 落盘(变更时)
      llm/stream → assistant/chunk* 逐块落盘 → assistant/message 落盘
      失败 → agent/request-error waterfall 决定重试还是保留原错误
      有工具调用 → executeToolCalls()(见 4.3)
    step/end 落盘
    自然停止且 inbox 空 → agent/turn-stopping serial 终末检查点(插件可续命)
turn/end 落盘(finally 中,携带结构化结束原因)

几个值得圈点的源码细节:

  1. max-tokens 是粘性的:一旦某一步撞上输出上限,后续正常完成的步骤不得把轮次结果"降级"回 completed。轮次结局是对用户体验的诚实记录。
  2. 每个失败都是结构化的:LlmError 保留全部事实,其他异常统一压成 errorChain 文本 + UNKNOWN 码。turn/end 的 reason 因此永远可机器消费。
  3. 被拒绝/被清空的输入也留痕:首次认领被拒或被改写为空,轮次照样以"零步骤"持久化关闭。日志记录这次尝试,审计无盲区。
  4. 错误恢复是瀑布式的:agent/request-error 让插件链决定重试策略;上下文溢出时 compaction 插件在此接管。

4.3 工具调度器:屏障 + 有界滚动池

tool-calls.ts(约 290 行)实现了一步之内多个工具调用的调度。

它的设计目标写在文件头注释里:分派可以重叠,但策略、结果和结果上下文必须保持模型顺序(model-ordered)。

调度规则包括:

  • 每个调用按 ctx.tools.executionMode() 实时分类为 exclusive 或 parallel;
  • exclusive 调用构成屏障(barrier),必须独占执行,前后排空;
  • parallel 调用进入有界滚动池,上限 maxParallelToolCalls;
  • 启动前会重新分类,因为注册表可能在运行中被 插件改动;
  • 结果提交用 commitReady() 推进一个只跨越连续已就绪槽位的指针,严格按模型顺序落盘 tool/result;
  • abort 语义完整覆盖:停止补货、排空已启动调用、为未启动的调用补记合成错误结果(TOOL_ABORTED_BEFORE_DISPATCH),保证回放永远有效;
  • 而调度器自身故障则排空已分派调用、抛出首个错误,绝不伪造工具结果。

这段代码是"并发性能"与"可回放性"正面相撞的地方。

DSH 的选择很清楚:执行可以并行抢时间,但落到唯一真源日志里的顺序,必须是模型当初发出的顺序。

4.4 工具执行管线:五段式受保护流水线

单个工具调用穿过 packages/core/tools 的流水线时,要过五道关卡。对应 docs/tool-execution-pipeline.md 的流程图:

tools/pre-execute (waterfall) ← 钩子、权限、沙箱策略
↓ allow / ask→审批 / deny
单调守卫(monotonic guards) ← 只能 deny 或弃权,不能翻案;身份受保护
↓
tools/execute (waterfall) ← 环绕分派:超时、重试、指标
↓
工具 execute() 本体 ← fs/* 事件闸门(先读后编辑)、工具自有日志事件
↓
tools/post-execute (waterfall) ← 接受、阻断、替换、追加上下文
↓
finalizeContent + tools/result ← 内容不变式最终检查 → 冻结的权威结果

其中两处设计尤其值得注意:

  • 审批是 fail-closed 的。ctx.approval(packages/interaction/user-approval)的结果集是封闭的 'allowed-once' | 'rejected' | 'cancelled' | 'una vailable'。除非明确拿到一次性许可,其余一律拒绝。应答者缺失、不负责、抛异常,全部归入 una vailable,而 una vailable 也是拒绝。会话级策略只有 ask / never 两档,never 确定性拒绝且不分发。
  • 结果要经过无损快照。注册表会对候选结果做 JSON 无损快照。快照失败先规范化为 isError,再由冻结的 finalizeContent 做最后一道"仅内容"的同步检查,最后 tools/result 广播的是不可变的权威结果。

钩子因此可以横跨所有工具家族工作,而工具自己完全不耦合任何策略服务。

还有一个大招藏在 tools 包里:Code Mode。

它注册一个保留的 run_code 工具,把其余工具的 schema 渲染成一份 TypeScript/Python SDK 注入提示词(tools:sdk 段落)。模型通过写程序来编排工具调用,子调用携带父级 token 走同一条管线并记录 tool/code-dispatch。

等于把"工具组合"从模型的 N 次往返压缩成一次代码执行。对长链路任务的可观测性和 token 开销,都是数量级的改善。

五、设计模式:能力 Seam 三角色

如果说 Cordis 回答了"代码怎么组织",那么能力 seam 回答的是"产品能力怎么可替换"。

架构文档给出定义:一个 seam 由三种角色构成——

角色职责例子
Service Definition声明接口dsh-shell 声明 ctx.shell 执行器接口
Service Provider实现接口本地进程树后端、bwrap/Landlock/Seatbelt 沙箱后端、E2B 远程后端
Consumer消费接口(通常是面向模型的工具)tool-bash、tool-terminal、lsp 工具

“添加一项能力 = 把三者一并设计”。

seam 是"换一个提供方就改变整个产品"的原因。文档给了一个典型例证:文件系统提供方与进程提供方共享同一个执行世界。

把它们指向远程沙箱后,Bash、PTY、LSP 就一并搬过去了,不需要任何提供方专用的 fork。

看两个 seam 的具体形态:

Subagent seam(packages/subagent/)采用“多提供方注册表”形式。

ctx.subagents按名称注册多个提供方。官方自带六个:spawn-in-process、fork-in-process、acp、codex、claude-code、dsh-sdk。

提供方通过静态能力描述符(SubagentCapabilities: outputSchema / depthLimit / toolFilter / persona)公布自身支持的内容。

如果请求了不支持的能力,启动前会被明确拒绝(UNSUPPORTED_CAPABILITY)。这就是所谓的“大声失败,绝不静默降级”。

Sandbox seam(packages/sandbox/)则是"单提供方"形态。

ctx.sandbox 只做一件事:把子进程 argv 包进文件效果策略。

  • 模式仅三种:read-only
  • workspace-write
  • danger-full-access(第三种直接绕过 seam 裸 spawn)

后端覆盖 Linux bwrap/Landlock、macOS Seatbelt、Windows ACL 受限令牌。

并且后端要如实报告执行完整度(full / partial)。较旧的 Landlock ABI 管不住全部文件效果时,必须如实上报,让要求绝对保证的消费方自行拒绝。

安全语义上的诚实,被做成了类型系统的一部分。

同样的三角色结构贯穿全库:LLM、持久化、压缩、设置、凭据、存储、技能、工作流、网页检索……数下来有近二十个 seam,全部同构。学会一个,就会了所有。

六、值得单独说的几个子系统

6.1 LLM seam 与双适配器策略

packages/llm/llm 定义对话与流式词汇:Message / ContentBlock / StreamChunk / 适配器契约。

其中 ContentBlock 包括 text / reasoning / image / tool-call / tool-result 五种块,且块类型 map 可声明合并扩展。

循环通过 ctx.llm.prepareCall(config) 解析路由。适配器按 provider 名注册,重复注册直接抛 DUPLICATE_ADAPTER。

有意思的是官方带了两条 DeepSeek 通路:

  • llm-deepseek:直连官方 API,fetch + SSE,路由名刻意叫 deepseek-official
  • llm-pi-ai:库实现,目录名 deepseek

一份组合可以同时挂两条路径做对比。

适配器配置里还能看到产品事实:默认模型目录是 V4-Flash / V4-Pro,默认上下文窗口 1,000,000 token,支持 reasoningEffort: off|low|high|max。

6.2 Compaction:以锁与事件表达的上下文压缩

压缩不在循环主干里,而是一个标准 seam。

它向 SessionEventMap 合并三个仅日志事件:compaction/start、compaction/summary、compaction/end。

compaction/start 取锁,compaction/summary 记录摘要、被遮蔽的 seq 集、token 数、生成它的那次 ctx.llm.stream() 调用,compaction/end 放锁。

锁括住整个操作。中途崩溃会表现为"有 start 无 end"的可检测遗留锁,而不是一个谎称完成的 end。

对 surface 的唯一修改,是一条 surfaceOp: replace 的 user/message。

历史原样保留,模型视角完成替换。触发侧挂在 agent/pre-step 和 agent/request-error 两个瀑布上。

6.3 Extensions:agent 修改自己的运行时

packages/extensions/ 是整个仓库里最大胆的一组包。

它把 Cordis 运行时本身暴露成模型工具:cordis_inspect_*、cordis_define、cordis_run、cordis_stop、cordis_undefine。

也就是,模型可以定义并运行模型自己写的动态插件,再撤销。

host 半边的动态包跑在 node:vm 沙箱里,浏览器半边由 client runner 求值为活插件。

这是"一切皆插件"的逻辑终点:既然系统的每一部分都是插件,那么 agent 当然也可以通过写插件来扩展它自己。

Skill 还只是提示词工程,而这里已经是运行时的自我改造。

6.4 Web 架构:前后端也是插件

Web GUI 拆成 host(packages/host/)与 client(packages/client/)两半。

host/webserver 是一个刻意"无知"的 node:http 插件。

它只提供具名路由注册表和 index.html 转换钩子,不理解任何 harness 概念。

所有功能路由,包括 /api 桥接、插件 bundle、HMR 事件流,都是由别的插件注册的。

前后端之间的 RPC 走自研的 Typert。构建期从源码类型生成类型图与产物,运行期经 ctx.typert 注册表和 API gateway 调用。前后端共享同一份类型真源。

进程外集成同样齐备:

  • packages/sdk 提供 JSON-RPC 协议与 TS 客户端
  • packages/acp 提供 ACP(Agent Client Protocol)服务器接入编辑器生态
  • packages/python/ 下还有 Python SDK

所有入口都是"一个薄的自执行组合 + 共享 boot 胶水"。

七、工程文化:从仓库组织看设计品味

源码之外,这个仓库的"元工程"同样值得关注。因为它们本身就是设计哲学的一部分:

  • Agent Notes 制度:.agents/notes/implemented/ 下按日期归档每一份设计决策,子系统文档直接链回对应笔记。决策的"为什么"和代码的"是什么"被制度性绑在一起。
  • 文档即代码的校验体系:子系统文档里的类型签名用 ```````ts type-equiv ````代码块粘贴,verify-type-equiv 提取器会与源码逐名比对,签名漂移直接 CI 失败;中英文文档通过"双语配对"机制(verify-translation-pairing + 专用 git merge driver)保证同步。
  • 生成的目录:事件的生产方/消费方映射、工具目录、配置目录、持久化事件目录全部由脚本从源码生成(gen-doc-graphs 等),文档不可能说谎。
  • 不变量即代码:几乎每个包都有 invariant.ts,架构级承诺是运行时会抛错的断言。
  • Fail loud 无处不在:启动审计(assertEntriesLoaded/Activated)、installFailLoud、能力不支持的 UNSUPPORTED_CAPABILITY、审批的 fail-closed、沙箱的 partial 诚实上报——整个系统对"静默降级"有种近乎偏执的敌意。

八、评价:Agent 时代的"安卓",以及它的代价

把 DSH 放进坐标系里看会更清楚。

对比 Claude Code / Codex 这类"精装单体"路线,DSH 的选择是另一个极端:没有核心,只有协议。

模型适配器是插件,工具是插件,持久化是插件,审批是插件,连 agent loop 这个"心脏"都只是 ctx.agentLoop 的一个可替换注册项。

这条路线的收益:

  1. 替换粒度无限细。换模型、换工具集、换执行环境(本地 → E2B 远程沙箱)、换循环本身,都不需要等官方发版,甚至不需要重启进程。
  2. 生态潜力大。插件就是 npm 包 + 一份 cordis.patch.yml,社区已有数千个 deepseek-harness topic 的仓库在生长。
  3. 可审计性。事件溯源 + 不变量 + fail-loud,让它天然适合对"agent 到底干了什么"敏感的场景。

代价也同样真实:

  1. 抽象税。每个能力都要走"接口 → 注入 → 事件"的间接层,热路径上是一次次 waterfall 分发;46 万行代码里有相当比例在为"可替换性"本身付费。
  2. 认知门槛。要改行为,先得理解 Cordis 的分发模式、realm/scope、layer 叠加规则。"一切皆插件"的自由度,是以学习曲线换的。
  3. 预览期的不稳定性。官方明言会有破坏兼容性的变更,插件 API 还在快速演化。

九、结语

读完 DeepSeek Harness 的源码,最强烈的感受是:这不是一个"带有插件系统的 agent 框架",而是一套插件协议恰好长成了 agent 框架的样子。

它的几乎所有设计——事件溯源的会话日志、waterfall 拦截点、三角色 seam、可逆注册、fail-loud 哲学——都在服务同一个目标:

  • 让"模型之外的一切"可组合
  • 可替换
  • 可审计

如果 Model + Harness = Agent 这个公式成立,那么 DeepSeek 开源的其实是等号右边那一半的完整工程答案。

它是否会成为"执行层标准"尚需时间验证,但就架构的彻底性和工程的纪律性而言,这份源码本身就是 2026 年 agent 工程领域最值得细读的文本之一。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多