DeepSeek Harness源码分析:给非 AI 开发者的 Agent 架构长文



对本文有任何问题,可加我的个人微信询问:kymjs666

写在前面

我自己的工作经验更偏客户端与工程化,而不是「天天训模型」。第一次接触 coding agent 时,最容易被两件事打懵:一是名词爆炸——Agent、Tool、Skill、MCP、Plugin、Harness 好像都在说「能扩展」,却说不清边界;二是打开像 DeepSeek Harness 这种仓库,包名与 ctx.xxx 服务键铺天盖地,不知道该先抓哪根绳子。

这篇博客是我整理给自己、也整理给所有非 AI 开发者的一条路径。

1. Agent 通识:先离开仓库谈清楚名词

1.1 Agent 是什么

如果把一次普通 ChatCompletion 想成「你问一句,模型答一句」,那么 Agent 更像「模型被放进一个循环里」:它读当前目标与上下文,决定下一步是继续推理,还是调用某个外部能力;外部能力返回结果后,结果再写回上下文,循环继续,直到任务结束或策略打断。

循环里通常至少有三样东西:

  • 模型:相当于大脑,负责提需求,点名调用什么;
  • 工具执行器:负责执行,去读文件、跑命令、搜网页等;
  • 会话状态:负责记住已经发生过什么,以便下一轮请求还能对齐。

Agent 不是模型本身,也不是某一个工具。它是把「多轮决策 + 执行」组织起来的运行时角色。

1.2 Harness 是什么

Harness(运行时架子) 指的是:围绕模型把会话、工具、策略、人机交互、扩展机制组合成「可运行系统」的那一层。你可以把它理解成 agent 的操作系统内核与应用框架的交界——没有它,你只有 API 调用;有了它,你才有可恢复的会话、可拦截的工具调用、可替换的执行后端、可组合的插件树。 一种近似的理解就是 Android 与 Linux 的关系。harness 就是 Android 系统里那个 ART+Framework 壳,大模型对应的就是 Linux 内核,共同组装成 Android 系统。

不同项目里「Harness」一词含义并不统一,网上也有很多讲 deepseek-harness 就不是 harness 的说法。这个我后面对比 OpenClaw 时我会专门讲,通识层面先记住:Harness = 让 Agent 真正跑起来并受控的整机装配,毕竟他翻译过来也是马具(字面指套在牲口上的装备)。

1.3 Tool、Plugin、Skill、MCP:差异与选用

Tool(工具)

Tool 是模型在一次回复里可以点名调用的函数契约:有名字、有参数 schema、有执行结果写回对话。典型例子:读文件、执行 bash、发起搜索,比如经常提供的 CLI,就是在工具层调用的。

Plugin(插件)

Plugin 是宿主运行时的扩展单元:挂载后向系统贡献服务、监听事件、注册工具或改写策略。插件是给「工程与运行时」用的,不是给模型直接「调用」的一等公民(当然插件可以注册出工具,让模型间接触达)。

Skill(技能)

Skill 通常是一份可按需加载的说明书(常是 Markdown / 结构化文档):告诉模型「这类任务按什么步骤做、注意什么、参考哪些资源」。模型先看到技能目录(短描述),需要时再加载正文。网上经常讲蒸馏自己,就是在这一层,把自己的工作和思考方式整理成一份skill,让大模型按照与自己完全一致的思考方式工作方法去处理问题。

MCP(Model Context Protocol)

MCP 是一种把外部服务器上的工具/资源目录桥接进当前 Agent 进程的协议与生态。你的 Harness 充当客户端,连到某个 MCP server,发现对方暴露的 tools,再以本地 Tool 的形式呈现给模型。

一张选用直觉

flowchart LR
  need["我要扩展 Agent 能力"]
  need --> q1{&#34;需要改运行时装配<br/>或挂服务/事件吗?&#34;}
  q1 -->|是| plugin[&#34;用 Plugin&#34;]
  q1 -->|否| q2{&#34;主要是给模型看的<br/>长流程说明书吗?&#34;}
  q2 -->|是| skill[&#34;用 Skill&#34;]
  q2 -->|否| q3{&#34;能力已由外部进程<br/>以 MCP 暴露吗?&#34;}
  q3 -->|是| mcp[&#34;接 MCP,映射为 Tool&#34;]
  q3 -->|否| tool[&#34;实现本地 Tool&#34;]

实操上常常组合:Plugin 注册 Tool;MCP 发现的也是 Tool;Skill 通过某个 skill Tool 被加载;Harness 决定这些东西如何组装与拦截。

flowchart LR
    A[Start] --> B{Decision}
    B -->|Yes| C[OK]
    B -->|No| D[Retry]

1.4 回到 deepseek-harness

当我把目光收回这个仓库时,上述名词大致这样的:

通识概念 本仓库中的内容(轻羽云笔记整理)
Agent Agent 接口 + 默认驱动 ReactLoopAgentpackages/core/agent-loop
Harness 以 Cordis 插件树组装的整机:profile / bundle / 核心包 / 能力 seam / 策略
Tool ctx.tools 注册表与执行流水线(packages/core/tools
Plugin Cordis 插件(Service / apply(ctx)),万物皆插件
Skill ctx.skills + 模型侧 dsh-tool-skill
MCP @deepseek-ai/dsh-mcp-client:发现后 ctx.tools.register(),名如 mcp__server__tool

还有几个通识里没单独开词、但读本仓库绕不开的概念:Session Log(会话日志)Capability Seam(能力缝)Turn/Step(轮次/步骤)、以及整条权限安全链。它们会出现在下一张万能图里。

2. 整体架构图

在分模块讲解之前,需要大家先花一分钟看「架构图」,这架构图是让 Cursor 画的,还挺传神。

轻羽云笔记

  1. 组装层ProfileBundlecordis.ymlpatchdsh-base——决定「机器上到底挂了哪些插件」。
  2. Cordis 底座PluginctxService+inject、事件(含 waterfall)、effect 可逆注册——决定「扩展如何挂上、如何卸下、如何拦截」。
  3. 脊柱 Agent Loop(图上亮色强调):ctx.agentsReactLoopAgentInboxTurn/Stepagent/pre-stepagent/request——决定「一轮任务如何推进」。
  4. Session + Prompt/LLM:日志与 deriveMessages(模型可见⟺已记录),以及 ctx.systemPrompt / ctx.llm——决定「模型看见什么、请求怎么发出」。
  5. Tools + Seam + 汇入:工具流水线;Capability Seam 三角色;Skill / MCP 如何汇入 ctx.tools
  6. 安全链pre-executectx.approvalToolGuardsandbox → presets/hooks——决定「危险动作如何被策略面拦住或关进隔离世界」。

当然,上面那张图只是为了便于大家理解,真正的 Agent 绝不可能是一层一层的,更像是这样。这个图是Image2生成的,下方的流程图是我画的。

轻羽云笔记

flowchart TD

    USER[&#34;User&#34;]

    subgraph H[&#34;DeepSeek-Harness 架构图(轻羽云笔记整理)&#34;]

        subgraph C[&#34;Cordis 框架&#34;]
            CORDIS[&#34;Cordis&#34;]
            CTX[&#34;Plugin Context&#34;]
            CORDIS --> CTX
        end

       LOOP[&#34;Agent Loop&#34;]

        subgraph S[&#34;Session 层设计&#34;]
            LOG[&#34;Session Log&#34;]
            SURFACE[&#34;Surface<br>即下文讲的deriveMessages&#34;]
            LOG --> SURFACE
        end

        subgraph X[&#34;ctx&#34;]
            CONTEXT[&#34;Context Builder&#34;]
        end

        LLM[&#34;大模型&#34;]

        subgraph T[&#34;Tool 执行器(推荐下载轻羽云笔记)&#34;]
            REGISTRY[&#34;Tool Registry&#34;]
            POLICY[&#34;Policy&#34;]
            PERMISSION[&#34;Permission&#34;]
            APPROVAL[&#34;Approval&#34;]
            SANDBOX[&#34;Sandbox Policy&#34;]
            SEAM[&#34;Capability Seam&#34;]

            REGISTRY --> POLICY
            POLICY --> PERMISSION
            PERMISSION --> APPROVAL
            APPROVAL --> SANDBOX
            SANDBOX --> SEAM
        end

        subgraph P[&#34;Capability Providers&#34;]
            PROVIDER[&#34;File / Shell / Web / Sandbox / Subagent&#34;]
        end

        WORLD[&#34;Real World&#34;]

        CTX --> LOOP
        CTX --> LOG
        CTX --> REGISTRY

        LOOP --> LLM

        LLM -->|&#34;Tool Call&#34;| REGISTRY

        SEAM --> PROVIDER
        PROVIDER --> WORLD

        REGISTRY --> LOG
        LOG --> SURFACE
        SURFACE --> CONTEXT
        CONTEXT --> LLM

    end

    USER --> LOOP
    APPROVAL -.->|&#34;Human Approval&#34;| USER

3. Agent Loop

Agent Loop 是所有 Agent 最重要的一套逻辑,他真正明确了交给 Agent 的任务能不能完成,Agent 会不会卡死。默认驱动是 ReactLoopAgentpackages/core/agent-loop/src/agent.ts)。一次 Agent Loop 最重要的是这四个步骤:Inbox、Turn、pre-step、Step。

轻羽云笔记

所有用户的输入统一进入 Inbox。这里 Inbox 有三类消息:followup(普通输入)、steer(打断/纠偏到下一步)、inject(一般是塞上下文)。除了inject之外,另外两类消息都会 wakeup 驱动器立刻开工;inject 的上下文直接进入 next-step 但不唤醒,直到另一条消息唤醒。

Step:一次模型请求,如上图,每个Step中,都会有一次模型请求,根据模型的请求,可以去做工具/MCP/或其他调用。

Turn:一次完整的对话过程,用户输出,模型执行完成。其中会包含零个或多个 Step,Step之间是串连的。参考 Android 知识,AGP 的思路就能快速理解了,​当前 Task 的 input 是上一个 Task 的 output,Task内部可以实现自己的DIY。如果要定制 Agent 的动作,就可以修改Step的实现。

如下是Turn/Step实现的核心代码

// packages/core/agent-loop/src/agent.ts — turn() 核心骨架
private async turn(): Promise<boolean> {
  const turn = phase.turn + 1
  // 持久边界:轮次开始必须先写入 session 日志
  this.session.append('turn/start', { turn })
  phase.turn = turn
  let target: InboxTarget = 'next-turn'
  while (true) {
    const step = phase.step + 1
    // 先决定「这一步模型该不该看见这些消息」
    const decision = await this.preStep(target, { turn, step })
    if (decision.kind === 'reject') { /* 记 blocked,不花 step */ return false }
    // 首步若被改写成空消息:仍关闭 turn,但不发起模型调用
    if (phase.step === 0 && decision.messages.length === 0) { /* completed */ return false }

    this.session.append('step/start', { turn, step })
    phase.step = step
    for (const message of decision.messages) {
      // 进入模型可见历史:写入 user/message
      this.session.append('user/message', message, { surfaceOp: 'append' })
    }
    const stepEnd = await this.step(decision.assembly)
    this.session.append('step/end', { turn, step })

    if (turnEnds && this.inbox.nextStep.length === 0) {
      // 轮次终止检查点:监听器可再 steer 一步
      await this.dispatch.serial('agent/turn-stopping', { turn, signal })
    }
    if (turnEnds && this.inbox.nextStep.length === 0) break
    target = 'next-step' // 后续步骤从 next-step 队列取输入
  }
  this.session.append('turn/end', { turn, reason: turnEnds! })
  return this.inbox.hasPending // 若仍有排队,外层驱动继续下一 turn
}

turn()session.append('turn/start'),再循环:preStepstep/startstep()step/end;若本步结束且 next-step inbox 空,则 serial('agent/turn-stopping'),最后 turn/end结束本次轮次,认为任务结束(不一定是处理完成,也可能是需要用户补充输入)。

每个 Step 开始前,会有一个叫agent/pre-step的 waterfall:决定是 enter(messages) 还是 reject。监听器可改写将进入模型的消息集合。这一步很重要,他直接决定了模型在这一步到底能看见什么。skill 目录、runtime context 协作改写都在这里明确。

// packages/core/agent-loop/src/agent.ts — preStep()
private async preStep(target: InboxTarget, position: { turn: number; step: number }) {
  const claimed = this.inbox.claim(target, position.turn)
  // 组装本步可用的 prompt sections + tool schemas,推荐下载轻羽云笔记
  const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
  const sections = renderContextSections(assembly)
  // 把运行时上下文投影成可能追加的 user 消息
  const context = this.runtimeContext.project(joinContextSections(sections), sections)
  const decision = await this.dispatch.waterfall(
    'agent/pre-step', { messages: claimed, ...position, signal },
    // 默认:claimed 消息 + 可选 runtime context
    () => Promise.resolve({
      kind: 'enter' as const,
      messages: context === undefined ? claimed : [...claimed, context],
    }),
  )
  return decision.kind === 'reject' ? decision : { ...decision, assembly }
}

执行完预处理,接下来就是 Step 了。Step 可以说是 Agent「行动」的发动机:没有它,前面所有组装都只是静态配置。一个 Step 中:先 buildRequest(含 session.deriveMessages())→ llm.stream → 追加 assistant/chunk → 合成 assistant/message → 若有 tool-call 则 executeToolCalls

// packages/core/agent-loop/src/agent.ts — step() 关键路径
private async step(assembly: PromptAssembly) {
  const system = renderPrompt(assembly)
  while (true) {
    // 历史不是「内存里随便拼的数组」,而是从 session 日志投影
    const { request, preparedCall } = await this.buildRequest(
      turn, step, assembly.tools, system, this.session.deriveMessages(), signal,
    )
    const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request)
    for await (const chunk of stream) {
      // 原始 chunk 落日志:保证 UI/回放保真
      chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
      assembler.push(chunk)
    }
    // ... 错误可走 agent/request-error waterfall 决定是否 retry ...
    this.session.append('assistant/message', { turn, step, message, ... }, { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })
    const toolCalls = message.content.filter(b => b.type === 'tool-call')
    if (toolCalls.length === 0) return { kind: 'completed' }
    // 工具结果会回到 next-step;若还欠模型请求,外层 turn 循环继续
    const { concluded } = await executeToolCalls(...)
    return concluded ? { kind: 'completed' } : null
  }
}

下面这张图就是 DeepSeek-harness 的 Agent Loop 完整流程。可以说整个项目最核心的地方就是ReactLoopAgent这里了,在源码里面被称为 Spine(脊柱)。Spine 向上依赖 Cordis 服务;向下写 Session;横向进 Tools 与安全链。万能图 Loop→LLM / Loop→Tools 的亮色箭头,就是这里。

sequenceDiagram
  participant U as 用户/SDK
  participant A as ReactLoopAgent
  participant S as Session
  participant L as ctx.llm
  participant T as ctx.tools
  U->>A: followup / steer
  A->>S: turn/start
  A->>A: pre-step waterfall
  A->>S: step/start + user/message
  A->>S: deriveMessages()
  A->>L: stream(request)
  L-->>A: chunks
  A->>S: assistant/chunk* + assistant/message
  alt 含 tool-call
    A->>T: execute pipeline
    T->>S: tool/call + tool/result
    A->>A: 可能进入下一 step<br>(推荐下载轻羽云笔记)
  end
  A->>S: step/end + turn/end

4. Session Log:模型看见的世界从哪来

4.1 Session 的设计

deepseek-harness 的 Session 层核心实现,是非常经典的设计,值得每一个做 Agent 的人学习。这也是我单独提出来写一段的原因:他的核心不是把 Session 当成一份“聊天记录”,而是把它做成 Event Sourcing(事件溯源)的 append-only event log。

我之前也设计过一个 Mac 控制手机的 Agent,叫 Custard https://github.com/kymjs/Custard,对这一块特意仔细看了一下。
先说一下我之前遇到的问题:对于频繁执行的任务,没办法记住此前的经验,造成每次都得大模型反复执行。最开始是参考了 Hermes 的设计(毕竟公认的他对于重复任务的处理是最好的),把每次执行的经验用户的对话,模型的返回,都记录到本地的markdown中,可是每次经验复用的时候都会出问题,要么是模型把经验压缩了,要么是经验没有被应用,要么就是跟其他经验混串了。

再说一下dsh的设计: 官方文档明确说,Session 是 Agent 整个交互历史的唯一真源,LLM 下一次请求需要的 Message[] 都是从这个 Log 重新派生出来的,而不是另外维护一份 message history。可以看作是

Session
   │
   ├── Event #0  session/start
   ├── Event #1  request/header
   ├── Event #2  user/message
   ├── Event #3  assistant/chunk
   ├── Event #4  assistant/chunk
   ├── Event #5  assistant/message
   ├── Event #6  tool/call
   ├── Event #7  tool/result
   ├── Event #8  usage
   ├── Event #9  ...
   │
   └── Event #N
          │
          ├── deriveMessages()
          ├── Trajectory
          ├── Session Log UI
          ├── Resume
          ├── Fork
          └── Replay

对应的每一个 Event 都有这样的一个接口对象:

interface SessionEvent {
  type: string   // 事件类型
  seq: number  // 递增序号,类似id
  time: number // 事件创建时间
  data: unknown // 事件内容

  // 部分事件才有
  sourceEventSeqs?: number[]
  surfaceOp?: ...
}

举个例子,比如用户输入:帮我看看这个项目有什么问题?
特别是单纯的保存一个问题,我之前就是因为这样存储经验造成的问题。

{
  "role": "user",
  "content": "帮我看看这个项目有什么问题"
}

dsh是可能产生一系列事件:

user/message
       ↓
request/header
       ↓
assistant/chunk
       ↓
assistant/chunk
       ↓
assistant/message
       ↓
tool/call
       ↓
tool/result
       ↓
assistant/chunk
       ↓
assistant/message

所以 Session Log 记录的是 Agent 的“行为轨迹”,而不仅仅是聊天内容。

DeepSeek 官方对 Harness 的描述也是:系统会记录 model 看到和执行的内容,包括 system prompt、reasoning、tool calls/results、subagent scheduling、context injection 等。

4.2 Session Log 持久化

在dsh中,内存里的叫 Session,持久化到硬盘的叫 Session Log 。是分开的。官方把这个叫:Session Persistence。大致是这样的一个结构:

flowchart TD
  need[&#34;内存中的 Session&#34;] --> |触发持久化| need2[&#34; append()&#34;]
  need2 --> q1{&#34;SessionPersistence 执行&#34;}
  q1 -->|dsh-session-persistence-jsonl| json[&#34;JSONL/Zstd&#34;]
  q1 -->|dsh-session-persistence-sqlite| sqlite[&#34;SQLite&#34;] --> |SessionEvent| id1[(session_id
seq
type
time
data
source_event_seqs
surface_op)]

其实这里还有一个设计,我看到 jsonl 中存的日志都是这种的

{"seq":0,"type":"session/start","time":1755820000000,"data":{}}
{"seq":1,"type":"user/message","time":1755820001000,"data":{"content":"你好"}}
{"seq":2,"type":"assistant/chunk","time":1755820002000,"data":{"delta":"你好"}}
{"seq":3,"type":"assistant/message","time":1755820003000,"data":{"content":"你好!"}}

如果是我,可能直接上来就是一个大json里面挨个嵌套了,搜了一下才知道这种设计的好处。因为 Agent 的执行过程是不可靠的,有可能某次工具调用之后挂掉,如果存一个大json就会造成,下次回复又得重头开始。

所以参考 dsh 重新设计一套 Session,应该就是这样,从 AgentLoop 开始

flowchart TD
  need[&#34;Agent Loop&#34;] -->  need2[&#34;Session.append&#34;]
  need2 --> q1{&#34;SessionEvent<br><br>seq<br>type<br>timestamp<br>data &#34;}
  q1 -->|&#34;deriveMessages()&#34;| json[&#34;LLM API&#34;] --> |新的 Event|need2
  q1 -->|存日志| save{&#34;SessionPersistence&#34;}
  save -->  |json| id1[(session)]
  save -->  |sqlite| id2[(session)]

这里面 deriveMessages() 也做了一层过滤,并不是所有的 Session 消息都要交给大模型,有一些已经执行的工具调用只需要告诉大模型结果即可甚至都不需要告诉模型,或者逻辑判断没必要告诉模型的内容,就是在这里过滤的。有点类似以前做的会话压缩,为了让大模型上下文占用更少,压缩、结构化、过滤、合并、截断、拦截器转换等实现。


5. Cordis 框架 与 Tools 调用

这里推荐一下我写博客的工具:轻羽云笔记。不仅支持各类markdown语法、私有化存储、RSA256加密、全平台支持。 【轻羽云笔记:https://note.kymjs.com

5.1 Capability Seam:可替换能力的三角色

轻羽云笔记

Seam(能力缝)= 完整可替换能力,必须具备三种角色:

  1. Service Definition(定义):拥有 ctx.<key> 与词汇的 Cordis Service(抽象类或注册表服务,不是裸 interface);
  2. Service Provider(实现):真正实现/注册后端;
  3. Consumer(使用):通常是模型侧 Tool 或其他插件,只注入服务键。

比如以 shell 举例:dsh-shelldsh-bash-localdsh-bash-sandboxdsh-tool-bash

dsh-shell:定义Shell 能力是什么 dsh-bash-local:在本机执行 Bash dsh-bash-sandbox:在 Sandbox 中执行 Bash dsh-tool-bash:把 Bash 能力暴露给模型

为什么需要 Capability Seam?
核心目的还是为了解耦,若捆在一个包,换沙箱执行器也会连带改动模型工具协议。三角色拆开后,换 Provider 不应逼模型重新学一套工具名。

C. 怎么实现
Definition 示例:


// packages/shell/shell/src/index.ts — Service Definition
export abstract class ShellExecutor extends Service {
  constructor(ctx: Context) {
    // 占用 ctx.shell;同上下文二次注册会按 Cordis 规则失败
    super(ctx, 'shell')
  }
  // 把调用方请求补全/钳制为可执行 Spec(显式 resolve,禁止 run 内隐藏默认)
  abstract resolve(request: ShellExecRequest): ShellExecSpec
  // 前台执行:非零退出等应 resolve 为结果,而不是随便 throw
  abstract run(spec: ShellExecSpec): Promise<ShellRunResult>
  // 后台进程:立即返回句柄
  abstract start(spec: ShellExecSpec): ShellProcess
}

Provider(如 dsh-bash-local)实现这个抽象类; Consumer(dsh-tool-bash)注册 bash tool,内部只调 ctx.shell

其实相比我们平时写代码,也就是多了一层专门给大模型调用的 Consumer。因为我们自己写代码,调用方是人,可以明确知道怎么调用一个三方SDK,但是大模型不知道,所以还得单独给他一个 Consumer 层。

当完整理解了 Capability Seam 以后,再从整体来看 Cordis 框架的实现。

5.2 Cordis:真正实现万物皆插件的核心框架

Cordis 是一个面向插件化运行时的依赖注入 + 生命周期 + 服务注册 + 事件通信框架。网上很多文章都讲了 DeepSeek Harness 让产品每一部分都是插件,包括模型适配、工具注册表、会话日志、agent loop 本身。他最核心的东西其实跟我们移动端做路由一样,一个注册,一个使用。这里借用现代化移动端路由框架 TheRouter https://github.com/HuolalaTech/hll-wp-therouter-android
实现的原理图来介绍

这里推荐一下我写博客的工具:轻羽云笔记。不仅支持各类markdown语法、私有化存储、RSA256加密、全平台支持。 【轻羽云笔记:https://note.kymjs.com

轻羽云笔记

flowchart LR
  pr[&#34;Provider&#34;] -->  |register|ctx[&#34;ctx.shell&#34;]
  consumer[&#34;Consumer&#34;] --> |inject|ctx 

例如 Consumer 说,我要 shell 能力。这时候不需要关心 shell 是谁提供的,他只需要知道有这样的一个能力让他用就行。Cordis 发现自己有这个能力,就直接给他了,而如果 Cordis 还没有这个能力,则需要等待对应的 Service 提供。另一边,Provider 提供了这个能力,只需要在 Cordis 中注册一下,Cordis 就知道他有这个能力了,后面谁要用他就可以直接提供。

ctx 是服务仓库,这里对 ctx 着重讲一下,做 Android 的应该很熟悉一个词Context, ctx实际上也就是Context的缩写。在 dsh 中,ctx 就表示当前 Agent 所必须的上下文,其中包含了工具、模型、环境会话等等信息,比如 ctx.toolsctx.llmctx.sessions
因为有了这样的架构,就不用再关心 Provider 和 Consumer 都是谁了。也就实现了官方说的一切皆插件的设计思想。而在这样的设计下,上图的 Provider 实际就是一个 Plugin。

5.3 Plugin 以及 Plugin间通信方案

flowchart TB

    subgraph R[&#34;Cordis Runtime(推荐下载轻羽云笔记)&#34;]
        direction TB

        subgraph P[&#34;Plugins&#34;]
            direction LR
            PA[&#34;Plugin A<br/><br/>提供 Service&#34;]
            PB[&#34;Plugin B<br/><br/>监听 Event&#34;]
            PC[&#34;Plugin C<br/><br/>注册 Tool&#34;]
        end

        SC[&#34;Shared Context&#34;]

        subgraph CAP[&#34;Capabilities&#34;]
            direction LR
            LLM[&#34;ctx.llm<br/><br/>LLM 能力&#34;]
            TOOLS[&#34;ctx.tools<br/><br/>Tool 能力&#34;]
            SHELL[&#34;ctx.shell<br/><br/>Shell 能力&#34;]
        end

        PA --> SC
        PB --> SC
        PC --> SC

        SC --> LLM
        SC --> TOOLS
        SC --> SHELL
    end


    classDef harness fill:#111827,color:#fff,stroke:#111827,stroke-width:2px;
    classDef runtime fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
    classDef plugin fill:#f3f4f6,color:#111827,stroke:#9ca3af,stroke-width:1.5px;
    classDef context fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:2px;
    classDef capability fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;

    class A harness;
    class PA,PB,PC plugin;
    class SC context;
    class LLM,TOOLS,SHELL capability

Plugin 是实现 Cordis Service ,也就是上文Service DefinitionService Provider 的对象,当然Plugin 可能是一个函数插件,也可能是一个 Service 子类。

Plugin之间主要是通过 event 协作,也就是上图中的箭头。当然 event 不止有一种,模式包括 emit(广播)、waterfall(最重要的)、parallel(所有Plugin一起并行处理)、serial(按顺序执行)。

单独讲一下waterfall:
Harness 里大量使用了 waterfall:agent/pre-stepagent/requesttools/pre-execute 等。
waterfall的本质是个责任链,有点类似于Gradle Task。每个Task都有自己的name、input、next 以及当前Task要处理的任务。

如下图,若使用 waterfall 传递 event,每个 Plugin 都会把上一个 Plugin 的输出作为输入,自己处理完成以后,调用 next 交给下一个处理。如果不调用 next,则表示当前就是最终结果,后面的都不执行了,直接返回。

sequenceDiagram
  participant AL as Agent Loop
  participant L1 as 监听器1
  participant L2 as 监听器2
  participant Default as 默认实现
  AL->>L1: waterfall(event)
  L1->>L2: next()
  L2->>Default: next()
  Default-->>L2: decision
  L2-->>L1: decision
  L1-->>AL: decision
  Note over L1: 若不调用 next() 则短路返回

完整看完了 Cordis,再回过头来看工具调用,这里说的工具包含 shell、文件读写、调api,逻辑上都是一样的,在调用上,都是当做 Plugin 来处理的,也就是说工具本身也是一个 Capability Seam,最终都是注册到了 ctx.tools 里面。

5.4 Tool 内部的调用链

每一个 Tool 内部也是有调用链的,这个链叫 Schema,不是单纯 js 调个方法就执行了,LLM 只能看到 Tool Schema,真正的 Tool 执行发生在 Harness Runtime。Tools Schema 大致的生命周期函数是这些:

flowchart TD

    LLM[&#34;LLM&#34;]

    REG[&#34;Tool Registry&#34;]
    DEF[&#34;Tool Definition&#34;]

    PRE[&#34;tools/pre-execute<br/>waterfall&#34;]
    DEC{&#34;allow / deny&#34;}

    EXEC[&#34;tools/execute<br/>waterfall&#34;]
    BODY[&#34;Tool Body(推荐下载轻羽云笔记)&#34;]

    POST[&#34;tools/post-execute<br/>waterfall&#34;]

    RESULT[&#34;Final Result&#34;]
    EMIT[&#34;tools/result<br/>emit&#34;]

    SESSION[&#34;Session&#34;]
    LOGGER[&#34;Logger&#34;]
    METRICS[&#34;Metrics&#34;]

    LLM -->|&#34;tool_call&#34;| REG
    REG --> DEF
    DEF --> PRE
    PRE --> DEC

    DEC -->|&#34;allow&#34;| EXEC
    DEC -->|&#34;deny&#34;| RESULT

    EXEC --> BODY
    BODY --> POST
    POST --> RESULT

    RESULT --> EMIT

    EMIT --> SESSION
    EMIT --> LOGGER
    EMIT --> METRICS


    classDef llm fill:#1f2937,color:#fff,stroke:#111827,stroke-width:2px;
    classDef registry fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
    classDef waterfall fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:1.5px;
    classDef decision fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:2px;
    classDef body fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
    classDef result fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
    classDef output fill:#f9fafb,color:#374151,stroke:#9ca3af,stroke-width:1.5px;

    class LLM,TC llm;
    class REG,DEF registry;
    class PRE,EXEC,POST waterfall;
    class DEC decision;
    class BODY body;
    class RESULT,EMIT result;
    class SESSION,LOGGER,METRICS output;

6. 组装层

flowchart TD

    P[&#34;Profile<br/><br/>“我要什么样的 Agent”&#34;]

    P -->|&#34;按顺序组合&#34;| B1
    P -->|&#34;按顺序组合&#34;| B2
    P -->|&#34;按顺序组合&#34;| B3

    subgraph B[&#34;Bundles · 组合包 / 模块&#34;]
        direction LR
        B1[&#34;Bundle A&#34;]
        B2[&#34;Bundle B&#34;]
        B3[&#34;Bundle C&#34;]
    end

    B1 --> PATCH1[&#34;cordis.patch.yml&#34;]
    B2 --> PATCH2[&#34;cordis.patch.yml&#34;]
    B3 --> PATCH3[&#34;cordis.patch.yml&#34;]

    PATCH1 -->|&#34;insert / replace&#34;| TREE[&#34;Cordis Plugin Tree&#34;]
    PATCH2 -->|&#34;insert / replace&#34;| TREE
    PATCH3 -->|&#34;insert / replace&#34;| TREE

    TREE --> RUNTIME[&#34;Plugin Runtime<br>(推荐下载轻羽云笔记)&#34;]
    RUNTIME --> CAP[&#34;Capability / Tools&#34;]


    classDef harness fill:#111827,color:#fff,stroke:#111827,stroke-width:2px;
    classDef profile fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
    classDef bundle fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
    classDef patch fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:1.5px;
    classDef tree fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:2px;
    classDef runtime fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
    classDef capability fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;

    class H harness;
    class P profile;
    class B1,B2,B3 bundle;
    class PATCH1,PATCH2,PATCH3 patch;
    class TREE tree;
    class RUNTIME runtime;
    class CAP capability;

6.1 Profile、Bundle、Patch

Profile: 一套完整运行配置,可以直接拿出去用的包。类似 Android 的一个安装包。

Bundle:一个可分发的配置层,它通过 patch 把一组 Cordis 配置行以及这些配置所引用的 Plugin 组合进 Profile。比如web、headless、my-coding-agent、my-research-agent每个都是一个独立的Bundle。官方对 Bundle 的定义就是 Cordis 配置项及其挂载代码的分发格式。有点类似 Android 的 feature 包,或者前端的一个 npm 包。

Patch:对插件树的声明式修改。通常会定义一些插件的依赖、替换规则,比如与哪个插件冲突时使用哪个插件,加载哪个插件必须先加载另一个插件。

之所以有这三个东西,最核心的原因还是为了解耦。由于 dsh 的设计思想是一切皆插件,那么就必须解决一个问题:如果有插件ABCDE,那么什么时候加载A,什么时候加载B,如果只跑web模式,C要不要加载,如果只跑无头模式,D要不要加载,如果AC都依赖E的时候,怎么E要在哪些模式加载。

这些逻辑最简单的办法就是一股脑的 if…else…,也能达到目的。但是不利于解耦,也不利于工程化的扩展和更新。这些其实也不是新东西了,Google 在 Android 工程新项目里面,就引入了 feautre 包的概念,每个 feature 包就是面向 APP 不同的功能提供的,然后每个 feature 包又有各种的 aar,aar又能依赖其他 aar。

放在 dsh 上,就是同一套核心功能,要有面向「带 Web UI 的应用」或者「无头模式的一次性 runner」或者 「桌面应用」或者其他。Profile 就是这种产品形态的封装,每个端就是一个Profile。

轻羽云笔记

如上图,在 $DSH_HOME/profiles/<name> 这个目录里可以定义自己的 Profile,一个json文件,一个cordis.patch.yml 文件。json文件定义了需要哪些bundle,yml文件定义了patch有哪些,patch需要操作的plugin。

6.2 Patch 操作 Plugin 的原理

首先需要明确一个点:Patch 无法修改 Plugin 的逻辑,Patch 只能决定要加载哪个插件,先加载哪个插件,替换哪个插件,但是无法决定哪个插件。

比如系统中有一个 bash 的插件,

id: bash
name: dsh-bash-local
config:
    sandbox: true

然后 patch 中,需要替换这个插件时,就是在 cordis.patch.yml 中声明:

- replace:
    - id: bash
      name: dsh-bash-remote
      config:
        sandbox: false

在加载的时候,dsh-bash-local,就被替换成了 dsh-bash-remote。

这就和我们前面讲的 Capability Seam 串起来了

把 5.2、5.4 和 6.1 的图拼接一下:

flowchart TD

P[&#34;Profile<br/><br/>“我要什么样的 Agent”&#34;]

    P -->|&#34;按顺序组合&#34;| B1
    P -->|&#34;按顺序组合&#34;| B2
    P -->|&#34;按顺序组合&#34;| B3

    subgraph B[&#34;Bundles · 组合包 / 模块&#34;]
        direction LR
        B1[&#34;Bundle A&#34;]
        B2[&#34;Bundle B&#34;]
        B3[&#34;Bundle C&#34;]
    end

    B1 --> PATCH1[&#34;cordis.patch.yml&#34;]
    B2 --> PATCH2[&#34;cordis.patch.yml&#34;]
    B3 --> PATCH3[&#34;cordis.patch.yml&#34;]

    PATCH1 -->|&#34;insert / replace&#34;| TREE[&#34;Cordis Plugin Tree&#34;]
    PATCH2 -->|&#34;insert / replace&#34;| TREE
    PATCH3 -->|&#34;insert / replace&#34;| TREE

    TREE --> RUNTIME[&#34;Plugin Runtime&#34;]
    RUNTIME --> CAP[&#34;Capability / Tools&#34;] --> |启动时注册|ctx

    LLM[&#34;LLM&#34;]

    REG[&#34;Tool Registry&#34;]
    DEF[&#34;Tool Definition&#34;]

    PRE[&#34;tools/pre-execute<br/>waterfall&#34;]
    DEC{&#34;allow / deny&#34;}

    EXEC[&#34;tools/execute<br/>waterfall&#34;]
    subgraph ToolExec[&#34;工具执行部分&#34;]
    BODY[&#34;Tool Body&#34;]

    POST[&#34;tools/post-execute<br/>waterfall&#34;] --> consumer[&#34;Consumer&#34;]

    ctx[&#34;ctx.shell<br>(推荐下载轻羽云笔记)&#34;] --> |原路径<br>不再调用|local[&#34;Local Provider&#34;]
    ctx --> |新路径<br>patch 中已替换|remote[&#34;Remote Provider&#34;]
    consumer[&#34;Consumer&#34;] --> ctx 

    end

    RESULT[&#34;Final Result&#34;]
    EMIT[&#34;tools/result<br/>emit&#34;]

    SESSION[&#34;Session&#34;]
    LOGGER[&#34;Logger&#34;]
    METRICS[&#34;Metrics&#34;]

    LLM -->|&#34;tool_call&#34;| REG
    REG --> DEF
    DEF --> PRE
    PRE --> DEC

    DEC -->|&#34;allow&#34;| EXEC
    DEC -->|&#34;deny&#34;| RESULT

    EXEC --> BODY
    BODY --> POST
    remote --> RESULT

    RESULT --> EMIT

    EMIT --> SESSION
    EMIT --> LOGGER
    EMIT --> METRICS

    classDef harness fill:#111827,color:#fff,stroke:#111827,stroke-width:2px;
    classDef profile fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
    classDef bundle fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
    classDef patch fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:1.5px;
    classDef tree fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:2px;
    classDef runtime fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
    classDef capability fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;

    class H harness;
    class P profile;
    class B1,B2,B3 bundle;
    class PATCH1,PATCH2,PATCH3 patch;
    class TREE tree;
    class RUNTIME runtime;
    class CAP capability;

    classDef llm fill:#1f2937,color:#fff,stroke:#111827,stroke-width:2px;
    classDef registry fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
    classDef waterfall fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:1.5px;
    classDef decision fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:2px;
    classDef body fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
    classDef result fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
    classDef output fill:#f9fafb,color:#374151,stroke:#9ca3af,stroke-width:1.5px;

    class LLM,TC llm;
    class REG,DEF registry;
    class PRE,EXEC,POST waterfall;
    class DEC decision;
    class BODY body;
    class RESULT,EMIT result;
    class SESSION,LOGGER,METRICS output;

最后,依赖冲突问题:

由于每个Profile可以有多个Bundle,每个Bundle又有一个 patch 声明,Profile 本身也可以有 patch 声明,如果多个 patch 之间互相矛盾,那么就需要一个优先级决定这个顺序,以谁为准。如下图:

轻羽云笔记

加载的顺序是从下到上,优先级的顺序是从上到下,同 id 依次覆盖。


7. 与其他 Agent 对比一下

这里推荐一下我写博客的工具:轻羽云笔记。不仅支持各类markdown语法、私有化存储、RSA256加密、全平台支持。 【轻羽云笔记:https://note.kymjs.com

首先 DeepSeek Harness 是以 Cordis 插件树组装的、以 Session Log 为模型上下文权威源的、用 Seam 换执行世界、用正交安全链管策略的 Agent 整机。 社区里另一些响亮名字优化的目标并不相同。下面只对比几个开源的 Agent (cc那个代码泄露其实也算是开源了):Claude Code、OpenClaw、Hermes

另外再补充一点:笑死我了,就在我看 dsh 代码的时候,codex 也开源了。看来也是被卷了,这么急着就开源了,等后面有时间再慢慢看吧。

轻羽云笔记

再对比来看,dsk 的代价也很实在概念多、配置与类型纪律强,虽然可定制化强,但必须遵循他的这套框架才行。总的来说,DeepSeek-Harness 解决“怎么构建 Agent Runtime”;OpenClaw 解决“怎么把 Agent 连接到人和各种渠道”;Hermes 解决“怎么让 Agent 长期学习、记忆和自主工作”;Claude Code 解决“怎么把 Agent 做成一个可靠的软件工程师”。