DeepSeek Harness 系列(二):核心架构与运行时骨架
理解 DeepSeek Harness,不能只从“模型调用工具”这条线看。那只是运行时最外层的行为。它真正的架构主线是:用 Cordis 组织插件树,用 Profile/Bundle 决定启动组合,用事件日志记录持久事实,用服务和事件承载扩展能力。
本文按整体到局部的方式拆它的核心架构。细节不会展开到每个包的实现,但会把几个关键设计讲透:配置如何变成运行树,Agent 如何完成一个轮次,工具为什么是一条流水线,会话日志为什么是模型历史的来源,以及业务扩展应该挂在哪里。
总体架构
DeepSeek Harness 可以先看成四层。
这四层的职责不同:
| 层 | 主要职责 |
|---|---|
| 应用表层 | 接收用户输入、展示事件流、启动或恢复 Agent。 |
| Agent 运行时 | 组装提示词、调用模型、执行工具、记录会话事实。 |
| Cordis 插件上下文 | 管理服务依赖、插件生命周期、事件分发和配置树。 |
| 宿主平台 | 提供文件系统、进程、沙箱、凭据、网络与持久化能力。 |
最容易误解的是应用表层。Web UI 不是内核,Headless 也不是另一套内核。它们都是 profile 组合出来的表层,底下复用同一组基础服务。
Profile 与 Bundle:运行树从哪里来
dsh 启动时不是直接加载一个固定入口,而是先解析 profile。profile 位于 Harness home 下,里面有 package.json 和用户自己的 cordis.patch.yml。package.json 的 dsh.profile.bundles 列出要叠加的 bundle。
源码文档给出的组合顺序如下。
这里有两个设计点很关键。
第一,bundle 是分发格式。dsh-base 提供模型适配器、工具、持久化、沙箱、审批策略、设置、凭据、遥测等基础能力;dsh-web-app 在它之上加 Web 宿主、API 网关、workspace、前端和浏览器插件;dsh-headless 则加一次性任务 runner,不挂服务器。
第二,patch 是显式覆盖。按 id 定位的 patch 会替换整条配置的 config,不是深度合并。这个规则牺牲了一点配置便利性,但换来了一个很强的性质:最终树可以被打印、检查、复现,运行时没有隐藏合并魔法。
Cordis:插件运行时的底座
Cordis 在这里承担三件事。
Cordis 的价值不只是“可以装插件”。更重要的是它让插件生命周期可被运行时追踪。一个工具注册、一个事件监听、一个提示词段落、一个模型适配器,本质上都是插件安装时产生的副作用。插件卸载或热重载时,这些副作用应该一起撤销。
所以 Harness 的扩展方式不是改内核,而是在对应服务或事件边界旁边挂插件。
核心服务图
下面这张图可以当成 DeepSeek Harness 的运行时地图。
这里的依赖方向很有意思。AgentLoop 是唯一包含具体循环逻辑的包,但它依赖的是抽象服务。工具注册表不直接依赖 UI;工具只是声明展示意图,Host 和 Client 再把展示意图映射成自己的视图。会话服务不关心 Web 页面;Web 监听会话事件流来渲染。
这种关系让每个模块可以独立演进。比如你要加一个内部模型网关,应该注册新的 LLM adapter;要加一个业务工具,应该注册 ctx.tools;要加一个审计策略,应该监听工具或 Agent 事件;要做自己的 UI,应该监听 session/event 并调用 Agent API。
Agent 轮次模型
Harness 把一次交互拆成轮次和步骤。
| 概念 | 含义 |
|---|---|
| 轮次 | 从领取一批输入开始,到当前没有欠下工作为止。 |
| 步骤 | 一次模型请求,以及这次响应中触发的一组工具调用。 |
一个轮次可以有多个步骤。典型场景是:模型请求工具,工具结果回来后,模型还要继续总结或发起下一组工具。
这张图省略了很多错误处理,但主线足够说明问题:Agent Loop 做的是编排,不负责所有策略。agent/pre-step 可以拦截进入步骤的消息;agent/request 可以包装模型请求;tools/pre-execute、tools/execute、tools/post-execute 可以包装工具执行;agent/turn-stopping 可以在自然停止前做终点检查。
默认循环尽量保持瘦,扩展能力挂事件。
工具流水线
工具执行是 Harness 最值得学习的一段设计。它不是“调用函数并返回字符串”,而是一条带策略点的流水线。
这条流水线拆开了四类关注点:
- 工具作者声明参数、输出和执行主体。
- 策略插件决定能否执行、是否审批、是否沙箱、是否超时。
- 后置插件可以改写展示内容、阻止结果或追加上下文。
- 会话日志只记录最终模型可见的权威结果。
这也是业务接入时最该遵守的边界。一个业务工具不应该把所有权限写在工具主体里,也不应该让模型自由拼接数据库语句。更好的做法是让工具只暴露“查询异常任务”“发起补偿”“读取补偿结果”这样的受约束能力,再由工具流水线承载审批、审计和脱敏。
会话日志:系统的事实来源
dsh-session 把会话定义为仅追加事件日志。模型请求的历史不是某个临时数组,而是从日志派生出来的 surface 投影。
这带来三个工程收益。
第一,可重建。模型可见的用户消息、助手消息和工具结果都在日志里,后续请求可以重新派生。
第二,可回放。UI 不需要保存另一份聊天状态;它可以监听 session/event,也可以从已有日志回放。
第三,可恢复。中断、取消、工具未完成、模型错误都能作为事件或恢复结果进入日志,而不是只留在进程内存里。
但这也带来一条硬纪律:新增模型可见输入时,不能只往请求对象里塞内容。它必须有对应的会话事件,能从日志投影出来。
LLM Runtime:模型是适配器,不是内核
dsh-llm 提供的是模型无关词汇和适配器注册表。适配器通过 provider 路由注册,Agent 请求通过 provider 和 model 选择适配器。
适配器必须把不同提供方的流式响应收束成统一的 StreamChunk:文本增量、reasoning 增量、工具调用参数增量、usage、finish 等。这样 Agent Loop 不需要理解每个模型 API 的差异。
对于企业来说,这个边界很实用。内部模型网关、审计代理、私有模型目录,都应该优先做成 LLM adapter 或 llm/stream 包装插件,而不是改 Agent Loop。
能力扩展应该放在哪里
可以用一张决策表来记。
| 你要做的事 | 推荐挂载点 |
|---|---|
| 新增模型提供方 | 注册 ctx.llm adapter。 |
| 新增模型可调用能力 | 注册 ctx.tools 工具。 |
| 限制某个 Agent 能看到的工具 | 使用 agent 作用域的 ctx.tools.restrict()。 |
| 工具执行前做权限判断 | 监听 tools/pre-execute 或注册 guard。 |
| 工具执行时加超时、重试、指标 | 包装 tools/execute。 |
| 工具结果脱敏、改写、补上下文 | 监听 tools/post-execute。 |
| 记录不可变审计结果 | 监听 tools/result。 |
| 改变模型请求内容 | 监听 agent/pre-step 或 agent/request。 |
| 新增持久会话状态 | 扩展 SessionEventMap 并实现投影。 |
| 新增 UI 展示节点 | 注册 conversation node renderer,或监听 session/event。 |
| 接入外部自动化协议 | 做协议驱动插件,通过 ctx.agents 创建或恢复 Agent。 |
这张表背后的原则是:尽量使用已有服务和事件,不改默认循环。只有当一个能力真的改变了“模型请求、工具执行、轮次关闭”的基本语义时,才需要进入更底层设计。
架构取舍
DeepSeek Harness 的设计不是只有优点。它的几个取舍需要一起看。
| 设计选择 | 换来的收益 | 付出的成本 |
|---|---|---|
| Cordis 插件树 | 能力可组合、可卸载、可替换。 | 插件作者必须遵守生命周期和依赖纪律。 |
| Profile/Bundle/Patch | 运行组合可分发、可覆盖、可检查。 | patch 替换整条配置,覆盖时更啰嗦。 |
| 事件溯源会话 | 可回放、可审计、可恢复。 | 新增模型可见状态必须补事件和投影。 |
| 工具流水线 | 权限、沙箱、审批、观测不侵入工具主体。 | 工具作者要区分规范值、模型渲染和 UI 展示。 |
| 默认循环保持瘦 | 新行为主要靠插件扩展。 | 复杂能力需要理解服务和事件图。 |
所以它更适合长期运行、需要扩展和审计的 Agent 产品,不一定适合一次性脚本或小型 Demo。
小结
DeepSeek Harness 的核心架构可以压缩成一句话:Profile 负责组合,Cordis 负责生命周期,Agent Loop 负责编排,Session 负责事实,Tools 和 LLM 负责能力边界。
当你带着这句话再读源码,很多包名就不再零散。dsh-base 是基础能力层,dsh-web-app 和 dsh-headless 是表层组合,dsh-agent-loop 是默认驱动器,dsh-session 是事实来源,dsh-tools 是工具边界,dsh-llm 是模型边界。
下一篇进入实操:如何把源码跑起来,如何配置模型,如何用 Web UI 选择工作区并启动一个真实会话。
