DeepSeek Harness 理解 Harness 的设计哲学 - 可组合的插件运行时

本文结合 deepseek-harness 仓库源码与官方 docs/cordis-tutorial/ 教程编写,并在最后一章与 Claude Code、Codex 以及 AgentScope(阿里) 做深度对比。读完你应能:理解 Harness 的设计哲学、跑通从"Hello 插件"到"真实编码 Agent"的完整链路、看懂 cordis.yml 组合与 HMR,并清楚它与其他主流框架的区别。

GitHub: deepseek-harness.github.io/deepseek-ha...

一、DeepSeek Harness 是什么

1.1 一句话定义

DeepSeek Harness 是 DeepSeek 官方出品的 Agent(智能体)运行框架 。它不是一个"写死的助手",而是一个可组合的插件运行时 :你用一份 YAML 配置文件(cordis.yml)把"会话管理、系统提示词、工具、LLM 适配器、文件访问、子进程、沙箱、乃至 agent 主循环本身"等全部能力像搭积木一样拼起来。

它的底层是一个名为 Cordis 的微型插件框架(源码 vendored 在 vendor/cordis/)。Cordis 提供一个共享的 Context(上下文),每个能力都是一个挂载到 ctx 上的插件。

一句话定位:

Claude Code 是"产品",DeepSeek Harness 是"框架",AgentScope 是"开发库"。 前者给你一个开箱即用的编码助手;Harness 给你一套可以自由拼装、自托管、可嵌入自己产品的 Agent 引擎;而 AgentScope 给你一套以 Python 为主的"构建智能体应用的库",侧重多智能体与工程化工具链。

1.2 多维度拆解:它到底是什么

要真正理解 Harness,需要从五个维度同时看它:

维度 它是什么 反例(它不是什么)
交付形态 一套框架 + 一组官方插件 + 一个 CLI/ACP 入口 一个封闭、开箱即用的产品
内核范式 基于 Cordis 的插件运行时,一切皆插件 一个硬编码逻辑的单体程序
组合方式 声明式 cordis.yml 配置驱动、按 id 装配 命令式代码里 new 实例
能力边界 抽象服务(Service Definition)+ 可替换 Provider + 消费者 把实现细节写死在调用方
适用范围 可自托管、可嵌入自有产品、可长期会话 仅终端交互的助手

1.3 设计思路:为什么这么设计(核心分析)

DeepSeek Harness 采取"一切皆插件"的设计思路。我们采用插件式开放架构来构建 Agent Harness:模型、工具、技能、会话、沙箱、存储、循环、调度、UI 等所有 Agent 能力均由插件组合而成,可自由替换、灵活重组。

Harness 的设计不是"为了可插拔而可插拔",而是围绕几个明确的工程目标与约束做出的系统性取舍。下面逐条分析。

思路一:用"一切皆插件"消除硬编码的循环逻辑

绝大多数编码助手的 agent 主循环(call model → run tools → repeat)是写死在核心里的。Harness 的反直觉决定是:整个仓库只有 dsh-agent-loop 一个包包含具体循环逻辑packages/core/agent-loop/README.md 明确如此),其余全是抽象服务或扩展点插件。

设计动机:

  • 循环只描述"驱动协议",不掺带具体能力。hooks、sandbox、plan mode、retry、subagent、compaction 等行为全部通过监听 agent/*tools/*session/* 事件实现,而非改循环代码。
  • 这带来"行为在扩展点上、不在循环里"的硬约束------AGENTS.md 原话:"Plugins, not loop changes: new behavior goes on documented extension points; changing agent-loop requires updating docs/architecture.md."
  • 底层原理:当循环成为唯一且稳定的驱动者,所有可变行为都被推到事件订阅侧,于是能力的增删=插件的挂载/卸载,与主循环彻底解耦。

思路二:能力分层的"三角色"模型(capability-seam)

每个能力被刻意拆成三个相互独立演化 的角色(见 docs/glossary.md#capability-seam):

  • Service Definition (服务定义):拥有 ctx.<key> 与词汇表类型的 Cordis Service,是抽象类或具体注册表(如 ShellExecutorWebRuntime),绝不是 TypeScript interface------因为它要作为真实服务被挂载。
  • Service Provider (服务提供者):一种或多种实现,如 dsh-shell-local / dsh-shell-pwsh
  • Consumer (消费者):注入该服务、面向模型暴露工具的插件,如 dsh-tool-bash

设计动机:

  • 角色独立演化:当只有 provider 需要换(本地→沙箱→E2B)时,定义和消费者代码一行都不用动。这把"变化"限制在最窄的边界内。
  • 以 shell 为例:dsh-shell(定义)→ dsh-shell-local / dsh-shell-pwsh(provider,按平台 disabled)+ dsh-bash-sandbox(沙箱 policy)。LLM 同理:dsh-llm(定义)→ dsh-llm-deepseek(原生)/ dsh-llm-pi-ai(多 provider 孪生)。
  • Swappable capability:seam 是"完整能力",不是单个角色------文档特别强调"reserve the term for that meaning",因为误把某一角色当能力,会导致消费者直接依赖实现而破坏可替换性。

思路三:注册即副作用(effect),把生命周期交给框架

AGENTS.md 铁律:"Registrations are effects: every contribution goes through ctx.effect() / ctx.on(); a registry's register() returns the disposer."

设计动机:

  • 插件不持有自己资源的"拆除责任" 。任何注册(ctx.tools.registerctx.on、子插件、服务实例)都附着在调用它的插件上,插件卸载时自动撤销。
  • ctx.plugin(child) 让一个插件把另一个插件挂为"子",父子一起 dispose,递归卸载。
  • 底层原理 :资源所有权 = 插件生命周期,而非手动 if 分支。这从机制上消灭了"忘记移除监听器/关闭定时器"这类资源泄漏------docs/defensive-patterns.md 把"Dispose must reach quiescence"列为头号缺陷类规则:拆除要异步 await 到真正静止,而不是只发一个 kill。

思路四:依赖注入是"持续跟踪",而非一次性检查

消费方写 inject: ['tools'],Cordis 会让插件保持 PENDING 直到 ctx.tools 存在,且运行期若服务消失(provider 被卸载/热替换),依赖方随之卸载,服务恢复后再加载。

设计动机:

  • 配置可替换服务 :卸载 dsh-shell-local、挂载另一个 shell provider,所有 inject: ['shell'] 的插件自动重启用新实现------这就是"框架级热替换"的物理基础。
  • 顺序无关cordis.yml 里插件行序不影响正确性,只影响就绪先后。彻底移除某服务后,依赖方保持 PENDING,既不崩溃也不会半运行。
  • 底层原理:依赖图是运行时动态满足的,而非构建期静态绑定,因此组合(composition)本身是数据(YAML),不是代码。

思路五:"模型可见 ⟺ 已记录"的审计约束

AGENTS.md 硬约束:"Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event."

设计动机:

  • 任何送达模型的输入(工具结果、系统提示词切片、变量)都必须对应一条会话事件,使得会话日志即真相(source of truth) ------可以回放、审计、fork、resume。
  • 会话是一等公民:session(JSONL/SQLite 持久化、投影、血缘)、session-query(SQLite 全文检索)、compaction(压缩+工具结果裁剪)共同支撑长期记忆与合规。
  • 底层原理:把"可重建性"上升为架构不变量,而非依赖开发者自觉。这使得调试一个错误回答 = 重放那条会话事件流,而不是猜测模型当时"看到了什么"。

思路六:显式优于隐式,错则明报,绝不静默

AGENTS.md 多项规则:

  • "Misconfiguration fails loud at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent."
  • "No hardcoded tunables in plugins: deployment-varying choices are validated Config fields changeable from cordis.yml."
  • 跨边界不透明 id 用 Branded<B> 品牌类型,非裸 string;只在校验边界 (config、模型/工具 JSON、文件、worker、进程、线)做运行时校验,同进程类型边界信任 TypeScript

设计动机:

  • 可部署性 来自"可变项都是可校验的 Config",而非代码里 ?? default 的隐藏默认值(那是协议常量/安全不变量才固定的)。
  • 可诊断性来自"缺引用就明报",避免"插件没反应却不知道为什么"(见教程第九章诊断器)。
  • 底层原理:把"部署差异"与"安全不变量"两类变化分离------前者进 Config 受 schema 校验,后者写死且不可被配置绕过。

思路七:安全是分层的,而非单点

docs/defensive-patterns.md 给出具体规则:

  • 生成命令拿到的是清洗过的环境 (丢弃 *KEY*/*SECRET*/*TOKEN*/*PASSWORD*),防止 harness 凭据泄漏进输出或 spill 文件。
  • 临时/spill 文件用私有(0700)目录、随机名、独占 owner-only 打开('wx'0o600),避免可预测路径导致的 symlink 竞争与泄露。
  • 沙箱是一等能力:sandbox(bwrap/Landlock/Seatbelt),执行与文件系统访问都可套沙箱 policy。

设计动机:把"执行不可信输出"当头等威胁,从环境、文件、进程三层同时设防,而非依赖"用户别跑奇怪命令"的约定。

1.3.1 一切皆插件设计

1.3.2 多种运行模式

针对不同的使用场景,DeepSeek Harness 提供四种模式,每种模式会默认加载不同的插件集合:

  • 标准模式:提供完整的工具组合;
  • PTC 模式:程序化工具调用(Programmatic Tool Calling),由模型生成的一段代码来组合多轮工具调用;
  • 极简模式:仅保留一个 shell 工具与一个文件编辑工具,用于最小环境下的模型基准测试;
  • 创造模式:可以检查当前运行时、在内存中试验 Cordis 插件,并据此组合和创作新的模式。

1.4 这七个思路如何收敛为一个系统

把以上七点串起来,Harness 的设计主线是:

用插件运行时(Cordis)承载一切能力,用"抽象服务 + 可替换 provider"隔离变化,用 effect 把生命周期交给框架,用注入的动态满足实现配置驱动的组合,用事件把行为推到扩展点,用会话日志作为可重建的真相,用显式校验与安全分层守住部署与执行边界。

它因此呈现出与 Claude Code / Codex(产品)、AgentScope(Python 开发库)截然不同的取向:前者关心"用户开箱即用",后者关心"研究者快速搭多智能体",而 Harness 关心的是 "平台/产品工程师如何可靠地自托管并长期演化一个 Agent 底座" 。这也是为什么它的工程纪律极严(100% 覆盖率门禁、type-equiv 文档同步、品牌类型、声明式 surface),因为底座的可靠性是上层一切的前提。

二、核心心智模型:一切皆插件

第一章回答了"为什么这么设计 "(七条思路 + 设计动机);本章回答"它实际怎么跑起来 ",并用一张整体架构图把这七条思路落到物理结构上。如果你跳过了第一章,只需记住一句话:Harness 里没有"写死的助手",所有能力都是挂在共享 ctx 上的插件。

DeepSeek Harness 基于具有时空可组合性的 Cordis 插件系统构建。Cordis 元框架只负责插件的加载与卸载以及依赖关系,Agent Harness 的所有具体组件都是不同的 Cordis 插件。插件通过 Cordis 服务与事件彼此协作,并可以在配置层自由组合。

开发者无需改动 DeepSeek Harness 的源码本身,就能以插件的方式独立选择、替换或扩展其中的任一能力。这就是 DeepSeek Harness 最重要的设计原则:一切皆插件。

整个仓库遵循一条铁律(见 AGENTS.md):

Everything is a plugin. 一切皆是插件。

这意味着:

  • 工具是插件(dsh-tools
  • 大语言模型适配器是插件(dsh-llm + DeepSeek provider)
  • 文件系统访问是插件(dsh-fs
  • shell / 子进程 / 终端是插件(dsh-shell / dsh-subprocess / dsh-terminal
  • 连 agent 主循环(agent-loop)本身都是可替换的插件dsh-agent-loop

所有插件共享同一个 ctx,通过三种机制协作:

机制 关键字 作用
依赖注入 inject: ['tools'] 插件声明它依赖某服务,Cordis 在该服务就绪后再启动它
注册 / effect ctx.effect() / ctx.on() 插件贡献能力(注册工具、监听事件);卸载时自动撤销
事件 ctx.on(event, cb) / ctx.waterfall() 解耦的插件间通信

关键设计:注册是"副作用"(effect) 。每个贡献都通过 ctx.effect() 完成,插件卸载时贡献自动撤销。这是 Cordis 生命周期管理的核心,也是它区别于"手动管理全局单例"类框架(如很多 Python agent 库)的根本点。

2.1 整体架构图(基于源码)

下图依据 packages/bundle/base/cordis.patch.yml(base bundle 的 45+ 个 id 行)与 packages/core/agent-loop/README.md(agent-loop 注入的 5 个服务)绘制,反映真实组合关系,而非示意。

graph TB subgraph RUNTIME[&#34;Cordis 运行时 (vendor/cordis)&#34;] ROOT[&#34;根 Context\n(共享 ctx)&#34;] LOADER[&#34;Loader 插件\n读 cordis.yml / --profile 补丁层&#34;] HMR[&#34;@cordis-plugin-hmr\n文件变更热重载&#34;] TIMER[&#34;@cordis-plugin-timer&#34;] end subgraph CORE[&#34;核心脊梁 (packages/core + base bundle)&#34;] AGENTLOOP[&#34;dsh-agent-loop\n(ctx.agentLoop)\n唯一具体循环驱动&#34;] AGENT[&#34;dsh-agent\n(ctx.agents 工厂)&#34;] TOOLS[&#34;dsh-tools\n(ctx.tools 注册表)&#34;] LLM[&#34;dsh-llm\n(ctx.llm 抽象 + DeepSeek provider)&#34;] SYSPROMPT[&#34;dsh-system-prompt\n(ctx.systemPrompt)&#34;] SESSION[&#34;dsh-session\n(ctx.session 持久化/投影)&#34;] end subgraph DRIVE[&#34;agent-loop 注入的 5 个接口服务(来自 README)&#34;] AGENTLOOP -.注入.-> AGENT AGENTLOOP -.注入.-> SESSION AGENTLOOP -.注入.-> LLM AGENTLOOP -.注入.-> TOOLS AGENTLOOP -.注入.-> SYSPROMPT end subgraph EXEC[&#34;执行 / 沙箱能力 (Provider 插件)&#34;] SHELL[&#34;dsh-shell-local / dsh-shell-pwsh&#34;] SUBPROC[&#34;dsh-subprocess-local&#34;] SANDBOX[&#34;dsh-sandbox-local\n+ sandbox-policy&#34;] FS[&#34;dsh-tool-fs / fs-search\n(dsh-fs-sandbox)&#34;] TERMINAL[&#34;terminal / code-runtime&#34;] end subgraph MODEL[&#34;模型 / 检索能力&#34;] WEB[&#34;dsh-web (web_search)&#34;] DEEPSEEK[&#34;dsh-llm-deepseek\n(原生适配器)&#34;] PIAI[&#34;dsh-llm-pi-ai\n(多 provider 孪生)&#34;] RETRY[&#34;dsh-llm-retry&#34;] end subgraph ORCH[&#34;编排 / 子任务能力&#34;] SUBAGENT[&#34;dsh-subagent\n(spawn / fork provider)&#34;] WORKFLOW[&#34;dsh-workflow\n(worker-thread)&#34;] JOBS[&#34;dsh-jobs-local&#34;] TODO[&#34;dsh-tool-todo&#34;] GOAL[&#34;dsh-goal / command-goal&#34;] end subgraph SESS[&#34;会话 / 人机协作&#34;] PERSIST[&#34;session-persistence-jsonl&#34;] QUERY[&#34;session-query-sqlite\n(全文检索, 可选)&#34;] PROJ[&#34;session-projection&#34;] COMPACT[&#34;compaction-basic\n+ tool-result-pruner&#34;] APPROVAL[&#34;user-approval + permission-presets&#34;] INTERACT[&#34;interaction / commands / plan-mode&#34;] end subgraph EXT[&#34;扩展 / 互操作&#34;] SKILL[&#34;dsh-skill + tool-skill&#34;] HOOKS[&#34;hooks-claude-code / hooks-codex\n(Hook 互操作桥接)&#34;] ACP[&#34;dsh-acp\n(自动化协议服务器)&#34;] EXTENSIONS[&#34;extensions\n(agent 自修改插件)&#34;] BUNDLE[&#34;dsh-bundle\n(--profile 补丁层)&#34;] end %% 配置驱动加载 LOADER -->|&#34;按 id 装配\n服务就绪即激活&#34;| CORE LOADER --> EXEC LOADER --> MODEL LOADER --> ORCH LOADER --> SESS LOADER --> EXT HMR --> LOADER %% 工具注册: 能力插件把工具注册进 ctx.tools SHELL --> TOOLS FS --> TOOLS WEB --> TOOLS SUBAGENT --> TOOLS WORKFLOW --> TOOLS TODO --> TOOLS SKILL --> TOOLS %% 模型链路 DEEPSEEK --> LLM PIAI --> LLM RETRY --> LLM %% 会话链路 PERSIST --> SESSION QUERY --> SESSION PROJ --> SESSION COMPACT --> SESSION %% 事件总线(解耦插件通信) EVENTS{{&#34;事件总线\nagent/* · tools/result · agent/request\napproval/request · session/event&#34;}} AGENTLOOP --> EVENTS TOOLS --> EVENTS SHELL --> EVENTS APPROVAL --> EVENTS SANDBOX --> EVENTS INTERACT --> EVENTS %% 入口 ENTRY[&#34;CLI (pnpm dsh)\n/ ACP / JSON-RPC 入口&#34;] ENTRY --> ROOT BUNDLE --> LOADER

2.2 图中关系对照源码说明

1. "一切皆插件"的物理形态

  • 启动器 = node --import tsx ../../vendor/cordis/bin.js,它只创建 root Context 并挂载 Loader。
  • Loader 读取 cordis.yml--profile 补丁层(dsh-bundle)。base bundle 在 packages/bundle/base/cordis.patch.yml 里用 45+ 个 id声明了全部默认插件,行序无关(激活由"服务可用性"驱动)。
  • 每一行就是一个插件;id(如 agent-looptoolsllm-deepseek)是 stable 标识,后续补丁层按 id 覆盖它。

2. agent-loop 是唯一的"具体循环"

  • dsh-agent-loop/README.md整个 harness 只有这一个包包含具体循环逻辑,其他全是抽象服务或扩展点插件。
  • 它注入并依赖 5 个接口服务:agentssessionsllmtoolssystemPrompt(图中虚线)。这 5 个都是 ctx 上的服务,具体 provider 可热替换。
  • "call model → run tools → repeat"之外的所有行为(hooks、sandbox、plan、retry、subagent、compaction)都通过监听 agent/*tools/*session/* 事件实现------这就是图中的事件总线

3. 工具是"注册"而非"硬编码"

  • bashfswebsubagentworkflowtodoskill 等执行/编排插件,通过 ctx.tools.register(...)(effect)把工具挂进 dsh-tools 注册表,再由 agent-loop 在 tools/result 等事件里消费。两插件互不知对方存在。

4. Provider 可替换三角色

  • 以 shell 为例:dsh-shell(定义)→ dsh-shell-local / dsh-shell-pwsh(provider,按平台 disabled)+ dsh-bash-sandbox(沙箱 policy)。LLM 同理:dsh-llm(定义)→ dsh-llm-deepseek(原生)/ dsh-llm-pi-ai(多 provider 孪生)。

5. 自修改与互操作

  • extensions 让 agent 运行时装载/卸载插件;hooks-claude-code/hooks-codex 桥接外部 Hook;acp 暴露自动化协议服务器。这些都在 base bundle 之外,按需叠加。

图中 dsh-agent-loop 的 5 条虚线注入、dsh-tools 的 7+ 条工具注册、--profile 补丁层装配,均直接来自 packages/bundle/base/cordis.patch.ymlpackages/core/agent-loop/README.md 的源码事实。

三、环境准备(5 分钟)

3.1 你需要先知道的背景(新手必读)

本文示例用 TypeScript 写插件,但不需要你精通 TS。只要理解下面四点即可:

  • ESM 与 import :代码用 import { x } from 'pkg' 引入依赖;本文所有相对导入都带 .ts 后缀(如 './hello.ts'),这是 Cordis loader 的约定。
  • workspace 包名@deepseek-ai/cordis@deepseek-ai/dsh-tools 等是仓库内部的 npm 包名(pnpm workspace 解析),不是从网络下载的。import type { Context } from '@deepseek-ai/cordis' 就是从 Cordis 取类型。
  • cordis.yml 是 YAML 列表 :每个 - name: ... 是一项插件;缩进用两个空格,不要混用 Tab。
  • ctx 是什么 :贯穿全文的 ctx 是 Cordis 的共享上下文,所有插件通过它注册能力与监听事件。你可以把它当成"整个运行时的总接线板"。

3.2 环境前置条件

前置条件(详见 docs/development.md):

  • Node.js 22.19+ 或 24+(CI 覆盖 22.19 / 24 / 26)
  • pnpm (启用 Corepack:corepack enable),仓库锁定 pnpm@11.7.0
  • Git 2.26+
  • 可选:DeepSeek API Key(DEEPSEEK_API_KEY),仅真实跑模型时需要;本教程第三至第十章(含 HMR 与工具管线)完全无密钥可运行
bash 复制代码
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run typecheck   # 验证环境就绪

创建教程临时目录(tmp/ 已被 git 忽略,不会被提交):

bash 复制代码
mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial

后续所有示例都从这同一个目录运行:

python 复制代码
node --import tsx ../../vendor/cordis/bin.js

这个单文件启动器会:① 创建根 Context;② 挂载 Loader 插件;③ 从当前目录读取 ./cordis.yml 并加载里面列出的每个插件。无需任何构建步骤(--import tsx 让 Node 直接跑 TS)。

四、动手:你的第一个插件

4.1 写插件

tmp/cordis-tutorial 下创建 hello.ts

javascript 复制代码
import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello'

export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}
  • 插件通过命名导出 apply 函数被 loader 挂载。
  • ctx 是 Cordis 上下文,插件通过它注册所有贡献。
  • name 是可选显示名,用于诊断信息。

4.2 组合应用

创建 cordis.yml

arduino 复制代码
- name: './hello.ts'

这是一个配置项列表name 是模块指定符(相对路径或 npm 包名)。

4.3 运行

python 复制代码
node --import tsx ../../vendor/cordis/bin.js

输出:

sql 复制代码
hello from my first plugin

4.4 三种插件形态

scala 复制代码
import { Service, type Context } from '@deepseek-ai/cordis'

// 1. 函数插件(最常用)
export function apply(ctx: Context) {}

// 2. 对象插件:带 apply 方法的对象
export const objectPlugin = { name: 'obj', apply(ctx: Context) {} }

// 3. 类插件:Service 子类(需要公开服务时用,见第六章)
export class MyService extends Service {
  constructor(ctx: Context) { super(ctx, 'myService') }
}

新手建议:在需要公开服务之前,一律使用函数形态。

4.5 容错行为(新手必知)

  • 若插件 apply 抛错 → 进程直接崩溃并报错(不会静默跳过)。
  • cordis.yml 里的模块路径/包名拼错 (解析失败)→ Cordis 只通过 logger 报告,不会崩溃。新插件"没反应"时,先检查拼写。

五、生命周期与 effect(资源自动回收)

Cordis 插件可能因修改配置、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 ctx.effect()

创建 lifecycle.ts

javascript 复制代码
import type { Context } from '@deepseek-ai/cordis'

export const name = 'lifecycle-demo'

function heartbeat(ctx: Context) {
  console.log('heartbeat plugin loading')
  ctx.effect(() => {
    const timer = setInterval(() => console.log('tick'), 200)
    return () => {
      clearInterval(timer)
      console.log('heartbeat cleaned up')
    }
  })
}

export function apply(ctx: Context) {
  const fiber = ctx.plugin(heartbeat)
  ctx.effect(() => {
    const timer = setTimeout(async () => {
      await fiber.dispose()
      console.log('disposed')
      process.exit(0)
    }, 700)
    return () => clearTimeout(timer)
  })
}

运行后输出:

复制代码
heartbeat plugin loading
tick / tick / tick
heartbeat cleaned up
disposed

三点关键:

  • ctx.plugin(heartbeat) 把一个来自代码 的函数挂载为插件,与 YAML loader 为每个配置项做的完全一致。调用返回 fiber------已加载插件实例的运行时句柄。
  • effect 主体在加载期间运行,返回的 disposer 在卸载期间运行。生命周期与插件一致的资源,你绝不需要手动调用 disposer。
  • fiber.dispose() 会等该插件所有清理(含异步 disposer)完成后才结束,并递归卸载它挂载的子插件。

Fiber 状态机

每个已加载插件实例都有 fiber,在以下状态间转换:

markdown 复制代码
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED
  • PENDING:已声明,但所需服务尚不可用(见第六章)。
  • LOADING / ACTIVEapply 正在运行/已完成。
  • FAILEDapply 或配置校验抛异常。
  • UNLOADING / DISPOSED:disposer 正在运行/已拆除。

已经是 effect 的操作(你很少需要手写 ctx.effect()

  • ctx.on(event, listener):监听器随插件卸载自动移除。
  • ctx.plugin(child):子插件随父插件一同 dispose。
  • 服务注册、harness 注册表(如 ctx.tools.register(...))的返回 disposer 都附着在调用插件上,自动撤销。

顺序注意:disposer 按注册逆序 启动,但多个异步 disposer 并发运行;若拆除必须按顺序,请把步骤放进同一个 disposer 内依次 awaits。

六、服务(Service):能力的注册与消费

服务 是插件提供、其他插件通过 ctx 消费的具名能力。在 harness 中,ctx.toolsctx.llmctx.agents 都是服务。消费方只指定 'tools' 这样的能力名,而不导入提供方------因此配置可以选择提供方,无需改动消费方代码

6.1 提供服务

greeter.ts

typescript 复制代码
import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    greeter: GreeterService
  }
}

export class GreeterService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }
  greet(who: string) {
    return `Hello, ${who}!`
  }
}

export const name = 'greeter'
export function apply(ctx: Context) {
  ctx.plugin(GreeterService)
}

两部分协同:

  • 运行时super(ctx, 'greeter') 以名称 greeter 注册实例,ctx.greeter 随处可访问;注册属于 effect,卸载时移除。
  • 编译时declare module 用 TS 声明合并把 greeter 加入 Context 接口,使消费方获得类型安全(无此声明运行时仍工作,但失去类型)。

6.2 消费服务(inject)

consumer.ts

javascript 复制代码
import type { Context } from '@deepseek-ai/cordis'

export const name = 'consumer'
export const inject = ['greeter']

export function apply(ctx: Context) {
  console.log(ctx.greeter.greet('world'))
}

inject 列出该插件需要的服务。Cordis 会让插件保持 PENDING 直到每项服务都存在,因此在 apply 内可保证 ctx.greeter 已就绪------加载顺序无关紧要。

arduino 复制代码
- name: './greeter.ts'
- name: './consumer.ts'

输出 Hello, world!交换两行顺序输出不变 ;若彻底移除 greeter.ts(或拼错包名),消费方会保持 PENDING / 启动失败------它既不崩溃、也不会在依赖缺失时半运行,而是明确停在等待状态(诊断器见第九章)。

术语区分 :本例 export const name = 'consumer'插件显示名inject: ['greeter'] 里的 greeter服务名 (由 super(ctx, 'greeter') 注册)。二者命名空间不同------插件可任意取名,但注入必须精确匹配服务名,否则永远 PENDING。

6.3 inject 是持续跟踪,而非一次性检查

若运行期间所需服务消失(如提供方被卸载、热替换),每个依赖插件会随之卸载,服务恢复后再加载。结合 effect,这防止消费方保留对不可用服务的引用。也正是配置能替换服务的原因 :卸载 dsh-shell-local、挂载另一个 shell 提供方,所有 inject: ['shell'] 的插件会重启并用新实现。

6.4 可选依赖

inject 是硬性依赖。缺失仍可工作时跳过 inject 并探测:

javascript 复制代码
export function apply(ctx: Context) {
  const greeter = ctx.get('greeter')
  console.log(greeter?.greet('maybe') ?? 'no greeter available')
}

原则 :扩展插件依赖 Service Definition(抽象服务) 而非具体 provider。这样 LLM 适配器、执行器等都能热替换、互不影响。服务名共用扁平命名空间,自有服务请加前缀(harness 已占用 toolsllm 等)。

七、事件系统:解耦通信与拦截

服务支持直接调用;事件 让插件无需知道谁在监听就能广播。harness 用事件处理工具结果、模型请求、审批决定等交互。

7.1 声明、发出、监听

stats.ts(计数服务):

typescript 复制代码
import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context { stats: StatsService }
  interface Events {
    'stats/report'(name: string, count: number): void
  }
}

export class StatsService extends Service {
  private counts = new Map<string, number>()
  constructor(ctx: Context) { super(ctx, 'stats') }
  bump(name: string) {
    const next = (this.counts.get(name) ?? 0) + 1
    this.counts.set(name, next)
    this.ctx.emit('stats/report', name, next)
  }
}

reporter.ts

typescript 复制代码
import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'

export const name = 'reporter'
export const inject = ['stats']

export function apply(ctx: Context) {
  ctx.on('stats/report', (name, count) => {
    console.log(`[stats] ${name} -> ${count}`)
  })
  ctx.stats.bump('tool_call'); ctx.stats.bump('tool_call'); ctx.stats.bump('prompt')
}

import type {} from './stats.ts' 让 TS 看到声明合并(运行时无副作用)。输出:

csharp 复制代码
[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1

ctx.on() 属于 effect,监听器随插件消失,绝不需手动 removeListener

注意 declare module '@deepseek-ai/cordis' { interface Events { ... } } 这处声明合并 :它把 'stats/report' 及其签名写入 Cordis 的全局事件表,于是 ctx.emit / ctx.on 在编译期就检查事件名与参数类型。漏写声明合并,事件仍是合法的 string 事件,但失去类型保护------这是基于 Cordis 开发时最常踩的坑。

7.2 五种分发模式

模式 调用 语义
emit ctx.emit(name, ...) 同步广播;不等待/不收集返回值
parallel await ctx.parallel(...) 全部并发并一同等待
serial await ctx.serial(...) 顺序等待;首个非 null/false/undefined 胜出并停止
bail ctx.bail(...) serial 的同步版
waterfall ctx.waterfall(name, ...args, next) 环绕中间件,可转换或短路

7.3 waterfall:转换或短路

typescript 复制代码
declare module '@deepseek-ai/cordis' {
  interface Events {
    'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
  }
}
// 监听器 1:包裹下游结果
ctx.on('demo/transform', async (input, next) => {
  const downstream = await next()
  return downstream.toUpperCase()
})
// 监听器 2:拥有决策时短路
ctx.on('demo/transform', async (input, next) => {
  if (input.includes('blocked')) return '** blocked **'
  return next()
})

await ctx.waterfall('demo/transform', 'hello', async () => 'hello')        // HELLO
await ctx.waterfall('demo/transform', 'blocked words', async () => '...') // ** BLOCKED **

纪律:只观察/标注的 waterfall 监听器必须调用 next() ;不调用代表有意短路。日志监听器若忘记 next() 会静默吞掉所有下游默认行为------这是本仓库常设规则。Harness 用 waterfall 处理协作决策:agent/request 允许插件替换模型调用配置,approval/request 允许策略代替用户作答。

八、配置:声明式与明确报错

cordis.yml 每个配置项都可带 config 块,插件导出 schema 在 apply 前校验。错误配置导致加载失败并给出准确错误:插件绝不会在配置不完整时启动

typescript 复制代码
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'config-demo'
export interface Config { greeting: string; targets: string[] }
export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  targets: Schema.array(String).default(['world']),
})
export function apply(ctx: Context, config: Config) {
  for (const target of config.targets) console.log(`${config.greeting}, ${target}!`)
}
arduino 复制代码
- name: './config-demo.ts'
  config:
    targets: ['alpha', 'beta']

输出 Hello, alpha! / Hello, beta!(未给 greeting 时用 schema 默认值补齐,apply 永远收到完整且已验证的配置)。

传入无效值:

css 复制代码
config: { targets: 'not-an-array' }
c 复制代码
ValidationError: invalid config:
  - $.targets expected array but got not-an-array (at targets)

fiber 进入 FAILED,启动器打印错误后退出码 1。明确报错优于静默跳过是本仓库的一贯约定。

loader 还支持 !!js 标签用于加载时计算值:

arduino 复制代码
config:
  greeting: !!js process.env.DEMO_GREETING ?? 'Hello'

!!js 仅在 config 与条目 disabled 内有效;disabled: !!js ... 可按平台/环境门控一行(本仓库扩展)。

九、组合、HMR 与诊断

cordis.yml 选择应用的插件树。配置项还能携带 iddisabled、嵌套 groupisolate 等元数据:

yaml 复制代码
- id: greeter
  name: './greeter.ts'
- id: consumer
  name: './consumer.ts'
  disabled: true     # 保留条目但跳过挂载

id 提供稳定标识,使 loader 区分"修改现有项"与"先删后加"。disabled: true 卸载插件而不删条目;改回即连同 PENDING 依赖一起重载。group 可把子列表作为单元加载/卸载;isolate 为组提供某服务名的独立实例(两组各自看到不同配置的 shell 提供方,互不影响)。

9.1 热模块替换(HMR)

卸载释放 effect,加载遵循依赖,因此 HMR 可先卸载再加载以替换运行中的插件。@deepseek-ai/cordis-plugin-hmr 监视文件,保存时执行该过程:

yaml 复制代码
- id: logger
  name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
  name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
  name: '@deepseek-ai/cordis-plugin-hmr'
  config: { root: ['.'] }
- id: hello
  name: './hello.ts'

编辑 hello.ts 保存后:

less 复制代码
hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin

旧实例先卸载(effect 回卷),新代码后加载。编辑 cordis.yml 本身也会触发更新:loader 按 id 比较,只改动变化部分。不带 id 的条目每次读取都获新 id,会被当作先删后加重新挂载 ------这就是显式 id 的意义。

9.2 诊断始终不加载的插件

依赖驱动加载的另一面:若 inject 指定了无人提供的服务,它会一直 PENDING、不输出。这不是错误(PENDING 是合法态)。可直接枚举状态:

javascript 复制代码
import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
export function apply(ctx: Context) {
  setTimeout(() => {
    for (const runtime of ctx.registry.values())
      for (const fiber of runtime.fibers)
        if (fiber.state === FiberState.PENDING)
          console.log(`${fiber.name} is PENDING --- a required service is missing`)
  }, 500)
}

inject: ['timer'] 无提供方时,诊断器会打印 needs-timer is PENDING --- a required service is missing"插件没反应"时,先看 fiber 状态。

十、把工具接进真实 Agent

继续用第三章创建的 tmp/cordis-tutorial 目录,所有文件都放在这里,运行命令仍是 node --import tsx ../../vendor/cordis/bin.js

这是理解 Harness 的"啊哈时刻":写一个可被模型调用的工具,穿过真实执行管线。无需密钥、不调模型。

10.1 工具插件 greet-tool.ts

php 复制代码
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet the named person.',
    parameters: {
      name: { type: 'string', required: true, description: 'Who to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))

  void (async () => {
    const result = await ctx.tools.execute({
      callId: CallId('demo-1'),
      name: 'greet',
      arguments: { name: 'Cordis' },
      signal: new AbortController().signal,
    })
    console.log('tool replied:', JSON.stringify(result.content))
  })()
}

10.2 观察插件 tool-logger.ts

typescript 复制代码
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'

export const name = 'tool-logger'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.on('tools/result', (exec, result) => {
    const text = result.content.map(b => (b.type === 'text' ? b.text : '')).join('')
    console.log(`[tool-logger] ${exec.name} -> ${text}`)
  })
}

10.3 组合运行

arduino 复制代码
- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'
- name: './greet-tool.ts'
python 复制代码
node --import tsx ../../vendor/cordis/bin.js
css 复制代码
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]

要点

  1. defineToolparameters 转成给模型的 JSON Schema,并execute 前校验参数
  2. 日志插件先触发:tools/result 在结果物化过程中发出,早于 execute 的 promise 兑现。两插件互不知对方------它们被注册表服务与事件连接。
  3. 工具插件若在组合里缺 systemPrompt 提供方,会保持 PENDING (缺依赖),正是 inject 机制的体现。

此刻你已能读懂 examples/headless-agent/cordis.yml 的每一项。真实 agent = 这套组合 + LLM 适配器 + agent-loop + 持久化 + 入口。

10.4 具象化:一次 Agent 循环的时序(思路一 / 思路五)

下面这张时序图直接来自仓库生成的权威生命周期图(docs/agent-lifecycle.md,由 scripts/gen-doc-graphs.ts 产出)。此时你已理解 tools/resultagent/*session/event 等事件,正好用它把第一章的思路一 (只有 agent-loop 是具体循环、其余行为靠事件订阅)与思路五(任何送达模型的东西都写进会话日志、日志即真相)落到一次真实回合(turn/step)的每一步。

rust 复制代码
sequenceDiagram
    participant User
    participant Agent
    participant Driver as dsh-agent-loop
    participant Hooks as hook listeners
    participant Prompt as ctx.systemPrompt
    participant LLM as ctx.llm
    participant Tools as ctx.tools
    participant Session
    participant SDK as UI/SDK listener

    User->>Agent: followup(content)
    Agent-->>SDK: agent/inbox/spliced / agent/inbox/inserted
    Agent->>Driver: queued work wakes driver
    Driver-->>SDK: agent/status running
    Driver->>Session: turn/start
    Note over Agent,Driver: claim pending next-step input + one queued prompt

    Driver->>Hooks: agent/pre-step waterfall
    Hooks-->>Driver: authoritative reject or enter(messages)
    alt pre-step rejected / failed
        Driver-->>Driver: claimed batch stays removed, turn spends no step
    else enter proposed step
        Driver->>Session: step/start
        Driver->>Session: user/message per entered message
        Driver->>Prompt: system-prompt/assemble waterfall
        Driver->>LLM: agent/request waterfall → llm/stream waterfall
        LLM-->>Driver: StreamChunk*
        Driver->>Session: assistant/chunk*
        Session-->>SDK: session/event assistant/chunk*
        alt adapter/terminal request failure
            Driver->>Session: step/end
            Driver->>Hooks: agent/request-error waterfall
            Hooks-->>Driver: retry action or keep original error
        else model request succeeded
            Driver->>Session: assistant/message
            Driver->>Tools: classify pending call by executionMode
            opt call starts
                Driver->>Session: tool/call
                Driver->>Tools: ordered pre, concurrent execute
                Tools-->>Session: tool-owned events when applicable
            end
            opt next model-order result ready
                Driver->>Tools: ordered post
                Driver->>Session: tool/result
            end
        end
        Driver->>Session: step/end
        opt natural stop + inbox empty
            Driver->>Hooks: agent/turn-stopping serial checkpoint
        end
    end
    Driver->>Session: turn/end
    Driver-->>SDK: agent/status idle

怎么读这张图(对应两条思路)

  • 思路一(一切皆插件,循环只驱动、不实现)

    • Driverdsh-agent-loop)只做"取输入 → 发事件 → 等结果"的骨架。hooks、sandbox、plan、retry、subagent、compaction 没有出现在循环体里 ,而是作为 agent/pre-stepagent/requestagent/request-erroragent/turn-stoppingwaterfall / serial 事件被外部插件订阅。
    • 例如 dsh-compaction-basicagent/pre-step 在请求构造前做压力检查、agent/request-error 只在上下文溢出时触发裁剪------这些都是"挂"在循环事件上的行为,而非循环内部的分支。这正是"Plugins, not loop changes"。
  • 思路五(模型可见 ⟺ 已记录,日志即真相)

    • 每一个送达模型 的东西都被记成会话事件:system-prompt/assemble(提示词切片)、agent/request(模型请求)、llm/streamassistant/chunk*(流式输出)、assistant/message(一次成功调用)、tool/call / tool/result(工具调用与结果)。
    • 这些事件的耐久副本 全在 session/event 上(Session-->>SDK: session/event ...),而 agent/* 只是"活的协调 API"(队列/状态/拦截/转向/续跑/错误)。
    • 因此一个错误回答可被完整重放 :从 session/event 流重建出模型当时看到的提示词、调用了哪些工具、拿到了什么结果------无需猜测。这就是"日志即真相"的物理落点。

一句话:循环负责"走流程",插件负责"加行为",会话日志负责"留真相" 。三者靠事件总线连接,互不硬编码对方。

十一、能力分层与实战运行

11.1 能力分层(Capability Seams)

Harness 把每个能力拆成三角色,各自独立演化

角色 职责
Service Definition 抽象接口(能力"是什么")
Service Provider 具体实现(如本地 / 云端 / E2B)
Consumer 面向模型的工具或调用方

以 shell 为例:dsh-shell 定义能力,dsh-shell-local / dsh-shell-pwsh 是 provider,dsh-shell 的模型工具是 Consumer。

完整能力清单(packages/README.md)节选:

  • 核心core(session、prompt、tools、agent、agent-loop)、apitypertsdk
  • LLMllm(抽象 + DeepSeek provider)
  • 执行shellsubprocessterminalcode-runtimesandbox(bwrap/Landlock/Seatbelt)
  • 工具fslspwebskillsubagentworkflowtodoplan
  • 会话/持久化sessionsession-querycompactionstorageattachment
  • 人机协作interaction(审批/权限/ask-user)、hooksacp
  • 自修改extensions(agent 可运行时检视/挂载自己的插件)
  • 组合分发bundledsh --profile 补丁层)、preset

设计哲学:可维护依赖优于手写;跨边界 id 用 Branded<B> 品牌类型(非裸 string);运行时只在校验边界(config、模型/工具 JSON、文件、worker、进程、线)做校验,同进程类型边界信任 TypeScript

11.2 实战:跑真实编码 Agent

需先构建(详见 docs/development.md)并设置 Key:

arduino 复制代码
pnpm run build
ini 复制代码
# 仓库根 .env 或环境变量
DEEPSEEK_API_KEY=sk-...
DEEPSEEK_BASE_URL=https://...   # 可选
arduino 复制代码
pnpm dsh --profile headless "summarize this workspace"

其他演示:

arduino 复制代码
pnpm run demo:cordis   # Agent 检视并修改自己的实时插件运行时
pnpm run demo:acp      # 用 JSON-RPC stdio 暴露自动化 Agent 会话(ACP 协议)

dsh --profile 背后是 dsh-bundle 补丁层:base 组合被部署 overlay 修补。

11.3 常用命令速查

bash 复制代码
pnpm install              # 安装 + lefthook 钩子
pnpm run typecheck        # 类型检查(pre-push 钩子)
pnpm run test             # vitest 单测
pnpm run test:coverage    # CI 覆盖率门禁(per-file 100%)
pnpm run lint
pnpm run build            # tsc 发射 lib/types + tsdown 打包
pnpm run hygiene          # knip + publint + 约束 + NodeNext 检查
pnpm run doc-sync         # 文档门禁(含 type-equiv 校验)

pnpm dsh --profile headless "task"   # 源码跑任务(需 Key)
pnpm run demo:cordis                 # 自引用 Cordis 演示(需 Key)
pnpm run demo:acp                    # ACP 自动化服务器(需 Key)

提交/推送前按 AGENTS.md 的"relevant checks"原则,只跑覆盖你所改面的检查,不必无脑跑全量------CI 才负责穷尽覆盖。

十二、优势分析 & 与 Claude Code / Codex / AgentScope 的对比

12.1 三方 + 一框架横向对比

维度 DeepSeek Harness Claude Code Codex (CLI) AgentScope(阿里)
本质 Agent 框架/运行时 闭源产品(编码助手) 闭源产品(编码 Agent) 开源开发库(Python 为主)
内核 Cordis 插件运行时,一切皆插件 单体应用 单体应用 类 + 管道 DSL,ReAct 范式
语言 TypeScript(Node) 未公开 未公开 主要是 Python
可组合性 极高:cordis.yml 拼装,含可换 agent-loop 低(settings/hooks) 低(settings/hooks) 中(组件可换,但靠代码组装而非配置声明)
模型绑定 LLM 层可替换,默认 DeepSeek 锁定 Claude 锁定 OpenAI 多模型(含通义/OpenAI/本地),模型无关
自托管/嵌入 ✅ 完全可自托管、可嵌入产品 ❌ SaaS ❌ SaaS ✅ 开源可自部署
Hook 互操作 内置 Claude Code/Codex 桥接 --- --- 无(独立生态)
会话持久化 一等公民(JSONL/SQLite/血缘/全文检索) 有(对话历史) 有(较弱) 有(Memory/长期记忆模块)
多 Agent 一等:subagentworkflowjobs 有限 有限 强项:内置 Debate、Concurrent、Handoffs 等工作流
可视化/工程化 acp + 文档化子系统 终端 UI 终端 UI Studio + Tracing + OpenJudge 评测 + RAG + TTS
沙箱 一等:sandbox(bwrap/Landlock/Seatbelt) 依赖 shell 限制 依赖沙箱环境 运行时沙箱(runtime sandbox)
自动化协议 ACP 服务器内置 无(靠 CLI/钩子) A2A(Agent-to-Agent)智能体
源码开放 ✅ 全仓库可读可改可贡献 ✅(Apache-2.0 类开源)
适用对象 平台/产品工程师、自研 Agent 团队 终端开发者 终端开发者 算法/应用开发者、多智能体研究者

12.2 DeepSeek Harness 的核心优势

  1. 真正的"可组合"而非"可配置"
    Claude Code / Codex 让你配置 已有行为;Harness 让你重写行为------连 agent 主循环、文件访问策略、权限模型都能换成自己的插件。这是"框架 vs 产品"的本质差别。
  2. 模型无关的能力层
    dsh-llm 把 LLM 抽象成 Service,DeepSeek 只是其中一个 provider。理论上换 provider 不改上层工具与循环。这点与 AgentScope 的"多模型无关"理念一致,但 Harness 通过 Cordis 的 inject/effect 把这种替换做成声明式、配置驱动、可热替换,比 AgentScope 在代码里换类实例更彻底。
  3. Hook 互操作
    通过 hooks-claude-code / hooks-codex 桥接包,你现有的 Claude Code / Codex hooks.json 能直接在 Harness 上跑。迁移成本极低,且原生扩展点是"类型化拦截点",比 shell hook 更强。这是 AgentScope 完全不具备的跨生态兼容。
  4. 会话是一等公民
    session + session-query 提供持久化、投影、血缘、语义过滤、SQLite 全文检索------适合长期记忆与知识库型应用。AgentScope 也有 Memory/长期记忆模块,但 Harness 把"会话日志即真相"(model-visible ⟺ logged)上升到架构约束,保证任何送达模型的输入都能从会话日志重建------这对审计、回放、合规极有价值。
  5. 工程纪律极严
    100% 覆盖率门禁、声明式 cordis-surface、type-equiv 文档同步、品牌类型、显式边界校验------使它适合作为生产级产品的底座,而非玩具。AgentScope 的工程化(Studio/评测/Tracing)更偏"应用开发体验",Harness 的工程化更偏"框架本身的可靠性与可维护性"。
  6. 自修改能力
    extensions 包让 agent 运行时检视/挂载/卸载自己的插件(即 demo:cordis)。这是 框架级 的插件自装载能力------区别于业务层的动态切换(如 AgentScope 的运行时换 agent),Harness 能在不重启进程的情况下增删真实 Cordis 插件并回卷其 effect。Claude Code / Codex 不提供此类机制。

12.3 与 AgentScope 的关键差异(重点)

AgentScope 是阿里开源的智能体应用开发库(Python 为主,1.0 论文见 arXiv:2508.16279),定位是"以开发者为中心构建 agentic 应用"。两者常被拿来比较,但取向不同:

取向 DeepSeek Harness AgentScope
范式 插件运行时(Cordis),一切皆插件、配置驱动 组件库 + 管道 DSL,ReAct 范式,代码驱动
语言生态 TypeScript / Node,天然适合前端/工具/IDE 集成 Python,天然适合算法/数据/ML 研究者
组合方式 声明式 cordis.yml,依赖图自动排序、HMR、服务隔离 命令式代码组装 Agent/Pipeline/Workflow
多智能体 一等公民 subagent/workflow/jobs,基于插件协作 强项:内置 Debate、Concurrent、Routing、Handoffs 等开箱即用工作流
可观测/评测 会话日志 + ACP 协议 + 子系统文档化 强项:Studio 可视化、Tracing、OpenJudge 评估器、RAG、TTS、Tuner
生产落地 框架级可靠性(100% 覆盖、类型边界、沙箱) 应用级工程化(沙箱、评测、可视化)齐全
模型 默认 DeepSeek,LLM 层可替换 多模型(通义/OpenAI/本地)开箱支持
runtime sandbox bwrap/Landlock/Seatbelt 一等支持 runtime sandbox 支持

一句话总结差异

  • 自建 Agent 平台/产品底座、需要可替换模型与循环、要长期会话与审计、要平滑迁移 Claude Code/Codex 钩子 → 选 DeepSeek Harness(TypeScript 生态、插件化、配置驱动)。
  • 用 Python 快速搭多智能体应用、要现成的辩论/并发/路由工作流、要 Studio 可视化与评测体系 → 选 AgentScope(应用开发体验、ML 生态)。
  • 给终端用户一个开箱即用的编码助手 → 选 Claude Code / Codex(产品形态)。

12.4 何时选谁(决策树)

  1. 终端开发者、要开箱即用编码助手 → Claude Code / Codex
  2. 平台/产品工程师、自托管、嵌入自家产品、可替换模型/循环/工具、长期会话与记忆、复用 Claude Code/Codex 钩子 → DeepSeek Harness
  3. 算法/应用开发者、Python 生态、多智能体编排、可视化与评测体系 → AgentScope

十三、学习路线图与下一步

新手 30 分钟路径

  1. 跑通第四章"第一个插件"(5 分钟,无密钥)
  2. 跑通第十章"把工具接进真实 Agent"(10 分钟,无密钥)
  3. examples/headless-agent/cordis.yml,逐行对照本文(10 分钟)
  4. 设置 DEEPSEEK_API_KEY,跑 pnpm dsh --profile headless "..."(5 分钟)

深入阅读(按 docs/

  • docs/cordis-tutorial/:7 章完整 Cordis 动手教程(本文是其浓缩与扩展)
  • docs/cordis-primer.md:概念速查
  • docs/architecture.md:系统地图(改 packages/ 前必读)
  • docs/capability-seams.md:能力三层设计
  • docs/user/:面向 Harness 插件开发(develop/basic/tool.md 等)
  • docs/cookbook/adding-a-tool.md:工具 UI 呈现设计
  • packages/*/README.md:每个包的目的、API、扩展点

进阶方向

  • 写一个自定义 Service Definition + Provider(参考 dsh-shell
  • dsh-bundle 做自己的 --profile 补丁层
  • hooks-claude-code 桥接现有 hook
  • extensions 实现 agent 自修改
  • 运行 pnpm run doc-sync 重新生成所有架构/生命周期图(含本文引用的 docs/agent-lifecycle.md,由 scripts/gen-doc-graphs.ts 产出)
  • 对照 AgentScope 论文,体会"配置驱动插件运行时"vs"命令式组件库"两种架构取舍
相关推荐
March.s3 小时前
项目实战 | 基于 LNMP(LAMP)架构从零搭建 WordPress 博客平台
架构
DFT计算杂谈3 小时前
Janus单层Cr2SSe中的应变可调多压电效应与谷电子学
人工智能·算法·机器学习
今天AI了吗3 小时前
从 LLM 到 Agent Skill:把 AI 底层概念串起来
数据库·人工智能·sql·深度学习·神经网络·算法·机器学习
DS随心转小程序3 小时前
巧用 AI 导出鸭攻克各类难题完善 ChatGPT 输出 word 文档转化工作
人工智能·chatgpt·aigc·word·豆包·deepseek·ai导出鸭
梦想的旅途23 小时前
企微 API 二次开发:结合 AI 打造考勤打卡与报表智能分析系统
人工智能·企业微信
Kari113 小时前
腾讯云 ADP 实施问题解析:回答异常时企业如何组织排查与支持协同?
人工智能
刘新洲3 小时前
别再只做会聊天的 Agent:我用 1 天把工具调用做成了可验证、可评测的工程系统
人工智能·python·openai
小唔w3 小时前
文件越攒越多?三步分类归档法 + 常用工具体验分享
人工智能
2401_894915533 小时前
新手落地 Geo 优化:源码下载、依赖安装、数据库初始化完整步骤
运维·服务器·数据库·人工智能·缓存·开源