昨晚上,DeepSeek Harness v0.1 Developer Preview 首次公开,DeepSeek 话题的热度依旧很高,从昨晚开始,朋友圈几乎被 dsh 相关的文章覆盖。
秉着来着不拒的态度,连夜用 codex 分析了下 deepseek-harness 的代码,也重新对照了 Pi。

如果只看产品形态,dsh 看起来像是一个带 Web 界面的 Coding Agent。但是从框架内部来看,它的核心对象是一套由 Cordis 组织起来的插件树,模型、工具、会话、沙箱、文件系统、Agent Loop、子 Agent、工作流和界面都在树上。运行中的 dsh,本质上是一套通过配置组合出来的 Agent 运行时。
我比较感兴趣的是,从这套架构里,既能看到 Pi 留下的骨架,也能看到一个 Agent 框架在能力持续增加之后,如何重新处理扩展边界和运行时组合问题。
一、从 dsh 里能看到哪些 Pi 的影子
Pi 把 Agent 的核心 Loop 做得非常干净: 消息进入上下文,模型开始流式输出,模型产生工具调用,工具结果重新进入上下文,循环继续执行,直到模型给出最终结果 。围绕这条链路,Agent 保存消息、模型、工具和执行状态,steering 可以在运行过程中补充方向,follow-up 可以在当前任务结束后继续提交工作。
dsh 的核心执行链仍然沿着这套思路展开。一次 Turn 可以包含多个 Step,每个 Step 对应一次模型请求及其工具调用;输入先进入 Agent 收件箱,系统提示词和工具定义在请求前完成组装,模型输出写入会话,工具调用经过执行管道后把结果送回下一次模型请求。steering、上下文注入、压缩、子 Agent 和后台任务也都还是围绕这条主链路切入的。
所以分析完 dsh 和 Pi 后,得到的结论是: Pi 给出了一套足够小的 Agent 执行骨架,dsh 继续向下拆解这副骨架,让构成 Agent 的每个部分都能够参与运行时组合。
Pi 的扩展系统主要围绕应用层展开。扩展可以注册工具、命令、模型提供方、生命周期事件和 TUI 组件,开发者用一个 TypeScript 文件就能改变 Coding Agent 的行为。dsh 把扩展边界继续下移,连会话、模型适配器、系统提示词注册表和 Agent Loop 都交给插件系统管理。这里已经能够看出两者的关系:底层循环具有明显的延续性,Cordis 负责把扩展能力从 Coding Agent 拉到整个 Harness。
这种联系也有直接的代码证据。dsh 提供了 @deepseek-ai/dsh-llm-pi-ai,它直接依赖 @earendil-works/pi-ai,把 Pi 的多模型协议与模型目录接到 dsh 的 LLM 能力接口上。dsh 同时保留自己的 @deepseek-ai/dsh-llm-deepseek 实现,两套适配器通过同一个接口进入运行时。
二、Cordis 为什么让 dsh 呈现出不同的框架形态
读 dsh 代码时,Agent Loop 和工具、会话、模型适配器拥有相同的插件地位,它们都挂载在 Cordis Context 上。其他插件通过稳定的 ctx.<key> 找到服务,通过 inject 声明依赖,等依赖满足后再激活。
Cordis 为插件准备了四种事件协作语义 。普通观察使用 emit,需要逐层包装和短路的执行链使用 waterfall,并发协作使用 parallel,有顺序要求的异步处理使用 serial。工具执行前后的策略、模型请求拦截和 Turn 停止判断,都可以挂到对应的事件上。新增行为通常只需要找到所属的服务或事件,不需要改动 Loop 代码。
Effect 和 Scope 进一步补上了生命周期语义 。工具、Prompt 片段、模型适配器和事件监听器的注册都带有释放动作,插件卸载时可以顺序撤销;每个 Agent 还可以拥有自己的作用域,同一个进程里的不同会话因此能够使用不同的能力组合。Profile、Bundle 和多层 patch 再把这套机制接到配置上,同一批插件可以组成 Web 运行时、headless 运行时或用户自己的定制版本,可玩性大大增加了。
会话日志也遵循相同的思路。dsh 要求所有模型可见内容都能从 SessionEvent 日志重建,模型消息、工具调用、权限变化和取消状态都进入追加式事件流,界面回放、会话恢复、Fork、持久化和遥测再从日志生成各自的投影。插件能够改变执行过程,日志负责留下可重建的事实。
插件数量增加以后,服务依赖、事件顺序、作用域和卸载时机都会进入开发者的认知范围。dsh 当前仍是 Developer Preview,仓库也明确说明后续会发生兼容性变化(要结合社区的反馈和验证,大多数开源框架都绕不过的宿命,吃苦的还是开发者,哈哈)。所以,对于这样一套高度动态的插件运行时,长期运行还需要版本约束、插件审查、依赖诊断和稳定的扩展协议,道阻且长~。
三、我为什么沿着 Pi 的骨架去做 delphi-agent
今年 5 月,我开始正式推进公司内部的 Agentic Runtime 建设。公司现有技术体系以 Java 和 Spring 为主,Agent 最终要进入真实交付环境,那就意味着必须要接入身份、权限、配置中心等等;另一方面,现在主流的 harness 大多是本地客户端的形态,这种形态在交付场景中无法真正落地,企业的出发点和个人PC 的出发点存在本质的冲突,因此我也必要要去解决将本地执行环境迁移到云端,并做好相应的改造和适配工作。基于这些前提条件,我选择了 Java 21 + Spring Boot + Spring AI 的技术栈体系,而非当前主流的 TS 的技术栈体系。
首先是核心 Loop 最初参考了 Pi。流式消息、工具调用与结果回灌、上下文转换、steering / follow-up、工具执行前后的处理,这些抽象经过验证,适合作为 Agent 内核。Java 重写只解决语言和技术栈适配,企业交付还会面对一些更加具体的问题:同一个会话同时收到两个请求时怎么处理;执行节点中途退出后谁接手;客户端断线后如何补齐事件;工作区切换节点后如何恢复;危险工具由谁批准;租户配额、审计和用量如何进入同一条运行链路等等。
这些问题逐渐决定了 delphi-agent 现在的模块结构。core 保存模型、消息、工具、事件和 Agent Loop,不依赖 Spring 和具体基础设施;harness 放置 Tool、Skill、Sandbox、工作区、工具策略和 Subagent;runtime 管理会话、运行、队列、事件、配额、审计、用量和节点生命周期;extensions 接入 Spring AI、MongoDB、Redis、S3、Docker、MCP、Micrometer 和 Nacos;transports、sdk 与 starter 负责框架接入和最终装配。

Studio 在这里承担产品角色。服务端通过 Starter 使用框架,界面通过 HTTP/SSE 使用服务端,框架内部的队列、租约和在线 Agent 状态只通过 DelphiAgentRuntime 暴露。换一个产品入口,底层运行链路还可以直接使用;换一个存储、沙箱或协议适配器,产品代码也不需要知道框架内部发生了什么。
四、一次 Prompt 在 delphi-agent 中会经过什么
一次请求进入 delphi-agent 后,会先经过运行时治理,再调用 Agent.prompt()。DelphiAgentRuntime 依次检查拦截器、节点 drain 状态、租户配额和当前会话是否存在活跃运行节点等。随后,RunQueueManager 根据配置返回 RUN_NOW、INTERRUPT、ENQUEUE、STEER、DROP 或 REJECT。同一个会话的并发输入由此获得明确语义,多个线程不会同时修改 Agent 状态。

获得执行权的节点会拿到租约和单调递增的 Fencing Token。新的持有者出现后,旧节点即使因为网络抖动继续运行,也无法用过期 Token 覆盖会话状态。每个运行由 JDK 的虚拟线程承载,代码可以保持同步调用结构,模型流、工具 I/O 和外部存储仍然能够获得较高并发度。
workspace 在 Agent 启动前恢复。WorkspaceStorage 从 S3 兼容存储准备会话快照,ExecutionBackend 决定命令运行在本地环境还是 Docker 沙箱中,或者是其他支持 E2B 协议的 Sandbox,结束后再把工作区写回快照。Agent 因此可以更换执行节点,这样文件状态就不会被固定在某个容器或某台机器上。
事件链路单独处理实时输出和可靠终态。普通事件通过 SSE 和 Redis 广播,客户端可以用 Stream ID 回放断线期间的内容;完成、失败和配额拒绝先写入 RuntimeEventOutboxStore,再由 Outbox Worker 追加到事件流并提交运行终态。Redis 暂时不可用时,已经落盘的 Outbox 会继续重试。租约限制谁可以执行,Fencing Token 限制谁可以提交,会话存储和事件流分别保存业务状态与客户端轨迹。
工具执行这块,ToolMetadata 描述工具来源和作用,ToolPolicyPipeline 根据租户、角色和运行上下文筛选满足能力的工具,需要确认的调用进入 HITL;MCP 工具也是一样的思路,叠加租户、命名空间和会话级连接隔离,私网访问与 STDIO 默认情况下是关闭的。
五、两种插件思路分别服务什么场景
dsh 的插件机制服务运行时重组。一个能力通常被拆成 Service Definition / Provider / Consumer,替换文件系统或子进程 Provider,可以让 Shell、终端和 LSP 一起切换到新的执行环境。插件还能够动态挂载、卸载并进入单 Agent 作用域,适合形成开放、可实验的 Harness 生态。
delphi-agent 产生的背景就是为了解决企业交付场景的一些问题,所以它的扩展机制一定是更接近企业模块装配。SessionStore、RunQueueManager、RunLeaseStore、RuntimeEventStore、WorkspaceStorage 和 ExecutionBackend 都是运行时抽象接口,具体实现以独立 Maven 模块发布(extensions 模块按需引入),并通过 Spring Boot AutoConfiguration 完成注册。Java 模块通常在应用启动时确定,运行期间动态变化的是模型、Skill、Prompt、MCP 和 Agent Definition 等目录资源,这些资源可以来自本地文件,也可以来自 Nacos 一类注册中心。
两条路线都把 “修改内核” 变成 “增加实现并重新组合”,动态范围各有侧重。dsh 更适合开发者直接改变运行时结构,delphi-agent 更重视类型约束、配置治理、跨节点一致性和基础设施替换。未来如果把稳定的 Java Extension SPI 与注册中心驱动的动态资源体系继续向外开放,工具、策略、模型、存储和协议适配器也是可以独立发布的,从产品角度来看,Skill、Prompt、MCP 与 Agent Definition 也可以完成版本选择、灰度和回滚。
插件机制不是什么新的概念,它改变的是还是协作方式的问题:核心仓库负责稳定接口,能力开发者负责具体实现,使用者按照场景完成组合。插件数量增长以后,框架逐渐具备生态属性,社区也有了持续参与的位置,Deepseek 目前的运作模式,不管是模型还是 Harness 都在从这个方面做渗透。对一家长期以开源建立技术影响力的企业来说,这是一个很高明的阳谋:代码吸引使用者和插件作者,真实任务持续暴露运行时问题,框架反馈又能推动模型和工具链演进。开源让这些讨论从产品体验进入架构和代码,也让独立框架有机会在同一套公开坐标上检验自己的设计。
六、这条技术路线是怎样形成的
回看 delphi-agent 的迭代过程,5月份完成了 分布式 SSE、会话与运行状态外置、分布式提示队列、全局计量、工作区存储分层、缓存一致性和 租约续期。这些也都对应了当时遇到的实际问题:服务多节点部署,会话跨节点连续执行,客户端断线后恢复事件,执行目录跟随会话迁移,运行状态从单个进程中解耦出来等等。
这些工作完成时,dsh 还没有公开。昨天看到它的代码后,我重新检查了自己对 Agent Runtime 的判断,dsh 用 Cordis 处理插件组合、作用域和生命周期,delphi-agent 用运行时接口抽象、基础设施适配器(extensions 机制)和分布式协调处理企业交付,两套实现最终都遇到了会话持久化、能力替换、事件记录、执行隔离和扩展治理。对我来说,这种相似更像一次方向上的相互印证: 当 Agent 开始承担长期任务并进入真实系统,框架迟早要从模型调用走向完整的运行时设计。
dsh 也给了我一些新的思考。它在插件作用域、可逆生命周期、配置组合和自举扩展上的完成度更高,这些设计值得继续研究和吸收;delphi-agent 已经投入较多精力的部分,集中在会话级并发、租约与 Fencing Token、可靠终态、工作区迁移、多租户治理以及 Java 语言特性上的应用。
delphi-agent 仍处在持续收敛的阶段,很多接口、默认实现和部署体验还需要更多真实场景验证。继续加油~
夜雨聆风