DeepSeek Harness 开源了,[一切皆插件],把他拆开看一看~
1. 终于等到DeepSeek的Harness2026-08-13DeepSeek 把 Harness 开源了v0.1 开发者预览MIT 协议写作日 WebSearch 核实。先抛我的总论点主流 Agent 换的是工具DeepSeek 把 Agent 本身都变成了插件。这一句不是宣传话术是字面事实。我拆了它的源码——本地一份47f9438的 HEAD——模型、工具、技能、会话、沙箱、存储、Agent Loop、调度、UI全是插件挂在同一个 Cordis 底座上没有特权内核。为什么值得拆Harness 正在成为 AI Coding 的主战场。Claude Code、Codex 比的是默认体验打磨DeepSeek 选了另一条路把整个运行时摊开给你换。两条路线谁对v0.1 阶段谁也说不死但架构意图一眼能看穿。这篇我从 harness 工程角度把「一切皆插件」这套设计拆开看能换什么、换完什么代价最后给一个本地实跑的配置观测。2. 底座Cordis 插件系统拆之前先认识底座。dsh 不是自己写了一套 Agent 框架它站在 Cordis 上——一个元框架只管插件怎么加载、怎么卸载、谁依赖谁。剩下的全是业务。这一句的分量要读出来模型适配器是插件工具注册表是插件会话日志是插件连 agent loop 本身都是插件。插件之间不靠改对方代码协作靠两样东西服务往共享上下文注册能力和事件广播事实、被监听拦截。一个插件卸载它注册的服务和副作用一并撤销——Cordis 管这个叫「可逆副作用」所以没有哪个插件欠着一屁股债赖着不走。运行的 dsh 是一棵插件树启动时由若干层按序叠加配置叠加从空根[]起步先按 profile 声明的顺序叠各 bundle 包web 就是 base → web-app再叠 profile 自己的cordis.patch.yml然后是 home 级那份最后是--patch指定的覆盖层。后层覆盖前层一条 patch 按 id 定位某个插件、整体替换它的 config。这四层有一个工程含义发行版、profile、用户、命令行四个改配置的口子分得清清楚楚。你改坏自己的层往上回溯到 bundle 就能排障。我本地实跑时--dump-default-config和--dump-config输出完全相同——因为干净环境里没有用户层和 patch 层正好说明这两个 flag 的分工前者只看 bundle后者看完整叠加。架构文档把这类可替换能力叫seam接缝声明接口的 Service Definition、实现它的 Service Provider、使用它的 Consumer 三者一起设计。一个 seam 换掉提供方就能改变整个产品——把文件系统与进程提供方指向远程沙箱Bash、PTY、LSP 就一起搬过去了不用给每个提供方写 fork。16 个以上的 seamllm/fs/shell/subprocess/sandbox/approval/codeRuntime/subagents/workflowEngine/lsp/web/compaction……排成一张能力图这是整个仓库的地图。先卖个关子连 Agent Loop 都能换意味着什么后面揭晓。3. 核心循环ReactLoopAgent 状态机先看默认的 Agent Loop 长什么样。ReactLoopAgent在packages/core/agent-loop/src/agent.ts:64它把工作切成两个单位turn轮次和step步骤。一个 step 是一次模型请求加上它调用的工具一个 turn 包含零个或多个 step——在领取首条输入之前打开不再欠任何工作时关闭。状态机就三条turn():246开着循环领 stepstep():332构建请求、流式收assistant/chunk、派发工具调用step 跑完看还有没有活——工具还欠一个请求或 next-step 输入到了——有就领下一个 step没有就agent/turn-stopping收尾turn/end关闭。消息怎么进来Inbox类packages/core/agent/src/inbox.ts:25维护两条队列next-turn和next-step。claim():71领走整批 next-step 输入如果要开新轮次再顺走一条 next-turn。有些消息立刻唤醒驱动注入的上下文则留在队列里等另一条消息把它叫醒。这一套我压成一句话循环不直接读「消息数组」循环读队列。输入、注入、中断都变成队列操作状态机只关心「队列里还有没有活」。分叉、恢复、回放全挂在同一个事件流上下一节展开循环本身反而保持得很薄。buildRequest:407组装一次请求时也要经过agent/request这个 waterfall让插件能改写请求——连「模型这次调谁、用什么参数」都是可插拔的。插件下沉到整个运行时架构不神秘真正的风险在生态。这句话我放在这儿是第三节的立场结论再打。4. 一行真相append-only 会话日志第四节讲一个被我低估的设计会话日志。dsh 的会话不是「消息列表」是一条append-only 的 SessionEvent 事件流packages/core/session/src/types.ts:404。每条事件带type、seq、time、data。seq就是log.lengthindex.ts:604的 append 里写死单调、连续、可寻址。事件必须是无损 JSON——BigInt、函数、Date 这类直接拒收——写进去之前先 deepFreeze。日志是不可变的事实谁也别想改。事件按角色分三类我叫它三域边界turn/start、step/start、request/header这类标记不产生模型可见消息表面user/message、assistant/message、tool/result真正出现在模型历史里的东西仅日志assistant/chunk、tool/code-dispatch、usage 这类为保真和回放存在但不进派生历史。设计原则一句话模型可见即已记录。抵达模型请求的一切都必须能从日志重建。这是一条运行时不变式packages/core/agent-loop/src/invariant.ts:21-54每次 llm 请求检查消息数组是否与deriveMessages()从日志算出来的一致不一致直接 fail。不是约定是断言。这条不变式把整个系统焊住了。UI 渲染读日志回放读日志fork 读日志index.ts:1081按 boundary seq 切一个子会话恢复也是日志的纯函数。日志是唯一真相其它全是投影。主流工具把会话状态当运行时内存来管dsh 把它当数据库来管——回放、分叉、继续这些「看起来很难」的功能在这里全是对同一事件流的不同读法。5. 安全与扩展的接缝工具执行流水线工具是模型和世界之间的接缝dsh 在这条接缝上放了一条完整的执行流水线。位置在packages/core/tools/src/index.tsprepareExecution:1463跑 pre-execute waterfall然后审批询问、单调守卫dispatchScheduledExecution:1569跑 executetimeout/retry 包在外面postExecute:1742跑 post-execute waterfall最后applyFinalContent:1649应用定义自带的内容变换tools/result通知最终结果。简化成六个阶段pre-execute waterfall → 审批询问 → 单调守卫 → execute → post-execute → finalizeContent tools/result。两个细节值钱。一是单调守卫和审批分开。守卫是已注册的「所有者策略」不能重新排序ctx.approval是一次性的人机询问放在守卫之前。它们各管一摊谁也不能绕过谁。审批缺了回答方、或回答不可用一律按 deny 处理——fail-closed。二是PTCProgrammatic Tool Calling不绕过安全流水线。PTC 是 code preset 的核心卖点模型不一个个调工具而是生成一段 TypeScript 程序把多步操作组合起来。但它复用的是同一个TOOL_RUNTIME_SCHEDULER——code-mode.ts:481拿到调度器:545对每个子调用走scheduler.prepare()。换句话说程序化调用里的每一个子调用仍然完整过一遍 pre-execute/守卫/审批/execute/post-execute。这不是给模型开后门是把批处理也关进同一条流水线。6. 四种模式 四套插件组合模式不是硬编码是四份 YAML。apps/cli/config/agent-presets/下四个目录每份preset.ymlagent.cordis.yml由agent-presets插件按default: standard挂载。standard标准模式功能完整的编码 Agent——文件编辑、Shell、检索、Skills、计划、目标、子代理、工作流全给。codePTC 模式具备 standard 全部能力再通过 Code Mode SDK 把工具呈现给模型让模型用一段 TypeScript 程序组合多步操作。minimal极简模式只有两个工具——持久 bash str_replace_editor。我读了它的agent.cordis.yml整份文件就两个 grouppersona 直接写死完整系统提示词连上下文压缩都没有。cordis创造模式给「想造 preset 的人」用的带运行时检查、插件实验和 preset 创作指导。注意一个坑standard/code/minimal/cordis 是 agent-preset不是dsh --profile能启动的 boot profile。CLI 随附的 boot profile 只有web和headless两个preset 是「这个 Agent 会话里装哪些工具、用什么人格」boot profile 是「启动哪一棵插件树」。我本地跑--profile standard --dump-config直接报错profile standard does not exist——这个区分最容易被配置文档坑到单独拎出来说。每份 preset 的agent.cordis.yml是「agent-plane」的组合里面涉及服务的行必须放在带isolaterealm 的 group 里否则服务会发布到 root realm 变成进程全局两个 preset 撞名就报错。这个机制保证不同 preset 各带各的私有实例——同一进程里standard 的 agent 和 minimal 的 agent 互不污染。到这里已经摸到第二节那个关子的边了「换」不是换个实现类是换一整套插件组合。但真正的「换循环」要看到多 Agent 与 workflow——下一节。7. 多 Agent 与 Workflow现在兑现那个关子第二节我说「连 Agent Loop 都能换」现在兑现。子 Agent 是一层。SubagentProvider接口packages/subagent/subagent/src/types.ts:285定义了什么是一次「委派」注册了五类实现spawnsubagent-spawn-in-process/src/index.ts:41新建子 AgentinheritsParentContext false孩子从零开始看不到父对话forksubagent-fork-in-process/src/index.ts:48继承父的已完成轮次前缀inheritsParentContext true从父的上下文续着干acpsubagent-acp/src/index.ts:146进程外的 ACP 子 Agent无父上下文claude-code / codex把一轮委派给 Claude Code 或 Codex 进程standard preset 里默认disabled: true注释写得明白复制 preset 再打开就能只让某个组合用。更「换循环」的是 workflow 引擎。packages/workflow/workflow-worker-thread/src/runtime.ts:90把一段 workflow 脚本用vm.Script编译进 worker thread脚本里能调用agent()、parallel():401各 thunk 并行非致命错误归 null、pipeline():428逐项过阶段、无跨阶段屏障。也就是说一个任务怎么编排不是写死在循环里而是跑一段可编程的脚本——循环被换成了可组合的脚本运行时。最极端的是 Ralphpackages/workflow/tool-ralph/src/index.ts。它内置一段固定脚本RALPH_SCRIPT:90每轮派一个全新的结构化子 Agent 去攻一个目标子 Agent 必须返回规范化 reportstatus/summary/evidence/nextSteps/blocker模型只能填数据不能改循环。requireFreshProvider:220强制子 Agent 必须是「真·干净上下文」——继承父上下文的 fork 直接被拒。standard preset 里给 Ralph 配了maxRounds: 64agent.cordis.yml:233。一句话收束spawn/fork/acp/codex/claude-code 换的是「子 Agent 从哪来」workflow 换的是「任务怎么编排」Ralph 换的是「整个循环归谁控」。底下的 turn/step 状态机还是那个但用不用它、怎么用它全由插件组合说了算。这就是「连 Agent Loop 都能换」的全部含义。回到第三节那个判断架构不神秘风险在生态。换的能力给够了谁来换出好东西是下一节要谈的成色问题。8. 本地实践跑起来看配置理论看到这儿落地看一下。我在干净的目录里真实装了一遍素材全文在 local-practice.md下面全是本机输出。安装单独建目录npm install deepseek-ai/dsh --no-fund --no-audit装到0.1.0-rc.6。别在源码仓库目录里直接 npm install——那是 pnpm workspace依赖带workspace:协议npm 会报EUNSUPPORTEDPROTOCOL workspace:。绕 shimWindows 上包 bin 指向lib/bin.jsESM用node node_modules/deepseek-ai/dsh/lib/bin.js直跑绕开.cmdshim。--version→0.1.0-rc.6。--help→ 子命令web、plugin选项--profile、--patch、--dump-config、--dump-default-config。--profile web --dump-config→ 490 行 YAML。顶部按# 层来源注释分组能看出每行来自哪个 bundle、被哪个层 patch 过。几个真实配置片段dump 原文默认模型agent-default-model配provider: deepseek-official、model: deepseek-v4-flashdump 时的默认值随版本可能变待核实沙箱sandbox-policy默认workspace-writebash-sandbox有disabled: !!js process.platform win32——Windows 上自动禁 bash 沙箱换成pwsh-sandbox审批approval默认ask只有danger-full-access才是never服务webserver默认host: 127.0.0.1、port: 3080默认 presetagent-presets的config.default: standard。最有意思的实操是--patch覆盖层。写一个十几行的 YAMLid 定向改agent-presets的 default- id: agent-presets config: default: code再--profile web --patch extra.yml --dump-configdiff 就两处多一行patched by extra.yml 路径注释default: standard改成default: code。不改源码、不重启整棵树就换掉了默认人格。诚实边界我照实说dsh web真正启动、dsh plugin add、四个 preset 的 YAML 挂载、!!js表达式在启动时的求值结果这几项我都是源码读到、没实跑验证——没配 API key没起 webserver。凡标了行号的都是源码事实凡说「真实输出」的都是上面这些本机跑出来的。9. 结论DeepSeek 押注了什么v0.1 的真实成色拆完我给一个总体判断。DeepSeek 押注的是架构开放把插件边界下沉到整个运行时——模型、工具、技能、会话、沙箱、存储、Agent Loop、调度、UI全是插件。这个选择和我之前写过的「Agent 为核心 能力/知识/协作」的 Agent 路线是同一件事的两面既然 Agent 是核心Agent 周围的一切就该可替换。但 v0.1 的真实成色我泼三盆冷水。第一「什么都能换」不等于任务成功率更高。换的能力给的是架构自由度真正兑现体验的是高质量默认插件、稳定组合范式、可信评测——这三样在 v0.1 都还在路上。README 明说当前是开发者预览、未来将出现破坏兼容性的变更接口快速变化是实打实的迁移成本。第二生态风险在「谁来换出好东西」。插件边界越深接口稳定性、依赖管理、版本兼容、性能、调试复杂度越难控制。可替换是能力可组合是纪律后者不是开源就自动有的。第三缺交互 CLI 是实打实的短板。boot profile 里headless是一次性任务执行dsh --profile headless run the testsweb是浏览器 UI没有一个驻留终端的持续对话入口。对一个面向开发者的 harness这一步挺影响上手手感。这也正好接上我之前的旧观点「人从操作者变管控者」。DeepSeek 把可替换性给到极致其实是在把「管控」这件事的前置做足——你可以严格限定 Agent 用什么工具、跑什么循环、在哪跑人退到定目标、审结果的位置。写代码变便宜之后验证/评审/决策才是瓶颈dsh 这套架构给「管控」提供了比主流工具更细的旋钮。最后如果你认同「插件下沉」这个判断——点个赞这套思路值得被更多人看见。你试 dsh 了吗卡在哪一步——是装环境还是--profile和 preset 分不清还是看完这篇才发现 boot profile 只有 web/headless评论区说说我每条都看。想深入了解哪一块也评论区告诉我。我猜有人会问 PTC 那段 TypeScript 程序到底怎么写、子 Agent 的inheritsParentContext在实际任务里怎么选也有人想问 Ralph 的 64 轮是怎么被编排出来的——都可以你点题我写。最后给大家送上仓库地址https://github.com/deepseek-ai/deepseek-harness