Clarify DeepSeek Harness architecture

This commit is contained in:
wuyang6
2026-08-14 11:23:05 +08:00
parent a03dc0e1e4
commit de38186ff4
4 changed files with 386 additions and 50 deletions
@@ -1,7 +1,7 @@
--- ---
{ {
"title": "DeepSeek Harness:全插件化 Agent 运行时与评测现状", "title": "DeepSeek Harness:全插件化 Agent 运行时与评测现状",
"summary": "从配置组合、Agent loop、工具审批、事件日志与恢复机制出发,解释 DeepSeek Harness 的架构取舍,并核对公开评测证据是否足以支持采用。", "summary": "从一个 greet 工具的完整生命周期出发,分开解释启动装配、请求调用、事件记录和协议边界,再核对 DeepSeek Harness 的架构取舍与公开评测证据。",
"date": "2026-08-14", "date": "2026-08-14",
"updated": "2026-08-14", "updated": "2026-08-14",
"topic": "agent-systems", "topic": "agent-systems",
@@ -20,54 +20,30 @@
## 调研范围 ## 调研范围
本文面向需要选择或建设 Agent Harness 的研发与技术管理者,回答三个问题:DeepSeek Harness 实际如何组织和运行 Agent;它与常见 coding agent 系统的关键差异是什么;当前公开证据是否足以支持观察、试点或正式采用。 本文面向理解大模型、API 和基本工具调用,但尚不了解 Cordis 或 Agent Runtime 内部结构的读者。它回答三个问题:DeepSeek Harness 实际如何组装和运行 Agent;它与常见 coding agent 系统的关键差异是什么;当前公开证据是否足以支持观察、试点或正式采用。
调研冻结在 2026 年 8 月 14 日,目标代码固定为提交 [`47f943859bef60e4160492346772ded9b24f765a`](https://github.com/deepseek-ai/deepseek-harness/tree/47f943859bef60e4160492346772ded9b24f765a)。横向对照也固定到同日抓取的 Codex、Gemini CLI 与 OpenHands 提交。本文不评价 DeepSeek 模型能力,不使用 GitHub star 数作为成熟度证据,也不把不同项目、不同模型、不同任务下的数字拼成排行榜。 调研冻结在 2026 年 8 月 14 日,目标代码固定为提交 [`47f943859bef60e4160492346772ded9b24f765a`](https://github.com/deepseek-ai/deepseek-harness/tree/47f943859bef60e4160492346772ded9b24f765a)。横向对照也固定到同日抓取的 Codex、Gemini CLI 与 OpenHands 提交。本文不评价 DeepSeek 模型能力,不使用 GitHub star 数作为成熟度证据,也不把不同项目、不同模型、不同任务下的数字拼成排行榜。
## 当前判断 ## 项目位置与必要概念
1. **最值得关注的是扩展边界。** dsh 允许替换的不只包括工具和模型适配器,还包括会话、Agent loop、工具注册表、策略、持久化和 UI。Profile、Bundle 与 patch 可以逐层改写这些配置,比“在固定 Agent 核心外围加插件”更激进。 DeepSeek Harness(命令名 `dsh`)不是一个模型,也不是某个单独工具的封装。它是一个 **Agent Harness**:负责把模型、工具、会话状态、执行循环、权限策略和用户入口组装成一个可以持续运行的 Agent 应用。
2. **插件由完整主链路连接。** 一次 turn 通过 durable session event、live agent event 和 capability waterfall 串成完整流程;工具审批、执行和结果回写有明确顺序;会话日志同时服务模型上下文、恢复、分叉和 UI 投影。
3. **工程质量证据多于效果证据。** 仓库有高覆盖率单元测试、keyless snapshot、真实 API e2e 入口、Web 性能/压力 runner 和 CI runner benchmark;但在冻结范围内没有发现公开的 Agent 任务成功率成绩。
4. **现在的合理动作是观察或隔离试点。** Developer Preview、兼容性主动破坏、会话格式 v0、无正式 GitHub Release,以及缺少可复现的目标任务评测,都会阻断生产级采用。
## 一次 Agent 请求如何流过系统 先建立四个最小概念:
![DeepSeek Harness 从配置组合到会话持久化的主链路](/articles/deepseek-harness-architecture-evaluation/dsh-agent-flow.svg) | 名称 | 白话解释 | 在 dsh 中负责什么 |
|---|---|---|
| Agent | 能接收目标、调用模型和工具、保留状态并继续工作的程序 | 一个 Agent 由模型、工具、会话和执行循环共同组成 |
| Agent Harness | 承载 Agent 的运行外壳 | 启动组件、接收请求、管理状态、执行工具并暴露 Web/协议入口 |
| Cordis | dsh 进程内部的插件装配器和生命周期管理器 | 决定有哪些插件、依赖是否满足,以及变化时如何卸载和重建 |
| Agent loop | 一次任务中的“模型—工具—模型”循环 | 组装上下文,调用模型,执行工具,再把结果交回模型直到结束 |
启动时,dsh 先从 Profile 读取有序 Bundle,再叠加 Profile、Harness home 和命令行 patch,得到实际 Cordis 插件树。官方架构文档明确写到,模型适配器、工具注册表、会话日志和 Agent loop 本身都是插件;配置行可以被更高层 patch 整体替换。[架构文档](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/architecture.md) dsh 主体是 TypeScript monorepo,运行在 Node.js ESM 环境;冻结提交的根配置要求 Node.js 22.19 以上或 24 以上。Cordis 插件在运行时是由 Node 加载的 JavaScript/TypeScript 模块,通常导出 `apply(ctx, config)`。[根 package.json](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/package.json) [第一个 Cordis 插件](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/01-first-plugin.zh.md)
运行时,输入先进入 Agent inbox。loop 在每个 step 前组装系统提示、工具 schema 与从会话日志派生的历史,然后发出 `agent/pre-step`;通过后写入 `step/start` 与用户消息,再经 `agent/request` 调用 LLM。模型产生工具调用时,执行路径依次经过 `tools/pre-execute`、审批、guard、`tools/execute`、工具本体、`tools/post-execute` 和结果固化。需要审批却没有可用 answerer 时,`ask` 会退化为拒绝,而不是静默放行。[Tools README](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/tools/README.md) 理解这套架构最重要的前提是把两个问题分开:**启动装配**回答“哪些组件存在、谁依赖谁、谁随谁卸载”;**请求执行**回答“用户的一条消息在已经启动的组件之间怎样移动”。Cordis 插件树主要回答前一个问题,不是用户请求的调用顺序。
这些事实被追加到 `SessionEvent` 日志。模型历史从日志派生;resume、fork、轨迹视图和持久化也复用同一事实源。若进程在工具调用中断,恢复逻辑会区分“工具尚未开始”和“工具结果未知”,后者会提示模型先验证副作用,避免盲目重试。[持久化说明](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/session/session-persistence/README.md) ## 一次完整的 `greet("Ada")` 走查
这条链路的价值在于,扩展不必侵入一个巨大核心:插件可以提供服务、监听或包裹 waterfall,并在卸载时撤销注册。Cordis 预印本将其归纳为时间可组合性与空间可组合性:一方面追踪可逆 effect,另一方面根据依赖变化重新协调组件。但论文仍处于 active revision,适合作为设计解释,不应当作稳定标准。[Cordis 预印本](https://github.com/cordiverse/paper) 假设现有 Profile 已经包含 LLM、工具注册表、会话、Agent loop 和 Web/ACP/SDK 入口。我们只增加一个虚构示例包 `@acme/dsh-greet-tool`,让模型能够调用 `greet`。下面代码根据官方教程的工具插件写法精简;它只演示插件机制,不是仓库已经发布的包。
## Cordis 插件树到底怎样实现
“插件树”容易让人误以为是一棵类继承树。dsh 里实际叠加了三层机制:
1. **配置层决定“挂载什么”。** Profile 先列出有序 Bundle;启动时再依次应用 Bundle patch、Profile 自身 patch、Harness home patch 和命令行 `--patch`。每个配置行用 `id` 作稳定身份,`name` 指向插件模块,还可带 `config`、`inject` 和 `disabled`。后层 patch 按 `id` 命中前层行时,替换整个 `config`,不做字段级深合并。[CLI 组合参考](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/apps/cli/reference/README.md)
2. **Cordis 运行时决定“何时激活”。** 根 `Context` 下的 Loader 把每行配置挂成一个插件实例,每个实例都有 Fiber 状态。插件通过 `apply(ctx, config)` 贡献能力,通过 `inject` 声明必需服务。因此 YAML 行的先后不决定加载顺序:依赖未就绪的插件保持 `PENDING`,服务出现后才激活。[Cordis 服务教程](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/03-services.zh.md)
3. **effect 层决定“怎样安全拆除”。** `ctx.tools.register(...)`、`ctx.on(...)`、服务注册和子插件都与当前插件生命周期绑定。配置删除、HMR 或服务提供方消失时,Cordis 会卸载相关 Fiber,反向撤销注册;服务恢复后,依赖它的插件再重新加载。[Cordis 生命周期教程](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md)
可以把一个简化后的运行时理解为:
```text
Root Context
└─ Loader
├─ tools provider Fiber → 提供 ctx.tools
├─ greet-tool Fiber → inject: tools
├─ llm provider Fiber → 提供 ctx.llm
├─ agent-loop Fiber → 消费 agents / tools / llm
└─ surface Fiber → Web、ACP 或 SDK JSON-RPC
```
这棵“树”同时包含两种关系:Loader 建立的父子生命周期,以及 `inject` 建立的服务依赖图。Bundle 是生成配置行的发布层,并不会成为一个不可拆的运行时黑盒。
### 一个最小例子:向模型增加 `greet` 工具
下面是官方 Cordis 教程示例的精简版。插件不自己启动 Agent,只声明“我需要 `tools` 服务,并向它注册一个工具”:
```ts ```ts
import type { Context } from '@deepseek-ai/cordis' import type { Context } from '@deepseek-ai/cordis'
@@ -91,7 +67,7 @@ export function apply(ctx: Context) {
} }
``` ```
假设将它发布并安装为虚构示例包 `@acme/dsh-greet-tool`,可在 Profile 的 `cordis.patch.yml` 插入一行: 把它加入 Profile 的 `cordis.patch.yml`:
```yaml ```yaml
- insert: - insert:
@@ -99,20 +75,123 @@ export function apply(ctx: Context) {
name: '@acme/dsh-greet-tool' name: '@acme/dsh-greet-tool'
``` ```
这个例子假定所选 Profile 已有 `tools`、LLM 和 agent loop 等基础行;它只演示新能力如何插入既有 Agent。完整过程是:Loader 看到 `greet-tool` 行 → 发现它需要 `tools`,Fiber 在缺失时保持 `PENDING` → `ctx.tools` 就绪后调用 `apply` 并进入 `ACTIVE` → `greet` 的 schema 进入模型工具列表 → 模型发出 `greet({"name":"Ada"})` 时经标准工具管线执行 → 返回 `Hello, Ada!`。删掉这行或 HMR 热替换包时,Loader 根据稳定 `id` 重挂变化节点;旧 Fiber 进入卸载并最终 `DISPOSED`,工具注册随 effect 自动撤销。官方完整例子还演示了如何监听 `tools/result` 事件。[进入 Harness 教程](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/07-into-the-harness.zh.md) 这个工具从配置到卸载会经历六个连续阶段。
### “接入协议”要先区分方向 ### 第一阶段:配置先决定“准备挂载什么”
| 接入面 | 连接方式 | 解决的问题 | dsh 不会直接把某一个 YAML 文件原样交给 Cordis。它先读取 Profile 指定的有序 Bundle,依次应用各 Bundle 的 patch,再叠加 Profile 自身 patch、Harness home patch 和本次命令行 `--patch`,最终得到一组有效配置行。`greet-tool` 是其中一行;它的 `id` 是稳定身份,`name` 指向要加载的模块。[CLI 组合参考](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/apps/cli/reference/README.md)
如果后层 patch 命中同一个 `id`,该行的 `config` 会整体替换,不是字段级深合并。这里的配置合成只回答“本次启动要挂载什么”,尚未执行任何用户请求。
### 第二阶段:Loader 创建 Fiber,`inject` 决定何时启动
启动器创建根 `Context` 并挂载 Loader。Loader 解析 `@acme/dsh-greet-tool`,为这次插件挂载创建一个 Fiber。Fiber 不是线程或操作系统进程,而是 **一个插件实例的运行时句柄**:它记录父上下文、配置、依赖、状态、注册的 effect 和清理过程。
插件导出了 `inject = ['tools']`,所以 Cordis 会检查名为 `tools` 的 Service 是否已经存在。若不存在,Fiber 保持 `PENDING`,不会调用 `apply`;当 `ctx.tools` 出现后,Fiber 进入 `LOADING`,执行 `apply(ctx)`,完成后进入 `ACTIVE`。因此 YAML 中哪一行写在前面并不决定启动顺序,服务依赖才决定。[Cordis 服务教程](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/03-services.zh.md)
`apply` 中的 `ctx.tools.register(...)` 把 `greet` 的名称、参数 schema、返回值渲染和执行函数注册到工具运行时。该注册本身属于一个 effect;Cordis 会记住对应的 disposer,以便插件卸载时自动注销工具。[进入 Harness 教程](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/07-into-the-harness.zh.md)
### 第三阶段:用户请求进入已经启动的 Agent
用户通过 Web、ACP 或 SDK 输入“向 Ada 问好”。入口适配器把消息交给对应 Agent 的 inbox。Agent loop 取得待处理输入,从会话日志派生历史,加入系统提示和当前可用工具 schema,然后进入一个 model step。此时 `greet` 已经是工具列表中的一项;Cordis 不再决定模型是否调用它,Cordis 只保证工具已经正确注册。
模型可以直接回答,也可以产生工具调用。这个例子中,模型返回 `greet({"name":"Ada"})`,于是 Agent loop 把调用交给统一 Tools Runtime。
### 第四阶段:工具经过统一执行管线
工具调用不会从模型直接跳进示例函数。标准路径依次经过 `tools/pre-execute`、审批、guard、`tools/execute`、工具本体、`tools/post-execute` 和结果固化。若某项策略返回 `ask`,但当前入口没有可用审批 answerer,系统会拒绝而不是静默放行。[Tools README](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/core/tools/README.md)
`greet` 执行后返回 `Hello, Ada!`。Tools Runtime 将结果交回 Agent loop,Agent loop 再把它放入下一次模型请求。模型据此生成最终回答,例如“已向 Ada 问好”。这就是 Agent loop 所负责的“模型 → 工具 → 模型”循环。
### 第五阶段:SessionEvent 在旁路记录事实
用户消息、step 开始、模型消息、工具调用、工具结果和最终回答会分别追加为 `SessionEvent`。它不是调用链中的“下一台服务器”,也不负责把工具结果转发给模型;它是执行过程中形成的 durable fact。模型历史、resume、fork、轨迹视图、持久化和 UI 投影都从同一事实源派生。[持久化说明](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/session/session-persistence/README.md)
如果进程在工具调用期间中断,恢复逻辑会检查日志:工具尚未真正开始,可以安全标记失败;工具可能已经产生副作用但没有留下结果,则标记“结果未知”,提示模型先验证外部状态,避免盲目重试。这解释了事件日志为什么与执行链同样重要,但二者仍是不同关系。
### 第六阶段:配置或依赖变化时自动撤销
Fiber 的主要状态为 `PENDING → LOADING → ACTIVE → UNLOADING`,加载失败时进入 `FAILED`。如果 `tools` Service 暂时消失,已经激活的 `greet-tool` 会卸载自己的 effect,回到等待依赖的状态;服务恢复后重新加载。如果配置行被删除,旧 Fiber 完成清理后进入 `DISPOSED`,不能再次启动。HMR 则卸载旧实例,再根据新模块挂载替代实例。[Cordis 生命周期教程](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md) [组合与 HMR](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/06-composition-and-hmr.zh.md)
卸载时,`greet` 的工具注册、事件监听、子插件和通过 `ctx.effect()` 管理的定时器或连接都会执行 disposer。插件不需要在每个退出分支手工寻找自己曾经注册的对象。这个“注册与撤销属于同一生命周期”是 Cordis 最核心的工程价值。
到这里可以用一句话复述完整过程:**配置决定挂载 `greet-tool`;`inject` 让它等待 `tools`;Fiber 激活后注册工具;Agent loop 在一次请求中让模型调用它;SessionEvent 记录事实;依赖或配置变化时 effect 自动撤销。**
## 启动装配:Profile 如何变成运行中的插件
![启动装配:谁被创建、谁等待谁;这不是请求调用顺序](/articles/deepseek-harness-architecture-evaluation/dsh-startup-assembly.svg)
这张图只回答启动问题。可以把它拆成配置层和运行时层。
### 配置层:Profile、Bundle 与 patch
- **Bundle** 是可复用的默认插件组合,例如一组基础服务和工具。它贡献配置行或 patch,但运行时不会把整个 Bundle 当成不可拆的黑盒。
- **Profile** 是一种产品或运行形态的入口。它选择有序 Bundle,并附加自己的 patch,例如组合 Web 或 headless 版本。
- **Harness home patch** 是用户或部署环境的持久覆盖。
- **CLI `--patch`** 是本次启动最后应用的临时覆盖。
合成结果是一组带 `id` 的配置行。每行通常包含 `name`、`config`、`inject` 和 `disabled` 等字段。稳定 `id` 让 Loader 判断一项变化是在更新已有节点,还是删除旧节点后增加新节点。
### 运行时层:Context、Loader 与 Fiber
根 `Context` 是进程内能力和事件的访问入口;Loader 根据有效配置加载模块,并把每次挂载变成 Fiber。官方教程支持函数、带 `apply` 的对象和 `Service` 子类三种插件形态。`apply(ctx, config)` 只描述插件向当前上下文贡献什么,启动器和 Loader 负责实际挂载。[第一个 Cordis 插件](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/cordis-tutorial/01-first-plugin.zh.md)
Fiber 负责把一次插件应用变成可观察、可等待、可失败、可卸载的运行实例。它记录所需 Service 的具体实现;提供方变化时,Cordis 可以比较依赖并只重载受影响的插件,而不是重启整个进程。
### 依赖层:Service 与 `inject`
Service 是插件提供给其他插件的具名能力,例如 `ctx.llm`、`ctx.tools` 和 `ctx.sessions`。消费者声明 `inject: ['tools']`,只表示“我需要工具服务”,不绑定某一个具体提供包。部署可以替换 provider,而消费插件代码无需改变。
`inject` 是硬依赖:缺失时 Fiber 保持 `PENDING`。可选能力则不应写入 `inject`,而是在使用处通过 `ctx.get(...)` 探测。服务名称共享一个命名空间,这也意味着大型部署必须治理名称、提供方和配置来源。
## 请求执行:一次消息如何经过 Agent loop
![一次 greet 请求:消息在已启动组件之间移动,SessionEvent 是记录旁路](/articles/deepseek-harness-architecture-evaluation/dsh-greet-request.svg)
这张图只回答运行问题。系统已经完成装配,因此请求链中不再出现 Profile、Bundle 和 Loader:
1. Web、ACP 或 SDK 入口把用户输入交给 Agent inbox。
2. Agent loop 取得输入,组装系统提示、历史和工具 schema。
3. Agent loop 调用 LLM;模型可以直接回答或请求工具。
4. 工具调用进入统一 Tools Runtime,经过策略、审批、guard 和执行阶段。
5. 工具结果回到 Agent loop,再进入下一次模型 step。
6. 模型生成最终消息,Agent loop 提交结果并在没有待处理工作时回到 idle。
dsh 同时存在两类容易混淆的“事件”。`SessionEvent` 是写入会话日志的 durable fact,承担重建、恢复和投影;`agent/pre-step`、`agent/request`、`tools/pre-execute` 等 live event 或 waterfall 是运行时扩展点,插件可以监听、修改、放行或短路当前过程。前者回答“发生过什么”,后者参与“现在怎样继续执行”。
## 插件树、依赖图和调用链不是同一张图
| 关系 | 它回答的问题 | `greet` 例子 |
|---|---|---| |---|---|---|
| Cordis 插件 | 进程内:TypeScript `apply(ctx, config)`、`inject`、Service、Event、effect | 增加或替换模型、工具、会话、策略、loop 和 UI 组件 | | 插件父子与生命周期 | 谁由谁挂载,父节点卸载时谁一起清理 | Loader 挂载 `greet-tool` Fiber;删除配置时旧实例被清理 |
| Profile / Bundle / patch | 部署时:YAML 配置行与稳定 `id` | 选哪些插件、如何覆盖默认值;它不是远程协议 | | Service 依赖 | 谁必须等待谁,提供方变化时谁需要重载 | `greet-tool` 通过 `inject` 等待 `tools` |
| Web | 浏览器 → dsh:HTTP POST 一元调用;dsh → 浏览器:WebSocket 事件下行 | 产品 UI、会话展示和交互式操作;业务 Remote 由 Typert 生成严格描述 | | 请求调用 | 一条消息在运行时经过哪些已经激活的组件 | Agent loop → LLM → Tools → `greet` → LLM |
| ACP | 外部 Agent 客户端 ↔ dsh:Agent Client Protocol over newline-delimited JSON-RPC stdio | 标准化的 `session/new`、`session/prompt`、取消和一次性权限回答 |
| Python / TypeScript SDK | 评测器或程序 ↔ dsh:项目自有的逐行 JSON-RPC 2.0 stdio | 批量任务、自动化和 benchmark runner;方法为 `initialize`、`session/prompt`、`shutdown` 及事件/状态通知 |
| MCP client | dsh ↔ 外部工具服务器:MCP stdio 或 Streamable HTTP | 发现工具并注册为 `mcp__<server>__<tool>`;当前不桥接 Resources 和 Prompts |
关键区别是:**Web 是浏览器产品面;ACP 和 SDK 是“从外部驱动整个 Agent”;MCP 是“把外部工具接给 Agent”;Cordis 则是“在进程内改变 Agent 由哪些部件构成”。** 它们解决的问题不同,不应放在同一层比较。ACP 只暴露基线自动化能力,而 SDK 线协议当前没有版本协商和单 prompt 取消,都不能等同于 Web 产品面的完整功能。[ACP 协议契约](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/acp/acp/README.md) [SDK 线协议](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/sdk/protocol/README.md) [Web Connection](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/client/connection/README.md) [MCP client](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/mcp/mcp-client/README.md) 所谓“Cordis 插件树”首先是一套运行实例的所有权结构;再叠加 `inject` 后,形成服务依赖图。一次请求的调用链则发生在这些实例都准备好之后。三者可能涉及相同插件,但箭头含义不同,不能用一棵树同时表示。
## Web、ACP、SDK、MCP 与 Cordis 的边界
![DeepSeek Harness 的进程与协议边界](/articles/deepseek-harness-architecture-evaluation/dsh-protocol-boundaries.svg)
先看进程边界,再看协议名称:Web、ACP 和 SDK 把请求从外部送入 Agent;MCP 把外部工具送入 Tools;Cordis 位于 dsh 的 Node.js 进程内部,负责组件装配,不是远程协议。
| 接入面 | 连接方向与载体 | 解决的问题 |
|---|---|---|
| Cordis 插件 | dsh 进程内的 ESM 模块:`apply`、Service、Event、effect | 增加或替换模型、工具、会话、策略、loop 和 UI 组件 |
| Web | 浏览器 → dsh 为 HTTP POST;dsh → 浏览器为 WebSocket 事件 | 产品 UI、会话展示和交互操作 |
| ACP | 外部 Agent Client ↔ dsh,newline-delimited JSON-RPC over stdio | 创建会话、发送 prompt、取消和一次性权限回答的基线自动化 |
| Python / TypeScript SDK | 评测器或程序 ↔ dsh,项目自有 line JSON-RPC 2.0 over stdio | 批量任务、自动化和 benchmark runner |
| MCP client | dsh ↔ 外部 MCP Server,stdio 或 Streamable HTTP | 发现外部工具并注册为 `mcp__<server>__<tool>`;当前只桥接 Tools |
Profile、Bundle 和 patch 不在表中,因为它们是部署配置方式,不是通信协议。ACP 也不等于完整 Web 产品面:它只暴露自动化基线;SDK 线协议当前没有版本协商和单 prompt 取消;MCP 当前不桥接 Resources 和 Prompts。[ACP 协议契约](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/acp/acp/README.md) [SDK 线协议](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/sdk/protocol/README.md) [Web Connection](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/client/connection/README.md) [MCP client](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/mcp/mcp-client/README.md)
## 架构判断
1. **最值得关注的是扩展边界。** dsh 允许替换的不只包括工具和模型适配器,还包括会话、Agent loop、工具注册表、策略、持久化和 UI。Profile、Bundle 与 patch 可以逐层改写配置,比“在固定 Agent 核心外围加插件”更激进。
2. **Cordis 解决的是组合变化。** Fiber、Service、`inject` 和 effect 让插件能随配置与依赖变化安全地加载、等待、卸载和重建;它并不替代 Agent loop。
3. **Agent loop、工具管线和事件日志形成执行主链。** loop 决定下一步,Tools 执行动作,SessionEvent 保留可恢复事实;三者职责分开。
4. **工程质量证据多于效果证据。** 仓库有高覆盖率单元测试、keyless snapshot、真实 API e2e 入口、Web 性能/压力 runner 和 CI runner benchmark;但在冻结范围内没有发现公开的 Agent 任务成功率成绩。
5. **现在的合理动作是观察或隔离试点。** Developer Preview、兼容性主动破坏、会话格式 v0、无正式 GitHub Release,以及缺少可复现的目标任务评测,都会阻断生产级采用。
## 这套设计解决了什么 ## 这套设计解决了什么
@@ -0,0 +1,84 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="860" viewBox="0 0 1440 860" role="img" aria-labelledby="title desc">
<title id="title">DeepSeek Harness greet 请求调用图</title>
<desc id="desc">用户请求进入已经启动的 Agent,模型调用 greet 工具并返回结果;SessionEvent 在下方记录事实,但不是调用链的下一跳。</desc>
<defs>
<style>
.bg { fill: #f7f4ec; }
.title { font: 700 30px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #183a34; }
.subtitle { font: 400 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #60706a; }
.box { fill: #fffdf8; stroke: #286458; stroke-width: 2; rx: 18; }
.focus { fill: #fff2df; stroke: #ca6715; stroke-width: 2.4; rx: 18; }
.event { fill: #edf5f1; stroke: #557b72; stroke-width: 2; rx: 18; }
.label { font: 650 19px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1f413a; }
.body { font: 400 16px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #354c47; }
.small { font: 400 15px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #66766f; }
.solid { stroke: #286458; stroke-width: 3; fill: none; marker-end: url(#arrow); }
.return { stroke: #ca6715; stroke-width: 3; fill: none; marker-end: url(#arrow-orange); }
.dash { stroke: #7a8c86; stroke-width: 2.2; stroke-dasharray: 8 8; fill: none; marker-end: url(#arrow-gray); }
</style>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#286458"/></marker>
<marker id="arrow-orange" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#ca6715"/></marker>
<marker id="arrow-gray" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#7a8c86"/></marker>
</defs>
<rect class="bg" width="1440" height="860" rx="32"/>
<text class="title" x="64" y="64">一次 greet 请求:消息在已启动的组件之间怎样移动</text>
<text class="subtitle" x="64" y="98">这是一张运行时调用图;Profile、Bundle 和 Fiber 装配已经完成。</text>
<rect class="box" x="58" y="168" width="180" height="106"/>
<text class="label" x="82" y="207">用户入口</text>
<text class="body" x="82" y="238">Web / ACP / SDK</text>
<rect class="box" x="288" y="168" width="180" height="106"/>
<text class="label" x="312" y="207">Agent inbox</text>
<text class="body" x="312" y="238">“向 Ada 问好”</text>
<rect class="box" x="518" y="150" width="220" height="142"/>
<text class="label" x="542" y="190">Agent loop</text>
<text class="body" x="542" y="222">组装历史、系统提示</text>
<text class="body" x="542" y="248">和可用工具 schema</text>
<rect class="focus" x="788" y="150" width="220" height="142"/>
<text class="label" x="812" y="190">LLM</text>
<text class="body" x="812" y="222">决定调用工具:</text>
<text class="body" x="812" y="250">greet({ name: "Ada" })</text>
<rect class="box" x="1058" y="132" width="320" height="178"/>
<text class="label" x="1082" y="170">Guarded tool pipeline</text>
<text class="body" x="1082" y="204">pre-execute → approval → guard</text>
<text class="body" x="1082" y="232">→ execute → post-execute → result</text>
<text class="small" x="1082" y="268">最终调用 greet handler</text>
<path class="solid" d="M238 221 H280"/>
<path class="solid" d="M468 221 H510"/>
<path class="solid" d="M738 221 H780"/>
<path class="solid" d="M1008 221 H1050"/>
<rect class="focus" x="1058" y="374" width="320" height="112"/>
<text class="label" x="1082" y="414">greet 工具返回</text>
<text class="body" x="1082" y="450">“Hello, Ada!”</text>
<path class="return" d="M1218 310 V366"/>
<rect class="box" x="788" y="374" width="220" height="112"/>
<text class="label" x="812" y="414">LLM 继续生成</text>
<text class="body" x="812" y="450">把工具结果写成回答</text>
<path class="return" d="M1058 430 H1016"/>
<rect class="box" x="518" y="374" width="220" height="112"/>
<text class="label" x="542" y="414">Agent loop 收束</text>
<text class="body" x="542" y="450">提交 assistant/message</text>
<path class="return" d="M788 430 H746"/>
<rect class="box" x="288" y="374" width="180" height="112"/>
<text class="label" x="312" y="414">用户收到回答</text>
<text class="body" x="312" y="450">“已向 Ada 问好”</text>
<path class="return" d="M518 430 H476"/>
<rect class="event" x="140" y="610" width="1160" height="156"/>
<text class="label" x="170" y="650">SessionEvent 日志:记录旁路,不是请求的“下一跳”</text>
<text class="body" x="170" y="687">用户消息、step/start、模型消息、工具调用、工具结果和最终回答分别追加为 durable facts。</text>
<text class="body" x="170" y="719">历史重建、resume、fork、故障修复和 UI 投影都从这些事实派生。</text>
<path class="dash" d="M378 274 C378 430 380 520 444 600"/>
<path class="dash" d="M1218 486 C1218 540 1130 560 1080 602"/>
<path class="dash" d="M628 486 C628 545 650 565 676 602"/>
<text class="small" x="66" y="824">记忆锚点:Agent loop 决定“下一步做什么”;Tools 执行“具体动作”;SessionEvent 记录“发生过什么”。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -0,0 +1,80 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="860" viewBox="0 0 1440 860" role="img" aria-labelledby="title desc">
<title id="title">DeepSeek Harness 协议边界图</title>
<desc id="desc">Web、ACP 和 SDK 从外部驱动 Agent;MCP 把外部工具接入 Tools;Cordis 位于 dsh 的 Node 进程内部,负责组件装配而不是远程通信。</desc>
<defs>
<style>
.bg { fill: #f7f4ec; }
.title { font: 700 30px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #183a34; }
.subtitle { font: 400 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #60706a; }
.outside { fill: #fffdf8; stroke: #70817b; stroke-width: 2; rx: 18; }
.inside { fill: #edf5f1; stroke: #286458; stroke-width: 2; rx: 18; }
.boundary { fill: none; stroke: #286458; stroke-width: 3; stroke-dasharray: 12 8; rx: 26; }
.focus { fill: #fff2df; stroke: #ca6715; stroke-width: 2.4; rx: 18; }
.label { font: 650 20px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1f413a; }
.body { font: 400 16px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #354c47; }
.small { font: 400 15px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #66766f; }
.arrow { stroke: #286458; stroke-width: 3; fill: none; marker-end: url(#arrow); }
.bi { stroke: #ca6715; stroke-width: 3; fill: none; marker-start: url(#arrow-orange-start); marker-end: url(#arrow-orange); }
</style>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#286458"/></marker>
<marker id="arrow-orange" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#ca6715"/></marker>
<marker id="arrow-orange-start" markerWidth="10" markerHeight="10" refX="1" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M9,0 L9,6 L0,3 z" fill="#ca6715"/></marker>
</defs>
<rect class="bg" width="1440" height="860" rx="32"/>
<text class="title" x="64" y="64">协议边界:谁从哪里接进 Harness</text>
<text class="subtitle" x="64" y="98">先看进程边界,再看协议名称;Cordis 位于 dsh 进程内部,不是远程协议。</text>
<text class="small" x="62" y="150">dsh 进程外:驱动端</text>
<rect class="outside" x="60" y="176" width="260" height="116"/>
<text class="label" x="86" y="216">浏览器</text>
<text class="body" x="86" y="250">产品 UI 和交互</text>
<rect class="outside" x="60" y="342" width="260" height="116"/>
<text class="label" x="86" y="382">ACP Client</text>
<text class="body" x="86" y="416">编辑器或另一个 Agent</text>
<rect class="outside" x="60" y="508" width="260" height="116"/>
<text class="label" x="86" y="548">Python / TS 程序</text>
<text class="body" x="86" y="582">自动化、批量任务、评测器</text>
<rect class="boundary" x="420" y="132" width="680" height="604"/>
<text class="label" x="450" y="170">dsh:TypeScript / Node.js 进程</text>
<rect class="inside" x="466" y="202" width="250" height="92"/>
<text class="label" x="490" y="240">Web adapter</text>
<text class="body" x="490" y="268">HTTP POST / WebSocket</text>
<rect class="inside" x="466" y="342" width="250" height="92"/>
<text class="label" x="490" y="380">ACP adapter</text>
<text class="body" x="490" y="408">ACP JSON-RPC over stdio</text>
<rect class="inside" x="466" y="482" width="250" height="92"/>
<text class="label" x="490" y="520">SDK server</text>
<text class="body" x="490" y="548">line JSON-RPC 2.0 over stdio</text>
<rect class="focus" x="792" y="274" width="250" height="118"/>
<text class="label" x="816" y="314">Agent loop</text>
<text class="body" x="816" y="346">Web / ACP / SDK</text>
<text class="body" x="816" y="370">都从外部驱动整个 Agent</text>
<rect class="focus" x="792" y="454" width="250" height="118"/>
<text class="label" x="816" y="494">Tools registry</text>
<text class="body" x="816" y="526">本地工具与 MCP 工具</text>
<text class="body" x="816" y="550">以统一名称注册</text>
<rect class="inside" x="548" y="634" width="414" height="68"/>
<text class="label" x="572" y="676">Cordis:装配并管理以上进程内插件</text>
<path class="arrow" d="M320 234 H458"/>
<path class="arrow" d="M320 400 H458"/>
<path class="arrow" d="M320 566 H458"/>
<path class="arrow" d="M716 248 C760 248 760 316 784 326"/>
<path class="arrow" d="M716 388 H784"/>
<path class="arrow" d="M716 528 C760 528 760 364 784 350"/>
<path class="arrow" d="M917 392 V446"/>
<text class="small" x="1146" y="150">dsh 进程外:能力提供端</text>
<rect class="outside" x="1140" y="342" width="250" height="146"/>
<text class="label" x="1166" y="382">MCP Server</text>
<text class="body" x="1166" y="416">stdio 或 Streamable HTTP</text>
<text class="small" x="1166" y="450">当前只桥接 Tools</text>
<path class="bi" d="M1042 512 C1094 512 1094 420 1132 420"/>
<rect class="outside" x="120" y="778" width="1200" height="54"/>
<text class="body" x="148" y="812">记忆锚点:Web / ACP / SDK 把“请求”送进 Agent;MCP 把“工具”送进 Tools;Cordis 只负责 dsh 进程内的组装与生命周期。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="860" viewBox="0 0 1440 860" role="img" aria-labelledby="title desc">
<title id="title">DeepSeek Harness 启动装配图</title>
<desc id="desc">Profile、Bundle 和 patch 先形成有效配置,Cordis Loader 再创建插件 Fiber;虚线表示服务依赖,不表示请求调用顺序。</desc>
<defs>
<style>
.bg { fill: #f7f4ec; }
.title { font: 700 30px -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans CJK SC", sans-serif; fill: #183a34; }
.subtitle { font: 400 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #60706a; }
.box { fill: #fffdf8; stroke: #286458; stroke-width: 2; rx: 18; }
.config { fill: #edf5f1; stroke: #286458; stroke-width: 2; rx: 16; }
.focus { fill: #fff2df; stroke: #ca6715; stroke-width: 2.4; rx: 18; }
.pending { fill: #fff9e8; stroke: #aa7a12; stroke-width: 2; rx: 18; }
.label { font: 650 20px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1f413a; }
.body { font: 400 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #354c47; }
.small { font: 400 15px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #66766f; }
.solid { stroke: #286458; stroke-width: 3; fill: none; marker-end: url(#arrow); }
.dash { stroke: #b17220; stroke-width: 2.5; stroke-dasharray: 9 8; fill: none; marker-end: url(#arrow-orange); }
.divider { stroke: #cdd8d2; stroke-width: 1.5; }
</style>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#286458"/></marker>
<marker id="arrow-orange" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="#b17220"/></marker>
</defs>
<rect class="bg" width="1440" height="860" rx="32"/>
<text class="title" x="64" y="64">启动装配:谁被创建、谁等待谁</text>
<text class="subtitle" x="64" y="98">这是一张依赖和生命周期图,不是一次用户请求的调用顺序。</text>
<text class="small" x="66" y="143">第一步:多层配置合成最终插件清单</text>
<rect class="config" x="64" y="165" width="240" height="102"/>
<text class="label" x="88" y="201">Bundle</text>
<text class="body" x="88" y="232">可复用的默认插件组合</text>
<rect class="config" x="344" y="165" width="240" height="102"/>
<text class="label" x="368" y="201">Profile patch</text>
<text class="body" x="368" y="232">为某种产品形态做选择</text>
<rect class="config" x="624" y="165" width="240" height="102"/>
<text class="label" x="648" y="201">Harness home patch</text>
<text class="body" x="648" y="232">用户或部署环境覆盖</text>
<rect class="config" x="904" y="165" width="210" height="102"/>
<text class="label" x="928" y="201">CLI --patch</text>
<text class="body" x="928" y="232">本次启动覆盖</text>
<rect class="focus" x="1154" y="165" width="222" height="102"/>
<text class="label" x="1178" y="201">有效配置行</text>
<text class="body" x="1178" y="232">按稳定 id 得到结果</text>
<path class="solid" d="M304 216 H336"/>
<path class="solid" d="M584 216 H616"/>
<path class="solid" d="M864 216 H896"/>
<path class="solid" d="M1114 216 H1146"/>
<text class="small" x="1065" y="292">后层命中同一 id 时,整段 config 替换,不做深合并</text>
<line class="divider" x1="64" y1="326" x2="1376" y2="326"/>
<text class="small" x="66" y="371">第二步:Loader 为配置行创建 Fiber;Fiber 是否启动由服务依赖决定</text>
<path class="solid" d="M1000 364 H1050"/>
<text class="small" x="1060" y="370">Loader 创建 Fiber</text>
<path class="dash" d="M1212 364 H1262"/>
<text class="small" x="1272" y="370">消费者 → 提供方</text>
<rect class="focus" x="524" y="394" width="392" height="92"/>
<text class="label" x="550" y="430">Root Context + Loader</text>
<text class="body" x="550" y="460">读取有效配置,为每一行挂载运行实例 Fiber</text>
<rect class="box" x="70" y="560" width="245" height="128"/>
<text class="label" x="94" y="598">LLM provider Fiber</text>
<text class="body" x="94" y="630">提供 ctx.llm</text>
<text class="small" x="94" y="660">ACTIVE</text>
<rect class="box" x="370" y="560" width="245" height="128"/>
<text class="label" x="394" y="598">Tools provider Fiber</text>
<text class="body" x="394" y="630">提供 ctx.tools</text>
<text class="small" x="394" y="660">ACTIVE</text>
<rect class="pending" x="670" y="560" width="245" height="128"/>
<text class="label" x="694" y="598">greet-tool Fiber</text>
<text class="body" x="694" y="630">inject: tools</text>
<text class="small" x="694" y="660">PENDING → ACTIVE</text>
<rect class="pending" x="970" y="540" width="330" height="168"/>
<text class="label" x="994" y="578">Agent loop Fiber</text>
<text class="body" x="994" y="610">inject: agents, sessions,</text>
<text class="body" x="994" y="638">llm, tools, systemPrompt</text>
<text class="small" x="994" y="674">依赖齐全后才能处理请求</text>
<path class="solid" d="M650 486 L206 550"/>
<path class="solid" d="M690 486 L500 550"/>
<path class="solid" d="M750 486 L792 550"/>
<path class="solid" d="M810 486 L1110 530"/>
<path class="dash" d="M670 624 H625"/>
<text class="small" x="628" y="610">等待 tools</text>
<path class="dash" d="M970 592 C850 505 350 500 248 551"/>
<text class="small" x="625" y="516">等待 llm</text>
<path class="dash" d="M970 650 C860 710 694 720 600 676"/>
<text class="small" x="735" y="744">等待 tools 等服务</text>
<rect class="config" x="64" y="770" width="1312" height="58"/>
<text class="body" x="88" y="806">记忆锚点:配置行决定“挂载什么”;inject 决定“何时能启动”;Fiber/effect 决定“变化时怎样卸载和重建”。</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB