news 2026/8/16 16:48:19

DeepSeek Harness 架构拆解:没有“核心“的 Agent 运行时,“万物皆插件“是怎么做到的

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek Harness 架构拆解:没有“核心“的 Agent 运行时,“万物皆插件“是怎么做到的

DeepSeek 在 2026-08-13 开源了 DeepSeek Harness(dsh),两天拿到 95K+ star(MIT,TypeScript,v0.1.0-rc.5)。它值得读的原因不是 star 数,而是架构上的一个激进选择:整个运行时没有 privileged core。模型适配器、工具注册表、会话日志、甚至 agent 循环本身,全都是插件——每个部分都可以从配置层面替换。底层是一个 vendored 的插件框架 Cordis(出自 cordiverse,设计文档是一篇叫A Programming Paradigm for Spatiotemporal Composability的论文)。这篇文章从它的源码文档拆四件事:插件模型、事件系统、turn 执行流、capability seams,外加一个工具插件的完整写法和它自己的测试设施。

Cordis 的五条核心思想

Cordis 的插件模型可以用五句话概括,理解了这五句话,整个 dsh 的代码结构就通了:

  1. 插件是一个实现 Service 的对象。可以是带可选inject/apply(ctx)字段的函数,也可以是Service子类,生命周期由 Cordis 挂载到当前 context。
  2. context 是服务的仓库。一个服务在 context 上占一个稳定的键(ctx.toolsctx.llmctx.sessions),别的插件通过键找服务,而不是 import 具体实现——这是"可替换"的地基。
  3. inject声明服务依赖。插件声明需要的服务后,要等这些服务存在了才会被激活,加载顺序由服务依赖推导,而不是手写 boot 序列。
  4. 类型化事件做通信。服务通过 TypeScript declaration merging 声明事件名,再按四种分发模式派发(见下表)。
  5. 注册都是可逆的 effect。prompt 段、工具 schema、适配器、provider、监听器,全部通过ctx.effect()/ctx.on()安装,插件卸载时按注册顺序自动回滚。

其中第 4 点值得展开。事件的分发模式是事件公共契约的一部分,新事件要用@mode标签标注,生成的目录会校验声明和派发点是否一致:

模式是否 await分发顺序有返回值
emit按注册顺序观察
waterfall按注册顺序,可包裹/改写
parallel全部并行观察
serial按注册顺序

ctx.waterfall本质是 around-middleware:监听器收到(...args, next),调用next()把(可能被改写过的)结果传给下一个服务;不调next()直接 return 就是短路。策略类监听器(比如权限门)用短路来"拍板",只做注解/观察的监听器必须委托。dsh 里agent/pre-stepagent/requestllm/stream、三个tools/*事件都是 waterfall,监听器不调next()就会断链。

Profile 与 Bundle:启动时的分层组合

一个跑起来的dsh是一棵在启动时按有序分层组合出来的插件树。

  • profile是命名组合,存在 Harness home 目录里,列出它叠了哪些 bundle、装了哪些树外插件、以及用户自己的cordis.patch.ymlwebheadless是自带模板。
  • bundle是 Cordis 配置行 + 它们挂载的代码的分发格式,它插入的任何东西都能被上层 patch 覆盖。dsh-base(模型适配器、工具、持久化、sandbox、审批策略、设置、凭据、遥测)是每个 profile 的第一层;dsh-web-app加浏览器应用;dsh-headless加无 server 的一次性 runner。

层叠顺序是:profile 里列出的每个 bundle(按列出顺序)→ profile 的cordis.patch.yml→ home 级 patch → 命令行--patch覆盖。patch 按行 id 定位,整行替换配置或插入新行。

想看你机器上真正 boot 出来的树:

dsh --profile web --dump-config

打印出来的每一行都可以用你自己的 patch 替换——这就是"没有核心要打补丁"的含义:扩展 dsh 的方式是往插件树里再挂一个插件。

Turn/Step 执行流:一次模型交互的完整时序

dsh 对执行流有两个精确定义:step = 一次模型请求 + 它调用的工具;turn = 零个或多个 step。turn 在第一个输入被认领前打开,在所有"欠账"还清后关闭。官方文档给的时序:

turn/start claim next-step input plus one queued message assemble prompt sections + tool schemas -> agent/pre-step reject | enter(messages) step/start append entered messages as user/message derive model history from the log agent/request -> llm/stream -> assistant/chunk* -> assistant/message tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result* step/end tools owe another request, or next-step input arrived -> claim -> next step -> agent/turn-stopping turn/end

几个关键设计:

  • turn/*step/*user/messageassistant/*tool/*持久化 session 事件(进日志);其余是三个域里的活扩展点。
  • agent/pre-step决定模型能看到什么。监听器可以改写被认领的消息或直接 reject;被 reject 或首个 claim 被改写成空,仍然会关闭一个"没花任何 step"的持久 turn——日志里会留下这次尝试。
  • 输入从一个 inbox到达 driver。部分消息会立刻唤醒它;injected context 只在有别的消息到达时才被消费(agent.inject()追加的是下一条模型请求可见的持久上下文,不是唤醒信号)。

Session log:"模型可见即日志可重建"

会话日志是模型上下文的唯一来源。deriveMessages()从日志投影出模型历史,原始assistant/chunk事件保真地保留 replay 和 UI 呈现。fork、resume、transcript、telemetry、持久化,全部从这条流派生。

这里有一条运行时不变式,是理解 dsh 数据流的关键:Model-visible means logged——任何到达模型请求的内容都必须能从日志重建,运行时有一个断言在强制它。所以"给模型加一种新输入"这件事的落点是:扩展SessionEventMap加一个新 session 事件类型,从日志渲染它。想绕过日志直接塞数据给模型,在架构上就是错的。

Capability seams:换一个 provider,换掉整个产品

dsh 里"可替换能力"是一个叫seam的结构化概念,包含三个角色:

  • Service Definition:声明接口
  • Service Provider:实现接口
  • Consumer:使用接口,通常是模型面工具

一个角色单独不构成 seam;加一项能力意味着三个角色一起设计。seam 的价值在于一次 provider 切换改变整个产品:filesystem 和 subprocess provider 共享同一个执行世界,把它们指向远程 sandbox,Bash、PTY、LSP 会跟着一起迁移,不需要为每个工具 fork provider。subagent provider 也一样——一个接口背后可以是全新子 agent,也可以是另一个产品里的委托 turn。

写一个工具插件:最小可用形态

官方 cookbook 给出的最小工具插件:

import { readFile } from 'node:fs/promises' import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'my-tool' export const inject = ['tools'] export function apply(ctx: Context) { ctx.tools.register(defineTool({ name: 'read_file', description: 'Read a file from disk.', // what the model sees parameters: { path: { type: 'string', required: true, description: 'Absolute path' }, limit: { type: 'number' }, // optional by default }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args, exec) { // args is TYPED from the schema: { path: string; limit?: number } // exec carries immutable identity + token; signal is the operational field return readFile(args.path, { encoding: 'utf8', signal: exec.signal }) }, })) }

注册走 effect 机制:插件 fiber 销毁时工具自动注销,schema 自动汇入 system-prompt 组装。execute()契约里有几条硬规则,踩过坑的会知道这些边界多重要:

  • args 自动校验defineToolexecute跑之前用统一的ParameterSchemaSpec校验模型生成的 arguments(类型、必填键、字面量约束、exact-one union、嵌套值),所以 execute 里的 args 类型和 schema 严格一致。schema DSL 表达不了的约束(非空字符串、正数、跨字段规则)才需要自己手检。
  • 执行身份受保护。注册表把 arguments 物化为 detached lossless JSON 并 freeze,分配不透明的exec.tokencallId/name/arguments/agent/token/signal在派发全程不可变。只有 around-dispatch 包装器能拿到可变视图,而且它只能替换exec.signal(比如加 deadline),不能删掉它。
  • 返回值是唯一的 canonical JSON 值output.schema定义根类型,execute只返回推断出的值;注册表快照、校验、freeze 后交给output.render(args, value)。别在 body 里 return content blocks,也别让调用方去解析散文里的 id。
  • 抛异常或返回非法值 =isError。基础设施故障用 throw;业务的"非理想状态"(比如进程非零退出)要放进 canonical value 表达。
  • presentCall/presentResult必须是纯函数。它们在直播流和 session-log replay 上都会跑,不能有 I/O、不能读 session 状态、不能用时钟/随机数——UI 卡片(generic/terminal/diff/search/web)的渲染意图要和模型可见内容彻底分离,格式化只服务 UI 的东西不许进 canonical value。

另外一个对测试开发者很友好的点:Code Mode 下每个注册的可见工具直接变成await tools.<name>(args),参数/返回类型从同一套 schema 推导,调用重新走完整执行管线(含 policy)。这意味着工具的注入点、参数契约、返回值契约都是类型化、可编程的——给 agent 写测试时的 mock/stub 边界非常干净。

这个仓库自己怎么测:vitest 矩阵

dsh 的测试设施本身就是一篇工程实践教材。根目录有一排 vitest 配置:vitest.config.tsvitest.e2e.config.tsvitest.snapshot.config.tsvitest.web.config.tsvitest.web.perf.config.tsvitest.web-stress.config.ts,对应脚本:

脚本覆盖
test/test:coverage单元 + 覆盖率
test:e2e真 API 端到端(无DEEPSEEK_API_KEY时自动跳过)
test:snapshot/test:snapshot:record快照与录制
test:web/test:web:perf/test:web:stressWeb 层功能/性能/压测
test:guiGUI 测试

两个值得抄走的做法:

  1. CI 的 Node 兼容矩阵:CI 覆盖 Node 22.19 / 24 / 26,engines声明^22.19.0 || >=24.0.0,pnpm 锁11.7.0。e2e 用真 API 但无 key 自跳过,普通提交不需要真 key,真 key 只进专门的 real-API workflow——把"需要外部依赖的测试"和"纯本地测试"在 CI 层面切干净。
  2. Host/Client 双 tsconfig aggregate:仓库把包分成 Host 和 Client 两个 aggregate 编译程序,原因是两边都在同一个 CordisContext接口上做 declaration merging,但注册的服务不同——合进一个ts.Program会直接类型冲突。这个冲突只存在于ts.Program内部,模块解析不会触发,所以 root solution 可以引用两个 aggregate,一个 paths facade 可以跨两侧。pre-pushpnpm run typecheck保证契约生成完备。给"双端共享接口 + 各自扩展"的 monorepo 提供了一个可复制的类型方案。

上手与踩坑

  • 当前是developer preview,官方明说"THERE WILL BE COMPATIBILITY-BREAKING CHANGES"。生产项目别急着锁版本。
  • 快速体验:npx @deepseek-ai/dsh web,Web UI 默认http://127.0.0.1:3080。跑 headless 一次性任务:pnpm dsh --profile headless "summarize this workspace"(需要DEEPSEEK_API_KEY)。
  • 本地跑源码要求 Node 22.19+/24+、corepack 启用的 pnpm(锁 11.7.0)。装完先pnpm run typecheck验证环境。
  • 写了插件想被生态发现,仓库打dsh-plugintopic。
  • 调试配置树用dsh --profile web --dump-config,比读文档准——打印的就是你机器上真实 boot 的树。

总结与进阶方向

dsh 的架构贡献是把"可扩展"从口头承诺变成结构性事实:插件树 + 分层 patch 消灭了 privileged core,typed events 的四种分发模式把观察/包裹/扇出/串行决策统一成一张表,append-only session log 用一条不变式(模型可见即日志可重建)管住了数据流,capability seams 让一次 provider 切换波及整个产品。对测试开发者来说,它同时是两样东西:一个 agent 测试底座(harness 的本义),和一个"大规模 vitest 矩阵 + 双端类型隔离"的工程范本。

进阶方向:读 Cordis 的Spatiotemporal Composability论文理解插件树的形式化基础;跟一遍docs/cookbook/adding-a-tool.md和 extension cookbook 里的 permission-gate 示例;研究agent/pre-step拦截改写——那是给 agent 注入测试上下文/录制输入的最干净钩子。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/16 16:37:07

告别千兆瓶颈:RTL8125驱动安装全流程与2.5G跑满实战指南

告别千兆瓶颈&#xff1a;RTL8125驱动安装全流程与2.5G跑满实战指南 【免费下载链接】realtek-r8125-dkms A DKMS package for easy use of Realtek r8125 driver, which supports 2.5 GbE. 项目地址: https://gitcode.com/gh_mirrors/re/realtek-r8125-dkms RTL8125驱动…

作者头像 李华
网站建设 2026/8/16 16:33:10

专注半固态营养食品研发,专业机构的核心优势与服务全解析

随着人口老龄化加剧及术后康复需求增长&#xff0c;半固态营养食品逐渐成为特殊营养支持领域的核心品类。这类产品兼顾营养密度与进食安全性&#xff0c;能有效解决吞咽障碍人群、术后患者等群体的营养摄入痛点。专业研发机构凭借技术积累与全链条服务&#xff0c;为该品类的规…

作者头像 李华
网站建设 2026/8/16 16:30:58

想找靠谱的展厅设计企业到底要怎么筛选才不会踩坑?

之前帮公司筛选展厅设计供应商的时候踩过的坑真的能说三天三夜&#xff1a;效果图做的高端大气&#xff0c;落地出来像盗版货&#xff1b;对接要找策划、设计、施工、设备4个不同的负责人&#xff0c;改个需求要传三四遍话&#xff1b;初期报价压的比谁都低&#xff0c;做到一半…

作者头像 李华
网站建设 2026/8/16 16:29:54

华为MetaERP Oracle EBS R12 成本组(Cost Group)完整深度详解一、基础定义与定位1、概念成本组(Cost Group) 是成本模块顶层静态配置业务对象,是一套绑定法

Oracle EBS R12 成本组&#xff08;Cost Group&#xff09;完整深度详解一、基础定义与定位1、概念成本组&#xff08;Cost Group&#xff09; 是成本模块顶层静态配置业务对象&#xff0c;是一套绑定法人实体 LE、独立存货估价科目体系、用于隔离存货所有权 / 核算口径的容器。…

作者头像 李华
网站建设 2026/8/16 16:28:47

Ventoy灵魂九问:一个U盘装下所有系统镜像的零基础指南

Ventoy灵魂九问&#xff1a;一个U盘装下所有系统镜像的零基础指南 【免费下载链接】Ventoy A new bootable USB solution. 项目地址: https://gitcode.com/GitHub_Trending/ve/Ventoy 刚刚装完 Windows&#xff0c;同事又甩来一个 Ubuntu 镜像&#xff1b;搞定 Ubuntu&a…

作者头像 李华